Skip to content

Fixes menu-bar strip re-renders at up to 20 Hz and re-applies unchanged images (40-60% CPU on macOS 27 beta) #1039

Description

@xinshoutw

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

  1. Pin a few metrics so the strip renders values
  2. Let the app run and sample ps -p $(pgrep -x OpenUsage) -o cputime= 60s apart during refresh activity
  3. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions