Skip to content

Cut idle CPU from ~15% to ~0%#1

Merged
spitfiresb merged 2 commits into
mainfrom
perf/idle-drain
Jul 22, 2026
Merged

Cut idle CPU from ~15% to ~0%#1
spitfiresb merged 2 commits into
mainfrom
perf/idle-drain

Conversation

@spitfiresb

Copy link
Copy Markdown
Owner

The problem

The notch was burning 11–19% CPU sitting completely idle, with a matching hit to battery life. Sampling the running process found five always-on loops doing work nobody could see:

  1. A system-wide CoreAudio tap ran 24/7 from launch. audioMeter.start() was called unconditionally, so a realtime IO thread filtered every audio sample through six biquads forever — even with nothing playing and the notch collapsed. It also kept the audio stack from power-idling, which is the real battery cost.
  2. A now-playing poll fired a synchronous AppleScript round-trip to Spotify every 3s on the main thread. MediaRemote is locked down on recent macOS, so the Spotify fallback path ran constantly — ~3% CPU on its own.
  3. The 60 Hz meter tick reassigned @Published bars unconditionally, so silence still invalidated and re-rendered the whole notch 60 times a second.
  4. A 16 Hz hover poll read the cursor position forever, whether or not the mouse moved.
  5. A 0.15s window-list poll hit WindowServer 6.7×/sec to detect Mission Control.

The fix

  • Audio tap follows playback. Starts on play, torn down 2s after pause (delay keeps track-skips from churning CoreAudio device setup). Bars freeze when paused anyway, so nothing is lost visually.
  • Spotify updates are event-driven. Listen for PlaybackStateChanged instead of polling; a 15s poll stays as a recovery net.
  • Publish only on change. The meter tick skips the assignment when no bar has visibly moved, so silence stops driving renders.
  • Hover is event-driven. Mouse-moved monitors replace the poll, plus a 0.5s safety tick for toast-expiry edge cases.
  • Overlay poll slowed 0.15s → 0.4s; the existing occlusion notification remains the fast path for detecting Mission Control opening.
  • Fallback wiggle capped at 30fps instead of running at 120 Hz display refresh.

Results

Measured on the running app with ps/sample:

Build Idle CPU
Before ~11–19% sustained
After audio + render + poll fixes ~3%
After event-driven Spotify ~0.9% avg, ~0% at true idle

A 10-second sample of the final build catches essentially nothing on-CPU — no AudioMeter.process, no SpotifyBridge, no renderer frames. Behavior is unchanged: play/pause and track changes still update instantly, and the bars spin up on play.

🤖 Generated with Claude Code

https://claude.ai/code/session_01V66hekkNTU1kraQvcYMZjN

spitfiresb and others added 2 commits July 22, 2026 13:40
… re-renders

- Start the CoreAudio system tap only while music is actually playing
  (2s delayed teardown on pause); previously the tap + realtime IO
  thread ran six biquads over every sample 24/7 from launch.
- Skip the 60Hz bars publish when levels haven't visibly moved, so
  silence no longer drives SwiftUI re-renders of the whole notch.
- Replace the 16Hz hover poll with mouse-moved event monitors plus a
  0.5s safety tick.
- Slow the CGWindowListCopyWindowInfo overlay poll from 0.15s to 0.4s;
  the occlusion watcher remains the fast open-detection path.
- Cap the fallback dancing-bars wiggle at 30fps instead of display
  refresh.

Idle CPU drops from ~11% sustained to ~0.1-0.2%.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V66hekkNTU1kraQvcYMZjN
…poll

MediaRemote is locked down on recent macOS, so every 3s poll ran a
synchronous AppleScript round-trip to Spotify on the main thread —
~3% steady CPU even after the audio-tap gating. Listen for Spotify's
PlaybackStateChanged distributed notification instead and keep only a
15s recovery poll.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V66hekkNTU1kraQvcYMZjN
@spitfiresb
spitfiresb merged commit 44252dd into main Jul 22, 2026
@spitfiresb
spitfiresb deleted the perf/idle-drain branch July 22, 2026 20:59
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