diff --git a/quest/m2/README.md b/quest/m2/README.md index 813b339b87..c68f03f77d 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -70,7 +70,6 @@ ranked just below deterministic TS legs, which it requires. - [Ordered owed wait](/quest/m2/ordered-owed-wait.md) - an ordered reader waits within max_delay for a missing group only while the track still owes it - [Unbounded groups](/quest/m2/unbounded-groups/README.md) - groups and frames lose their size caps; the cache sheds parts of a group under pressure and refills the holes in place - [Rust-backed @moq/net](/quest/m2/rs2ts/README.md) - measure browser baselines, complete WASM bindings, and adopt Rust behind @moq/net before the later JS-only stage -- [Front deadlines](/quest/m2/front-deadline-index.md) - a front's per-event cost stops growing with its track count: an expiry index and per-track wakes, proven by a churn benchmark - [Front parking](/quest/m2/origin-front-parks.md) - an unroutable request waits on a front instead of re-asking on every route-table move - [Route wakes](/quest/m2/route-wakes.md) - a route change wakes only the fronts it can move, so pool churn stops scaling with served paths - [Route health](/quest/m2/route-health/README.md) - epoch-only active/passive track recovery over metadata-only subscriptions diff --git a/quest/m2/front-deadline-index.md b/quest/m2/front-deadline-index.md deleted file mode 100644 index 1d8eb123fd..0000000000 --- a/quest/m2/front-deadline-index.md +++ /dev/null @@ -1,39 +0,0 @@ -# [M] Front deadlines scale with the touched track - -## Goal - -A relay front's per-event cost no longer grows with the number of tracks it -holds. Today `front.rs` `next_deadline` scans every track to find the next -linger expiry, and `serve_front`'s per-wake poll in `origin.rs` scans every -track too, so each event on one track costs O(tracks) and a broadcast that -churns track names pays O(tracks²). After this, finding the next expiry and -reacting to one track's event touch only that track. - -## Plan - -- Benchmark first, in `rs/moq-net/benches/origin.rs`: churn tracks through one - front, swept over tracks per front and readers per track, so today's slope - shows before the fix and the fix's flatness after. -- Keep parked tracks in an expiry index ordered by `since + linger` (a - `BTreeMap` or a heap with lazy deletion), updated on every - `Parked`/unparked transition, so `next_deadline` is its first entry and the - deadline sweep pops only what expired. -- Replace the driver's scan-every-track poll with per-track wakes, so an - event on one track polls that track. This also removes the quadratic - rescan in `serve_front`'s `kio::wait`, where each pass handles one ready - track query and then rescans every track: a route swap measured 186 µs at 32 tracks, 769 µs at - 64, and 2.16 ms at 128 (#4995's `origin/copy_walk`), which is this fix's - regression bench. -- Keep the front's exhaustive walk test passing, and add a unit test that the - index agrees with a full scan across random transitions. -- Not in scope: kio's level-only demand, which lets a reader that comes and - goes between polls skip restarting the linger. It costs one extra source - request, so it doesn't justify a kio generation counter (decided - 2026-09-28). - -Public API: none. Wire: none. - -## Related - -- [Route wakes](/quest/m2/route-wakes.md) - narrows which fronts a route change wakes, in the same origin driver; one owner of `origin.rs` at a time -- [Front parking](/quest/m2/origin-front-parks.md) - also changes what a front holds, and wants a churn benchmark over requesters diff --git a/quest/m2/route-wakes.md b/quest/m2/route-wakes.md index 7a3de9ff46..fb146fe5a8 100644 --- a/quest/m2/route-wakes.md +++ b/quest/m2/route-wakes.md @@ -83,5 +83,4 @@ Public API: none. Wire: none. ## Related - [Routes and announces](/quest/m1/cluster-routing/routes.md) - the identity and selection views the index serves -- [Front deadlines](/quest/m2/front-deadline-index.md) - each spurious wake also pays that per-track poll and deadline scan - [Front parking](/quest/m2/origin-front-parks.md) - replaces the `routed_broadcast` watch loop this wakes diff --git a/rs/moq-net/src/model/front.rs b/rs/moq-net/src/model/front.rs index 2eaf988cd8..88da908756 100644 --- a/rs/moq-net/src/model/front.rs +++ b/rs/moq-net/src/model/front.rs @@ -753,6 +753,21 @@ mod tests { assert_eq!(format!("{actual:?}"), format!("{expected:?}")); } + /// The expiry index and next deadline agree with a full scan of the tracks. + #[track_caller] + fn assert_index_matches_scan(front: &Front) { + let scanned: BTreeSet<_> = front + .tracks + .iter() + .filter_map(|(name, track)| match track.state { + TrackState::Parked { since } => Some((since + front.linger, name.clone())), + _ => None, + }) + .collect(); + assert_eq!(front.parked, scanned); + assert_eq!(front.next_deadline(), scanned.first().map(|(at, _)| *at)); + } + /// A front serving `source` through `candidate`, with `video` spliced and read. fn serving(candidate: Candidate, source: u64) -> Front { let mut front = Front::new(LINGER); @@ -1543,6 +1558,42 @@ mod tests { assert_actions(front.step(Event::Closed), &[Action::End { err: Error::Dropped }]); } + #[test] + fn deadline_index_matches_scan_across_track_transitions() { + use rand::{RngExt, SeedableRng}; + + let mut rng = rand::rngs::StdRng::seed_from_u64(0x66726f6e74); + let mut front = serving(remote(1), 100); + let mut now = Instant::now(); + for _ in 0..4096 { + let track = name(&format!("track-{}", rng.random_range(0..32))); + let event = if !front.tracks.contains_key(&track) { + Event::TrackAssigned { track, now } + } else { + match rng.random_range(0..4) { + 0 => Event::Used { track }, + 1 => Event::Unused { track, now }, + 2 => { + now += LINGER / 4; + Event::Deadline { now } + } + _ => Event::TrackInfo { + track, + source: 100, + closing: false, + result: Ok(info()), + }, + } + }; + for action in front.step(event) { + if let Action::Forget { track } = action { + front.step(Event::Forgotten { track }); + } + } + assert_index_matches_scan(&front); + } + } + /// Every sequence of events up to a small depth, over a small alphabet, /// checked against the invariants the driver relies on. #[test] @@ -1625,6 +1676,9 @@ mod tests { let actions = next.step(event.clone()); *sequences += 1; + // Route-wide events skip the index update, so they must leave parked tracks alone. + assert_index_matches_scan(&next); + // An event that changes nothing produces nothing, except letting // go of a source the front never took. if format!("{next:?}") == before {