Avoid duplicate peer snapshot size serialization (takeover of #27010)

Merged
TE
6d ago
+309-10

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 counting io::Write (with its own write_all) instead of building a Vec to read its length. The replace and evict paths now size the removed record once instead of twice. disk_tests checks that the size is unchanged (line terminator included) and that sizing allocates nothing.
  • tests/daemon_memory.rs gains 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 plus peer_snapshot over every remote chat.

Review BLOCK fixes (on top of #27010)

  1. 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_MB and ACPMUX_PEER_SNAPSHOT_CPU_MS override them. Like #27048's list benchmark it is #[ignore]: run it with --ignored --test-threads=1.
  2. Off Linux, peak_rss_kib was one current-RSS sample. The peak now comes from wait4'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.
  3. 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's HERMETIC now 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:

metricbeforeafterdeltaharness or run link
daemon CPU per case, median of 185,915 ms5,670 ms-4.1%run 41015043124
daemon CPU per round (6 cases), rounds 1 / 2 / 335,770 / 36,250 / 31,870 ms35,160 / 34,170 / 32,160 ms-1.7% / -5.7% / +0.9%run 41015043124
peak RSS per case, median of 18781 MiB797 MiB+2.0% (noise: one case spans 706-1,000 MiB on head, 720-936 on base)run 41015043124
peak RSS, worst case1,073 MiB1,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:

metricbeforeafterdeltaharness or run link
daemon CPU per case, median of 186,630 ms8,275 ms+24.8%run 41012834259
peak RSS per case, median of 18807 MiB839 MiB+4.0%run 41012834259
peak RSS / CPU, worst case1,080 MiB / 8,510 ms1,124 MiB / 9,540 msrun 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_tests and 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_maxrss unit (read from the man page, not run on a Mac).

Agent session: agent session

šŸ¤– Generated with Claude Code