measure: relaunch to the first live chat event (relaunch-reconnect bench)
Leo, 2026-11-04: "i feel like the restarted after nightly update does too many layout shifts/flicker renderings and takes too long to reconnect acpmux or whatever". This is the reconnect half (data: manaflow-ai/cmuxterm-hq#6338); restore layout shifts are a separate PR.
What this adds
A relaunch-reconnect scenario in the shared perf harness, beside idle-chats: bench_power.py --scenarios relaunch-reconnect (scripts/cmux-next/bench_relaunch_reconnect.py), run on the fleet through the existing entry:
cmux-ci run --class exclusive --measurement --display --label measure \
--script scripts/measure/perf-idle-power.sh --ref SHA --arg=scenarios=relaunch-reconnect \
--arg=config=bench --artifact artifacts/perf-idle-power.json
It opens 6 local agent chats on the app's own acpmux (the test agent), one streaming 10 words a second and shown. Then it quits (Quit and Keep Sessions) and relaunches 3 times in each mode:
- warm: the daemon keeps running.
- update: the bundle is replaced in place while the app is gone, as an update does.
- cold: the daemon is stopped.
Each round records every phase, in ms since the relaunched process started, from process start to the shown chat's first live event.
Marks added for it:
- App launch marks (
debug.timings), for the acpmux lookup (start, build homes adopted, status answered, bundled build read, handoff, spawn start/ready, end) and the pane's transport open (start, listener checked, accepted checked, open). A lookup of a daemon the app does not start is markedacpmux.other.*.AgentPaneLaunchTimingsnow takes marks from any thread, since the lookup runs on the host actor, the wayDaemonLaunchTimingsalready does. process_start_epoch_msindebug.timings, so page marks line up with the launch marks.- The page's own wall-clock connect path (
perf.tslaunchTimeline, read bydebug.agent_pane perf_statsaslaunch): first handshake start, handshake answered, connect start, connected, first event of the shown chat, and first live event. A live event is one that arrives within 1 s of when acpmux recorded it, so a replayed backlog does not count.
The daemon log since the quit is kept per round.
What it found
These are medians, ms since the relaunched process started. Same harness, Mac16,11 display worker, at e8e06a63034 (3 rounds per mode):
| phase | warm | update | cold |
|---|---|---|---|
| app main | 1563 | 4387 | 1546 |
| did finish launching | 1953 | 4823 | 1954 |
| acpmux lookup start | 2005 | 4909 | 2023 |
| daemon status answered | 2007 | 5232 | 2418 |
| daemon ready (log) | 2173 | 4961 | 2077 |
| lookup end | 2175 | 4965 | 2079 |
| pane view created | 1916 | 4784 | 1915 |
| page handshake start | 2920 | 5793 | 2911 |
| handshake answered | 3087 | 5959 | 3066 |
| transport open | 3083 | 5956 | 3078 |
| page connected | 3145 | 6026 | 3134 |
| pane painted | 3329 | 6164 | 3245 |
| first live event | 3238 | 126164 (1 of 3 rounds; 2 never within 120 s) | never within 120 s (0 of 3) |
- The slowest phase by far is on the daemon side. When the daemon is gone before the app relaunches, the adopted streaming turn shows nothing new for about 120 s, or not at all within the bench's 120 s. Its log shows the old daemon stopping on its own when the bundle is replaced (
SIGTERM (a restart handoff)) or for the cold mode, and the next one starting about 6 s later. 120 s is acpmux's adoption replay ceiling (hub/hosts.rsREPLAY_CEILING). After that, the next quit's daemon finds no turn to keep it ("nothing used the daemon for 2s: no client, agent, turn or task"). The fix is a separate PR with a red/green test. - Warm relaunch: about 1.7 s from process start to the first live event after app main.
- The app's lookup is quick (status in 2 ms).
- The longest app-side phase is pane view created to page handshake start: about 1.0 s of page load before the page asks for anything.
- The handshake itself takes about 170 ms.
- Connect is about 60 ms.
- A warm relaunch restarts the daemon. The relaunched app asks the running daemon (same build) to stop ("stopping: a client asked it to") and starts a new one. This is the person-key enroll's handoff (
AcpmuxPersonKey.enroll). It costs about 100 ms here, but every adopted agent goes through adoption again. This is a follow-up. - Update mode spends about 2.8 s more before
main. This is the first launch of a new copy of the bundle, which macOS checks. It is outside the app.
Performance
This PR changes no behavior. Its numbers are the baseline above. Its own cost is a handful of timestamps, each recorded once per launch or once per page, plus one Date.now() compare per event of the shown chat until its first live event.
| metric | before | after | delta | harness |
|---|---|---|---|---|
| relaunch phases measured | 0 | 21 per round | +21 | relaunch-reconnect, Mac16,11 |
Red/green
bun test test/agent-pane-launch-timeline.test.ts(webviews): at base it fails withTypeError: perf.markLaunch is not a function. Both tests pass at head.- Bench script: ran end to end at a2e1444f3fb and e8e06a63034 on fleet display workers (above).
Risk: low. The Swift adds launch marks (signpost events plus a first-time-per-name mark) on the acpmux lookup and transport open paths. AgentPaneLaunchTimings becomes a Sendable class with a lock, the same shape as DaemonLaunchTimings. The page marks are stored once per page. bench_memory.launch can relaunch into an earlier launch's scratch; its default path is unchanged.
Observed / Not run
- Ran:
- The bench twice on fleet display workers.
- CmuxNext compile check at e788a259eac and e8e06a63034 (passed).
- The new bun test.
scripts/web-lint.sh: its only failures arecheck:design-tokens(subagents.css) andvp fmt --check(cloudRun/cloudStartProgress), all in files this PR does not touch, red on base 660552b8d93.prepush-gate.py(clean).
- Not run: a NIGHTLY with real agents and an actual Sparkle update. The bench stands in with a bundle swap.
Changelog
- none
Agent session: agent session
🤖 Generated with Claude Code