acpmux: one serialization per streamed entry; agent host 27 -> 13 us/token (perf100)
What
Per-token copies removed on the claude-stdio path (what Lawrence's chats use):
- Agent host: each entry is encoded once, from a borrowed view, and kept as its frame bytes. The live send and a resume send the same bytes. Before, each entry was serialized once to weigh it, deep-copied to send it, and serialized again by the writer.
Message::from_valuemoves the fields out instead of copyingparams. NewMessage::into_value; the host builds records with it. The raw Claude line moves into its record. It is now translated first, and the raw record still comes first.Message::to_lineserializes from the borrowed fields. Before,to_valuecopied every param for each client line.- The preview grows in place instead of being copied on every chunk.
Numbers (rbx, perf100 bench: 10 sessions x 4,000 deltas, one attached client; base 26e79e1a8483; two runs each)
| Metric | Before | After | Ratio |
|---|---|---|---|
| Agent host CPU per token (release) | 27–28 µs | 13–14 µs | 2.0x |
| Daemon CPU per token (release) | 89.5–93.5 µs | 75.5–85.7 µs | 1.1–1.2x |
Agent host CPU per token (debug cargo test) | 33–35 µs | 23 µs | 1.5x |
| Agent host RSS after a 20,000-delta turn (debug) | 64–89 MB | 36–40 MB | ~2x |
| Log bytes / write syscalls per token | 674 B / 2.35 | same | 1x |
Most of the daemon's per-token cost is still in the hub: two records per token, the log line, the event ring's record copy, and the fan-out. That is the next slice; it needs a profiler on rbx (perf is blocked for the rbx user).
Test
tests/claude_stream_budget.rs: a recorded-shape turn of 20,000 deltas from a stand-in Claude Code (tests/fake_claude_stream.py), run through the CLI. It checks that the reply arrives whole. It also checks that the agent host uses at most 70 µs and the daemon at most 330 µs of CPU per delta, about 3x the measured debug values. It is a budget guard: on base the debug numbers (host 33–35 µs, daemon 129–136 µs) also pass.
Review
A correctness review subagent checked this change:
- The encoded bytes are identical to
write_frame(&HostFrame::Entry), and the controller decodes them. from_valuegives the same result for every input.- The raw record still comes before the translated records and the stdin taps.
- The writer still does
write_allandflushper frame.
Nits that remain open:
- An entry over the frame limit now becomes a stderr note. If it was a response with an id, that request is not answered. Before, the controller was dropped on every resume. Input lines are capped below the limit, so this is hard to reach.
- The buffer weight now counts the frame overhead, so the stderr drop threshold triggers slightly earlier.
Changelog
none
Observed / Not run
- Observed on rbx: the bench, the budget test on head and on base, and
cargo clippy -p acpmux --all-targets -D warnings, where the only errors are the base redscmp_owned(#22906) andunnecessary_sort_byin cmux-worktree-care. The full suite runs in the landing gate. - Not run: macOS. The acp-fork lane's Codex bench was not rerun; the Codex bridge shares
from_valueandinto_value.
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Removes per-token copies on the claude-stdio path. The agent host encodes each streamed entry once and keeps its frame bytes, sending the same bytes live and on resume; Message::from_value and the new Message::into_value move fields instead of copying params; Message::to_line serializes borrowed fields; and the streaming preview grows in place. Agent host CPU per token drops from 27–28 to 13–14 µs (release, perf100 bench) and RSS after a 20,000-delta turn from 64–89 to 36–40 MB; daemon CPU per token drops from ~90 to ~78 µs.
Budget test
tests/claude_stream_budget.rsruns a recorded-shape 20,000-delta turn through a stand-in Claude Code (tests/fake_claude_stream.py) and asserts the reply arrives whole and that host/daemon CPU per delta stay under 3x the measured debug values.- An entry over the frame limit now becomes a stderr note instead of dropping the controller on resume, and the stderr drop threshold triggers slightly earlier because the buffer weight counts frame overhead.