acpmux: start agent harnesses at background CPU class on macOS, so cmux and its renderers go first

Merged
TE
7d ago
+389-16

Leo (2026-10-28): "Likewise, holy hell, the resizing of the Chromium elements. ... It's still the exact same as it was, like, 24 hours ago, where it's just slow as hell."

Reference: cmuxterm-hq#4947 (private).

Cause, measured. The drag lag is CPU contention, not cmux code. On a fleet bench Mac with 14 busy processes standing in for agents, a Chromium page trails its dragged pane by up to 55 ms at p95. With the same load at background class (taskpolicy -b), it trails by 6-10 ms, and cmux's main thread spends about 45 % less CPU per drag. Profiling agrees: cmux's main thread sat idle in mach_msg in 44 of 51 samples. Agent harnesses, and the builds and tests they run, compete with cmux and its renderers at the same priority. acpmux lowered them only after a session passed its CPU budget.

Fix. On macOS each session's harness now starts with PRIO_DARWIN_BG, the class cmux-git already gives its git children (cmux-git/src/run.rs). The class is set in the harness between fork and exec, both for a direct child (agent.rs) and under an agent host (SpawnSpec.cpu_class, which the daemon decides). So the harness and every tool it starts inherit it. No running process is reniced.

  • Configurable (cpu_class.rs). The first source that sets it wins:
    1. A profile's env ACPMUX_SESSION_CPU_CLASS (normal or background), so one build-heavy profile can opt out.
    2. The daemon's env ACPMUX_SESSION_CPU_CLASS.
    3. Config session.cpuClass (acpmux config --set session.cpu_class=normal).
    4. Otherwise background.
  • Why background, not utility or nice. Utility, nice -n 10 and nice -n 20 gave no steady gain over the default class. On the heavy page they dropped 23-26, 6-36 and 19-32 frames per drag; background dropped 0-7. Only background keeps the agents off the cores the GUI needs.
  • No raise for the focused chat. The daemon knows which chats a client shows (hub/seen.rs). Raising one again would mean renicing its running process group, and children it already started would stay low. So every session starts at background.
  • Not touched: the CPU and memory budget enforcement, the Linux cgroup paths (Linux reads the class and ignores it), and cmux's own app and renderer processes.
  • Compatibility: a spawn spec from an older daemon has no cpu_class and starts its harness as before.

Performance

Harness: scripts/cmux-next/split-resize-bench.py through scripts/measure/split-spring.sh on a fleet bench Mac (bench/resize-harness-tip), --pages=probe,heavy --drag-hz=120 --cpu-load=14 --cpu-load-class=default|background|utility. All runs were on the 60 Hz display. The busy processes at a class stand in for agents at that class. The heavy page is shown, as the median of 4 runs per class (fleet steps listed below).

metricbefore (load at default class)after (load at background class)deltaharness or run link
page trails pane, p95 ms (heavy page)23.0 (runs: 55.2, 23.1, 22.9, 9.7)7.8 (runs: 6.0, 9.7, 7.9, 7.7)-66 %split-resize, steps below
page frames dropped per drag (heavy)13.5 (22, 14, 13, 13)1.5 (7, 1, 2, 1)-89 %same
page frame p95 ms (heavy)42.7 (50.3, 66.7, 33.4, 35.1)17.7 (33.3, 16.8, 18.6, 16.8)-59 %same
cmux main-thread CPU ms per drag (heavy)245 (250, 225, 240, 258)134 (155, 140, 128, 128)-45 %same
busiest renderer CPU ms per drag (heavy)448 (448, 425, 448, 460)359 (368, 365, 353, 325)-20 %same
agent turn running a cold crate build, s (through acpmux, idle M4 Mac)18.5 (18.4-19.3)115.8 (113.1-125.2)+525 %bench=build-turn, cmux-ci step a898e4fd0e06d5836335fd34
the same build run directly, s (default / utility / background)17.8119.6 (background; utility 18.0)+572 %same step, ACPMUX-BUILD-RAW lines

The build rows come from scripts/measure/acpmux-overhead.sh bench=build-turn (#27582): Claude Code through acpmux runs one Bash call per turn, a cold debug build of cmux-history in a fresh target dir, with before= at base. The build shell's base priority was 20 before and 4 after.

Runs 5 and 6 of the same harness, heavy page, page trails p95 ms / dropped / main ms (default, nice 10, nice 20, background):

  • default: 117.3 / 49 / 260 and 9.7 / 23 / 250
  • nice 10: 8.4 / 6 / 245 and 23.4 / 36 / 258
  • nice 20: 43.0 / 32 / 240 and 10.5 / 19 / 235
  • background: 9.7 / 0 / 125 and 6.2 / 2 / 113

These are on a scratch copy of the harness that adds --cpu-load-class=nice10|nice20.

Steps (default / background), run 1: 33cca097f4eca66b198f1e00 / 6e0128696605b93a94c61c9a; run 2: 54bbedbce6966501aad74092 / 49b59f0fa56e39d5f04e59f4; run 3: a295cbda82f1726c6d8fae0a / ac79343ddd56d8e24ece5b43; run 4: 6faf28c898ae1741ad13db10 / 2d4ab680341ccb8b6e609c5d. Utility: 8da2362168f642090f9629ee, 9c51a10f9c6ffa1e99cc3bdc. Runs 5-6 (default, nice 10, nice 20, background): 55bd43c2125b46652a0d4648, b2136d2a00ac7f2d62080e85, d80f174564b92f273602f725, 2c0cf1728c3fc31bf127044c; 22430ef0f1dc008153ce0658, 80fd10b30e1725dd44c6a7df, 09e1c94720313ec88204cd55, 854796bcc2d9540f06065267.

Tests (red, then green).

  • The red commit 2b4a97f0 adds tests/cpu_class.rs, which is macOS only. A direct harness and a hosted one each start a background sleep, and the test reads both processes' base priority with ps -o pri= (4 is darwinbg). The two background tests failed on a fleet Mac in step 449b586ce3a30d6dfe904411.
  • At this head they pass, with the normal opt-out test, a ${env:...} reference that resolves to normal, the resolve-order unit test and the config round trip (step fcf491e6fe72a06b1b5c24dd, macOS). macOS clippy is green (0f69df34c5b3bd6abe07c1cf), and so are Linux fmt, clippy and tests (b273610db33137ec07df61c1).

Risk: medium.

  • The cost is real. Background class confines an agent's processes to the efficiency cores and throttles their disk I/O, even on an idle Mac. A cold build inside an agent turn took 6.3x as long (18.5 s to 115.8 s on an M4). Agents on interactive Macs are meant to send builds to the fleet, so most local agent work is light, but a lane that builds locally pays this.
  • The opt-out is one env key per profile (ACPMUX_SESSION_CPU_CLASS=normal, for a build-heavy lane), or one config value for the host (acpmux config --set session.cpu_class=normal).
  • macOS only. Linux and Windows spawn as before.

Agent session: agent session

šŸ¤– Generated with Claude Code