From efec9a91e926839b2745a83584e16b328171c95d Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 30 Sep 2026 11:47:43 -0700 Subject: [PATCH 1/5] quest(m2): plan cut-through group streams Co-Authored-By: Claude Opus 5.5 --- quest/m1/qos/README.md | 2 + quest/m1/quic/upstream.md | 3 +- quest/m2/README.md | 1 + quest/m2/cut-through/README.md | 69 ++++++++++++++++++++++++++++++ quest/m2/cut-through/bench.md | 33 ++++++++++++++ quest/m2/cut-through/frames.md | 32 ++++++++++++++ quest/m2/cut-through/loss-delay.md | 39 +++++++++++++++++ quest/m2/cut-through/noq.md | 32 ++++++++++++++ quest/m2/cut-through/relay.md | 42 ++++++++++++++++++ quest/m2/cut-through/transport.md | 32 ++++++++++++++ 10 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 quest/m2/cut-through/README.md create mode 100644 quest/m2/cut-through/bench.md create mode 100644 quest/m2/cut-through/frames.md create mode 100644 quest/m2/cut-through/loss-delay.md create mode 100644 quest/m2/cut-through/noq.md create mode 100644 quest/m2/cut-through/relay.md create mode 100644 quest/m2/cut-through/transport.md diff --git a/quest/m1/qos/README.md b/quest/m1/qos/README.md index 96e59d38cd..71ac462ef6 100644 --- a/quest/m1/qos/README.md +++ b/quest/m1/qos/README.md @@ -52,3 +52,5 @@ on `main`. - [Media stats](/quest/m1/stats/README.md) - publishers and viewers report their own media, transport, and playback health, the media half of a verdict +- [Loss delay](/quest/m2/cut-through/loss-delay.md) - an ingress counter of + bytes a loss held back by at least one RTT, on the same rows diff --git a/quest/m1/quic/upstream.md b/quest/m1/quic/upstream.md index 796f087fd9..cc97663a6a 100644 --- a/quest/m1/quic/upstream.md +++ b/quest/m1/quic/upstream.md @@ -65,7 +65,8 @@ been offered and answered. - [Keep-alive by deadline](/quest/m2/quic-keep-alive.md), [Careful resume on reconnect](/quest/m2/quic-careful-resume.md), [L4S on the backbone](/quest/m2/quic-ecn.md), - [Discover media headroom](/quest/m2/quic-probe.md) - m2 features offered + [Discover media headroom](/quest/m2/quic-probe.md), + [Offset writes](/quest/m2/cut-through/noq.md) - m2 features offered upstream when they land - [BBR media study](/quest/m2/quic-bbr-natural-drain.md), [Receive timestamps](/quest/m3/quic-receive-ts.md), [GCC](/quest/m3/quic-gcc.md) - diff --git a/quest/m2/README.md b/quest/m2/README.md index f79539a25f..021ae6af59 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -114,3 +114,4 @@ waiting on an upstream release waits in [m4](/quest/m4/README.md). - [Announcement shapes](/quest/m2/announce-shapes.md) - moq-lite announcements and interests carry prefix, exact, suffix, or prefix+suffix shapes that survive relay hops, benchmarked over the announce table - [MSFTS convergence](/quest/m2/msfts-convergence.md) - the demultiplexed TS lane converges on MSFTS where the two still differ: program tables and the ES payload unit - [VAAPI encode and decode](/quest/m2/video-vaapi.md) - H.265 encode and decode, pre-generated bindings, and pooled resize surfaces, including the moq-dev/vaapi release that carries them +- [Cut-through](/quest/m2/cut-through/README.md) - a relay forwards bytes behind a QUIC stream hole before the retransmission fills it, if a lossy-hop bench says it is worth it diff --git a/quest/m2/cut-through/README.md b/quest/m2/cut-through/README.md new file mode 100644 index 0000000000..13b35c2fba --- /dev/null +++ b/quest/m2/cut-through/README.md @@ -0,0 +1,69 @@ +# Cut-through group streams + +## Goal + +A relay forwards group stream bytes that arrive past a QUIC stream hole +instead of holding them until the retransmission fills it. Payload after a +lost packet reaches the next hop at its own output offset while the hole is +still being recovered, so a loss on one hop no longer delays everything behind +it on the next. + +The line first measures whether this is worth it, then builds it only if the +measurement says so. A measured no-go deletes the build quests; the loss-delay +metric and the bench stay either way. + +Non-goals: end consumers (players, `moq-cli`) keep ordered reads, since a +decoder needs whole frames. No MoQ wire change; QUIC already allows a sender to +send stream offsets in any order. + +## Plan + +Decided 2026-09-30: + +- Measure first, and plan the build now so the verdict only decides go or + no-go, not the design. The build quests require the [bench](/quest/m2/cut-through/bench.md). +- The gain is roughly the time the relay would spend bursting the held bytes + downstream after the hole fills: small when the egress hop has headroom, + larger for a big I-frame on a tight hop. The bench sweeps exactly that. +- A hole that covers a frame header blocks every later frame on that stream: + the next header's input position and every output header's length depend on + earlier header values (lite's timestamp delta, IETF's object id delta). A hole + inside a payload blocks nothing, since the frame sizes locate both the input + and the output bytes. +- The frame model gains an offset write and a range read, crate-private in + `moq-net`, since the lite and IETF publishers and subscribers that use them + live in the same crate. Cut-through frames keep the received chunks by + offset instead of copying into a pre-allocated buffer, so memory tracks bytes + received and [the allocation budget](/quest/m0/frame-alloc-budget.md) is + unaffected. +- `web-transport-trait` gains an unordered chunk read and an offset write whose + defaults report unsupported, and moq-net falls back to ordered I/O. Only the + noq backends implement them; browsers, qmux, and iroh keep the defaults. + Additive, so it lands on `main`. +- Always on wherever both of the relay's streams support it. No config knob. +- The relay's own egress is noq for every native and browser viewer, so browser + viewers benefit too; only the relay's side needs the feature. + +Public API: additive `web-transport-trait` methods; the loss-delay counter on +`moq-stats` ingress rows. Wire: none for MoQ; the counter is a `moq-stats` +field. + +End to end: once [the relay quest](/quest/m2/cut-through/relay.md) lands, re-run the +bench on the same sweep and record before and after in this README before +closing the line. No new doc page: nothing user-facing changes beyond the +stats field, which its quest documents inline. + +## Required + +- [Loss delay](/quest/m2/cut-through/loss-delay.md) - relays report, per broadcast, the ingress bytes a loss held back by at least one RTT +- [Bench](/quest/m2/cut-through/bench.md) - a lossy relay hop swept over loss, frame size, and egress headroom, against a direct-connection bound, with a go or no-go verdict +- [Offset writes in noq](/quest/m2/cut-through/noq.md) - the fork's send streams accept writes past a gap and send them right away +- [Frame ranges](/quest/m2/cut-through/frames.md) - a frame fills out of order and a reader sees each range as it lands +- [Transport trait](/quest/m2/cut-through/transport.md) - `web-transport-trait` carries unordered reads and offset writes, implemented for noq +- [Relay cut-through](/quest/m2/cut-through/relay.md) - lite and IETF group streams read out of order and write each range at its output offset + +## Related + +- [Unordered qmux](/quest/m2/p2p/unordered.md) - the same head-of-line problem on the data channel transport +- [Hierarchical stream scheduling](/quest/m1/quic/scheduler.md) - orders streams; offset writes order ranges within one +- [QoS](/quest/m1/qos/README.md) - the loss-delay counter follows its per-broadcast ingress row conventions diff --git a/quest/m2/cut-through/bench.md b/quest/m2/cut-through/bench.md new file mode 100644 index 0000000000..6172f2afa6 --- /dev/null +++ b/quest/m2/cut-through/bench.md @@ -0,0 +1,33 @@ +# [M] Benchmark head-of-line blocking at a relay hop + +## Goal + +A nightly bench measures how long a frame takes to complete at the viewer when +the publisher's hop to the relay loses packets, and how much of that a +cut-through relay could win back. It ends with a written go or no-go for the +rest of the [cut-through line](/quest/m2/cut-through/README.md). + +## Plan + +Decided 2026-09-30: real stacks, not a mock. The loss recovery being measured +belongs to noq, so a mock transport with a made-up retransmit model would +partly measure its own assumptions. + +- Publisher, then a seeded `moq-shaper` (loss and delay), then `moq-relay` over + noq, then a clean or rate-limited hop to the viewer. Reuse the shaper wiring + from `rs/moq-relay/tests/drills.rs`. +- Sweep loss rate, frame size (audio-sized through a large I-frame), and egress + headroom (egress rate relative to the media rate). +- Report per-frame completion at the viewer, from publish to last byte, p50 and + p99. The bound is the same path with the relay replaced by a direct + publisher-to-viewer connection over the same shaper: roughly what perfect + cut-through approaches. The gap between relay and direct is the opportunity. +- Record the loss-delay counter from the same runs, if + [Loss delay](/quest/m2/cut-through/loss-delay.md) has landed, so production + numbers can be read against the lab. +- It runs on wall-clock time, so it is a nightly bench, not a unit test. Wire it + into the nightly workflow. + +Write the numbers and the verdict into this line's README. The maintainer makes +the go or no-go call from them; no threshold is set in advance. On a no-go, +delete the build quests and keep the bench. diff --git a/quest/m2/cut-through/frames.md b/quest/m2/cut-through/frames.md new file mode 100644 index 0000000000..1b07388254 --- /dev/null +++ b/quest/m2/cut-through/frames.md @@ -0,0 +1,32 @@ +# [M] Frames fill out of order + +## Goal + +A `moq-net` frame accepts payload ranges at any offset, and a reader can +consume each range as soon as it lands. Existing in-order readers and writers +behave exactly as before. + +## Plan + +Decided 2026-09-30: + +- `frame::Producer` gains an offset write and `frame::Consumer` gains a read + that yields `(offset, Bytes)` ranges in arrival order. Both are + `pub(crate)`: the only users are the lite and IETF sessions in the same crate. +- A frame written out of order keeps the received chunks by offset instead of + copying into a pre-allocated buffer, so its memory tracks bytes received and + [the allocation budget](/quest/m0/frame-alloc-budget.md) does not apply. The + in-order readers (`poll_read_chunk`, `poll_read_all`) still see contiguous + bytes up to the first hole, and `finish` still requires every byte. +- Overlapping or out-of-bounds ranges are errors. +- Today's single sequential writer (`model/frame.rs`, the one `written` + watermark and its safety comments) is the invariant being changed; keep the + ordered fast path, including the whole-frame zero-copy install, unchanged. + +Extend `rs/moq-net/benches/group.rs` so the ordered path shows no regression, +and test that a frame filled in shuffled ranges reads back identical through +both readers. + +## Required + +- [Bench](/quest/m2/cut-through/bench.md) - a go verdict diff --git a/quest/m2/cut-through/loss-delay.md b/quest/m2/cut-through/loss-delay.md new file mode 100644 index 0000000000..157f61582f --- /dev/null +++ b/quest/m2/cut-through/loss-delay.md @@ -0,0 +1,39 @@ +# [M] Report bytes delayed by loss at relay ingest + +## Goal + +The relay's `moq-stats` ingress rows carry, per broadcast, a cumulative +`loss_delay_bytes` counter: stream bytes that arrived behind a hole and stayed +unreadable for at least one smoothed RTT. Compared with bytes received, it +shows how much of a publisher's media a lossy uplink held back, and it is the +production number that sizes the cut-through opportunity. + +## Plan + +Decided 2026-09-30: one monotonic byte counter, no hold-time histogram, on the +ingress (`Role::Subscriber`) rows next to +[publisher timeliness](/quest/m1/qos/publisher-timeliness.md)'s fields. +Independent of the bench; it can start any time. + +- noq counts per receive stream. The assembler stamps each chunk buffered past + the contiguous frontier with its arrival time; when the frontier passes it, + bytes held at least one smoothed RTT are counted. Plain reordering fills + within an RTT and is not counted. The count is by arrival and gap fill, not by + when the application reads, so it stays meaningful once reads go unordered. +- The fork exposes the per-stream count, and `web-transport-trait`'s + `RecvStream` gains an accessor returning `Option` (`None` when the backend + cannot see it), following the trait's `Stats` convention. Release both, as + [poll_acked in web-transport](/quest/m2/quic-ack-hook.md) does. +- The lite and IETF subscribers read the count when a group stream ends and add + it to the broadcast's ingress row. Nothing on the per-byte path. +- Update the stats section of `doc/bin/relay/config.md`. + +Test: a seeded loss on a noq pair counts the held bytes; a reorder shorter than +an RTT counts nothing. + +Public API: an additive trait accessor. Wire: a new `moq-stats` field, so the +PR targets whichever branch the stats wire needs. + +## Related + +- [QoS](/quest/m1/qos/README.md) - the per-broadcast ingress conventions this follows diff --git a/quest/m2/cut-through/noq.md b/quest/m2/cut-through/noq.md new file mode 100644 index 0000000000..86d4960652 --- /dev/null +++ b/quest/m2/cut-through/noq.md @@ -0,0 +1,32 @@ +# [L] Offset writes in the noq fork + +## Goal + +A send stream in moq-dev/noq accepts a write at an offset past unwritten +bytes, sends it right away, and fills the gap when those bytes are written +later. A released `moq-noq-proto` and `moq-noq` carry it. + +## Plan + +Decided 2026-09-30: no write-ahead limit beyond QUIC's own flow control. + +- An offset write reserves the gap and queues the range. Stream and connection + credit is charged up to the highest written offset, as it already is on the + receiver's side. +- Within a stream, lower offsets are sent first once they are written, and + retransmits keep the existing stream priority path. +- `finish` waits until every range below the final size is written; a write + that overlaps an existing range is an error. +- The append-only `write` and `write_chunks` stay as they are; the offset write + is a new method, since a writer that never skips pays nothing. +- Tests in noq-proto's simulated pair: an out-of-order writer delivers the same + bytes as an ordered one, flow control blocks at the highest offset, FIN waits + for the gap, and a reset with a gap outstanding is clean. + +Release the fork crates with the change and record the parent noq commit, per +the [QUIC line's rules](/quest/m1/quic/README.md). Offer it upstream once it has +shipped and settled, like the fork's other m2 features. + +## Required + +- [Bench](/quest/m2/cut-through/bench.md) - a go verdict diff --git a/quest/m2/cut-through/relay.md b/quest/m2/cut-through/relay.md new file mode 100644 index 0000000000..c8f8208de8 --- /dev/null +++ b/quest/m2/cut-through/relay.md @@ -0,0 +1,42 @@ +# [L] Relay cut-through for group streams + +## Goal + +The lite and IETF sessions read incoming group streams out of order and write +each payload range to every subscriber's stream at its output offset, so bytes +behind an ingress hole reach the next hop before the hole is filled. On the +[bench](/quest/m2/cut-through/bench.md), frame completion moves toward the +direct-connection bound. + +## Plan + +Decided 2026-09-30: always on when the ingress and egress streams both support +it, no config knob; otherwise today's ordered path. + +- Ingest: read the stream header in order, then switch to unordered reads. + Frame headers are parsed from the contiguous prefix; once a header is known, + its frame's payload ranges go into the frame with the + [offset write](/quest/m2/cut-through/frames.md) wherever they land. A hole + covering the next header blocks later frames, because that header's position + depends on it. Lite: `FrameIngest` in `lite/subscriber.rs`; IETF: + `GroupIngest` in `ietf/subscriber.rs`. `Reader::poll_read_frame` in + `coding/reader.rs` is the ordered path they share today. +- Egress: each subscriber's output offset for a payload range is its stream + prefix plus every earlier frame's encoded header and size, plus this frame's + header, plus the range offset. That is known once every earlier input header + has been parsed, including across a lite and IETF translation, since output + header lengths depend on values (timestamp and object id deltas). Write the + header, then the ranges with the + [offset write](/quest/m2/cut-through/transport.md). Lite: `GroupServe` in + `lite/publisher.rs`; IETF: `GroupServe` in `ietf/publisher.rs`. +- The fetch paths stay ordered. +- Tests over the mock transport with shuffled chunk delivery: every subscriber + version receives byte-identical streams to the ordered path, and a + header-covering hole holds later frames. + +Re-run the bench and record the result in the line's README. + +## Required + +- [Frame ranges](/quest/m2/cut-through/frames.md) - the model primitive +- [Transport trait](/quest/m2/cut-through/transport.md) - the I/O primitive diff --git a/quest/m2/cut-through/transport.md b/quest/m2/cut-through/transport.md new file mode 100644 index 0000000000..f376b4dee2 --- /dev/null +++ b/quest/m2/cut-through/transport.md @@ -0,0 +1,32 @@ +# [M] Unordered reads and offset writes in web-transport-trait + +## Goal + +A released `web-transport-trait` lets a receive stream yield chunks with their +offsets in arrival order and lets a send stream write at an offset. The noq +backends implement both; every other backend reports that it cannot. + +## Plan + +Decided 2026-09-30: additive methods with defaults, not a moq-net-private +extension trait, which would need per-session detection behind the generic +`Session`. + +- `RecvStream` gains an unordered chunk read returning `(offset, Bytes)`, and + `SendStream` gains an offset write, both in the poll style. Their defaults + report unsupported in the return type (as + [poll_acked](/quest/m2/quic-ack-hook.md) does, since a default cannot build + `Self::Error`), and the caller falls back to ordered I/O. +- noq allows no ordered read after an unordered one + (`moq-noq-proto` `assembler.rs`), so the unordered mode is entered once per + stream and never left. +- Implement both in `web-transport-moq` (released from the fork) over + `RecvStream::into_unordered` and the [offset write](/quest/m2/cut-through/noq.md), + and natively in `rs/moq-uring/src/quic/noq/stream.rs`, which today reads with + `read(true)` and drops the chunk offset. moq-tokio's poll adapter forwards + them. `web-transport-wasm`, qmux, and iroh keep the defaults. +- Bump the pins here. + +## Required + +- [Offset writes in noq](/quest/m2/cut-through/noq.md) - the send-side primitive this exposes From 452f27645bf63541afe587f9a9b494345f434ce8 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 30 Sep 2026 12:16:17 -0700 Subject: [PATCH 2/5] quest(m2): address cut-through review Co-Authored-By: Claude Opus 5.5 --- quest/m2/cut-through/README.md | 10 +++++++--- quest/m2/cut-through/bench.md | 15 ++++++++++++--- quest/m2/cut-through/frames.md | 18 +++++++++++++----- quest/m2/cut-through/loss-delay.md | 2 ++ quest/m2/cut-through/relay.md | 11 ++++++++++- 5 files changed, 44 insertions(+), 12 deletions(-) diff --git a/quest/m2/cut-through/README.md b/quest/m2/cut-through/README.md index 13b35c2fba..033191bac3 100644 --- a/quest/m2/cut-through/README.md +++ b/quest/m2/cut-through/README.md @@ -29,7 +29,11 @@ Decided 2026-09-30: the next header's input position and every output header's length depend on earlier header values (lite's timestamp delta, IETF's object id delta). A hole inside a payload blocks nothing, since the frame sizes locate both the input - and the output bytes. + and the output bytes. Passing a hole into later frames needs several + in-flight frames per group; the relay quest prototypes both scopes and keeps + the one the bench justifies. +- The opportunity is measured on the real two-hop path, not against a direct + connection: the gain exists only with a relay in the middle. - The frame model gains an offset write and a range read, crate-private in `moq-net`, since the lite and IETF publishers and subscribers that use them live in the same crate. Cut-through frames keep the received chunks by @@ -56,9 +60,9 @@ stats field, which its quest documents inline. ## Required - [Loss delay](/quest/m2/cut-through/loss-delay.md) - relays report, per broadcast, the ingress bytes a loss held back by at least one RTT -- [Bench](/quest/m2/cut-through/bench.md) - a lossy relay hop swept over loss, frame size, and egress headroom, against a direct-connection bound, with a go or no-go verdict +- [Bench](/quest/m2/cut-through/bench.md) - a lossy relay hop swept over loss, frame size, and egress headroom, measuring the post-hole drain time, with a go or no-go verdict - [Offset writes in noq](/quest/m2/cut-through/noq.md) - the fork's send streams accept writes past a gap and send them right away -- [Frame ranges](/quest/m2/cut-through/frames.md) - a frame fills out of order and a reader sees each range as it lands +- [Frame ranges](/quest/m2/cut-through/frames.md) - frames fill out of order, several at once per group, and a reader sees each range as it lands - [Transport trait](/quest/m2/cut-through/transport.md) - `web-transport-trait` carries unordered reads and offset writes, implemented for noq - [Relay cut-through](/quest/m2/cut-through/relay.md) - lite and IETF group streams read out of order and write each range at its output offset diff --git a/quest/m2/cut-through/bench.md b/quest/m2/cut-through/bench.md index 6172f2afa6..eeecd4b53d 100644 --- a/quest/m2/cut-through/bench.md +++ b/quest/m2/cut-through/bench.md @@ -19,9 +19,18 @@ partly measure its own assumptions. - Sweep loss rate, frame size (audio-sized through a large I-frame), and egress headroom (egress rate relative to the media rate). - Report per-frame completion at the viewer, from publish to last byte, p50 and - p99. The bound is the same path with the relay replaced by a direct - publisher-to-viewer connection over the same shaper: roughly what perfect - cut-through approaches. The gap between relay and direct is the opportunity. + p99. The opportunity is the drain time on this same two-hop path: per frame, + the time from the relay's ingress hole filling to the frame completing at the + viewer, minus the same for frames with no hole. That excess is the burst + cut-through removes. No direct publisher-to-viewer control: the gain exists + only with a relay in the middle, and one QUIC hop already reassembles out of + order, so a direct run measures a different topology (review of #4616). +- Split the held bytes into the frame that has the hole and later frames, so + the verdict shows what [multiple in-flight frames](/quest/m2/cut-through/frames.md) + add beyond the current frame. +- Take publisher and subscriber counts as parameters, so + [relay cut-through](/quest/m2/cut-through/relay.md) reuses the rig for its + fan-out sweep. - Record the loss-delay counter from the same runs, if [Loss delay](/quest/m2/cut-through/loss-delay.md) has landed, so production numbers can be read against the lab. diff --git a/quest/m2/cut-through/frames.md b/quest/m2/cut-through/frames.md index 1b07388254..787f9562e5 100644 --- a/quest/m2/cut-through/frames.md +++ b/quest/m2/cut-through/frames.md @@ -1,10 +1,10 @@ -# [M] Frames fill out of order +# [L] Frames fill out of order ## Goal -A `moq-net` frame accepts payload ranges at any offset, and a reader can -consume each range as soon as it lands. Existing in-order readers and writers -behave exactly as before. +A `moq-net` frame accepts payload ranges at any offset, a group can hold several +in-flight frames at once, and a reader can consume each range as soon as it +lands. Existing in-order readers and writers behave exactly as before. ## Plan @@ -19,13 +19,21 @@ Decided 2026-09-30: in-order readers (`poll_read_chunk`, `poll_read_all`) still see contiguous bytes up to the first hole, and `finish` still requires every byte. - Overlapping or out-of-bounds ranges are errors. +- A group holds several in-flight frames, not only the one `partial` in + `GroupState` (`model/group.rs`), and `create_frame_owned` stops rejecting a + second open frame. Frames are still created in order: frame N+1 exists only + once N's header, and so its size, is known, because that is what locates + N+1's header. The group consumer still yields frames in order. Decided + 2026-09-30 after review of #4616, so a hole in frame N does not hold back + N+1's bytes. - Today's single sequential writer (`model/frame.rs`, the one `written` watermark and its safety comments) is the invariant being changed; keep the ordered fast path, including the whole-frame zero-copy install, unchanged. Extend `rs/moq-net/benches/group.rs` so the ordered path shows no regression, and test that a frame filled in shuffled ranges reads back identical through -both readers. +both readers, and that frame N+1's ranges reach a range reader while N still +has a hole. ## Required diff --git a/quest/m2/cut-through/loss-delay.md b/quest/m2/cut-through/loss-delay.md index 157f61582f..f6b9be6823 100644 --- a/quest/m2/cut-through/loss-delay.md +++ b/quest/m2/cut-through/loss-delay.md @@ -20,6 +20,8 @@ Independent of the bench; it can start any time. bytes held at least one smoothed RTT are counted. Plain reordering fills within an RTT and is not counted. The count is by arrival and gap fill, not by when the application reads, so it stays meaningful once reads go unordered. + When a stream ends with a hole still open (reset, stop, or session close), + buffered bytes that have already waited one smoothed RTT are counted then. - The fork exposes the per-stream count, and `web-transport-trait`'s `RecvStream` gains an accessor returning `Option` (`None` when the backend cannot see it), following the trait's `Stats` convention. Release both, as diff --git a/quest/m2/cut-through/relay.md b/quest/m2/cut-through/relay.md index c8f8208de8..f3efcc3d35 100644 --- a/quest/m2/cut-through/relay.md +++ b/quest/m2/cut-through/relay.md @@ -34,7 +34,16 @@ it, no config knob; otherwise today's ordered path. version receives byte-identical streams to the ordered path, and a header-covering hole holds later frames. -Re-run the bench and record the result in the line's README. +Prototype both scopes on the bench: cut-through within the current frame only, +and across in-flight frames, where N+1's payload passes N's hole once N's +header is parsed. Keep the one the numbers justify and delete the other; a +regression test asserts N+1 is forwarded before N's hole fills when the +multi-frame scope ships. + +Sweep the bench over publisher and subscriber counts as well as the network +axes: range bookkeeping, wakeups, and offset writes run per egress subscriber, +so relay CPU for cut-through against the ordered path must be flat in the +table size. Re-run it and record the result in the line's README. ## Required From b8bc12bf8242cf87e9ea6e38f070a81d5b010d15 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 30 Sep 2026 12:34:27 -0700 Subject: [PATCH 3/5] quest(m2): address cut-through re-review Co-Authored-By: Claude Opus 5.5 --- quest/m2/cut-through/bench.md | 10 +++++++--- quest/m2/cut-through/loss-delay.md | 5 +++-- quest/m2/cut-through/relay.md | 11 ++++++++--- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/quest/m2/cut-through/bench.md b/quest/m2/cut-through/bench.md index eeecd4b53d..d008df7b97 100644 --- a/quest/m2/cut-through/bench.md +++ b/quest/m2/cut-through/bench.md @@ -25,9 +25,13 @@ partly measure its own assumptions. cut-through removes. No direct publisher-to-viewer control: the gain exists only with a relay in the middle, and one QUIC hop already reassembles out of order, so a direct run measures a different topology (review of #4616). -- Split the held bytes into the frame that has the hole and later frames, so - the verdict shows what [multiple in-flight frames](/quest/m2/cut-through/frames.md) - add beyond the current frame. +- Split the held bytes three ways: bytes in the frame that has the hole, bytes + in later frames whose headers were known while the hole was open, and bytes + behind a lost header. Only the first two are forwardable; the last blocks with + or without cut-through and is reported but excluded from the opportunity. The + first split shows what [multiple in-flight frames](/quest/m2/cut-through/frames.md) + add beyond the current frame. Report the drain estimate as an upper bound, and + include both a payload-loss and a header-loss case to validate the split. - Take publisher and subscriber counts as parameters, so [relay cut-through](/quest/m2/cut-through/relay.md) reuses the rig for its fan-out sweep. diff --git a/quest/m2/cut-through/loss-delay.md b/quest/m2/cut-through/loss-delay.md index f6b9be6823..8f5a90d374 100644 --- a/quest/m2/cut-through/loss-delay.md +++ b/quest/m2/cut-through/loss-delay.md @@ -26,8 +26,9 @@ Independent of the bench; it can start any time. `RecvStream` gains an accessor returning `Option` (`None` when the backend cannot see it), following the trait's `Stats` convention. Release both, as [poll_acked in web-transport](/quest/m2/quic-ack-hook.md) does. -- The lite and IETF subscribers read the count when a group stream ends and add - it to the broadcast's ingress row. Nothing on the per-byte path. +- The lite and IETF subscribers read the count at each frame boundary and when + the group stream ends, adding the delta to the broadcast's ingress row, so a + long group does not leave the row stale. Nothing on the per-byte path. - Update the stats section of `doc/bin/relay/config.md`. Test: a seeded loss on a noq pair counts the held bytes; a reorder shorter than diff --git a/quest/m2/cut-through/relay.md b/quest/m2/cut-through/relay.md index f3efcc3d35..d164e530c9 100644 --- a/quest/m2/cut-through/relay.md +++ b/quest/m2/cut-through/relay.md @@ -14,6 +14,10 @@ Decided 2026-09-30: always on when the ingress and egress streams both support it, no config knob; otherwise today's ordered path. - Ingest: read the stream header in order, then switch to unordered reads. + `Reader` may already hold bytes past the header in its buffer, so hand that + suffix over with its absolute stream offset rather than dropping or + re-basing it; test a transport chunk that carries both the header and the + first payload bytes. Frame headers are parsed from the contiguous prefix; once a header is known, its frame's payload ranges go into the frame with the [offset write](/quest/m2/cut-through/frames.md) wherever they land. A hole @@ -41,9 +45,10 @@ regression test asserts N+1 is forwarded before N's hole fills when the multi-frame scope ships. Sweep the bench over publisher and subscriber counts as well as the network -axes: range bookkeeping, wakeups, and offset writes run per egress subscriber, -so relay CPU for cut-through against the ordered path must be flat in the -table size. Re-run it and record the result in the line's README. +axes. Range bookkeeping, wakeups, and offset writes run per served +subscriber, so total CPU grows with the subscribers a range reaches; the cost +per served subscriber, and the cost from table entries a range does not touch, +must stay flat against the ordered path. Re-run it and record the result in the line's README. ## Required From 8c935b902c70b82ba70ea793c1f880b4b8cdc8d4 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 2 Oct 2026 21:21:20 -0700 Subject: [PATCH 4/5] quest: move cut-through from m2 to m3, align with moq-quic The line is a speculative optimization with no consumer, so it parks in m3. Main hard-forked quinn as moq-quic, so the noq quest becomes quic.md and lands in-tree; the frame alloc budget link points at the shipped Budget. Co-Authored-By: Claude Opus 5.5 --- quest/m1/qos/README.md | 2 +- quest/m2/README.md | 1 - quest/m2/cut-through/transport.md | 32 ------------------ quest/m3/README.md | 1 + quest/{m2 => m3}/cut-through/README.md | 29 +++++++++------- quest/{m2 => m3}/cut-through/bench.md | 12 +++---- quest/{m2 => m3}/cut-through/frames.md | 4 +-- quest/{m2 => m3}/cut-through/loss-delay.md | 20 ++++++----- .../noq.md => m3/cut-through/quic.md} | 20 +++++------ quest/{m2 => m3}/cut-through/relay.md | 10 +++--- quest/m3/cut-through/transport.md | 33 +++++++++++++++++++ 11 files changed, 87 insertions(+), 77 deletions(-) delete mode 100644 quest/m2/cut-through/transport.md rename quest/{m2 => m3}/cut-through/README.md (76%) rename quest/{m2 => m3}/cut-through/bench.md (84%) rename quest/{m2 => m3}/cut-through/frames.md (92%) rename quest/{m2 => m3}/cut-through/loss-delay.md (76%) rename quest/{m2/cut-through/noq.md => m3/cut-through/quic.md} (53%) rename quest/{m2 => m3}/cut-through/relay.md (89%) create mode 100644 quest/m3/cut-through/transport.md diff --git a/quest/m1/qos/README.md b/quest/m1/qos/README.md index eebbebb649..3d70c72e36 100644 --- a/quest/m1/qos/README.md +++ b/quest/m1/qos/README.md @@ -52,5 +52,5 @@ on `main`. - [Media stats](/quest/m1/stats/README.md) - publishers and viewers report their own media, transport, and playback health, the media half of a verdict -- [Loss delay](/quest/m2/cut-through/loss-delay.md) - an ingress counter of +- [Loss delay](/quest/m3/cut-through/loss-delay.md) - an ingress counter of bytes a loss held back by at least one RTT, on the same rows diff --git a/quest/m2/README.md b/quest/m2/README.md index 14c121abfd..58a5f4b264 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -120,4 +120,3 @@ waiting on an upstream release waits in [m4](/quest/m4/README.md). - [moq-transport cluster peers](/quest/m2/ietf-cluster-peers.md) - an extended cluster draft lets moq-transport relays peer again, stitching failover on the reply's origin - [MSFTS convergence](/quest/m2/msfts-convergence.md) - the demultiplexed TS lane converges on MSFTS where the two still differ: program tables and the ES payload unit - [VAAPI encode and decode](/quest/m2/video-vaapi.md) - H.265 encode and decode, pre-generated bindings, and pooled resize surfaces, including the moq-dev/vaapi release that carries them -- [Cut-through](/quest/m2/cut-through/README.md) - a relay forwards bytes behind a QUIC stream hole before the retransmission fills it, if a lossy-hop bench says it is worth it diff --git a/quest/m2/cut-through/transport.md b/quest/m2/cut-through/transport.md deleted file mode 100644 index f376b4dee2..0000000000 --- a/quest/m2/cut-through/transport.md +++ /dev/null @@ -1,32 +0,0 @@ -# [M] Unordered reads and offset writes in web-transport-trait - -## Goal - -A released `web-transport-trait` lets a receive stream yield chunks with their -offsets in arrival order and lets a send stream write at an offset. The noq -backends implement both; every other backend reports that it cannot. - -## Plan - -Decided 2026-09-30: additive methods with defaults, not a moq-net-private -extension trait, which would need per-session detection behind the generic -`Session`. - -- `RecvStream` gains an unordered chunk read returning `(offset, Bytes)`, and - `SendStream` gains an offset write, both in the poll style. Their defaults - report unsupported in the return type (as - [poll_acked](/quest/m2/quic-ack-hook.md) does, since a default cannot build - `Self::Error`), and the caller falls back to ordered I/O. -- noq allows no ordered read after an unordered one - (`moq-noq-proto` `assembler.rs`), so the unordered mode is entered once per - stream and never left. -- Implement both in `web-transport-moq` (released from the fork) over - `RecvStream::into_unordered` and the [offset write](/quest/m2/cut-through/noq.md), - and natively in `rs/moq-uring/src/quic/noq/stream.rs`, which today reads with - `read(true)` and drops the chunk offset. moq-tokio's poll adapter forwards - them. `web-transport-wasm`, qmux, and iroh keep the defaults. -- Bump the pins here. - -## Required - -- [Offset writes in noq](/quest/m2/cut-through/noq.md) - the send-side primitive this exposes diff --git a/quest/m3/README.md b/quest/m3/README.md index 02d73b4c1c..8695f91439 100644 --- a/quest/m3/README.md +++ b/quest/m3/README.md @@ -26,6 +26,7 @@ deleted when it goes stale; git history keeps it. - [#3201: moq-uring: use SENDMSG_ZC for large UDP GSO trains](/quest/m3/3201-moq-uring-use-sendmsg-zc-for-large-udp-gso-trains.md) - complete the prerequisite issue first - [#3204](/quest/m3/3204-moq-uring-register-tx-pool-buffers-for-zero-copy-sends.md) - moq-uring: register TX-pool buffers for zero-copy sends - [QUIC GCC](/quest/m3/quic-gcc.md) - receive timestamps in ACKs and a measured verdict on delay-based congestion control for media egress, shipping as `RealTime` +- [Cut-through](/quest/m3/cut-through/README.md) - a relay forwards bytes behind a QUIC stream hole before the retransmission fills it, if a lossy-hop bench says it is worth it - [AF_XDP UDP path](/quest/m3/af-xdp.md) - the kernel-bypass verdict on today's virtio hosts that gates DPDK - [Unreal prototype](/quest/m3/unreal.md) - a UE5 module on the C++ package with exceptions disabled, rendering a subscribed broadcast to a texture - [Conan remote](/quest/m3/cpp-conan.md) - a remote we own serves the same tarball to `conan install` diff --git a/quest/m2/cut-through/README.md b/quest/m3/cut-through/README.md similarity index 76% rename from quest/m2/cut-through/README.md rename to quest/m3/cut-through/README.md index 033191bac3..8a8021b871 100644 --- a/quest/m2/cut-through/README.md +++ b/quest/m3/cut-through/README.md @@ -18,10 +18,15 @@ send stream offsets in any order. ## Plan +Decided 2026-10-02: the line moved from m2 to m3. It is a speculative +optimization with no consumer asking for it, so it parks until one does and +is deleted if it goes stale. The QUIC primitives land in `moq-quic` after +[the hard fork](/quest/m1/quic/fork/README.md) rather than in moq-dev/noq. + Decided 2026-09-30: - Measure first, and plan the build now so the verdict only decides go or - no-go, not the design. The build quests require the [bench](/quest/m2/cut-through/bench.md). + no-go, not the design. The build quests require the [bench](/quest/m3/cut-through/bench.md). - The gain is roughly the time the relay would spend bursting the held bytes downstream after the hole fills: small when the egress hop has headroom, larger for a big I-frame on a tight hop. The bench sweeps exactly that. @@ -38,33 +43,33 @@ Decided 2026-09-30: `moq-net`, since the lite and IETF publishers and subscribers that use them live in the same crate. Cut-through frames keep the received chunks by offset instead of copying into a pre-allocated buffer, so memory tracks bytes - received and [the allocation budget](/quest/m0/frame-alloc-budget.md) is - unaffected. + received and the per-session pre-allocation `Budget` (`model/frame.rs`, + #4609) is unaffected. - `web-transport-trait` gains an unordered chunk read and an offset write whose defaults report unsupported, and moq-net falls back to ordered I/O. Only the - noq backends implement them; browsers, qmux, and iroh keep the defaults. + `moq-quic` backends implement them; browsers, qmux, and iroh keep the defaults. Additive, so it lands on `main`. - Always on wherever both of the relay's streams support it. No config knob. -- The relay's own egress is noq for every native and browser viewer, so browser +- The relay's own egress is `moq-quic` for every native and browser viewer, so browser viewers benefit too; only the relay's side needs the feature. Public API: additive `web-transport-trait` methods; the loss-delay counter on `moq-stats` ingress rows. Wire: none for MoQ; the counter is a `moq-stats` field. -End to end: once [the relay quest](/quest/m2/cut-through/relay.md) lands, re-run the +End to end: once [the relay quest](/quest/m3/cut-through/relay.md) lands, re-run the bench on the same sweep and record before and after in this README before closing the line. No new doc page: nothing user-facing changes beyond the stats field, which its quest documents inline. ## Required -- [Loss delay](/quest/m2/cut-through/loss-delay.md) - relays report, per broadcast, the ingress bytes a loss held back by at least one RTT -- [Bench](/quest/m2/cut-through/bench.md) - a lossy relay hop swept over loss, frame size, and egress headroom, measuring the post-hole drain time, with a go or no-go verdict -- [Offset writes in noq](/quest/m2/cut-through/noq.md) - the fork's send streams accept writes past a gap and send them right away -- [Frame ranges](/quest/m2/cut-through/frames.md) - frames fill out of order, several at once per group, and a reader sees each range as it lands -- [Transport trait](/quest/m2/cut-through/transport.md) - `web-transport-trait` carries unordered reads and offset writes, implemented for noq -- [Relay cut-through](/quest/m2/cut-through/relay.md) - lite and IETF group streams read out of order and write each range at its output offset +- [Loss delay](/quest/m3/cut-through/loss-delay.md) - relays report, per broadcast, the ingress bytes a loss held back by at least one RTT +- [Bench](/quest/m3/cut-through/bench.md) - a lossy relay hop swept over loss, frame size, and egress headroom, measuring the post-hole drain time, with a go or no-go verdict +- [Offset writes in moq-quic](/quest/m3/cut-through/quic.md) - send streams accept writes past a gap and send them right away +- [Frame ranges](/quest/m3/cut-through/frames.md) - frames fill out of order, several at once per group, and a reader sees each range as it lands +- [Transport trait](/quest/m3/cut-through/transport.md) - `web-transport-trait` carries unordered reads and offset writes, implemented for `moq-quic` +- [Relay cut-through](/quest/m3/cut-through/relay.md) - lite and IETF group streams read out of order and write each range at its output offset ## Related diff --git a/quest/m2/cut-through/bench.md b/quest/m3/cut-through/bench.md similarity index 84% rename from quest/m2/cut-through/bench.md rename to quest/m3/cut-through/bench.md index d008df7b97..bdbe9dc11f 100644 --- a/quest/m2/cut-through/bench.md +++ b/quest/m3/cut-through/bench.md @@ -5,16 +5,16 @@ A nightly bench measures how long a frame takes to complete at the viewer when the publisher's hop to the relay loses packets, and how much of that a cut-through relay could win back. It ends with a written go or no-go for the -rest of the [cut-through line](/quest/m2/cut-through/README.md). +rest of the [cut-through line](/quest/m3/cut-through/README.md). ## Plan Decided 2026-09-30: real stacks, not a mock. The loss recovery being measured -belongs to noq, so a mock transport with a made-up retransmit model would +belongs to the QUIC stack, so a mock transport with a made-up retransmit model would partly measure its own assumptions. - Publisher, then a seeded `moq-shaper` (loss and delay), then `moq-relay` over - noq, then a clean or rate-limited hop to the viewer. Reuse the shaper wiring + QUIC, then a clean or rate-limited hop to the viewer. Reuse the shaper wiring from `rs/moq-relay/tests/drills.rs`. - Sweep loss rate, frame size (audio-sized through a large I-frame), and egress headroom (egress rate relative to the media rate). @@ -29,14 +29,14 @@ partly measure its own assumptions. in later frames whose headers were known while the hole was open, and bytes behind a lost header. Only the first two are forwardable; the last blocks with or without cut-through and is reported but excluded from the opportunity. The - first split shows what [multiple in-flight frames](/quest/m2/cut-through/frames.md) + first split shows what [multiple in-flight frames](/quest/m3/cut-through/frames.md) add beyond the current frame. Report the drain estimate as an upper bound, and include both a payload-loss and a header-loss case to validate the split. - Take publisher and subscriber counts as parameters, so - [relay cut-through](/quest/m2/cut-through/relay.md) reuses the rig for its + [relay cut-through](/quest/m3/cut-through/relay.md) reuses the rig for its fan-out sweep. - Record the loss-delay counter from the same runs, if - [Loss delay](/quest/m2/cut-through/loss-delay.md) has landed, so production + [Loss delay](/quest/m3/cut-through/loss-delay.md) has landed, so production numbers can be read against the lab. - It runs on wall-clock time, so it is a nightly bench, not a unit test. Wire it into the nightly workflow. diff --git a/quest/m2/cut-through/frames.md b/quest/m3/cut-through/frames.md similarity index 92% rename from quest/m2/cut-through/frames.md rename to quest/m3/cut-through/frames.md index 787f9562e5..e57bbb274d 100644 --- a/quest/m2/cut-through/frames.md +++ b/quest/m3/cut-through/frames.md @@ -15,7 +15,7 @@ Decided 2026-09-30: `pub(crate)`: the only users are the lite and IETF sessions in the same crate. - A frame written out of order keeps the received chunks by offset instead of copying into a pre-allocated buffer, so its memory tracks bytes received and - [the allocation budget](/quest/m0/frame-alloc-budget.md) does not apply. The + the per-session pre-allocation `Budget` (`model/frame.rs`) does not apply. The in-order readers (`poll_read_chunk`, `poll_read_all`) still see contiguous bytes up to the first hole, and `finish` still requires every byte. - Overlapping or out-of-bounds ranges are errors. @@ -37,4 +37,4 @@ has a hole. ## Required -- [Bench](/quest/m2/cut-through/bench.md) - a go verdict +- [Bench](/quest/m3/cut-through/bench.md) - a go verdict diff --git a/quest/m2/cut-through/loss-delay.md b/quest/m3/cut-through/loss-delay.md similarity index 76% rename from quest/m2/cut-through/loss-delay.md rename to quest/m3/cut-through/loss-delay.md index 8f5a90d374..fbf9337e40 100644 --- a/quest/m2/cut-through/loss-delay.md +++ b/quest/m3/cut-through/loss-delay.md @@ -13,29 +13,33 @@ production number that sizes the cut-through opportunity. Decided 2026-09-30: one monotonic byte counter, no hold-time histogram, on the ingress (`Role::Subscriber`) rows next to [publisher timeliness](/quest/m1/qos/publisher-timeliness.md)'s fields. -Independent of the bench; it can start any time. +Independent of the bench; it can start once `moq-quic` lands. -- noq counts per receive stream. The assembler stamps each chunk buffered past +- `moq-quic` counts per receive stream. The assembler stamps each chunk buffered past the contiguous frontier with its arrival time; when the frontier passes it, bytes held at least one smoothed RTT are counted. Plain reordering fills within an RTT and is not counted. The count is by arrival and gap fill, not by when the application reads, so it stays meaningful once reads go unordered. When a stream ends with a hole still open (reset, stop, or session close), buffered bytes that have already waited one smoothed RTT are counted then. -- The fork exposes the per-stream count, and `web-transport-trait`'s +- `moq-quic` exposes the per-stream count, and `web-transport-trait`'s `RecvStream` gains an accessor returning `Option` (`None` when the backend - cannot see it), following the trait's `Stats` convention. Release both, as - [poll_acked in web-transport](/quest/m2/quic-ack-hook.md) does. + cannot see it), following the trait's `Stats` convention. Release the trait + and implement it in the in-tree `web-transport-moq`, as + [poll_acked](/quest/m2/quic-ack-hook.md) does. - The lite and IETF subscribers read the count at each frame boundary and when the group stream ends, adding the delta to the broadcast's ingress row, so a long group does not leave the row stale. Nothing on the per-byte path. - Update the stats section of `doc/bin/relay/config.md`. -Test: a seeded loss on a noq pair counts the held bytes; a reorder shorter than +Test: a seeded loss on a `moq-quic` pair counts the held bytes; a reorder shorter than an RTT counts nothing. -Public API: an additive trait accessor. Wire: a new `moq-stats` field, so the -PR targets whichever branch the stats wire needs. +Public API: an additive trait accessor. Wire: a new `moq-stats` field. + +## Required + +- [Hard fork](/quest/m1/quic/fork/README.md) - the `moq-quic` assembler this counts in ## Related diff --git a/quest/m2/cut-through/noq.md b/quest/m3/cut-through/quic.md similarity index 53% rename from quest/m2/cut-through/noq.md rename to quest/m3/cut-through/quic.md index 86d4960652..9dc6c1009f 100644 --- a/quest/m2/cut-through/noq.md +++ b/quest/m3/cut-through/quic.md @@ -1,10 +1,10 @@ -# [L] Offset writes in the noq fork +# [L] Offset writes in moq-quic ## Goal -A send stream in moq-dev/noq accepts a write at an offset past unwritten +A send stream in `moq-quic` accepts a write at an offset past unwritten bytes, sends it right away, and fills the gap when those bytes are written -later. A released `moq-noq-proto` and `moq-noq` carry it. +later. ## Plan @@ -19,14 +19,14 @@ Decided 2026-09-30: no write-ahead limit beyond QUIC's own flow control. that overlaps an existing range is an error. - The append-only `write` and `write_chunks` stay as they are; the offset write is a new method, since a writer that never skips pays nothing. -- Tests in noq-proto's simulated pair: an out-of-order writer delivers the same - bytes as an ordered one, flow control blocks at the highest offset, FIN waits - for the gap, and a reset with a gap outstanding is clean. +- Tests in `moq-quic`'s simulated pair: an out-of-order writer delivers the + same bytes as an ordered one, flow control blocks at the highest offset, FIN + waits for the gap, and a reset with a gap outstanding is clean. -Release the fork crates with the change and record the parent noq commit, per -the [QUIC line's rules](/quest/m1/quic/README.md). Offer it upstream once it has -shipped and settled, like the fork's other m2 features. +The change lands in-tree with its consumer, per the +[QUIC line's rules](/quest/m1/quic/README.md); no fork release or pin bump. ## Required -- [Bench](/quest/m2/cut-through/bench.md) - a go verdict +- [Hard fork](/quest/m1/quic/fork/README.md) - `moq-quic` exists and MoQ runs on it +- [Bench](/quest/m3/cut-through/bench.md) - a go verdict diff --git a/quest/m2/cut-through/relay.md b/quest/m3/cut-through/relay.md similarity index 89% rename from quest/m2/cut-through/relay.md rename to quest/m3/cut-through/relay.md index d164e530c9..79677cb1f5 100644 --- a/quest/m2/cut-through/relay.md +++ b/quest/m3/cut-through/relay.md @@ -5,7 +5,7 @@ The lite and IETF sessions read incoming group streams out of order and write each payload range to every subscriber's stream at its output offset, so bytes behind an ingress hole reach the next hop before the hole is filled. On the -[bench](/quest/m2/cut-through/bench.md), frame completion moves toward the +[bench](/quest/m3/cut-through/bench.md), frame completion moves toward the direct-connection bound. ## Plan @@ -20,7 +20,7 @@ it, no config knob; otherwise today's ordered path. first payload bytes. Frame headers are parsed from the contiguous prefix; once a header is known, its frame's payload ranges go into the frame with the - [offset write](/quest/m2/cut-through/frames.md) wherever they land. A hole + [offset write](/quest/m3/cut-through/frames.md) wherever they land. A hole covering the next header blocks later frames, because that header's position depends on it. Lite: `FrameIngest` in `lite/subscriber.rs`; IETF: `GroupIngest` in `ietf/subscriber.rs`. `Reader::poll_read_frame` in @@ -31,7 +31,7 @@ it, no config knob; otherwise today's ordered path. has been parsed, including across a lite and IETF translation, since output header lengths depend on values (timestamp and object id deltas). Write the header, then the ranges with the - [offset write](/quest/m2/cut-through/transport.md). Lite: `GroupServe` in + [offset write](/quest/m3/cut-through/transport.md). Lite: `GroupServe` in `lite/publisher.rs`; IETF: `GroupServe` in `ietf/publisher.rs`. - The fetch paths stay ordered. - Tests over the mock transport with shuffled chunk delivery: every subscriber @@ -52,5 +52,5 @@ must stay flat against the ordered path. Re-run it and record the result in the ## Required -- [Frame ranges](/quest/m2/cut-through/frames.md) - the model primitive -- [Transport trait](/quest/m2/cut-through/transport.md) - the I/O primitive +- [Frame ranges](/quest/m3/cut-through/frames.md) - the model primitive +- [Transport trait](/quest/m3/cut-through/transport.md) - the I/O primitive diff --git a/quest/m3/cut-through/transport.md b/quest/m3/cut-through/transport.md new file mode 100644 index 0000000000..ec2962e3ef --- /dev/null +++ b/quest/m3/cut-through/transport.md @@ -0,0 +1,33 @@ +# [M] Unordered reads and offset writes in web-transport-trait + +## Goal + +A released `web-transport-trait` lets a receive stream yield chunks with their +offsets in arrival order and lets a send stream write at an offset. The +`moq-quic` backends implement both; every other backend reports that it cannot. + +## Plan + +Decided 2026-09-30: additive methods with defaults, not a moq-net-private +extension trait, which would need per-session detection behind the generic +`Session`. + +- `RecvStream` gains an unordered chunk read returning `(offset, Bytes)`, and + `SendStream` gains an offset write, both in the poll style. Their defaults + report unsupported in the return type (as + [poll_acked](/quest/m2/quic-ack-hook.md) does, since a default cannot build + `Self::Error`), and the caller falls back to ordered I/O. +- quinn's assembler, which `moq-quic` keeps, allows no ordered read after an + unordered one, so the unordered mode is entered once per stream and never + left. +- Implement both in the in-tree `web-transport-moq` over the unordered + `read_chunk` and the [offset write](/quest/m3/cut-through/quic.md), and + natively in moq-uring's QUIC stream (today `rs/moq-uring/src/quic/noq/stream.rs`, + renamed by the [switch](/quest/m1/quic/fork/switch.md)), which reads ordered + and drops the chunk offset. moq-tokio's poll adapter forwards them. + `web-transport-wasm`, qmux, and iroh keep the defaults. +- Release `web-transport-trait` and bump its pin here. + +## Required + +- [Offset writes in moq-quic](/quest/m3/cut-through/quic.md) - the send-side primitive this exposes From b782ff2dfb214a57b8ca714cd2d95ce92b35331f Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 2 Oct 2026 21:22:14 -0700 Subject: [PATCH 5/5] quest: address cut-through relay goal and cache charge review Co-Authored-By: Claude Opus 5.5 --- quest/m3/cut-through/README.md | 5 +++-- quest/m3/cut-through/frames.md | 4 +++- quest/m3/cut-through/relay.md | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/quest/m3/cut-through/README.md b/quest/m3/cut-through/README.md index 8a8021b871..7b85b846b5 100644 --- a/quest/m3/cut-through/README.md +++ b/quest/m3/cut-through/README.md @@ -43,8 +43,9 @@ Decided 2026-09-30: `moq-net`, since the lite and IETF publishers and subscribers that use them live in the same crate. Cut-through frames keep the received chunks by offset instead of copying into a pre-allocated buffer, so memory tracks bytes - received and the per-session pre-allocation `Budget` (`model/frame.rs`, - #4609) is unaffected. + received and the per-session pre-allocation `Budget` (`model/frame.rs`) + does not apply. The group cache already charges each frame by bytes written + (#4609), so several in-flight frames charge only what has arrived. - `web-transport-trait` gains an unordered chunk read and an offset write whose defaults report unsupported, and moq-net falls back to ordered I/O. Only the `moq-quic` backends implement them; browsers, qmux, and iroh keep the defaults. diff --git a/quest/m3/cut-through/frames.md b/quest/m3/cut-through/frames.md index e57bbb274d..da2e6588ed 100644 --- a/quest/m3/cut-through/frames.md +++ b/quest/m3/cut-through/frames.md @@ -15,7 +15,9 @@ Decided 2026-09-30: `pub(crate)`: the only users are the lite and IETF sessions in the same crate. - A frame written out of order keeps the received chunks by offset instead of copying into a pre-allocated buffer, so its memory tracks bytes received and - the per-session pre-allocation `Budget` (`model/frame.rs`) does not apply. The + the per-session pre-allocation `Budget` (`model/frame.rs`) does not apply. + Each in-flight frame keeps the existing charge-by-bytes-written cache + accounting (`charge_partial` in `model/group.rs`), per frame. The in-order readers (`poll_read_chunk`, `poll_read_all`) still see contiguous bytes up to the first hole, and `finish` still requires every byte. - Overlapping or out-of-bounds ranges are errors. diff --git a/quest/m3/cut-through/relay.md b/quest/m3/cut-through/relay.md index 79677cb1f5..f0ddee7649 100644 --- a/quest/m3/cut-through/relay.md +++ b/quest/m3/cut-through/relay.md @@ -5,8 +5,8 @@ The lite and IETF sessions read incoming group streams out of order and write each payload range to every subscriber's stream at its output offset, so bytes behind an ingress hole reach the next hop before the hole is filled. On the -[bench](/quest/m3/cut-through/bench.md), frame completion moves toward the -direct-connection bound. +[bench](/quest/m3/cut-through/bench.md), cut-through shrinks the post-hole +drain time on the same two-hop path. ## Plan