cmux NEXT: host pressure indicator and popover in the top row, off by default (part 2)
cmux NEXT gets a host pressure indicator in the window's top row, after the toolbar band and the incognito badge, with a popover. It's part 2 of cmuxterm-hq#2995, on the monitor that #24298 landed. While the host is normal it shows nothing. At warning or critical it shows a still glyph in the theme's attention color, with no pulse or animation. Clicking it opens a popover with the load against its limit, acpmux's other reasons, its turns and processes, its busiest sessions with Stop (it asks first, and the session stays to resume), and Open Agent Activity.
It ships off by default behind Debug toggle hostPressure.enabled (cmux#22662). Release and RC never activate the tunable store, so they keep it off.
coord-deputy's order, quoted:
"the indicator plus popover, with one 15 s timer that stops when the window is hidden and states its cleanup."
"Split HostBudget.swift for the god-file warning."
"It's a new dev surface, so both ship off by default behind a Debug/Labs toggle (cmux#22662)."
"Codex review at the exact head, green compile, before/after numbers for the timer's cost."
Off means inert
With hostPressure.enabled off (the default), the indicator never creates HostPressureService. That means no monitor, no memory pressure source, no 15 s timer and no sysctl or acpmux reads. The only thing it keeps is an Observation of that one tunable key, which is event-driven and costs no wakeups. HostPressureForce (the hostPressure.force Debug Settings choice for captures) does nothing while the toggle is off.
The timer: stop condition and cleanup
There is one 15 s timer (2 s leeway) for the whole app: HostPressureMonitor's DispatchSourceTimer, not one per window. It runs only while at least one window's indicator is subscribed. An indicator is subscribed while HostPressureSampling.samples is true, meaning the toggle is on and its window can be seen: the app isn't hidden, and the window is isVisible, not minimized, and occlusionState.contains(.visible).
- Stop condition: an indicator unsubscribes when the toggle goes off, or when its window hides (app hidden or another Space), minimizes, is fully occluded or closes. It hears about these through
didChangeOcclusionState,didMiniaturize,didDeminiaturize,willClose, the app'sdidHideanddidUnhide, and the toggle's Observation. When the last indicator unsubscribes,HostPressureServicecallsHostPressureMonitor.stop(). App quit calls it too. - Cleanup:
stop()cancels the memory pressure source, the timer and any read in flight, and drops a level change that is still queued. Its window and app notifications (throughNotificationCenter.notifications), the toggle Observation and the level Observation are tasks held inHostPressureTasksbags, and each body returns at once if its task was cancelled. A window change,willCloseand the nonisolated deinit cancel them on whatever thread they run, and the indicator unsubscribes. Its subscription is aHostPressureSubscriptiontoken: if the token is released withoutcancel()(an indicator freed off the main thread), it unsubscribes on the main actor, so a window never leaves a subscriber that would keep the timer running (aSubscriptionReleasedWithoutCancelStillUnsubscribes). plans/cmux-next/idle-wakeups.mdhas the same row.HostPressureIndicatorTestscovers the stop condition.theToggleShipsOffAndOffNeverMakesTheServicechecks that the default is off and that, with the toggle off and the window visible, the indicator never creates a service.itSamplesOnlyWhileOnAndVisiblechecks that with the toggle on and the window visible the monitor runs, that it stops when the window becomes invisible, that it runs again when the window is visible again, and that it stops when the toggle goes off.theGateIsTheToggleAndTheWindowchecks the gate's truth table.
Timer cost (one harness, fleet mini)
scripts/cmux-next/bench_idle.py on fleet mini cmux20-bench (M-series, 12 cores, macOS 27, logged-in console). Each run is one tagged DEV build launched clean, with 30 s settle and then 300 s measured per scenario. The app's CPU and wakeups come from proc_pid_rusage at the start and end of each window. Base is c7e203d361 and head is 9a9b4d7dc8, each with one measurement-only commit (an hp-on scenario that sets hostPressure.enabled through debug.tunables; branches app-eff/hpi-bench-base and app-eff/hpi-bench, not for merge). The later review fixes change when a window subscribes and guard the start, not the timer's interval.
| build | scenario | app CPU % | app wakeups/s | app wakeups/min |
|---|---|---|---|---|
| base | window visible (no indicator code) | 0.74 | 251.8 | 15,109 |
| base | window minimized | 0.66 | 280.1 | 16,807 |
| head | toggle off (default), window visible | 1.13 | 251.6 | 15,098 |
| head | toggle on, window visible | 0.65 | 284.4 | 17,065 |
| head | toggle on, window minimized | 0.12 | 5.1 | 304 |
What this shows: off = no change. Head with the toggle off measures 251.6/s against base's 251.8/s in the same scenario. The timer's expected cost when on is 4 wakeups/min (0.07/s), which is far below this harness's run-to-run noise. Base alone moves 28/s between its two scenarios, and almost all of the app's ~250/s idle wakeups come from outside this code (the WakeupLedger shows CEFMessagePump at about 0.7/s and BusyWatchdog at 0.1/s; the rest isn't attributed). So the on-visible (+33/s) and minimized (-275/s) rows are noise around a large baseline, not this timer. The timer's cost and its stop come from the code and its tests instead: one DispatchSourceTimer at 15 s with 2 s leeway, so 4 fires/min while running. itSamplesOnlyWhileOnAndVisible shows isMonitoring going false (timer cancelled) on hide and on toggle-off, and theToggleShipsOffAndOffNeverMakesTheService shows no service, and so no timer, while off.
Size
About 1,050 changed lines, over the ~800 target. The popover, indicator, service and their tests are one feature behind one toggle, and the parts that could stand alone (the HostBudget.swift split and the 21-locale catalog) already landed as #24506 and #24508.
Split into PRs that each land on their own
- This PR: the indicator, popover, service, Debug toggle, tests and tour, with English text as the
String(localized:)default values. - #24506 (merged as
effb5e4019):HostBudget.swiftsplit in two (6 top-level types, limit 3), with no behaviour change. - #24508 (merged as
97377b197b):HostPressure.xcstringsin all 21 locales. It comes whole rather than as non-English only, because the localization debt ratchet fails a catalog that has English but lacks the required locales.
The Codex review took six passes. They found and fixed these: app hide wasn't observed; an off-main deinit left observers, tasks or a subscriber running (fixed with HostPressureTasks and HostPressureSubscription); a start hop overtaken by the last leave still started the timer (fixed with HostPressureMonitor.start(while:), which checks on the monitor actor before it builds the timer, so this PR touches the part-1 library by 5 lines); a callback queued before cancellation could still resubscribe; a newly shown indicator didn't get laid out; and a forced level's sample sessions showed a Stop that did nothing (now hidden). The tests await the monitor's own isRunning (aStartOvertakenByTheLastLeaveStartsNothing, aStartNoLongerWantedStartsNothing, windowAndAppNotificationsStartAndStopSampling with occlusion, hide, unhide and close posted to an injected center). This PR also fixes a compiler crash on the branch ("failed to produce diagnostic" at HostPressureService.swift:53). The acpmux reader is now a plain closure instead of Optional.map returning a closure.
Observed / Not run
- Ran:
prepush-gate.py(no new violations),cmux-ci compile-check --appand theHostPressureIndicatorTests,HostPressureMonitorTests,HostPressureTrackerTestssuites on the fleet at the head (results in comments), and the idle bench above. - Not run: a capture of the popover in this description. The
host-pressuretour (scripts/cmux-next/media/tours/host-pressure.json) now turns the toggle on first.
No capture: a Debug-only surface that is off by default (hostPressure.enabled); the host-pressure tour drives it for design review.
Takes back #24298: only its plans/cmux-next/idle-wakeups.md row, which part 1 wrote and this part 2 extends with the toggle and window stop condition, and the 5-line HostPressureMonitor.start(while:) guard; part 1's behaviour is otherwise unchanged.
Fast gates note: its Sidebar-Look finding names 163 CmuxNextSidebar/Views files this PR never touches. The job checks out at --depth=2, so the head commit's parent is missing, and git diff-tree --root lists the whole tree as that one commit's change. It's a shallow-checkout artifact of the gate, not this diff.
Leo-ask: ask-427cc7d8f5
Changelog
none (Debug-only, off by default)
Agent session: cosmic-gray
🤖 Generated with Claude Code