Focus App CPU Bug on Dual-Monitor Mac: Root Cause & Fix

The Problem

The Focus productivity timer (com.ftustudio.focustime v1.6) was consuming ~55% CPU sustained on a dual-monitor Mac, while running at 0% on an identical single-monitor setup.

Root Cause

Focus uses the deprecated NSStatusItem.view API with a custom StatusItemView containing a CAShapeLayer (circular progress indicator). On dual-display setups with "Displays have separate Spaces" enabled, macOS replicates the menu bar across both screens. This triggers an infinite redraw loop:

NSStatusBarButtonCell drawWithFrame:inView:
  → NSStatusItemScene updateSettings:transition:
    → _windowNeedsReplicantUpdate:
      → performSelector:withObject:afterDelay:0
        → (immediate redraw, loop repeats)

Each redraw causes a replicant update via XPC to FrontBoardServices, which triggers another redraw — an infinite feedback loop that pegs Focus at 50-60% CPU and WindowServer at ~52%.

Key evidence: disconnecting the second monitor → 0% CPU instantly. Reconnecting → immediate spike back to 55%.

Workaround Tried: Background QOS Throttle

Before finding the root cause, we deployed a launch agent that auto-throttles Focus:

taskpolicy -b -p <FOCUS_PID>   # background QOS class
renice 20 -p <FOCUS_PID>        # lowest scheduling priority

This reduced CPU from 55% to ~4% — usable but not a real fix. The daemon (~/.local/bin/throttle-focus.sh) polls every 5 seconds and auto-applies throttling when Focus launches.

Solution

System Settings → Desktop & Dock → Mission Control → disable "Displays have separate Spaces"

This eliminates menu bar replication entirely. Without replication, the _windowNeedsReplicantUpdate: feedback loop never triggers. Result: 0% CPU.

Trade-off: you lose per-display Spaces (fullscreen apps can span both monitors, Dock shows on all displays). For most workflows this is fine.

Debugging Notes

  • Binary hashes matched between affected and healthy machines — not a corrupted install
  • Fresh reinstall reproduced the bug — not a preferences issue
  • Profiled with sample <PID> 5 to identify the hot path in NSStatusBar drawing code
  • The bug is entirely in Focus's use of the deprecated NSStatusItem.view; modern apps using NSStatusItem.button are unaffected