Repository navigation
bench(net): measure complete datagram delivery - #7517
Conversation
Quest-Claim: 9487e34f156ee78f
Add `session_delivery_datagram` to the model and relay session benches, sweeping publishers x viewers, payload size and cadence on lite-06, lite-07-wip and moq-transport-22, with every datagram checked for its sequence and payload. `SESSION_ALLOCS=1` now also reports allocated bytes and, through a thread perf counter, user instructions per delivered datagram or viewer-group. Add a nightly moq-tokio benchmark sending datagrams below and above a bufferbloated bottleneck to a loopback viewer and a throttled one, reporting offered, received, lost, queue-dropped and reordered datagrams and their latency, and failing if the loopback viewer loses any. No optimization lands: the dominant per-datagram cost is the subscription's per-wake polling shared with groups, and draining buffered datagrams per step regresses the one-per-wake case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…survey/datagram-delivery
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review.
|
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: 5633ec505114c25797587180ab66ef5b04219e02.
[P2] Include the drain in shaper verification and drop accounting — rs/moq-tokio/tests/integration/datagram_delivery.rs:292-297. Shaper::verify() returns a snapshot, but this is taken immediately after the last append, before the receivers finish draining the congested sender. If queued traffic overflows the shaper during that drain, its loss appears in the final lost column but is omitted from overflowed; a forwarding failure during the same interval also escapes verification and can look like permitted best-effort loss. Await both receiver results while retaining their viewer handles, then verify/snapshot the shaper before reporting, so all columns cover the same observation interval.
Overall direction is sound: reuse the model/relay harness for protocol cost and keep real-QUIC congestion measurements separate. Leaving scheduling unchanged is simpler than landing the workload-sensitive drain optimization. No public API or wire change identified.
Verification: static GitHub review of the changed paths and relevant model/shaper code only; I did not run tests, benchmarks, or reproduce the reported performance figures.
(Written by OpenAI)
There was a problem hiding this comment.
ℹ️ No critical issues — one measurement-accuracy nit inline.
Reviewed changes
- In-memory session bench:
session_delivery_datagramwritesburstdatagrams per watched track and reads every one from every viewer with sequence/payload assertions, over a publishers × viewers × payload × cadence sweep. The newRoom::nextbookkeeping correctly relies on groups and datagrams sharing the model's sequence namespace, and the extractedgroup_roundis behavior-preserving for the existing group benches. - Alloc/instruction report:
SESSION_ALLOCS=1now also reports allocated bytes and, on Linux little-endian with perf access, user instructions via a hand-rolledperf_event_open. - Real QUIC test:
benchmark_datagram_deliverypublishes to a loopback viewer and a bufferbloated one, reporting offered/received/lost/overflowed/reordered/latency and failing on duplicate, corrupt, or loopback loss. ReusesAnyCert/rand_seedfromreceive_credit.rs. - Nightly and docs: nextest slow-timeout override, combined
rust-testinvocation,bench/README.md; deletes the completed quest and renumbers the audit.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
The shaper's drops and forwarding check now cover the same window as the received and lost columns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: 9234af7d8755d6aa6c779451c5743592590799b1 (incremental review from 5633ec505114c25797587180ab66ef5b04219e02; one direct-child fix, not a rebase).
Prior P2 fixed: rs/moq-tokio/tests/integration/datagram_delivery.rs:292–300 now awaits every receiver while retaining its Viewer, then verifies/snapshots the shaper before reporting. This addresses the drain-window accounting and forwarding-check issue in the prior review, also identified in the existing thread.
No new actionable bugs found in this update. The small two-phase collect-then-report change is appropriately scoped; receiver tasks still drain concurrently, and no scheduling or wire redesign is needed.
Verification: static GitHub inspection of the complete incremental diff, receiver/report lifecycle, and shaper implementation. I did not run tests or benchmarks or independently reproduce the reported measurements.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Since the prior pullfrog review, one commit changes the real-QUIC test's tail.
- Drain the viewers before snapshotting the shaper:
measurenow awaits every receiver, keeping eachViewer, before a singleshaper.verify(), sooverflowedand the forwarding-failure check cover the same interval asreceivedandlost. This resolves the prior P2.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
|
codex / Codex land fixer note:
(written by codex / Codex) |
|
codex / Codex land fixer note:
(written by codex / Codex) |
|
codex / Codex land fixer: .expect("relay task panicked")
.expect("relay exited with an error");
assert!(
signalled.elapsed() >= DRAIN_TIMEOUT,
"relay exited after {:?}, short of the {DRAIN_TIMEOUT:?} drain window",
signalled.elapsed()
);
}
async fn a_signal_right_after_ready_drains_inner() {
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
let relay = Relay::load(relay_config()).await.expect("load relay");
let port = relay.tcp_addr().expect("TCP listener bound").port();
let ready = relay.ready();
let run = tokio::spawn(relay.run());
ready.wait().await.expect("relay ready");
Checking moq-tokio v0.19.23 (/work/rs/moq-tokio)
Checking moq-stats v0.2.12 (/work/rs/moq-stats)
Checking moq-relay v0.17.2 (/work/rs/moq-relay)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 4m 04s
codex
The run finished with 4,229 passes and one different failure: `shutdown_signal::a_reconnecting_client_rides_out_a_restart` exceeded its reconnect timeout. That test is unchanged by #7517. Both session benchmark checks passed. I’ll rerun the suite with the repository’s local limit of four test workers to see whether the timeout was transient.
exec
/bin/bash -lc 'cargo nextest run --locked --tests -p moq-net -p moq-relay -p moq-tokio --features moq-net/fuzz,moq-relay/test-support --profile ci --no-fail-fast -j 4 > /tmp/land-7517-retest.log 2>&1' in /work
exec
/bin/bash -lc "tail -5 /tmp/land-7517-retest.log; rg 'FAIL|TIMEOUT|quic_client_close_drains_finished_track|a_reconnecting_client_rides_out_a_restart' /tmp/land-7517-retest.log" in /work
exited 1 in 0ms:
PASS [ 0.023s] (2966/4230) moq-net::integration lazy_discovery::requesting_an_unwatched_path_discovers_its_instance
PASS [ 0.020s] (2967/4230) moq-net::integration lazy_discovery::serving_the_peer_asks_nothing
PASS [ 0.026s] (2968/4230) moq-net::integration lazy_discovery::waiting_on_a_path_asks_about_it
PASS [ 0.105s] (2969/4230) moq-net::integration mesh_withdraw::cross_node_close_retracts_once_lite06
PASS [ 0.103s] (2970/4230) moq-net::integration mesh_withdraw::cross_node_close_retracts_once_lite07
exec
/bin/bash -lc "tail -6 /tmp/land-7517-retest.log; rg 'FAIL|TIMEOUT|quic_client_close_drains_finished_track|a_reconnecting_client_rides_out_a_restart' /tmp/land-7517-retest.log" in /work
succeeded in 0ms:
PASS [ 0.026s] (3952/4230) moq-tokio server::tests::serving_nothing_is_an_error
PASS [ 0.091s] (3953/4230) moq-tokio server::tests::close_releases_quic_socket
PASS [ 0.029s] (3954/4230) moq-tokio server::tests::stalled_setup_reply_is_closed_at_the_deadline
PASS [ 0.029s] (3955/4230) moq-tokio server::tests::stalled_setup_is_closed_at_the_deadline
PASS [ 0.019s] (3956/4230) moq-tokio server::tests::stream_listener_config_parses
PASS [ 0.021s] (3957/4230) moq-tokio server::tests::stream_only_config_has_no_quic
PASS [ 1.157s] (3624/4230) moq-relay::integration shutdown_signal::a_reconnecting_client_rides_out_a_restart
codex
The relay restart test passed on the rerun, which has reached more than 3,900 passing tests with no failures. If the remaining tests pass, I’ll record both PRs for unchanged requeue and name the observed flaky tests in the result.
succeeded in 88875ms:
exec
/bin/bash -lc "tail -5 /tmp/land-7517-retest.log; rg 'quic_client_close_drains_finished_track' /tmp/land-7517-retest.log; git rev-parse pr-7520 pr-7517; git status --short" in /work
succeeded in 0ms:
PASS [ 0.060s] (4228/4230) moq-tokio::integration worker::the_other_wildcard_spelling_is_refused
PASS [ 0.191s] (4229/4230) moq-tokio::integration worker::spawner_contains_a_factory_panic
PASS [ 3.106s] (4230/4230) moq-tokio::integration reconnect::two_peers_redirecting_to_each_other_escalate_the_backoff
────────────
Summary [ 85.873s] 4230 tests run: 4230 passed, 38 skipped
PASS [ 0.088s] (4070/4230) moq-tokio::integration backend::quic_client_close_drains_finished_track
f19b868b7407552e18a2453129e065028da07e6b
dec0914ef77034a134adc091c6947226faad8b25
exec
/bin/bash -lc "git switch base; python3 - <<'PY'
import json
from pathlib import Path
result = [
{
\"number\": 7520,
\"action\": \"requeue\",
\"reason\": \"Healthy unchanged; batch flake moq-tokio::integration backend::quic_client_close_drains_finished_track passed. Passed: cargo nextest run --locked --tests -p moq-audio -p moq-tokio --profile ci --no-fail-fast (774 tests); cargo check --locked -p moq-audio --bench batches; bash -n sh/gh/nightly.sh.\",
\"comment\": \"The batch failed in the unchanged test "'`backend::quic_client_close_drains_finished_track` with `subscribe failed: Unroutable`. Neither PR changes that test or its production path. All 774 audio and transport tests passed on this pinned branch, including that test, and the new audio benchmark target compiled. Requeue unchanged; follow up the intermittent QUIC test separately."
},
{
"number": 7517,
"action": "requeue",
"reason": "Healthy unchanged; batch flake moq-tokio::integration backend::quic_client_close_drains_finished_track passed. Local flake moq-relay::integration shutdown_signal::a_reconnecting_client_rides_out_a_restart passed on full rerun. Passed: cargo nextest run --locked --tests -p moq-net -p moq-relay -p moq-tokio --features moq-net/fuzz,moq-relay/test-support --profile ci --no-fail-fast -j 4 (4230 tests); cargo check --locked -p moq-net -p moq-relay --bench session --features moq-net/fuzz,moq-relay/test-support; bash -n sh/gh/nightly.sh.",
"comment": "The batch'"'s unchanged test "'`backend::quic_client_close_drains_finished_track` passed on this pinned branch. The initial local suite had 4,229 passes and one failure in the unchanged relay test `shutdown_signal::a_reconnecting_client_rides_out_a_restart`: `the publisher gave up during the restart: Err(Reconnect('"\\\"reconnect timed out after 333.333333ms: cancelled\\\"))"'`. A complete rerun with four test workers passed all 4,230 tests, including both tests. Both modified session benchmark targets compiled. Requeue unchanged; follow up both intermittent tests separately."
}
]
Path('"'quest-land-result.json').write_text(json.dumps(result, indent=2) + '\\n')
assert len(result) == 2
assert {x['number'] for x in result} == {7520, 7517}
PY
git status --short" in /work
succeeded in 145ms:
Switched to branch 'base'
?? quest-land-result.json
codex
Wrote [quest-land-result.json](/work/quest-land-result.json): both PRs requeue unchanged, with no blame assigned.
All 774 audio/transport tests and 4,230 network/relay/transport tests passed, along with benchmark compilation checks. Recorded both observed flaky tests. Branches remain unchanged; the result file is uncommitted.
tokens used
70,645
Wrote [quest-land-result.json](/work/quest-land-result.json): both PRs requeue unchanged, with no blame assigned.
All 774 audio/transport tests and 4,230 network/relay/transport tests passed, along with benchmark compilation checks. Recorded both observed flaky tests. Branches remain unchanged; the result file is uncommitted.
fixer result written
(written by codex / Codex) |
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: 432ea3ba9549bb425c3e4355125de3e56c0a2e8a, incrementally from 9234af7d8755d6aa6c779451c5743592590799b1, separating intervening main merges from the benchmark changes.
No new actionable bugs found. The allocation/requested-byte window now excludes cache ticks like the instruction window; checking perf enabled/running times avoids silently accepting multiplexed undercounts. Separate synchronous nightly invocations avoid these two real-time measurements competing with each other. Sharing the test-only QUIC dial helper is a simpler, appropriately scoped refactor; production scheduling and wire behavior remain unchanged.
The prior drain-window accounting finding remains fixed: receivers finish while their viewer handles are retained, before the shaper snapshot (see previous review).
Verification: static GitHub inspection of the incremental commits, surrounding counter/runtime and receiver lifecycle code, and nightly wiring. I did not run tests or benchmarks or independently reproduce the reported measurements.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Since the prior pullfrog review, three commits land: a merge of newer main, the counter/nightly changes, and a one-line visibility fix.
- Fail on multiplexed perf counts:
Instructions::readrequestsPERF_FORMAT_TOTAL_TIME_ENABLED | RUNNINGand assertstime_enabled == time_running, so a multiplexed counter fails the run instead of silently undercounting. - Scope allocation counting to the timed round:
measuretogglesCOUNTINGaroundround(), excluding the cache tick and matching the instruction window;allocationsno longer sets the flag.ALLOCATED's doc now states it sums requested allocation/reallocation sizes. - Share the QUIC dial helper:
dialis extracted inreceive_credit.rs; bothdatagram_deliveryandsetup_throughcall it. - Keep
AnyCertvisible:432ea3brestorespub(crate)for the cross-module use inreceive_ts.rs; the9607e2fnarrowing would not have compiled, and the net change vs. base is nil. - Isolate the real-time nightly runs:
benchmark_receive_windowsandbenchmark_datagram_deliveryrun in separatejust rs testinvocations;bench/README.mddocuments the counter scope and the multiplexing guard.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Finishes
quest/m2/performance-survey/datagram-delivery.md.Problem
Session-level datagram delivery had no complete-delivery benchmark or real-QUIC congestion measurements.
Approach
Adds session-level datagram measurement. The measurements show no datagram-specific per-object work worth removing yet, so no optimization lands.
rs/moq-net/benches/session/mod.rs):session_delivery_datagramwritescadencedatagrams per watched track each round and reads every one back from every viewer. It runs on lite-06, lite-07-wip and moq-transport-22. Each datagram must arrive in order with its sequence and one of 16 distinct payloads, so a lost or crossed datagram fails the run. The sweep covers publishers {1, 16} × viewers {1, 16, 64} at 64 B and cadence 1, payload 1024 B, and cadence {16, 64}. Throughput counts delivered datagrams. A priming group binds the moq-transport alias before the first datagram. The relay target registers the same group asrelay_session_delivery_datagram.SESSION_ALLOCS=1now also reports allocated bytes. On Linux it also reports user instructions from a per-thread perf counter (perf_event_open) read around each timed round. Results are given per delivered datagram or per viewer-group. Every task runs on the bench's single thread, so the count covers all protocol work, and an A/A rerun repeats within ±0.2%.rs/moq-tokio/tests/integration/datagram_delivery.rs): a stock server publishes 1000 B datagrams for 8 s to two viewers. One is on loopback. The other sits behind a 2 Mbit/s, 250 ms-queue shaper (10 ms delay, no random loss) at 1 Mbit/s ("under") and 4 Mbit/s ("over"). Per viewer it reports offered, received, lost, shaper queue drops, QUIC DATAGRAM frames, reordered arrivals and append-to-receive latency. It fails on a duplicate or corrupted datagram, or if the loopback viewer loses any. This is the slow-viewer isolation check.rust-bench/rust-relaylegs (--bench '*'and the relay session target).benchmark_datagram_deliveryruns separately after the receive-window benchmark in therust-testleg insh/gh/nightly.sh, with a nextest slow-timeout override likebenchmark_receive_windows.sh/gh/test-inventory.pypasses.bench/README.md. The quest file and its link in the survey epic are deleted. The performance audit no longer lists session datagrams as unbenchmarked.Impact
Measurements
The original report below is retained for its measured comparisons. Its allocation counts included cache ticks; the final head corrects that scope to match instruction counts. Current measurements are recorded under Validation. Neither set is a production optimization.
Host: Intel Core Ultra 7 270K Plus, 24 CPUs, Linux 7.0.0, rustc 1.98.1,
benchprofile (fat LTO). The load average was 10–14 on a shared host. Instruction counts are per thread and repeat regardless of load. The process was pinned withtaskset -c 7. Baseline:17e00c8148plus this bench. After merging main14e9973dc8(including lite-07 padding datagrams, moq-dev/moq#7435), every row moved by −0.2% to −0.5%.SESSION_ALLOCS=1 taskset -c 7 target/release/deps/session-* --bench session_delivery, model target, 256 rounds per shape:All counts are per delivered datagram. A 1024 B payload costs the same instructions as a 64 B one; only the allocated bytes grow (1,793 → 3,713 per datagram on lite-06), which is the per-subscriber encode copy. The relay target (
relay_session_delivery_datagram) is within a few percent of the model except at 1×1, where per-round stats and admission work is not amortized.CPU time from Criterion (
SESSION_CPU=1 … --warm-up-time 1 --measurement-time 2 session_delivery_datagram_cpu): lite-06 is 7.3 µs per datagram at 1×16, 14.0 µs at 16×16 cadence 1 and 4.2 µs at cadence 64. moq-transport-22 is 6.3, 12.4 and 2.7 µs.Attribution
perf record -e instructions:u --call-graph fpon theprofilingbuild, lite-06, 16×16, cadence 1 (inclusive shares):SubscribeServe::poll56%. Inside it,track::Subscriber::poll_dropis 29% (Cursor::probe19%,Misses::judge14%,RangeMap::remove8%),poll_finished13%,poll_group11%, andresume::Subscriber::poll_recv_datagram12% (Reader::sync/learn_end/let_go,WaiterList::register).serve_datagram(encode + send) 2.4%,insert_wire_datagram3.9%,push_datagram2.9%,Ledger::account2.6%. Allocator time is about 3%.So most of a datagram's cost is the subscription's per-wake polling, which every group frame pays as well. Each served datagram returns
ContinuefromTrackRun::poll_step, and the next step re-polls drop forwarding, group readiness and the boundary before the next datagram. The cadence sweep shows the split: about 41k instructions per datagram marginal versus about 81k per wake at 16×16.Alternatives
Draining every buffered datagram in one lite
TrackRunstep, as the IETF publisher'spoll_datagramsalready does, gave these paired, deterministic results against the same baseline:lite-07-wip matched: +3.3–3.4% at cadence 1 and −33–37% at cadence 16–64. moq-transport-22 was unchanged (−0.3%). The extra poll that finds the buffer empty costs more than the drain saves when each wake carries one datagram. That is the case for live audio, the main datagram consumer. The prototype also changes the documented one-item-per-step contract that lets the caller watch the requester between items.
Real QUIC (
benchmark_datagram_delivery)Two runs (seeds printed) gave matching results; the second run:
(The lite-07-wip and moq-transport-22 "under" rows and loopback rows match lite-06.)
Choices made without asking
benchmark_receive_windows.rand_seedfromreceive_credit.rs; the dial helper also uses its sharedAnyCert.Follow-ups
poll_drop/Cursor::probe/Misses::judgeandresume::Reader::syncdominate both datagram and paced-group delivery. Waking only the facet whose state changed would cut the per-wake cost for datagrams and groups alike. The datagram drain becomes worth revisiting once an empty poll is cheap.kio::WaiterList::registerthat initializes itsOnceLock<Arc<Waker>>allocates (about 5% of instructions at 1×1). Ask Perf whether that is already owned.Validation
Current head:
432ea3ba9549bb425c3e4355125de3e56c0a2e8a. Base merged:693a0ec2292ed6e28a7f36dcab5af293bfc52383.quest gate nix develop --ignore-environment --keep HOME --keep USER --keep SSH_AUTH_SOCK --keep QUEST_GATE_ADMITTED --command bash -c 'export PATH="/nix/var/nix/profiles/default/bin:$PATH"; exec just ci origin/main': passed end to end, 11,333 Rust tests passed, 128 skipped, plus selected lint, docs, benchmark compile and tooling checks.backend::quic_client_close_drains_finished_trackpassed in both complete test runs. No failing test was retried. The separate root repair moq-dev/moq#7634 was neither duplicated nor merged into this branch; a pass here does not repair that race.quest check --base origin/main --branch quest/m2/performance-survey/datagram-deliveryandpython3 sh/gh/test-inventory.py: passed.SESSION_ALLOCS=1 taskset -c 7 cargo bench --locked -p moq-net -p moq-relay --features moq-net/fuzz,moq-relay/test-support --bench session -- session_delivery: passed twice, with 27 datagram shapes per backend per run. Every delivery was checked. The largest instruction A/A difference was 0.96%; this is measurement spread, not a production optimization. All accepted counts had equal enabled and running counter times. An earlier unpinned report was rejected by the new multiplexing guard (272,467,773 ns enabled vs 271,320,840 ns running).just rs test -p moq-tokio --release -E 'test(benchmark_datagram_delivery)' --run-ignored only --success-output immediate: passed on the final fixture, with the finite queue, overflow accounting, strict loopback delivery and post-drain snapshot intact. Full output rows and replay seeds follow.Current CPU-7 counted delivery sweep (second A/A run)
All values are per delivered datagram; requested bytes include complete reallocation requests. Cache ticks are excluded. Each row delivered and checked every datagram.
Current real-QUIC delivery measurements
Replay seeds in version/load order (lite-06 under/over, lite-07 under/over, mt-22 under/over):
Earlier validation
just ci origin/main(insidenix develop, throughquest gate) passed on head9234af7d87: 10852 tests passed.benchmark_datagram_deliverypassed on9234af7d87(68.0 s). Its rows match the earlier runs: 2972–2982 of 4000 received behind the bottleneck, p99 8.2 s, and 0 shaper queue drops now counted over the full drain window.5633ec5051:nix develop --command python3 sh/gh/test-inventory.pyandquest checkpassed, and the datagram report reran on the merged head (table deltas above).Review
Claude Code, Claude Opus 5.5, medium effort,
quest review --base origin/main --harness claude, sessionfe2b77db-7e12-4db0-ba24-fa5d012b3ab6, round 2, head432ea3ba9549bb425c3e4355125de3e56c0a2e8aagainstea1822d6ca0d638fcdab73bd6d359798a3635c29: no findings.Round 1 dispositions, accepted in round 2:
The prior online snapshot finding remains fixed: both receivers finish before the final shaper verification and drop snapshot.
(Written by GPT-6.1 Sol)