From 88b288517aea9b4d0175ac466e48e15af898d497 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 25 Sep 2026 19:26:18 -0700 Subject: [PATCH] chore(moq-net): bench lite-06, smoke-run benches nightly, refresh perf quests The session bench now covers moq-lite-06, the draft production negotiates, beside moq-lite-07-wip and moq-transport-22. Nightly runs every moq-net Criterion routine once, since nextest never executes harness = false targets and a bench that panics otherwise goes unnoticed. Lite route rescan is done by #4216 (delivery flat at ~365 us from 16 to 4096 announced routes on every version), so its quest is deleted. The remaining findings are re-measured on current main: announce replay and group cost still reproduce; cache growth under the default pool now shows on lite too, so that quest drops its IETF-only premise. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/nightly.yml | 9 +++++++++ quest/m1/README.md | 2 +- quest/m1/cache-expiry-growth.md | 24 ++++++++++++++---------- quest/m1/perf/README.md | 1 - quest/m1/perf/announce-replay.md | 12 +++++++----- quest/m1/perf/group-cost.md | 9 +++++---- quest/m1/perf/lite-route-rescan.md | 29 ----------------------------- rs/moq-net/benches/session.rs | 5 +++-- 8 files changed, 39 insertions(+), 52 deletions(-) delete mode 100644 quest/m1/perf/lite-route-rescan.md diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 62484ea295..d15d3ff914 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -77,6 +77,15 @@ jobs: nix develop --command bun install --frozen-lockfile nix develop --command bun js/net/bench/broadcasts.ts + # Runs each moq-net Criterion routine once, so a bench that panics (an + # unparsable version, a shape that deadlocks) fails here instead of on the + # next manual run. Timing is not compared; nextest never runs a + # `harness = false` target. `fuzz` enables the announce bench, which the + # `'*'` glob otherwise refuses. + - name: moq-net benchmark smoke + if: ${{ !cancelled() }} + run: nix develop --command cargo bench --locked -p moq-net --features fuzz --bench '*' -- --test + - name: Audit if: ${{ !cancelled() }} run: nix develop --command just rs audit diff --git a/quest/m1/README.md b/quest/m1/README.md index 88edaefce0..1f8fc33b00 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -91,7 +91,7 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [Watch worker](/quest/m1/watch-worker.md) - watch playback runs in a worker onto an OffscreenCanvas, so main-thread jank never stalls video or audio - [Closure counters](/quest/m1/closure-counters.md) - a departed node's return never regresses the closure counters a consumer already saw - [RTMP interleaving](/quest/m1/rtmp-interleaving.md) - isolate partial messages before optimizing assembly copies -- [Cache expiry growth](/quest/m1/cache-expiry-growth.md) - with the default pool, relay memory plateaus at the expiry window on moq-transport as on moq-lite +- [Cache expiry growth](/quest/m1/cache-expiry-growth.md) - with the default pool, relay memory plateaus at the expiry window on every version - [Relay memory](/quest/m1/relay-memory.md) - remeasure what an announcement costs after prefix routes - [PoP skipping](/quest/m1/pop-skipping/README.md) - short cold paths for unpopular broadcasts without losing warm backhaul dedup - [Route cost in the JS origin](/quest/m1/route-cost.md) - the browser origin ranks routes by cost and hops like Rust instead of newest-first diff --git a/quest/m1/cache-expiry-growth.md b/quest/m1/cache-expiry-growth.md index 17cac468ed..fc2ea10f9d 100644 --- a/quest/m1/cache-expiry-growth.md +++ b/quest/m1/cache-expiry-growth.md @@ -1,23 +1,27 @@ -# [S] Cached groups expire on the IETF path +# [S] Cached groups expire under the default pool ## Goal With the default cache pool, a relay's memory plateaus once groups start -reaching the expiry window, on moq-transport as on moq-lite, or the cause of -continued growth is found and fixed. +reaching the expiry window, on every version, or the cause of continued +growth is found and fixed. ## Plan In `session_delivery_broadcasts` with 4096 announced broadcasts and the -default unbounded pool (30 s expiry), IETF RSS reached 1.5 GB at 10 s, 3.5 GB at -30 s, and 4.9 GB at 50 s, still climbing past the expiry window. Lite -reached about 0.36 GB by 50 s. With a 16 MiB bounded pool both stay flat, so -the retained bytes are cached groups, not a leak elsewhere. +default unbounded pool (30 s expiry), RSS keeps climbing past the expiry +window on both wires (2026-09-25, Apple M4, sampled every 10 s): IETF +1.9 GB at 30 s and 4.4 GB at 120 s, lite-06 2.3 GB at 30 s and 3.5 GB at +120 s, both oscillating by a gigabyte between samples. With a 16 MiB bounded +pool both stay flat (0.5 GB IETF, 0.4 GB lite), so the retained bytes are +cached groups, not a leak elsewhere. An earlier run saw growth on IETF only, +but lite was then 30x slower per round, so it wrote far fewer groups; compare +by groups written, not by wall time. Reproduce with a focused test first. Unexpired groups at higher throughput, -expiry not running on a path IETF uses, or groups held outside the pool's -accounting would each explain it. Fix what is actually wrong. Consider -whether an unbounded default is the right default for an origin at all. +expiry not running on some path, or groups held outside the pool's accounting +would each explain it. Fix what is actually wrong. Consider whether an +unbounded default is the right default for an origin at all. ## Related diff --git a/quest/m1/perf/README.md b/quest/m1/perf/README.md index c855d6344d..b0b17f4d6f 100644 --- a/quest/m1/perf/README.md +++ b/quest/m1/perf/README.md @@ -43,7 +43,6 @@ row per io_uring worker. - [Open contract](/quest/m1/perf/uring-open-contract.md) - plan concurrent WebTransport opening and cancellation -- [Lite route rescan](/quest/m1/perf/lite-route-rescan.md) - a moq-lite session's per-group cost stops growing with the routes its peer announced - [Announce replay](/quest/m1/perf/announce-replay.md) - the initial announce set replays in linear time, so joins don't slow with the route count - [Group cost](/quest/m1/perf/group-cost.md) - count and cut the allocations and time spent relaying one small group to one viewer - [One enter per turn](/quest/m1/perf/uring-one-enter.md) - a parking turn pays one io_uring_enter, submits flush deferred completions, and SQEs per enter is a counter diff --git a/quest/m1/perf/announce-replay.md b/quest/m1/perf/announce-replay.md index db7f81b036..3a59f0c7cb 100644 --- a/quest/m1/perf/announce-replay.md +++ b/quest/m1/perf/announce-replay.md @@ -13,10 +13,12 @@ The moq-lite publisher's initial replay (`AnnounceRun::init` in for every route it drains: `initial.retain` on Lite05+, `init.contains` on the Lite01/02 init. That is quadratic in the replay size. -Measured with `session_join_broadcasts` (2026-09, one relay): lite join takes -199 µs with 1 announced broadcast, 517 µs with 64, and 8.9 ms with 1024, -about 8.7 µs per route at the top. IETF is 7.2 ms at 1024 with no quadratic -scan; its profile is SipHash hashing and `Path` comparison, so check whether -a faster hasher for path-keyed maps pays off on both. +Measured with `session_join_broadcasts` (2026-09-25, one relay, Apple M4): +lite-06 join takes 128 µs with 1 announced broadcast, 317 µs with 64, and +5.0 ms with 1024, about 4.9 µs per route at the top; lite-07-wip matches. +IETF is 95 µs, 311 µs, and 4.05 ms, with no quadratic scan, so the scan is +the ~1 ms gap at 1024. An earlier Linux profile of the IETF path was SipHash +hashing and `Path` comparison, so check whether a faster hasher for +path-keyed maps pays off on both. Keep the replay's order and its last-update-wins semantics. diff --git a/quest/m1/perf/group-cost.md b/quest/m1/perf/group-cost.md index b558d76b62..cec74b3338 100644 --- a/quest/m1/perf/group-cost.md +++ b/quest/m1/perf/group-cost.md @@ -8,10 +8,11 @@ change to what is delivered. ## Plan -`session_delivery_viewers` (2026-09) spends about 32 µs per viewer per -4-frame, 64-byte group over the in-memory transport, through a publisher -session, a relay origin, and a viewer session. No single function dominates. -The profile spreads it over `kio` waiter registration and parking, +`session_delivery_viewers` spends about 26 µs per viewer per 4-frame, +64-byte group over the in-memory transport, through a publisher session, a +relay origin, and a viewer session: ~420 µs at 16 viewers on every version +(2026-09-25, Apple M4; the 256-viewer point is too noisy on that machine to +quote). No single function dominates. An earlier Linux profile spread it over `kio` waiter registration and parking, `TrackState::evict_expired_scan` (4-5%), `Waiter`'s lazily allocated shared waker (`Once::call`, about 3%, so waiters are created per poll), and malloc/free (10-12%). diff --git a/quest/m1/perf/lite-route-rescan.md b/quest/m1/perf/lite-route-rescan.md deleted file mode 100644 index 6591609912..0000000000 --- a/quest/m1/perf/lite-route-rescan.md +++ /dev/null @@ -1,29 +0,0 @@ -# [S] Lite subscriber polls only routes with a request - -## Goal - -A moq-lite session's per-group cost stops growing with the number of -broadcasts its peer has announced: delivery over a session holding thousands -of announced routes costs what it costs with a handful, as it already does -over moq-transport. - -## Plan - -`Announced::poll_serve` (`rs/moq-net/src/lite/subscriber.rs`) polls every -attached route's `Dynamic::poll_requested_broadcast` on every driver wake, so -each incoming group pays for every route, and each pending poll registers the -driver's waiter on every idle route's list. The IETF subscriber runs one task -per route and only wakes the one that got a request. - -Measured with `cargo bench -p moq-net --bench session` (2026-09, one relay, -16 viewers each watching one broadcast): - -- `session_delivery_broadcasts`: lite 445 µs at 16 announced, 15.2 ms at - 4096; IETF flat around 458 µs. 57% of lite CPU is `WaiterList::register` - under `poll_requested_broadcast`. -- `session_delivery_scale` at 256 publishers x 256 viewers: lite 40 ms, IETF - 16.5 ms, since every viewer session holds all 256 routes. - -Wake only routes with a pending request. A task per route like IETF's or a -ready set both fit; pick by what keeps the driver's fairness rules. Land -with the before/after of those two groups. diff --git a/rs/moq-net/benches/session.rs b/rs/moq-net/benches/session.rs index 42e65e2fd2..b093dc6db1 100644 --- a/rs/moq-net/benches/session.rs +++ b/rs/moq-net/benches/session.rs @@ -28,8 +28,9 @@ use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_m use moq_net::{Hop, Timestamp, Version, broadcast, cache, origin, track}; use support::harness::{MockConnectOptions, MockPair, connect_mock}; -/// Newest lite draft (`moq-lite-07-wip`, opt-in) and newest IETF draft. -const VERSIONS: [&str; 2] = ["moq-lite-07-wip", "moq-transport-22"]; +/// The lite draft production negotiates, the next one (opt-in), and the newest +/// IETF draft. +const VERSIONS: [&str; 3] = ["moq-lite-06", "moq-lite-07-wip", "moq-transport-22"]; /// Frames per group, so per-frame and per-group costs both appear. const FRAMES: usize = 4;