Skip to content

test(net): record fastest-reader relay and qmux evidence - #7664

Merged
kixelated merged 10 commits into
mainfrom
quest/m1/unbounded-groups/fastest-reader
Nov 5, 2026
Merged

kixelated merged 10 commits into
mainfrom
quest/m1/unbounded-groups/fastest-reader

Conversation

@kixelated

@kixelated kixelated commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The remaining fastest-reader work after moq-dev/moq#7388 and moq-dev/moq#7624:

  • two-hop retention evidence
  • qmux heap and throughput evidence
  • real relay WebSocket accept-path evidence
  • a transport-sized cache pacing window: ingest still paced at a fixed model::pace::AHEAD = 16 MiB, even with 1 MiB of stream credit.

Decisions

Decided 2026-11-03 (maintainer, /quest-unblock):

  • ✅ Transport-sized cache window. Keep the decided plan: ingest and its tests take the cache pacing window from the transport's stream credit, replacing the fixed model::pace::AHEAD = 16 MiB. Reason: memory follows what the peer can actually take in flight.
  • Accept the fixed 16 MiB and correct the Plan (not chosen).

Decided 2026-11-04 (maintainer): no test host is quiet, so the comparison accepts noisy numbers from the quietest host available, recording its load during each run. This replaces the earlier ## Waiting item asking for a host without concurrent workloads.

Approach

  • Transport-sized pacing window. Each ingest stream takes its pacing window when it is accepted or opened, from its session's receive_window (model::pace::window, coding::Pause::window). The window is the stream credit, no more than the connection window, capped at the 16 MiB ahead half of the live tail (AHEAD). group::Producer::poll_paced, frame::Producer::poll_ready and recv::Group::poll_admit now take the window. Local writers (group::Producer::poll_ready/ready) keep 16 MiB, since they have no transport. A transport that reports no window never pauses, as before.
    • Reading the window once per stream keeps the per-chunk pacing check lock-free; it costs one windows read per accepted stream.
  • Two-hop evidence. Carry a 64 MiB group through two simulated session relays on lite-06, lite-07-wip, and moq-transport-22. Park the final reader, check cache charges and origin backpressure, then verify every byte and clean completion.
  • Relay WebSocket accept path. Start an actual Relay from operator configuration and inspect its WebSocket qmux transport parameters before admission. Pin connection, all stream receive windows, and stream counts across qmux-00/01/02.
  • qmux receive benchmark (Nightly). Bounded TCP/WebSocket byte pipes, qmux-00/01/02, 1/8/64 sessions, and 1/8 streams. Native unread leases hold 512 KiB each, so eight fill a 4 MiB connection window. Every round verifies payloads, releases credit, and refills.
  • Quest and docs. The quest records the decisions, the benchmark comparison, and the decision it leaves. doc/concept/moq-lite.md describes the per-hop window as the stream receive window capped at 16 MB.

Evidence

Two-relay cache charges while parked, with 4 MiB connection and 1 MiB stream credit per hop:

Window Upstream relay (incl. sent history) Downstream relay
Transport-sized (this head) 3,481,056-3,546,640 B 985,952-986,000 B
Fixed 16 MiB (previous head) 33,544,512-33,544,560 B 16,737,968-16,738,016 B

With the transport-sized window, the origin stopped after 327 of 1,024 frames. All 64 MiB arrived after reading resumed. Cache charges include frame slots and relay metadata. They exclude transport buffers and RSS.

The one-hop idle reader now holds three 1 MiB frames under 4 MiB of stream credit, and fifteen when the credit is past the 16 MiB cap.

All 36 qmux cases completed with exactly one heap report per case. With 64 sessions and eight streams per session, each receiver held 4,194,304 unread bytes:

Framing qmux draft Live heap bytes/session Payload GiB/s, point estimate
pipe 02 4,506,163 2.8730
ws 02 4,671,849 3.0469
pipe 01 4,506,154 4.2098
ws 01 4,671,843 3.3289
pipe 00 4,506,265 3.1196
ws 00 4,671,979 3.2538

How to read the qmux table:

  • Heap counts live requested allocator bytes for both endpoints, tasks, stream tables, pipes, and receive backings, after two untimed refill rounds. It excludes the runtime and the shared source payload.
  • Throughput includes allocation counters, payload verification, and copy cost.
  • These are shared-host observations, not a regression verdict. Criterion's automatic comparisons are not acceptance evidence.
  • The fixture isolates receive ownership from TLS, kernel sockets, congestion, and full MoQ protocol costs.

Benchmark comparison (2026-11-04)

