bench: agent chat click-to-first-content, idle and under load

Merged
TE
7d ago
+699-0

Leo, 2026-10-29: "i also like the way it just loads in the subagent stuff instantly :(" / "as in i want ours to load fast too :(" / "but i also wonder how much of this is simply putting the desktop at higher priority?"

A fleet bench for click to first content when opening an agent chat, so the fixes for that ask start from a measurement and every fix PR carries a before/after from the same harness. No product code changes.

scripts/cmux-next/chat-open-bench.py times each open with the app's own chat-open trace (debug.chat_open_trace): milliseconds from the action to the first frame showing the chat's content, the longest empty run (cover or unpainted page), flashes (a return to an empty surface after content), each stage mark, the page's own timing and the new page's acpmux wire log (acp_log, which requests the open waited on).

Cases:

  • sidebar: a chat this app never opened, in a new workspace (agent.openSessionWorkspace, focused)
  • reopen: a chat this app showed before, opened again
  • subagent: a click on a subagent's line, which opens its pane (chat.subagentPane)
  • big: a chat of 3,000 turns
  • bloated: a chat whose acpmux log is mostly tool_call_update records that each re-send the whole output so far. One Codex log on Leo's Mac is 152 MB this way: 27,564 updates for 219 calls, and 123 MB of it is cumulative content.
  • restore: the app quit (quitKeepSessions) and launched again on the same state, timed from process start by a launch trace. The saved launch image is reported separately from the first live content.

Each case runs under four conditions, so the priority share separates from the app's own path (Leo's question):

  • idle
  • load: the QoS lane's agent-like load at default priority (CPU spinners on every core, disk writers, metadata walks and readers)
  • load_utility: the same load at utility QoS, CPU and disk (taskpolicy -c utility -d utility, the agent tier)
  • load_nice: the same load at nice 20. An unprivileged script cannot raise the app itself, so this raises the app relative to the load instead.

What survives the load once it is demoted is the app's own architecture: round trips, replay and render.

The chats come from a seeding acpmux daemon (idle_cost_load.PeerDaemon) on scripts/cmux-next/chat_open_agent.py, which is acpmux's test agent with realistic turn:, subagents: and bloat: prompts. Each template, written by acpmux itself, is copied into the app's store under fresh ids, so every cold open is a chat no page has read. The store also gets 1,500 filler chats (60% closed), so the list is as long as a long-used daemon's.

scripts/measure/chat-open.sh builds the tagged app in the bench configuration and runs the bench on an exclusive worker:
cmux-ci run --class exclusive --label measure --script scripts/measure/chat-open.sh --ref <sha> --artifact artifacts/chat-open.json --artifact artifacts/chat-open.md

The controller runs an unlisted measure script only at a commit on feat-cmux-next, so the baseline numbers come in the first fix PR's Performance table once this lands.

The bench kills only what it started: the app through bench_memory's bundle-scoped teardown, the seeding daemon, and the load's own process groups. Every wait on the app has a deadline. The load stops in a finally.

Performance

No perf numbers: this adds the bench only and changes no product code. Its first numbers, before and after, go in the first fix PR's Performance table (#27759). manaflow-ai/cmuxterm-hq#5219 lists scripts/measure/chat-open.sh, so after this lands the bench also runs at a fix PR's head.

Testing

  • Seeding checked against a scratch acpmux daemon (acpmux 1.0.0, 6ce9c0a7e). turn: takes 44 ms a turn on a loaded Mac (load 186). subagents: 3 logs three subagents with their tool calls and a completed state. bloat: logs the expected update count.
  • python3 -m py_compile on both scripts.
  • Not yet run end to end on a fleet worker; that needs this commit on feat-cmux-next (see above).

šŸ¤– Generated with Claude Code

Agent session: agent session


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Adds a fleet bench for click-to-first-content when opening an agent chat, idle and under load, so the fixes for that slow open (and Leo's question about how much of it is CPU priority) start from a measurement. No product code changes.

Bench

  • chat-open-bench.py times each open with the app's own chat-open trace: milliseconds to the first content frame, the longest empty run, flashes back to an empty surface, stage marks, and the page's acpmux wire log.
  • Cases: a first open in a new workspace, a reopen, a subagent's pane, a 3,000-turn chat, a chat with a bloated log (tool_call_update records that each re-send the whole output), and a restored launch after quitKeepSessions.
  • Each case runs idle, under an agent-like load at default priority, at utility QoS, and at nice 20, so the share that is priority separates from the app's own round trips, replay, and render.
  • Every condition starts from a wiped app state, so a later condition's opens and restore don't carry the earlier ones' workspaces.
  • A case with any open missing a first-content or longest-empty time is reported FAILED (k/N valid), never a median over the survivors, and the bench exits non-zero after writing its outputs; the summary lives in chat_open_summary.py, covered by unit tests.
  • Chats come from a seeding acpmux daemon, copied into the store under fresh ids, plus 1,500 filler chats; chat-open.sh builds the tagged app and runs the bench on an exclusive worker via cmux-ci run.
  • The bench kills only processes it started and deadlines every wait on the app.

Not yet run end to end on a fleet worker: the controller runs the unlisted measure script only at a commit on feat-cmux-next, so the baseline numbers land with the first fix PR's Performance table.