From 5bf6e5951b52f0f78664cde17d357b053b5aee19 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 24 Oct 2026 22:39:48 -0700 Subject: [PATCH 1/3] chore(quest): claim quest/m1/subscribe-ranges/fetch-range.md Quest-Claim: d9a528edd5ea470a From 89c615beb4e10162ddd5be567f1dcc60db47d390 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 24 Oct 2026 23:15:43 -0700 Subject: [PATCH 2/3] test(net): benchmark range filling over range count and span track_range_filling drains one reader's disjoint ranges below the feed's start through fills, swept over how many ranges and how many groups each spans: the filling counterpart of track_range_serving. It shows the per group cost growing with both axes. The fetch-range quest is rewritten to what remains: wire publishers keep parking on a bounded downstream SUBSCRIBE, route recovery keeps its frame-start fetch, and three open questions (a miss with no feed frontier, cached backfill, relay demand before lite-07) plus the fill path's slope block replacing fetch_group. Co-Authored-By: Claude Opus 5.5 --- quest/m1/subscribe-ranges/fetch-range.md | 57 ++++++++++++-- rs/moq-net/benches/track.rs | 95 +++++++++++++++++++++++- 2 files changed, 146 insertions(+), 6 deletions(-) diff --git a/quest/m1/subscribe-ranges/fetch-range.md b/quest/m1/subscribe-ranges/fetch-range.md index fd2e214ff02..3c1f5d5d90d 100644 --- a/quest/m1/subscribe-ranges/fetch-range.md +++ b/quest/m1/subscribe-ranges/fetch-range.md @@ -1,19 +1,20 @@ -# [M] Fetch as a range +# [L] Fetch as a range ## Goal `fetch_group` is gone: every caller reads a past group through a one-range subscription, which ends once its range is accounted for. That covers the relay, moq-archive, hls, moq-ffi and every binding wrapper with its `doc/lib` -page, and `moq fetch` through `moq_relay::fetch_group`. A benchmark covers -filling over range count and span as separate axes. +page, and `moq fetch` through `moq_relay::fetch_group`. ## Plan Split out of dynamic range requests, which replaced `requested_group` with `requested_fill`, by the maintainer on 2026-10-20 ([moq-dev/moq#6123](https://github.com/moq-dev/moq/pull/6123)), so each PR -breaks one API once. +breaks one API once. Resized from [M] to [L] on 2026-10-24, when the first +attempt ([moq-dev/moq#6722](https://github.com/moq-dev/moq/pull/6722)) found +the open questions below and landed only the `track_range_filling` benchmark. - **Ending a bounded read** (decided 2026-10-20). `recv_group` returns `None` once every sequence in a bounded window is delivered, dropped, stale, or @@ -35,4 +36,50 @@ breaks one API once. `Error::Lagged` rather than the group being skipped (maintainer, 2026-10-20). Only `fetch_group` requests a head fill today, so the one-range read has to request it instead of letting `Lagged` reach the caller. -- Benchmark filling beside `track_range_serving`, which covers serving. + `fill::Ask` already carries a start frame, so the miss judge can count a + headless slot as a hole for a bounded window. +- **Wire publishers keep parking.** The lite subscriber reads an upstream FIN + as the track's end, because "bounded downstream demand alone never FINs; the + publisher parks" (`lite/subscriber.rs`). Once `recv_group` ends a bounded + read, the lite and IETF publishers keep parking on a bounded downstream + SUBSCRIBE rather than FIN it, or a peer on a published version reads the + range's end as the track's. +- **Route recovery keeps a frame start.** `resume::Recover` fetches the rest of + a group from frame N on the new route (`group::Fetch::frame_start`), which a + whole-group range cannot say, so a crate-private fetch stays for it. +- **Open: a miss with no feed frontier.** A reader asks fills only below the + newest group the live feed produced (decision 2 on + [moq-dev/moq#6123](https://github.com/moq-dev/moq/pull/6123)). On a track + nothing feeds live, such as the moq-archive reader's or a relay copy no + subscription feeds, a one-range read never asks and parks forever: + `Range::groups(5..6)` on a fill-only track asks no fill. `fetch_group` + queues a one-group fill instead. Options: holes in a bounded range are + misses up to its end while the track has no live feed (recommended); every + bounded range's holes are misses up to its end, which asks fills for groups + a live feed has yet to produce; or a by-sequence read stays beside + subscriptions. +- **Open: cached backfill.** A range reader never reads a group another + reader's fill cached: the arrival walk skips backfill and the miss judge + counts it held, so a second `0..1` read after a first reader's fill cached + group 0 parks with no fill asked. `fetch_group` reads it from the cache. + Recommended: a cached backfill group in a reader's window is owed to it like + its own fill's answer, as a relay behaving the same warm or cold requires. + That retires `fetched_backfill_not_subscribed` and + `refilled_sequence_stays_out_of_subscriptions`, which assert that a + subscriber never sees backfill. +- **Open: relay demand before lite-07.** A one-range read's ranges join the + track's aggregate, so a relay puts a bounded SUBSCRIBE upstream where + `fetch_group` puts a FETCH. `fetch_only.rs` asserts that a moq-transport + fetch never subscribes, and a lite SUBSCRIBE before lite-05 cannot name a + missing group, so the read would park on an upstream miss. Options: a range + wholly below the live edge stays out of the upstream SUBSCRIBE on a wire + without range SUBSCRIBE and is filled by per-group FETCH, keeping today's + wire behavior (recommended); or it goes upstream as a bounded SUBSCRIBE that + SUBSCRIBE_START and SUBSCRIBE_DROP account for, lite-05 and later only. +- **The fill path is superlinear.** `track_range_filling` drains one reader's + R disjoint ranges of S filled groups each, and the cost per group grows with + both: at span 64, 568K groups/s over 1 range, 69K over 16, and 1.9K over 256. + Each poll's drift edge and each candidate's successor search walk `lookup` + over the reader's runs, and a filled group is never shown, so each passes + every filled group; `Misses::retain` also rescans every filled group on each + poll. One-range reads put every past read on this path. diff --git a/rs/moq-net/benches/track.rs b/rs/moq-net/benches/track.rs index 057fff21af1..435c694edbf 100644 --- a/rs/moq-net/benches/track.rs +++ b/rs/moq-net/benches/track.rs @@ -90,6 +90,11 @@ //! fill, so asking, dropping, and settling must cost each reader the same per run, and a //! cost that grows with the audience or the runs beyond that shows up as a slope. //! +//! `track_range_filling` has one reader fill disjoint ranges below where the feed starts, +//! swept over how many ranges and how many groups each spans, the filling counterpart of +//! `track_range_serving`: each group filled must cost the same, so a walk over the ranges or +//! the groups of each fill shows up as a slope. +//! //! Run with `cargo bench -p moq-net --bench track`. #![expect( @@ -1723,6 +1728,93 @@ fn bench_fill(c: &mut Criterion) { group.finish(); } +/// One reader whose disjoint ranges all fall below where the feed starts, and the handler +/// that answers its fills; see [`bench_range_filling`]. +struct RangeFills { + _broadcast: broadcast::Producer, + _track: track::Producer, + dynamic: track::Dynamic, + reader: track::Subscriber, + groups: u64, +} + +impl RangeFills { + fn new(ranges: u64, span: u64) -> Self { + let broadcast = broadcast::Info::default().produce(); + let mut track = broadcast.create_track("bench", timed()).unwrap(); + let dynamic = track.dynamic(); + // The feed starts past every range, so each is a miss asked at once. A gap between + // neighbors keeps every range its own fill. + let start = ranges * (span + 1); + track.start_at(start).unwrap(); + let mut group = track.create_group(group::Info { sequence: start }).unwrap(); + group + .write_frame(Timestamp::from_millis(start).unwrap(), Bytes::from_static(b"x")) + .unwrap(); + group.finish().unwrap(); + let subscription = track::Subscription::default() + .with_max_delay(AGED_BUDGET) + .with_ranges((0..ranges).map(|i| { + let first = i * (span + 1); + track::Range::groups(first..first + span) + })); + let reader = track.subscribe(subscription); + Self { + _broadcast: broadcast, + _track: track, + dynamic, + reader, + groups: ranges * span, + } + } + + /// The reader asks for its misses, the handler accepts every group of every fill, and the + /// reader drains what they filled. + fn run(&mut self) { + let waiter = kio::Waiter::noop(); + assert!( + self.reader.poll_recv_group(&waiter).is_pending(), + "everything is a miss" + ); + while let Poll::Ready(Ok(fill)) = self.dynamic.poll_requested_fill(&waiter) { + while let Some(sequence) = fill.next() { + let mut group = fill.accept(sequence, None).unwrap(); + group + .write_frame(Timestamp::from_millis(sequence).unwrap(), Bytes::from_static(b"x")) + .unwrap(); + group.finish().unwrap(); + } + } + let mut filled = 0; + while let Poll::Ready(Ok(Some(group))) = self.reader.poll_recv_group(&waiter) { + black_box(group); + filled += 1; + } + assert_eq!(filled, self.groups); + } +} + +/// One reader filling disjoint ranges below the feed's start, swept over how many ranges and +/// how many groups each spans. Each group filled must cost the same, so a walk over the ranges +/// or the groups of each fill shows up as a slope over either axis. +fn bench_range_filling(c: &mut Criterion) { + let mut group = c.benchmark_group("track_range_filling"); + group.sample_size(10); + for span in SERVED_SPAN { + for ranges in SERVED_RANGES { + group.throughput(Throughput::Elements(ranges * span)); + group.bench_function(BenchmarkId::new(format!("span_{span}"), ranges), |b| { + b.iter_batched_ref( + || RangeFills::new(ranges, span), + RangeFills::run, + BatchSize::PerIteration, + ); + }); + } + } + group.finish(); +} + criterion_group!( benches, bench_range_widening, @@ -1750,6 +1842,7 @@ criterion_group!( bench_wall, bench_empty_run, bench_time_end, - bench_fill + bench_fill, + bench_range_filling ); criterion_main!(benches); From 3df5d0d467271163de4bdaa3f73334c1ded68e5e Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 25 Oct 2026 08:37:08 -0700 Subject: [PATCH 3/3] quest: record the fetch-range decisions and the fill-linear dependency Co-Authored-By: Claude Opus 5.5 --- quest/m1/subscribe-ranges/fetch-range.md | 69 ++++++++++++------------ 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/quest/m1/subscribe-ranges/fetch-range.md b/quest/m1/subscribe-ranges/fetch-range.md index 3c1f5d5d90d..d8b0715fc9d 100644 --- a/quest/m1/subscribe-ranges/fetch-range.md +++ b/quest/m1/subscribe-ranges/fetch-range.md @@ -14,7 +14,7 @@ Split out of dynamic range requests, which replaced `requested_group` with ([moq-dev/moq#6123](https://github.com/moq-dev/moq/pull/6123)), so each PR breaks one API once. Resized from [M] to [L] on 2026-10-24, when the first attempt ([moq-dev/moq#6722](https://github.com/moq-dev/moq/pull/6722)) found -the open questions below and landed only the `track_range_filling` benchmark. +the questions decided below and landed only the `track_range_filling` benchmark. - **Ending a bounded read** (decided 2026-10-20). `recv_group` returns `None` once every sequence in a bounded window is delivered, dropped, stale, or @@ -47,39 +47,40 @@ the open questions below and landed only the `track_range_filling` benchmark. - **Route recovery keeps a frame start.** `resume::Recover` fetches the rest of a group from frame N on the new route (`group::Fetch::frame_start`), which a whole-group range cannot say, so a crate-private fetch stays for it. -- **Open: a miss with no feed frontier.** A reader asks fills only below the - newest group the live feed produced (decision 2 on - [moq-dev/moq#6123](https://github.com/moq-dev/moq/pull/6123)). On a track +- **A miss with no feed frontier** (maintainer, 2026-10-25). A reader asks + fills only below the newest group the live feed produced (decision 2 on + [moq-dev/moq#6123](https://github.com/moq-dev/moq/pull/6123)), so on a track nothing feeds live, such as the moq-archive reader's or a relay copy no - subscription feeds, a one-range read never asks and parks forever: - `Range::groups(5..6)` on a fill-only track asks no fill. `fetch_group` - queues a one-group fill instead. Options: holes in a bounded range are - misses up to its end while the track has no live feed (recommended); every - bounded range's holes are misses up to its end, which asks fills for groups - a live feed has yet to produce; or a by-sequence read stays beside + subscription feeds, `Range::groups(5..6)` asks no fill and parks forever. + While the track has no live feed, holes in a bounded range are misses up to + its end. This asks nothing of a live feed; the rejected options were + treating every bounded range's holes as misses, which asks fills for groups + a live feed has yet to produce, and keeping a by-sequence read beside subscriptions. -- **Open: cached backfill.** A range reader never reads a group another - reader's fill cached: the arrival walk skips backfill and the miss judge - counts it held, so a second `0..1` read after a first reader's fill cached - group 0 parks with no fill asked. `fetch_group` reads it from the cache. - Recommended: a cached backfill group in a reader's window is owed to it like - its own fill's answer, as a relay behaving the same warm or cold requires. - That retires `fetched_backfill_not_subscribed` and +- **Cached backfill is owed** (maintainer, 2026-10-25). A range reader never + reads a group another reader's fill cached today: the arrival walk skips + backfill and the miss judge counts it held, so a second `0..1` read after a + first reader's fill cached group 0 parks with no fill asked. A cached + backfill group inside a reader's window is owed to it like its own fill's + answer, so a relay behaves the same warm or cold. This retires + `fetched_backfill_not_subscribed` and `refilled_sequence_stays_out_of_subscriptions`, which assert that a - subscriber never sees backfill. -- **Open: relay demand before lite-07.** A one-range read's ranges join the - track's aggregate, so a relay puts a bounded SUBSCRIBE upstream where - `fetch_group` puts a FETCH. `fetch_only.rs` asserts that a moq-transport - fetch never subscribes, and a lite SUBSCRIBE before lite-05 cannot name a - missing group, so the read would park on an upstream miss. Options: a range - wholly below the live edge stays out of the upstream SUBSCRIBE on a wire - without range SUBSCRIBE and is filled by per-group FETCH, keeping today's - wire behavior (recommended); or it goes upstream as a bounded SUBSCRIBE that - SUBSCRIBE_START and SUBSCRIBE_DROP account for, lite-05 and later only. -- **The fill path is superlinear.** `track_range_filling` drains one reader's - R disjoint ranges of S filled groups each, and the cost per group grows with - both: at span 64, 568K groups/s over 1 range, 69K over 16, and 1.9K over 256. - Each poll's drift edge and each candidate's successor search walk `lookup` - over the reader's runs, and a filled group is never shown, so each passes - every filled group; `Misses::retain` also rescans every filled group on each - poll. One-range reads put every past read on this path. + subscriber never sees backfill. Re-filling a cached group was rejected: it + costs an upstream FETCH for a group already in the cache. +- **Relay demand before lite-07** (maintainer, 2026-10-25). A one-range read's + ranges join the track's aggregate, so a relay would put a bounded SUBSCRIBE + upstream where `fetch_group` puts a FETCH. `fetch_only.rs` asserts that a + moq-transport fetch never subscribes, and a lite SUBSCRIBE before lite-05 + cannot name a missing group. A range wholly below the live edge stays out of + the upstream SUBSCRIBE on any wire without range SUBSCRIBE and is filled by + per-group FETCH, keeping today's wire behavior until + [lite-07 ranges](/quest/m1/subscribe-ranges/lite.md) land. A bounded + upstream SUBSCRIBE accounted by SUBSCRIBE_START and SUBSCRIBE_DROP was + rejected: it works on lite-05 and later only. +- **The fill path goes linear first** (maintainer, 2026-10-25). One-range + reads put every past read on the fill path, which `track_range_filling` + measures as superlinear in range count and span: at span 64, 215K groups/s + over 1 range, 69K over 16, and 1.9K over 256. This quest waits on the + linear fill path quest (`quest/m1/subscribe-ranges/fill-linear.md`, planned + in [moq-dev/moq#6790](https://github.com/moq-dev/moq/pull/6790)); once it is + on main, list it under Required here.