Maintainer decision 2026-11-04: no host is quiet, so accept noisy numbers on the quietest available host and record its load. Surveyed with uptime at 17:55 and 19:12 UTC:

  • nixos2 (12 threads): load 4.6 at 17:55, quietest then. It went to 9-12 once another session's pinned relay-CPU bisect (quest/m1/release-perf/relay-cpu-bisect) began running there. That session asked me to stop because each run voids the other's numbers, so I stopped my first round on nixos2 and discarded it.
  • nixos (Ryzen 9 5900XT, 16 cores, 32 threads): load 25-29, similar per core to nixos2, and no pinned benchmark ran there. All numbers below come from nixos.
  • kixtop (10 cores): load 15-24.

Method. cargo bench -p moq-net --features async,fuzz --bench group, built separately for each revision. Each run used Criterion with 1 s warm-up and 2 s measurement. The revisions alternated per group, and the order reversed every round. Each cell is the median over six rounds, with the spread as min/max relative to that median. Ratios are medians, and each geomean is taken over a group's cases.

Summary (ratio < 1 is faster):

Group Head ÷ main (this PR) Head ÷ e7a5c10 (cumulative) #7388 ÷ its parent (Run B)
group_read_frames (35 cases) 0.926 1.018 0.979
group_relay_frames (18) 0.993 1.141, 17 of 18 cases >5% slower 1.035, all 12 cases with 512+ frames +4.6-8.1%
group_contended (36) 1.078 0.712 -
group_paced (3) 1.042 (1: 1.009, 8: 1.056, 64: 1.062) - -

