mactop v2.1.6#75
Conversation
Memory used was computed as total - (free + inactive_count). macOS keeps a large share of anonymous app memory — e.g. MLX/LLM model weights — in the INACTIVE queue, but those pages are dirty and not freely reclaimable. Counting all of inactive as available made 'used' collapse toward 0 whenever a workload parked gigabytes there (gauge reading 0% at ~79% real usage) and flicker as pages migrated between the active and inactive queues. It wasn't chip-specific — it surfaced on whichever machine ran that workload. Switch to the Activity Monitor 'Memory Used' definition: App Memory (non-purgeable anonymous, internal_page_count) + Wired + Compressed. internal_page_count spans both the active and inactive queues, so the reading is correct and stable regardless of how macOS ages the pages. Reproduced on M1 Ultra with 45 GB of incompressible anonymous memory: old formula read 53-57% and drifted; new reads a stable 77.6%, matching top/Activity Monitor (and the reporter's Mole reading). Extracted activityMonitorMemoryUsed with a unit test covering the inactive-anon, purgeable>anon, and clamp cases.
On M5 Max / macOS 27 the PMP performance-floor IOReport channels that mactop derives ANE utilization from (ANE-AF-BW / ANE-DCS-BW) return zero channels for a non-root process, so ANE usage was stuck at a constant 0% even under on-device inference. Fall back to the Apple Neural Engine driver's (H11ANEIn) IOPowerManagement.CurrentPowerState in the IORegistry (0 = idle, 1 = powered), sampled as a duty cycle across the measurement window. It is readable without root and used only when no PMP ANE utilization channel is present, so chips that do expose PMP keep their higher-resolution floor-residency signal. Because the signal is binary and system-wide (reflects any ANE use, including macOS background ML), it is surfaced as a powered/idle state rather than a percentage: new ANEPowered flag threaded through to the gauge, SoC history chart, info panel, CLI, and headless JSON (ane_powered). docs/m5max_ane_diag/ documents the diagnosis and ships a standalone scanner.
On chips where the AMC 'ANE RD/WR' byte counters are kernel-blocked (M5+, and M-series Ultra/Max variants on macOS 27), PMP is the only ANE bandwidth source — and on those chips PMP exposes ANE only as the combined 'ANE0 RD+WR' histogram, with no separate RD/WR. The AF BW parser explicitly skipped any channel containing 'RD+WR' (to avoid double-counting the split channels), so those chips fell through to 0 GB/s and a 0% ANE gauge even under full Foundation Models load (fm respond --model system) — the symptom anemll/ivanfioravanti hit on M5 Max and M3 Ultra. Parse the combined RD+WR histogram into a separate accumulator and use it as the total when neither AMC nor split PMP RD/WR is available. Source priority unchanged: AMC exact bytes > split PMP RD/WR > combined PMP RD+WR. No read/write split is fabricated; the combined total drives the gauge and utilization, which is what these chips need. Validated on M1 Ultra / macOS 27: AMC path unchanged (~28 GB/s under fm); forcing AMC off exercises the new path and reads ~16 GB/s from ANE0 RD+WR where it previously read 0.
The earlier fix (05a180e) only suppressed the permission PROMPT when the grant was missing — but when Screen Recording was already granted (e.g. from prior overlay use), --headless still created the CGDisplayStream and lit up the 'mactop is recording your screen' indicator. Reporters running headless on a machine that had granted the permission saw screen capture engage, which should only ever happen with --overlay. Display FPS is the sole screen-capture consumer and a niche metric for a cron/SSH/script tool, so stop starting the counter in headless at all. Now only the overlay touches Screen Recording. display_fps and frame_interval_ms are omitted from headless output (omitempty). Removes the now-dead HasScreenRecordingAccess / hasScreenRecordingAccess helper. Verified: with Screen Recording granted, 'mactop --headless --count 1' now omits display_fps (no stream created); other metrics unaffected.
There was a problem hiding this comment.
Pull request overview
This PR updates macOS metric sourcing to better match system behavior on newer Apple Silicon / macOS versions: it aligns “Memory Used” with Activity Monitor, adjusts headless behavior to avoid any Screen Recording / CGDisplayStream activation, and improves ANE bandwidth fallback parsing when AMC counters are unavailable.
Changes:
- Compute memory used as non‑purgeable anonymous + wired + compressed (Activity Monitor–style) via a new helper and update
GetNativeMemoryMetricsto use it. - Extend PMP “AF BW” ANE histogram parsing to support combined channels (e.g.,
RD+WR) for platforms where split counters aren’t available. - Remove Screen Recording preflight plumbing from the headless/displayfps Go/C bridge and add a unit test for the new memory-used helper.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/app/native_stats.go | Adds Activity Monitor–style memory-used helper and switches native memory metrics to use it. |
| internal/app/ioreport.m | Updates ANE PMP “AF BW” parsing to handle combined channels when split RD/WR counters are missing. |
| internal/app/headless.go | Ensures headless mode never starts CGDisplayStream-based FPS capture and updates comments accordingly. |
| internal/app/displayfps.m | Removes the non-prompting Screen Recording access helper (headless no longer uses it). |
| internal/app/displayfps.go | Removes HasScreenRecordingAccess Go wrapper and its cgo declaration. |
| internal/app/app_test.go | Adds TestActivityMonitorMemoryUsed coverage for the new memory-used formula. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Sample all H11ANEIn IORegistry nodes on Ultra-class chips (OR aggregate for activity, per-die duty cycle for cluster metrics). Expose ane_cluster_count/ane_cluster_active in headless JSON and the info panel. On layout 19 (history_soc), render separate red ANE0/ANE1 history traces with consistent powered/idle titles and a display stagger when both lines would otherwise overlap at identical values.
Integrate M5 Max and Ultra dual-ANE detection with upstream v2.1.5 ANE refactor (aneUtilizationPercent, bandwidth latch, localized history_soc charts). Resolves merge conflicts across ioreport, metrics, and app UI.
On M5 / M5 Max the ANE is an exclave-based driver (IOExclaveProxy=Yes, IONameMatched "ane,*exclave"). Its IOPowerManagement.CurrentPowerState is binary and stays pinned high while any background macOS ML service uses the ANE, so the power-state duty cycle is only meaningful as a powered/idle indicator, never a utilization %. Previously the power-state fallback latched bandwidth mode (watts read 0 on this silicon), so the gauge fell through to the %-form template and rendered the binary value as a misleading 100% in both the title and the gauge bar label. Detect exclave ANE at runtime and thread ANEExclave through PowerMetrics -> SocMetrics -> CPUMetrics: - Exclave: collapse util to 0/100, show "ANE: ON" / "idle" in the gauge title, bar label, and info panel; never latch bandwidth/residency modes. - Non-exclave (e.g. M3 Ultra dual-die): unchanged dual-cluster duty-cycle behavior. Detection is automatic per node, no hardcoded chip model. Adds TestANEExclaveBinary.
The non-exclave IORegistry power-domain tier (Ultra dies on macOS 27 with empty PMP for non-root) has no bandwidth component, so latching bandwidth mode would mislabel it as "@ 0.00 GB/s". aneBWLabelMode already exempts ANEPowered, so the gauge correctly shows "powered/idle (W)" instead.
…nto merge-v2.1.6
sawAneUtilChannel is set only by the util-floor residency channels, not by the combined RD+WR PMP bandwidth path (v2.1.6). So the binary power-state fallback could override a real ANE bandwidth signal on chips with combined-BW-but-no-util-floor (M1 Ultra, or M5 Max as root). Gate the fallback on no util channel AND no bandwidth (AMC or PMP RD/WR/RD+WR), and only surface aneIsExclave when the fallback actually engages. Any working channel now keeps its real %/GB-s display; the binary ON/idle is a strict last resort.
Remove docs/m5max_ane_diag/ (investigation scanner + raw scan output; its rationale is in the code comments and commit history) and revert mactop.rb to the development baseline so the change set is purely the ANE feature.
The exclave binary treatment was only applied to the ANE gauge; the history_soc ANE chart still printed the raw percent (e.g. 100.0%). Mirror the gauge: on exclave ANE (M5 / M5 Max) the layout-19 chart title and data label now read ON/idle, matching the gauge instead of a misleading %.
ANE utilization on M5/M5 Max/Ultra (macOS 27): per-chip detection with non-root fallback
There was a problem hiding this comment.
Gitzilla has reviewed your changes and found 2 potential issues.
Autofix is OFF. To automatically fix reported issues, enable autofix in the Gitzilla dashboard.
Extract ANE gauge title logic into aneGaugeTitle so title/label selection (including the IORegistry power-state fallback that must not show a wattage) is unit-testable. Add shouldRenderDualANEClusters to gate plotting of per-die ANE traces so the history chart only renders dual-cluster traces when the gauge is driven by the power-state duty cycle (ANEPowered/ANEExclave), avoiding divergence when PMP/residency or bandwidth channels drive the gauge. Update renderANEHistoryChart and updateCPUGaugeTitles to use the new helpers, and add unit tests (TestANEPowerStateGaugeTitle, TestANEDualClusterChartGatesOnPowerStateTier). Also add strings import for tests.
There was a problem hiding this comment.
Gitzilla has reviewed your changes and found 4 potential issues.
Autofix is OFF. To automatically fix reported issues, enable autofix in the Gitzilla dashboard.
Introduce aneChartTitle to centralize ANE history chart title logic and handle ANEExclave and ANEPowered (power-state) fallbacks so titles don't incorrectly show wattage like "0.00W". Refactor renderANEHistoryChart to use the new helper and broaden the power-state condition to include ANEPowered && !bwMode. Add TestANEPowerStateChartTitle to verify powered/idle wording is used (no wattage or percentage) and that normal ANE samples still render wattage where appropriate. This prevents a regression where power-state samples were routed through wattage-bearing templates.
Replace the previous len(lastCPUMetrics.ANEClusterActive) > 1 check with shouldRenderDualANEClusters(lastCPUMetrics) and add a comment explaining the rationale. This ensures per-cluster ANE status is only shown when the ANE power-state tier is appropriate (ANEPowered/ANEExclave) to avoid showing IORegistry duty-cycle values that would diverge from residency % on multi-die chips with PMP residency. Keeps the existing fallback logic for cluster count.
There was a problem hiding this comment.
Gitzilla has reviewed your changes and found 1 potential issue.
Autofix is OFF. To automatically fix reported issues, enable autofix in the Gitzilla dashboard.
Introduce aneClusterLabelMode and aneClusterLabelModeFor to centralize ANE label wording (percent / powered / exclave). Move the ANEPowered power-state branch ahead of the compact-layout wattage branch in aneGaugeTitle to avoid rendering contradictory “0.0W” on IORegistry-powered fallbacks. Update formatDualANEClusterStatus and formatDualANEClusterChartText to accept the label mode and produce consistent ON/idle, powered/idle or percentage wording. Wire the mode through renderANEHistoryChart and the info panel. Add unit tests to guard the compact power-state title and exclave dual-cluster labels.
Reads Amperage (mA, signed) and Voltage (mV) directly from the AppleSmartBattery IOKit registry entry — IOPSGetPowerSourceDescription does not reliably expose these keys on Apple Silicon. The product (mA × mV / 1000) gives milliwatts; positive = charging, negative = discharging. The absolute wattage is appended to the battery state label, e.g. "Battery: 72% (charging, 25.9 W)" or "Battery: 60% (on battery, 12.4 W)". No wattage is shown when the value is zero (desktop Macs, or battery full on AC). Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Show instantaneous charging/discharging watts in power panel
Cursor review of #77: on the ANEPowered IORegistry fallback the gauge title reads powered/idle but the inner bar label still showed 'NN%' (only ANEExclave overrode it), mixing a binary title with a percentage. Extract aneGaugeInnerLabel (mirrors aneGaugeTitle's power-state branches) so exclave -> ON/idle and ANEPowered -> powered/idle, with a unit test (TestANEGaugeInnerLabel). Cursor's second finding (matching-dict leak in collectAneServices) is a false positive: IOServiceGetMatchingServices consumes one reference to the dictionary on all paths (macOS 10.5+), matching the four other call sites in ioreport.m — releasing it would be an over-release. No change. The merged ANE PRs (#76/#77 + follow-ups) left five functions over the gocyclo>15 limit, so 'make sexy' was failing. Restore it with behavior-preserving extractions: aneBandwidthUtilization (metrics.go), aneClusterActiveSlice (ioreport.go), renderDualANEClusterChart + renderPowerStateANEChart + aneGaugeInnerLabel (app.go), insertANEClusterLine + buildFanLines (info.go), and collapse formatDualANEClusterStatus's three identical switches into a word select. Full gate green; all ANE tests pass; M1 Ultra gauge unchanged.
Fan speed read 0 RPM on some Macs (reported on Mac Studio M2 Ultra)
because SMCGetFloatValue only decoded 'flt ' and 'ui8 ', returning 0.0
for every other type. The fan RPM key F<n>Ac is 'flt' on this M1 Ultra
(reads fine) but other Macs expose it as a fixed-point or wide-integer
type, which fell through to 0.
Decode the full set of SMC numeric encodings: flt, ui8/ui16/ui32 and
si8/si16 (big-endian), and the 'fpXY'/'spXY' fixed-point family (raw
big-endian 16-bit scaled by 2^-frac, the last type char = fractional
bits) — covering fan RPM ('fpe2', /4) and fixed-point temps ('sp78',
/256). Purely additive: flt/ui8 behave identically (M1 Ultra fans still
read ~3500 RPM, no regression).
Also add an SMC fan-key section to --dump-temps (key, FourCC type,
decoded value) so 'fan reads 0' reports are self-diagnosable. Validated
the new path on real data: F0CR (ui16) now decodes to 38400 where it
previously read 0.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a8554ec. Configure here.
collectAneServices stopped at MAX_ANE_SERVICES via the loop condition, but the && short-circuit had already called IOIteratorNext (returning a +1-refed entry) before the count check failed — leaking that one entry. Only reachable on a chip with more H11ANEIn nodes than the cap of 4 (no current Mac: max 2 ANE dies), but a valid latent leak. Drain the full iterator, releasing entries beyond the cap; stored entries are still released by the caller.
There was a problem hiding this comment.
Gitzilla has reviewed your changes and found 1 potential issue.
Autofix is OFF. To automatically fix reported issues, enable autofix in the Gitzilla dashboard.
|
I tested it specifically against the fan-RPM regression in #78 on the affected hardware. Setup Mac Studio, Apple M2 Ultra; macOS 26.5.1 (25F80) Steps & results The new SMCGetFloatValue path adds fixed-point (fpe2/spXY) decoding, which fixes machines where F*Ac was a fixed-point type previously decoding to 0. On my M2 Ultra, F0Ac is already flt — handled identically by the old and new decoder — and the SMC returns a genuine 0.00. So this is a different failure mode than the one the patch addresses: the value is missing at the source, not mis-decoded. F0Mn/F0Mx (also flt ) decode correctly, confirming SMC access works. |
Fan RPM reads 0 on M2 Ultra (issue #78) even though TG Pro shows it. Root cause is not decoding: F0Ac is type flt and reads 0, while F0Mn/Mx read fine. Apple Silicon fan tach SMC keys are near-arbitrary per-generation FourCCs (iStat/TG Pro keep a hand-curated per-model map; Asahi's macsmc reads the key from the device tree) — F0Ac is the tach on M1 Ultra but an empty/vestigial key on M2 Ultra, whose real RPM is under a different key that can't be guessed. Add a candidate scanner to --dump-temps that prints every SMC key whose decoded value is in a plausible fan-RPM range, so the actual-RPM key on an affected model can be identified (by matching another tool's reading) and then mapped in. Diagnostic only; no behavior change to fan reads.
Enhance dumpSMCFanKeys to surface read result and raw bytes for SMC fan keys. Previously the function printed only the declared type and SMCGetFloatValue, which is ambiguous because SMCGetFloatValue returns 0.0 for both real zero and failed reads. The new code initializes key info, calls SMCReadKey to get a result code and raw bytes, prints a clear read-failed line with the result code, or prints type, size, raw hex and decoded value on success. This makes it easier to distinguish unreadable tach keys (e.g. privileged-only reads on M2 Ultra) from genuine 0 RPM readings.

Note
Medium Risk
Changes core native sampling and UI metric interpretation across many Apple Silicon/OS combinations; regressions could affect displayed ANE/memory/battery values, though behavior is heavily unit-tested.
Overview
v2.1.6 improves how mactop reports ANE, memory, battery, and bandwidth when macOS 27 / M5-class hardware leaves IOReport or energy counters empty (especially without root).
The native sampler (
ioreport.m) adds an IORegistryH11ANEInpower-domain duty fallback, per-die cluster metrics, exclave detection (binary ON/idle), and PMP combined ANE RD+WR bandwidth when split counters are missing. Go layers (metrics.go,app.go,info.go) route gauges, history charts, and the info panel through shared helpers so power-state tiers show ON/idle or powered/idle instead of misleading % or 0.00 W; history_soc can plot dual ANE0/ANE1 traces only when the gauge is on the same power-state tier.Memory used now follows Activity Monitor (anonymous + wired + compressed), fixing collapsed usage when large inactive anonymous pages are present. Battery lines include instantaneous W from
AppleSmartBattery. SMC decoding adds fixed-point types for fan RPM; debug dumps add fan key diagnostics. Headless mode no longer starts the display FPS counter (avoids Screen Recording prompts). Expanded unit tests cover ANE labeling and memory math.Reviewed by Cursor Bugbot for commit 9ce10f9. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Low Risk
Overview
This PR releases mactop v2.1.6, focusing on improved Apple Silicon telemetry robustness when IOReport/PMP data is missing or misleading, particularly on macOS 27 and M5-class hardware without root privileges. Key changes include a substantially rewritten
ioreport.m/ioreport.gosampling path, expanded SMC access viasmc.candbattery.go(notably fan RPM), and refined ANE utilization labeling so the value reflects actual neural engine activity rather than a fallback. The TUI/metrics layer inapp.go,metrics.go, andinfo.gowas restructured to surface these new signals, with corresponding headless export updates inheadless.goand CLI flag tweaks incli.go. Comprehensive unit-test coverage was added inapp_test.go, alongside a minordisplayfpscleanup and a version bump inmactop.rb.Written by Gitzilla for commit 9ce10f9. This will update automatically on new runs. Configure in the Gitzilla dashboard.