Summary
The menu-bar strip's render loop does more work than the visible content changes justify, and on macOS 27 beta that turns into 40-60% sustained CPU.
Two compounding causes in StatusItemImageUpdater:
update() calls apply(image) unconditionally — even when MenuBarStripRenderer's memo returns the same NSImage instance. Setting the same image on the status button still costs a full status-item redraw round-trip through WindowServer.
- The observation re-render waits only 50ms, so bursty snapshot writes (several per refresh pass) can re-run the loop at up to 20 Hz.
On stable macOS the redundant sets are cheap enough to go unnoticed; the wasted work is the same, just invisible.
Evidence
- 60s cputime deltas on the running app: sustained ~50% CPU (129 CPU-minutes over a 4h20m session on an M1 Max).
- A 3s spindump puts the main thread's cpu time on the
com.apple.CoreGraphics.RemoteContexts queue (strip draws), with NSEventThread pulling the matching WindowServer event storm (167/301 samples in SLEventCreateNextEvent).
- Amplifier, not root cause: with an accessibility-customized cursor color, macOS 26+ re-renders the cursor image in-process on every
NSCursor set — that part is an Apple regression (filing via Feedback Assistant). The redundant strip redraws multiplied it; removing them is a win on any OS.
Steps to Reproduce
- Pin a few metrics so the strip renders values
- Let the app run and sample
ps -p $(pgrep -x OpenUsage) -o cputime= 60s apart during refresh activity
- On macOS 27 beta (26A5388g), especially with any accessibility cursor customization, the sustained burn is immediate
Expected / Actual
Expected: the strip renders when its visible content changes — a few times per refresh at most, near-zero CPU between changes.
Actual: up to 20 render/apply passes per second during observation churn, 40-60% sustained CPU on macOS 27 beta.
Proposed fix
Skip apply when the memoized instance is unchanged, and widen the coalesce window 50ms → 250ms (caps the loop at 4 Hz). Measured on the same machine: ~50% → 3.5%. No visual change. Draft PR: #1038 — happy to adjust scope if you'd rather take only one of the two changes.
Environment
- OpenUsage main @ 842feae (dev build via
script/build_and_run.sh); same behavior on the installed release build (0.7.6)
- macOS 27.0 beta (26A5388g), Apple Silicon (M1 Max)
Summary
The menu-bar strip's render loop does more work than the visible content changes justify, and on macOS 27 beta that turns into 40-60% sustained CPU.
Two compounding causes in
StatusItemImageUpdater:update()callsapply(image)unconditionally — even whenMenuBarStripRenderer's memo returns the sameNSImageinstance. Setting the same image on the status button still costs a full status-item redraw round-trip through WindowServer.On stable macOS the redundant sets are cheap enough to go unnoticed; the wasted work is the same, just invisible.
Evidence
com.apple.CoreGraphics.RemoteContextsqueue (strip draws), withNSEventThreadpulling the matching WindowServer event storm (167/301 samples inSLEventCreateNextEvent).NSCursor set— that part is an Apple regression (filing via Feedback Assistant). The redundant strip redraws multiplied it; removing them is a win on any OS.Steps to Reproduce
ps -p $(pgrep -x OpenUsage) -o cputime=60s apart during refresh activityExpected / Actual
Expected: the strip renders when its visible content changes — a few times per refresh at most, near-zero CPU between changes.
Actual: up to 20 render/apply passes per second during observation churn, 40-60% sustained CPU on macOS 27 beta.
Proposed fix
Skip
applywhen the memoized instance is unchanged, and widen the coalesce window 50ms → 250ms (caps the loop at 4 Hz). Measured on the same machine: ~50% → 3.5%. No visual change. Draft PR: #1038 — happy to adjust scope if you'd rather take only one of the two changes.Environment
script/build_and_run.sh); same behavior on the installed release build (0.7.6)