Reading it:

  • This PR (transport-sized window): no measurable cost over main. group_contended's 16-thread cases swing by two orders of magnitude between rounds at this load; its 1- and 4-thread cases geomean 1.036 and 1.021, against 1.185 at 16 threads. group_paced is +1-6%, inside its -18/+45% round spread.
  • Pacing itself (feat(net): pace relay ingest by the group's fastest reader #7388): under the quietest conditions (Run B, spreads mostly within ±5%), group_read_frames is unchanged. group_relay_frames is 4.6-8.1% slower in every case of 512 or 8,192 frames at 1, 8, and 64 subscribers, and its 4-frame cases are unchanged. The bench frames carry 64-byte payloads, so a per-frame cost shows at its largest share. That fits the per-read progress report feat(net): pace relay ingest by the group's fastest reader #7388 added (Pace::took in rs/moq-net/src/model/pace.rs) and the pace offset it stores on each log entry; it wasn't profiled. Media-sized frames dilute it.
  • main drift: in the noisy Run A, main reads group_relay_frames about 15% slower than e7a5c10. Pacing explains about 4% of that, so most of it comes from other commits between e7a5c10 and main.
group_relay_frames, Run B: #7388 against its parent (load 1.8-16.5)
case parent b51eb8c #7388 7ad8d42 #7388 ÷ parent
plain/4/1 188 ns (-4/+4%) 184 ns (-4/+2%) 0.979
properties/4/1 213 ns (-3/+15%) 214 ns (-4/+3%) 1.002
plain/4/8 1.5 µs (-7/+4%) 1.5 µs (-2/+5%) 0.973
properties/4/8 1.7 µs (-7/+4%) 1.7 µs (-4/+2%) 0.990
plain/4/64 12.4 µs (-8/+2%) 12.1 µs (-4/+24%) 0.971
properties/4/64 13.4 µs (-7/+4%) 13.3 µs (-3/+19%) 0.992
plain/512/1 13.9 µs (-5/+19%) 15.0 µs (-4/+12%) 1.081
properties/512/1 16.3 µs (-5/+7%) 17.5 µs (-4/+21%) 1.069
plain/512/8 112.5 µs (-6/+6%) 121.6 µs (-5/+9%) 1.081
properties/512/8 132.4 µs (-5/+2%) 140.9 µs (-4/+2%) 1.064
plain/512/64 905.3 µs (-5/+2%) 964.9 µs (-4/+11%) 1.066
properties/512/64 1.04 ms (-4/+4%) 1.10 ms (-1/+9%) 1.060
plain/8192/1 224.4 µs (-5/+3%) 235.2 µs (-1/+44%) 1.048
properties/8192/1 265.0 µs (-6/+4%) 277.4 µs (-2/+19%) 1.047
plain/8192/8 1.77 ms (-4/+1%) 1.91 ms (-2/+36%) 1.079
properties/8192/8 2.10 ms (-4/+4%) 2.21 ms (-3/+26%) 1.051
plain/8192/64 14.38 ms (-5/+4%) 15.14 ms (-7/+9%) 1.053
properties/8192/64 16.71 ms (-3/+5%) 17.48 ms (-4/+14%) 1.046
group_relay_frames, Run A
case base main head head ÷ base head ÷ main
plain/4/1 198 ns (-8/+77%) 222 ns (-8/+56%) 228 ns (-10/+42%) 1.149 1.025
properties/4/1 222 ns (-6/+76%) 252 ns (-8/+39%) 260 ns (-10/+26%) 1.171 1.030
plain/4/8 1.6 µs (-5/+68%) 1.8 µs (-5/+76%) 1.8 µs (-7/+65%) 1.131 1.027
properties/4/8 1.8 µs (-8/+70%) 1.9 µs (-4/+55%) 2.0 µs (-8/+57%) 1.126 1.051
plain/4/64 13.4 µs (-10/+60%) 15.6 µs (-13/+57%) 14.6 µs (-8/+58%) 1.091 0.936
properties/4/64 13.7 µs (-5/+69%) 16.1 µs (-8/+59%) 15.5 µs (-6/+57%) 1.128 0.960
plain/512/1 14.6 µs (-4/+45%) 19.7 µs (-22/+33%) 16.7 µs (-7/+35%) 1.149 0.847
properties/512/1 16.6 µs (-4/+43%) 20.1 µs (-11/+39%) 19.8 µs (-9/+28%) 1.188 0.984
plain/512/8 129.8 µs (-14/+30%) 139.0 µs (-10/+59%) 134.1 µs (-8/+37%) 1.033 0.965
properties/512/8 140.7 µs (-9/+4%) 165.7 µs (-11/+42%) 154.8 µs (-7/+28%) 1.100 0.934
plain/512/64 938.5 µs (-8/+16%) 1.14 ms (-7/+35%) 1.14 ms (-13/+32%) 1.220 1.002
properties/512/64 1.08 ms (-7/+33%) 1.29 ms (-6/+30%) 1.25 ms (-6/+31%) 1.157 0.968
plain/8192/1 232.6 µs (-7/+52%) 264.3 µs (-4/+47%) 271.8 µs (-9/+30%) 1.169 1.028
properties/8192/1 270.2 µs (-5/+46%) 310.2 µs (-6/+41%) 327.5 µs (-12/+24%) 1.212 1.056
plain/8192/8 1.97 ms (-12/+43%) 2.10 ms (-6/+49%) 2.28 ms (-13/+32%) 1.152 1.082
properties/8192/8 2.24 ms (-9/+37%) 2.47 ms (-6/+44%) 2.56 ms (-11/+28%) 1.142 1.037
plain/8192/64 15.60 ms (-10/+43%) 17.77 ms (-11/+34%) 17.81 ms (-12/+34%) 1.142 1.002
properties/8192/64 18.13 ms (-9/+30%) 20.46 ms (-10/+8%) 19.90 ms (-6/+24%) 1.098 0.973
group_read_frames, Run A
case base main head head ÷ base head ÷ main
single/4 294 ns (-25/+21%) 310 ns (-15/+27%) 282 ns (-6/+54%) 0.959 0.907
batch4/4 310 ns (-14/+62%) 413 ns (-31/+24%) 318 ns (-7/+69%) 1.029 0.771
batch8/4 341 ns (-11/+67%) 448 ns (-28/+33%) 358 ns (-7/+65%) 1.050 0.799
batch32/4 610 ns (-44/+38%) 496 ns (-20/+64%) 436 ns (-7/+54%) 0.713 0.878
batch128/4 1.2 µs (-36/+14%) 775 ns (-13/+9%) 841 ns (-10/+47%) 0.729 1.085
single/5 323 ns (-17/+32%) 358 ns (-14/+11%) 352 ns (-10/+43%) 1.091 0.983
batch4/5 484 ns (-32/+25%) 408 ns (-13/+52%) 406 ns (-4/+78%) 0.838 0.995
batch8/5 417 ns (-24/+51%) 448 ns (-17/+67%) 454 ns (-19/+45%) 1.090 1.013
batch32/5 580 ns (-40/+27%) 624 ns (-23/+36%) 498 ns (-17/+48%) 0.860 0.799
batch128/5 996 ns (-35/+29%) 847 ns (-10/+36%) 904 ns (-22/+50%) 0.907 1.067
single/32 1.5 µs (-19/+18%) 1.6 µs (-5/+22%) 1.6 µs (-6/+36%) 1.008 0.987
batch4/32 1.5 µs (-21/+43%) 1.4 µs (-11/+52%) 1.4 µs (-7/+37%) 0.942 0.964
batch8/32 1.4 µs (-18/+38%) 1.4 µs (-12/+51%) 1.3 µs (-6/+51%) 0.961 0.937
batch32/32 1.2 µs (-13/+55%) 1.6 µs (-21/+40%) 1.4 µs (-4/+66%) 1.085 0.832
batch128/32 1.6 µs (-13/+52%) 2.0 µs (-12/+60%) 2.0 µs (-19/+47%) 1.245 0.986
single/33 1.5 µs (-16/+26%) 1.7 µs (-6/+33%) 1.6 µs (-2/+14%) 1.083 0.969
batch4/33 1.4 µs (-15/+49%) 1.6 µs (-10/+35%) 1.5 µs (-4/+41%) 1.064 0.954
batch8/33 1.8 µs (-38/+19%) 1.6 µs (-17/+37%) 1.5 µs (-5/+7%) 0.798 0.938
batch32/33 1.9 µs (-41/+52%) 1.7 µs (-19/+32%) 1.6 µs (-4/+8%) 0.839 0.929
batch128/33 2.4 µs (-41/+19%) 2.4 µs (-32/+34%) 2.0 µs (-6/+42%) 0.850 0.836
single/512 21.1 µs (-10/+22%) 27.2 µs (-19/+9%) 23.6 µs (-2/+22%) 1.117 0.866
batch4/512 19.8 µs (-21/+31%) 21.9 µs (-20/+28%) 19.4 µs (-4/+17%) 0.981 0.885
batch8/512 16.3 µs (-9/+51%) 22.6 µs (-21/+23%) 18.6 µs (-5/+13%) 1.141 0.824
batch32/512 15.7 µs (-15/+48%) 20.5 µs (-15/+30%) 18.3 µs (-5/+22%) 1.163 0.890
batch128/512 20.4 µs (-33/+23%) 21.4 µs (-18/+30%) 18.8 µs (-2/+12%) 0.917 0.876
single/2048 80.2 µs (-10/+35%) 97.3 µs (-5/+32%) 95.6 µs (-5/+24%) 1.192 0.983
batch4/2048 74.1 µs (-16/+39%) 80.8 µs (-5/+44%) 80.4 µs (-10/+36%) 1.084 0.995
batch8/2048 73.7 µs (-20/+36%) 79.4 µs (-6/+44%) 85.0 µs (-17/+26%) 1.153 1.070
batch32/2048 68.7 µs (-23/+36%) 95.8 µs (-23/+15%) 77.5 µs (-12/+12%) 1.128 0.809
batch128/2048 64.2 µs (-17/+48%) 97.9 µs (-29/+17%) 78.8 µs (-7/+43%) 1.229 0.805
single/8192 341.8 µs (-12/+27%) 455.1 µs (-13/+24%) 397.1 µs (-6/+37%) 1.162 0.873
batch4/8192 305.7 µs (-17/+59%) 352.6 µs (-16/+41%) 319.1 µs (-7/+17%) 1.044 0.905
batch8/8192 278.8 µs (-14/+56%) 319.5 µs (-9/+56%) 313.5 µs (-5/+62%) 1.124 0.981
batch32/8192 257.3 µs (-15/+17%) 302.7 µs (-3/+92%) 351.8 µs (-17/+30%) 1.367 1.162
batch128/8192 293.7 µs (-27/+41%) 311.2 µs (-6/+58%) 320.0 µs (-7/+19%) 1.089 1.028
group_read_frames, Run B
case parent b51eb8c #7388 7ad8d42 #7388 ÷ parent
single/4 219 ns (-1/+2%) 220 ns (-3/+2%) 1.005
batch4/4 250 ns (-3/+3%) 254 ns (-4/+1%) 1.018
batch8/4 296 ns (-2/+6%) 288 ns (-4/+2%) 0.973
batch32/4 333 ns (-4/+4%) 334 ns (-2/+27%) 1.003
batch128/4 633 ns (-8/+9%) 610 ns (-3/+7%) 0.964
single/5 264 ns (-4/+2%) 262 ns (-2/+3%) 0.992
batch4/5 312 ns (-4/+5%) 316 ns (-2/+2%) 1.016
batch8/5 328 ns (-5/+28%) 318 ns (-3/+1%) 0.968
batch32/5 368 ns (-5/+28%) 360 ns (-3/+7%) 0.978
batch128/5 672 ns (-7/+36%) 634 ns (-3/+24%) 0.943
single/32 1.3 µs (-3/+22%) 1.2 µs (-3/+3%) 0.985
batch4/32 1.1 µs (-4/+42%) 1.1 µs (-3/+13%) 0.982
batch8/32 1.1 µs (-2/+22%) 1.1 µs (-2/+8%) 0.974
batch32/32 1.0 µs (-4/+40%) 1.0 µs (-2/+10%) 0.992
batch128/32 1.4 µs (-8/+35%) 1.3 µs (-2/+5%) 0.939
single/33 1.3 µs (-2/+29%) 1.3 µs (-1/+3%) 0.991
batch4/33 1.1 µs (-1/+64%) 1.1 µs (-2/+2%) 1.009
batch8/33 1.2 µs (-1/+15%) 1.1 µs (-2/+4%) 0.970
batch32/33 1.1 µs (-3/+21%) 1.1 µs (-3/+2%) 0.967
batch128/33 1.6 µs (-14/+6%) 1.4 µs (-4/+8%) 0.863
single/512 18.8 µs (-3/+9%) 18.2 µs (-2/+7%) 0.972
batch4/512 14.9 µs (-2/+2%) 14.7 µs (-2/+10%) 0.987
batch8/512 15.3 µs (-3/+3%) 14.4 µs (-1/+26%) 0.940
batch32/512 13.6 µs (-2/+5%) 13.3 µs (-2/+9%) 0.978
batch128/512 13.8 µs (-1/+13%) 13.5 µs (-2/+5%) 0.977
single/2048 74.0 µs (-1/+4%) 72.5 µs (-1/+6%) 0.980
batch4/2048 58.4 µs (-1/+4%) 59.0 µs (-3/+1%) 1.011
batch8/2048 59.5 µs (-1/+7%) 57.8 µs (-2/+10%) 0.971
batch32/2048 54.0 µs (-3/+6%) 53.2 µs (-2/+9%) 0.984
batch128/2048 53.9 µs (-3/+7%) 53.1 µs (-2/+5%) 0.986
single/8192 306.0 µs (-3/+8%) 306.6 µs (-2/+5%) 1.002
batch4/8192 237.4 µs (-2/+11%) 241.7 µs (-3/+8%) 1.018
batch8/8192 244.6 µs (-2/+11%) 242.6 µs (-3/+7%) 0.992
batch32/8192 224.7 µs (-4/+7%) 218.9 µs (-2/+8%) 0.974
batch128/8192 230.9 µs (-6/+5%) 221.5 µs (-1/+23%) 0.959
group_contended, Run A
case base main head head ÷ base head ÷ main
burst_1/threads_1/16 1.9 µs (-20/+48%) 2.0 µs (-12/+79%) 2.3 µs (-31/+14%) 1.258 1.158
burst_1/threads_1/256 22.5 µs (-14/+37%) 22.0 µs (-7/+49%) 21.2 µs (-3/+32%) 0.939 0.961
burst_1/threads_1/1024 74.0 µs (-7/+70%) 90.2 µs (-11/+15%) 82.2 µs (-2/+37%) 1.111 0.911
burst_1/threads_4/16 4.7 µs (-20/+2365%) 4.5 µs (-3/+7%) 4.6 µs (-8/+39%) 0.984 1.024
burst_1/threads_4/256 53.9 µs (-37/+158%) 40.1 µs (-11/+276%) 39.0 µs (-12/+17%) 0.724 0.972
burst_1/threads_4/1024 184.0 µs (-29/+82%) 156.4 µs (-9/+28%) 159.7 µs (-16/+19%) 0.868 1.021
burst_1/threads_16/16 180.8 µs (-93/+911%) 15.1 µs (-13/+436%) 34.3 µs (-65/+567%) 0.189 2.274
burst_1/threads_16/256 192.2 µs (-65/+248%) 94.9 µs (-23/+578%) 143.2 µs (-55/+127%) 0.745 1.509
burst_1/threads_16/1024 426.7 µs (-44/+205%) 332.3 µs (-9/+561%) 404.2 µs (-42/+46%) 0.947 1.216
burst_8/threads_1/16 8.1 µs (-20/+53%) 8.2 µs (-10/+31%) 8.3 µs (-11/+21%) 1.027 1.005
burst_8/threads_1/256 89.0 µs (-19/+61%) 94.4 µs (-5/+36%) 98.6 µs (-12/+30%) 1.108 1.045
burst_8/threads_1/1024 391.2 µs (-28/+33%) 368.6 µs (-5/+17%) 397.8 µs (-13/+27%) 1.017 1.079
burst_8/threads_4/16 15.0 µs (-21/+517%) 12.8 µs (-3/+12%) 12.7 µs (-7/+133%) 0.848 0.996
burst_8/threads_4/256 166.9 µs (-50/+98%) 168.9 µs (-3/+2%) 165.6 µs (-23/+30%) 0.992 0.981
burst_8/threads_4/1024 632.7 µs (-8/+106%) 643.9 µs (-9/+12%) 651.0 µs (-19/+8%) 1.029 1.011
burst_8/threads_16/16 229.3 µs (-92/+819%) 18.7 µs (-4/+575%) 22.8 µs (-24/+7078%) 0.099 1.220
burst_8/threads_16/256 645.4 µs (-85/+335%) 95.5 µs (-3/+661%) 101.7 µs (-7/+3170%) 0.158 1.065
burst_8/threads_16/1024 1.49 ms (-77/+101%) 374.0 µs (-7/+177%) 374.0 µs (-8/+515%) 0.251 1.000
subscribed/burst_1/threads_1/16 2.6 µs (-34/+64%) 2.4 µs (-14/+63%) 2.7 µs (-37/+24%) 1.009 1.098
subscribed/burst_1/threads_1/256 28.0 µs (-31/+209%) 24.3 µs (-3/+75%) 24.9 µs (-11/+51%) 0.889 1.022
subscribed/burst_1/threads_1/1024 132.4 µs (-42/+69%) 98.1 µs (-4/+61%) 117.9 µs (-23/+26%) 0.891 1.203
subscribed/burst_1/threads_4/16 6.7 µs (-7/+996%) 5.6 µs (-9/+10%) 5.8 µs (-16/+49%) 0.880 1.037
subscribed/burst_1/threads_4/256 69.9 µs (-29/+193%) 48.7 µs (-7/+29%) 52.4 µs (-17/+49%) 0.750 1.077
subscribed/burst_1/threads_4/1024 233.4 µs (-7/+71%) 209.5 µs (-11/+36%) 215.6 µs (-21/+24%) 0.924 1.029
subscribed/burst_1/threads_16/16 147.2 µs (-92/+1159%) 16.4 µs (-18/+392%) 15.5 µs (-18/+340%) 0.105 0.941
subscribed/burst_1/threads_16/256 179.8 µs (-55/+243%) 110.6 µs (-35/+122%) 81.7 µs (-12/+146%) 0.455 0.739
subscribed/burst_1/threads_16/1024 396.1 µs (-27/+160%) 302.9 µs (-15/+70%) 287.6 µs (-10/+146%) 0.726 0.949
subscribed/burst_8/threads_1/16 9.1 µs (-30/+20%) 8.4 µs (-5/+10%) 8.3 µs (-5/+16%) 0.915 0.988
subscribed/burst_8/threads_1/256 96.4 µs (-21/+32%) 93.3 µs (-2/+21%) 93.2 µs (-3/+24%) 0.968 0.999
subscribed/burst_8/threads_1/1024 360.3 µs (-19/+43%) 382.9 µs (-6/+20%) 380.9 µs (-7/+12%) 1.057 0.995
subscribed/burst_8/threads_4/16 13.3 µs (-9/+244%) 13.9 µs (-9/+6%) 14.2 µs (-22/+62%) 1.071 1.025
subscribed/burst_8/threads_4/256 165.1 µs (-23/+128%) 169.4 µs (-5/+9%) 173.3 µs (-7/+10%) 1.049 1.023
subscribed/burst_8/threads_4/1024 667.7 µs (-14/+120%) 663.8 µs (-21/+10%) 702.9 µs (-8/+30%) 1.053 1.059
subscribed/burst_8/threads_16/16 29.6 µs (-39/+6189%) 23.0 µs (-18/+43%) 23.2 µs (-22/+1774%) 0.785 1.011
subscribed/burst_8/threads_16/256 253.9 µs (-60/+201%) 119.7 µs (-10/+443%) 177.2 µs (-44/+1419%) 0.698 1.481
subscribed/burst_8/threads_16/1024 539.2 µs (-31/+132%) 418.6 µs (-11/+300%) 596.5 µs (-40/+338%) 1.106 1.425
group_paced, Run A
case main head head ÷ main
1 434 ns (-18/+42%) 438 ns (-14/+45%) 1.009
8 1.0 µs (-7/+35%) 1.1 µs (-14/+35%) 1.056
64 3.3 µs (-12/+32%) 3.5 µs (-17/+23%) 1.062

Impact

  • Public API: none.
  • Wire: none.
  • Production behavior: a relay or client now buffers about one stream receive window (1 MiB at the default [quic] windows) of a paced group past its fastest reader, instead of 16 MiB. A transport whose stream credit is 16 MiB or more paces at 16 MiB as before.

Validation

All local. just ci ran on fdec1c3, the merge of main; head 23ffbbfa adds only the quest edit:

  • just ci: every lane passed except check flake, including all 11,676 Rust tests (133 skipped).
  • check flake is a pre-existing failure. The Nix github-runner image build fails with unshare: write failed /proc/self/uid_map: Operation not permitted.
  • The quest edit passed quest check and quest check --base origin/main.
  • just check on head passed every lane, including 11,676 Rust tests.
    • test gh was cut off by a tool time limit while it waited in quest gate.
    • It then passed alone (CHECK_MODULES=gh just ci --all).

Follow-ups

  • The broader real-transport MoQ session benchmark series stays in transport x version benches.
  • Out of scope: the relay advertises qmux-02 as a bare fallback, while versioned {qmux}.{moq} pairs list only 00/01. Recommend a separate quest to decide whether to advertise versioned qmux-02 pairs; this attempt does not change negotiation.
  • If the Waiting decision picks the follow-up option: trim pacing's per-frame progress report on the batch read path.
    • Cases: group_relay_frames with 512+ frames.
    • Baseline: b51eb8c.
  • Recommend a quest to bisect the remaining ~10% group_relay_frames drift between e7a5c10 and main. Measure it on a quiet host first, since Run A was noisy. The relay-CPU bisect in quest/m1/release-perf/ may already cover it.

Decisions (maintainer, 2026-11-04)

  • Pacing's 4.6-8.1% cost on relay fan-out reads (moq-dev/moq#7388, 512+ tiny frames):
    • ✅ Accept it and finish this quest, with a follow-up trim. That's quest/m1/release-perf/pacing-report.md, added in this PR.
    • Trim it in this PR first
    • Accept it with no follow-up
  • main's remaining ~10% group_relay_frames drift:
    • ✅ Fold it into the existing release-perf relay-CPU bisect quest. The evidence is added there.
    • A separate quest
    • Drop it

This PR now finishes the quest. It deletes quest/m1/unbounded-groups/fastest-reader.md and points prose links to it at this PR.

Review

  • Latest: Codex gpt-6.1-sol, effort medium (quest review), on head f4d1a02dbe609a6dfcd412d528eeeb180c30c8aa, no findings. That head deletes the finished quest and adds the release-perf follow-ups on top of 23ffbbfa5, which had its own clean round.
  • quest check --base origin/main (605 documents) and sh/markdown.sh check pass.

🤖 Generated with Claude Code

(Written by Claude Opus 5.5)

kixelated and others added 4 commits November 2, 2026 21:00
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
@kixelated kixelated changed the title Fastest-reader pacing test(net): record fastest-reader relay and qmux evidence Nov 3, 2026
kixelated and others added 3 commits November 3, 2026 12:46
A session's ingest now stops reading a group's stream once it is the
stream's receive credit past the group's fastest reader, capped at the
16 MiB ahead half of the live tail, instead of always 16 MiB. Each
stream takes the window from its session's receive windows when it is
accepted or opened. Local writers keep the 16 MiB window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rvey

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Design change: per the maintainer's 2026-11-03 /quest-unblock decision, ingest now paces each group at its stream's receive credit, capped at 16 MiB, instead of the fixed model::pace::AHEAD = 16 MiB. Each stream reads the window from its session's receive_window when it is accepted or opened. Local writers keep 16 MiB.

With 1 MiB of stream credit per hop, the two-relay test's downstream cache drops from about 16.7 MB to about 0.99 MB. The upstream cache, including sent history, drops from about 33.5 MB to about 3.5 MB.

The quiet-host benchmark comparison is still waiting on a free host, so this stays a draft.

(Written by Claude Opus 5.5)

kixelated and others added 2 commits November 4, 2026 09:55
The maintainer accepted noisy numbers on 2026-11-04, so the paired
group benchmarks ran on nixos with load recorded per run. The
transport-sized window costs nothing measurable over main; pacing
itself costs group_relay_frames 4.6-8.1% on groups of 512+ frames,
which the quest now waits on the maintainer to accept or trim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… note main's drift

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review November 4, 2026 23:55
@kixelated
kixelated enabled auto-merge November 4, 2026 23:55
@coderabbitai

coderabbitai Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 13 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 81443fa3-4cb7-4987-ac4c-9362caf3b3cb
Commits

Reviewing files that changed from the base of the PR and between f37d964 and f4d1a02.

Files ignored due to path filters: 1
  • Cargo.lock is excluded by !**/*.lock
Files selected for processing: 25
  • bench/README.md
  • doc/concept/moq-lite.md
  • quest/m1/release-perf/README.md
  • quest/m1/release-perf/pacing-report.md
  • quest/m1/release-perf/relay-cpu-bisect.md
  • quest/m1/socket/README.md
  • quest/m1/unbounded-groups/README.md
  • quest/m1/unbounded-groups/fastest-reader.md
  • quest/m1/unbounded-groups/flights.md
  • quest/m2/js-fastest-reader.md
  • quest/m2/receive-pool.md
  • quest/m2/stream/README.md
  • quest/m2/stream/rust.md
  • rs/moq-net/src/coding/reader.rs
  • rs/moq-net/src/model/frame.rs
  • rs/moq-net/src/model/group.rs
  • rs/moq-net/src/model/mod.rs
  • rs/moq-net/src/model/pace.rs
  • rs/moq-net/src/recv.rs
  • rs/moq-net/src/transport/boxed.rs
  • rs/moq-net/tests/integration/fastest_reader.rs
  • rs/moq-relay/src/web.rs
  • rs/qmux/Cargo.toml
  • rs/qmux/benches/receive.rs
  • sh/gh/nightly.sh
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@kixelated
kixelated added this pull request to the merge queue Nov 4, 2026

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed commit: f4d1a02

The per-stream pacing direction is reasonable and I found no actionable production pacing defect in static inspection. The new nightly comparison has a baseline-discovery issue below.

Static review only; I did not run tests or reproduce the reported measurements.

Comment thread sh/gh/nightly.sh
;;
# Keep added coverage out of the rust-bench and rust-shared legs.
rust-extra)
criterion qmux --manifest-path rs/qmux/Cargo.toml --bench receive

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Scope baseline target discovery to this manifest. criterion_build_base recognizes -p/--package but not --manifest-path, so on a previous-nightly baseline without qmux's new receive benchmark it finds moq-quic's same-named target across the workspace and retains --bench receive. The qmux baseline build then fails rather than treating the benchmark as new, causing the paired scheduled gate to fail. Teach discovery to honor the manifest (and test a new target sharing another package's name).

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — one minor doc omission.

Reviewed changes

  • Transport-sized ingest pacing — a session's ingest stream now takes its pacing window from the session's receive credit (min(stream, connection, 16 MiB AHEAD)) once at accept/open, instead of the fixed 16 MiB; local writers keep AHEAD. Pause::window, coding::Reader, recv::Group::poll_admit, and the boxed stream carry it through.
  • Two-hop retention evidence — new 64 MiB two-relay test pins each hop's cache charge while the final reader is parked, then verifies every byte and clean completion.
  • Relay WebSocket accept evidence — a real Relay test decodes the QMux transport parameters advertised during the WS upgrade and pins the operator's windows and stream counts across qmux-00/01/02.
  • qmux receive benchmark — new qmux_receive target sweeps pipe/WebSocket × versions × 1/8/64 sessions × 1/8 streams with a live-allocator heap counter, wired into Nightly's rust-extra leg.
  • Quest/docs — the finished quest is deleted, prose links point at this PR, and the pacing-report/relay-CPU follow-ups are recorded.

I traced the production path: the stored window is this endpoint's grant to the peer, window() min's it with the connection window and the 16 MiB tail half, and pause() still reserves min(stream, connection), so a paced stream always has credit to reserve; a reader that took every committed frame is never paced, so large frames still cannot deadlock. None windows keep the old AHEAD behavior, and nothing here changes the public API or wire.

ℹ️ Relay config reference still describes the old fixed 16 MB ingest window

doc/bin/relay/config.md:366 still tells operators the relay "receives at most 16 MB of it past the fastest one". After this PR the bound is the stream receive window capped at 16 MB (1 MiB at the client defaults), so the one user-facing place that explains the relay's memory under [cache] now overstates it by 16x. The same paragraph was updated in doc/concept/moq-lite.md, so this looks like a missed sibling rather than an intentional choice.

Technical details
# Relay config reference still describes the fixed 16 MB window

## Affected sites
- `doc/bin/relay/config.md:366` — "the relay receives at most 16 MB of it past the fastest one" predates the transport-sized window.

## Required outcome
- The paragraph states that a hop holds its stream receive window of a group past the fastest reader, capped at 16 MB (half the 32 MB tail), matching `doc/concept/moq-lite.md:397-401` and the PR's Impact section.

## Open questions for the human
- Whether to mention the client default (1 MiB) explicitly here, since operators tuning `[quic].stream_receive_window` now also move this bound.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

Merged via the queue into main with commit 925a293 Nov 5, 2026
11 checks passed
@kixelated
kixelated deleted the quest/m1/unbounded-groups/fastest-reader branch November 5, 2026 00:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant