Avoid duplicate peer snapshot size serialization (takeover of #27010)
Takes over #27010 (its five commits carried with their authorship), rebased onto feat-cmux-next with the tests/daemon_memory.rs conflict against #27048 resolved, plus a fix for the review BLOCK at 1d96c98741.
What it changes
record_bytes(peer snapshot store) counts a record's serialized size through a countingio::Write(with its ownwrite_all) instead of building aVecto read its length. The replace and evict paths now size the removed record once instead of twice.disk_testschecks that the size is unchanged (line terminator included) and that sizing allocates nothing.tests/daemon_memory.rsgains a peer snapshot benchmark. It has a real remote daemon with ten retained closed chats of 400 object-heavy records each, and a local daemon with 0, 288 or 1000 idle or closed fillers. It makes two passes of attach pluspeer_snapshotover every remote chat.
Review BLOCK fixes (on top of #27010)
- The benchmark asserted nothing and compared against no baseline, so a regression passed silently. It now fails when any case's lifetime peak RSS or snapshot CPU passes a recorded bound: 1,400 MB and 12,000 ms by default, which is the dev-profile worst case below plus about 25%.
ACPMUX_PEER_SNAPSHOT_PEAK_MBandACPMUX_PEER_SNAPSHOT_CPU_MSoverride them. Like #27048's list benchmark it is#[ignore]: run it with--ignored --test-threads=1. - Off Linux,
peak_rss_kibwas one current-RSS sample. The peak now comes fromwait4's rusage (ru_maxrss) on every Unix, in KiB on Linux and bytes on macOS. It is read after a SIGKILL, so no shutdown work moves it. - Found while measuring: the hermetic test re-start clears the environment, so
ACPMUX_MEMORY_BUDGET_MB, the bench sizes and the new bounds never reached the tests.daemon_memory'sHERMETICnow keeps those five variables. They hold numbers only, no credential.
Performance
Same harness for both sides: one cmux-ci test job on a 32-vCPU Linux runner. It builds head and a base tree (this branch with peer_snapshot.rs and disk.rs restored to feat-cmux-next's, byte for byte), then runs base, head, base, head, base, head. That is three rounds of the six cases (idle or closed Ć 0, 288 or 1000 fillers), 18 samples per side. CPU is the local daemon's user+sys time over the snapshot passes. Peak is the daemon's lifetime ru_maxrss.
Release profile (what ships), run on ea947612c5:
| metric | before | after | delta | harness or run link |
|---|---|---|---|---|
| daemon CPU per case, median of 18 | 5,915 ms | 5,670 ms | -4.1% | run 41015043124 |
| daemon CPU per round (6 cases), rounds 1 / 2 / 3 | 35,770 / 36,250 / 31,870 ms | 35,160 / 34,170 / 32,160 ms | -1.7% / -5.7% / +0.9% | run 41015043124 |
| peak RSS per case, median of 18 | 781 MiB | 797 MiB | +2.0% (noise: one case spans 706-1,000 MiB on head, 720-936 on base) | run 41015043124 |
| peak RSS, worst case | 1,073 MiB | 1,061 MiB | -1.1% | run 41015043124 |
The CPU is the whole daemon's work over the snapshot passes. Record sizing is one small part of it, so the gain is small and within round-to-round noise. Memory is unchanged within noise.
Dev profile (cargo test), run on 5abd31e0f4, before the write_all override:
| metric | before | after | delta | harness or run link |
|---|---|---|---|---|
| daemon CPU per case, median of 18 | 6,630 ms | 8,275 ms | +24.8% | run 41012834259 |
| peak RSS per case, median of 18 | 807 MiB | 839 MiB | +4.0% | run 41012834259 |
| peak RSS / CPU, worst case | 1,080 MiB / 8,510 ms | 1,124 MiB / 9,540 ms | run 41012834259 |
The dev profile builds acpmux at opt-level 0 and its dependencies at 3. The counting writer is serde_json's serializer instantiated for a type defined in acpmux, so it runs unoptimised there, while base's to_vec runs serde_json's optimised code. This affects test builds only. The bounds come from this worst case, so a plain --ignored run has headroom.
Changelog
none
Observed / Not run
Observed:
- The two paired runs above, base and head alternating on one runner.
disk_testsand the benchmark built and ran in both runs.- The base tree's two source files match feat-cmux-next byte for byte.
Not run:
- The benchmark with its default bounds (both runs passed overrides so every sample was recorded; every release sample is under the defaults).
- The macOS
ru_maxrssunit (read from the man page, not run on a Mac).
Agent session: agent session
š¤ Generated with Claude Code