idle-chats bench: name the shown renderer's idle work, and count a footprint read as 0 as unread
Stop condition (perf coordinator): "cmux NIGHTLY idles under 5% CPU per process with about 40 chats open, measured with the shared perf harness". Data: manaflow-ai/cmuxterm-hq#6338 (comment 6604253564).
The shared idle bench (perf-idle-power.sh, idle-chats, display worker) read the shown agent page's renderer at 5.27% CPU at c004201, and the tree's footprint at 7.3 GB before and 13.8 GB after. This PR gives the bench what it needed to name both, and fixes the footprint read that made the memory jump look real.
What the bench now records (after the measured window, so the window is unchanged)
idle_probe: for 3.5 s, what the shown chat's page asks for, by call site: animation frames, timeouts, intervals, DOM mutations, running animations, long animation frames (debug.agent_pane idle_probe,webviews/src/agent-session/acpmux/idleProbe.ts). The wrappers are restored when the window ends.idle_profile: for each shown cmux page, Chromium's page metrics per second (task, script, style, layout) and a 10 s V8 CPU profile by function, throughdebug.cef_page_devtools {browser, method, params}(new, bench and DEBUG builds only, cmux's own pages only; browser tabs are refused).debug.cef_pagesnow also gives a cmux page's path.native_samples:sampleof each renderer over 1% CPU (main-thread call tree and heaviest frames).- Footprint: a live process never has 0 MB. A 0 is a failed read, from a renderer that exits during the window, so it is now
Noneand counted infootprint_unread. Before this change it was summed as 0.
What it found
- No periodic work in the shown pane. Same harness on four display workers: the shown chat's page makes 0 animation frames, 0 timeouts, 0 intervals and runs 0 animations in 3.5 s, with at most one timestamp mutation a minute. Every shown cmux page reads 0 ms of Blink tasks in 10 s and an idle V8 profile, both just before and just after the window. A native sample of the busy renderer after the window shows its main thread waiting in
mach_msgfor all 8 s. The 2-5% the window reads is work right after the 40-chat load that stops on its own: atsettle=420instead of 60, the shown agent renderer is at 0.05%. So there is no loop to stop in the pane code. - The memory jump is not a regression. In the 6500d46 runs, 24 of 46 (one host) and 16 of 47 (another host) renderers read 0 MB and no main thread. These renderers exited inside the window, and they were summed as 0. At c004201 the same exits happen after the window. On the same host, 6500d46 read 9.2 GB with 16 renderers unread, and c004201 read 14.3 GB with none unread. With the unread ones at the mean of the rest, 6500d46 is about 13 GB. At
settle=420, once acpmux has hibernated the idle agents (2 min) and their hidden chat pages have gone, the whole tree is at 2.6 GB.
Performance
Same harness, perf-idle-power.sh --arg=scenarios=idle-chats --arg=config=bench --display, 40 local chats, 120 s window, two Apple M4 Pro (Mac16,11) display workers, at ef35ed9 (this branch plus a diagnostic profile before the window, since dropped):
| metric | settle 60 s | settle 420 s | delta | harness |
|---|---|---|---|---|
| shown agent renderer CPU % | 2.37 | 0.05 | -2.32 | idle-chats, display |
| busiest process CPU % | 2.83 (app) | 0.24 (app) | -2.59 | same |
| total CPU % (all processes) | 18.32 | 0.55 | -17.77 | same |
| tree footprint MB | 14158 | 2593 | -11565 | same |
| processes | 101 | 23 | -78 | same |
| renderer footprints unread | 0 | 0 | 0 | same |
And the footprint read on the 6500d46/c004201 pair (one host, before this fix):
| metric | 6500d46 | c004201 | harness |
|---|---|---|---|
| renderers that read 0 MB (exited inside the window) | 16 of 47 | 0 of 47 | idle-chats, display, one host |
| tree footprint as summed | 9152 MB | 14260 MB | same |
| mean footprint of renderers that read | 230 MB | 259 MB | same |
Stop and cleanup: no timer, observer or loop is added to the app. idleProbe restores requestAnimationFrame, setTimeout and setInterval, disconnects its MutationObserver and PerformanceObserver when its window ends (at most 30 s), and runs only when the debug socket asks. The bench turns off the Profiler and Performance domains after its window.
Red/green
python3 -m unittest scripts/cmux-next/tests/test_bench_power_loaded.py (macOS; on Linux with libproc stubbed):
test_footprint_read_as_zero_is_unread_not_zero: at baseAssertionError: 0.0 is not None; passes at head.test_profile_summary_self_time_by_function: at baseAttributeError: module 'bench_power' has no attribute 'profile_summary'; passes at head.
All 12 tests in the file pass at head.
bun test test/agent-pane-idle-probe.test.ts (webviews): "a second probe while one runs is refused and the first still restores" fails on the probe before the review fix (the second probe ran and returned a result) and passes at head; "a probe counts timeouts by call site and restores the page's functions" passes.
Review follow-ups from the first Codex receipt, fixed here: a probe started while another runs is refused (it used to keep the first one's wrappers installed), and debug.cef_page_devtools reads the browser id with Int32(exactly:) instead of trapping.
Risk: low. The Swift adds two debug socket methods under #if DEBUG || CMUX_BENCH_HOOKS (debug.cef_page_devtools, debug.agent_pane idle_probe) and a path field in debug.cef_pages, given only for cmux-page:// pages. Shipped builds have none of them. The bench spends about 35 s more after its window. A Release build's page includes idleProbe.ts, but nothing calls it.
Observed / Not run
- Ran: the idle-chats bench at this branch's intermediate heads on four display workers (probe, profile and sample runs),
settle=420at the final tree, the bench unit tests (red at base, green at head), CmuxNext compile checks at 687f2e7 and the final head,scripts/web-lint.sh(the only failure ischeck:design-tokensonchief-web.css, which is red on base b0b87e8 too). - Not run: a
settle=420pair at 6500d46 and c004201 on one host (queued; numbers go on #6338). A NIGHTLY with Leo's real chats: his 95% readings need long transcripts, remote peers and browser tabs, which this load lacks.
Changelog
- none
Agent session: agent session
š¤ Generated with Claude Code