Skip to content

Count memory the way Activity Monitor does - #17

Open
jondkinney wants to merge 1 commit into
kemalandic:mainfrom
jondkinney:fix-memory-gauge
Open

Count memory the way Activity Monitor does#17
jondkinney wants to merge 1 commit into
kemalandic:mainfrom
jondkinney:fix-memory-gauge

Conversation

@jondkinney

Copy link
Copy Markdown

The bug

The memory gauge counts every page that is not free or speculative as used, which sweeps in the file cache. On a 128 GB machine it reads 116 GB / 91% while Activity Monitor and iStat Menus both report 63%:

EdgeControl 2.3.1 Activity Monitor / iStat
Memory used 116.0 GB — 91% ~83 GB — 63%
Pressure 91% 7%

The error scales with how much RAM the machine has spare to cache with, so it's worst on exactly the machines this dashboard is aimed at.

Memory pressure has a second problem: it's derived from the same free-page count, so it is effectively a duplicate of the used percentage. Both gauges move together and sit pinned near the top, which means a genuine pressure spike — the thing that gauge exists to show — has nowhere left to travel.

The fix

SystemMetricsService.currentMemorySnapshot():

  • Used = app memory (internal_page_count − purgeable_count) + wired + compressed, the three figures Activity Monitor adds up. File-backed and purgeable pages are cache the kernel hands back on demand, so they aren't counted.
  • Pressure = (wired + compressed) / physical — only what the kernel cannot reclaim, which is what memory_pressure reports as unavailable. On the sample above that gives 5% against iStat's 7%.

Incidentally this also drops a small double-count: vm_statistics64.free_count already includes speculative pages, so the old free_count + speculative_count counted them twice.

Tests

The arithmetic moves into a MemoryReading struct so it can be exercised without reading the live host. Tests/EdgeControlTests/MemoryReadingTests.swift runs the vm_stat page counts from that 128 GB machine through it and pins used and pressure to what Activity Monitor showed, plus the edge cases (clamping, purgeable_count > internal_page_count, zero physical size). Reverting the source to the old formula fails them with exactly the 116.1 GB / 90.7% from the screenshot.

Behaviour is unchanged otherwise: same struct fields, same call site, same fallback to the previous sample when host_statistics64 fails.

One note

I could not run the suite on a clean main — on Xcode 16.4 / Swift 6, main fails to compile before any of this (PluginWidgetProvider.swift:58, non-Sendable NSImage in a Sendable struct, and main-actor isolation errors in LayoutEngineTests). I verified these tests with those build errors patched locally. Happy to send that as a separate PR if it would be useful.

The gauge treated every page that was not free or speculative as used, so
the file cache counted against it. On a 128 GB machine that reads 116 GB —
91% — while Activity Monitor and iStat Menus both say 63%. Used memory is
now app (anonymous) + wired + compressed, the same three figures Activity
Monitor adds up.

Pressure came off that same free-page count, which made it a second copy
of the used percentage: both gauges sat at 91%, and a real pressure spike
would have had nowhere to go. It now counts only what the kernel cannot
reclaim — wired + compressed — which tracks what `memory_pressure`
reports, 5% against iStat's 7% on the sample above.

The arithmetic moves into MemoryReading so it can be tested away from the
host: the tests run the page counts from that 128 GB machine through it
and pin both figures to what Activity Monitor showed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant