Repository navigation
perf(moq-net): lite-07 frame bounds, timed datagram admission, sparse reverse search, and shared time-anchor walks stop walking every range - #7542
Conversation
Quest-Claim: 796724c0c717bdc5
…ion instead of walking every range Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h, and bench per-poll cost over range count rangemap's overlapping iterator walks back from the last run, so each reverse leap in find_servable cost every run above it: live_edge over a sparse feed was quadratic in the reader's ranges. The new ranges bench sweeps range count for each per-poll path the range-count quest names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…of copying them per wake Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Unsettled time ends whose walks agree share one, holding their times in sorted order. A group starting between them splits off the ends before it, which settle as a walk of their own, and walks that come to agree merge. Each end settles once, and a settle costs the walks, not the ends. What a walk has put in range for good moves into the window's fixed groups, so a walk reads one run and a move rebuilds the fixed groups plus one run per walk. ranges_time_end is now flat in range count: 2.9 us per step at 1 and at 16384 ranges, down from 1.19 ms at 16384. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ranges_time_cap lands each group yet to start, then starts it inside a time end, so the window moves twice per group beside fixed ranges far ahead. A move still costs every fixed run, which the quest now records as remaining work. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…x a reader's live groups Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the fills that answered Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cremental passes against whole ones Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…group Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Design change: the maintainer's 2026-11-01 answers to Decisions 2 to 4 are now implemented here, and the local review restarted with
The quest is finished and deleted. Measurements are in the PR body. (Written by Claude Opus 5.5) |
…nk with the cache A group's expiry that moves to a replacement copy searches its reader's servable on that copy's track, whose arrival order and stamps are its own. Keep one index per track searched, keyed weakly by the track's cache account, so neither track's searches skip the other's arrivals. A prune now runs once entries pass twice the live groups, rather than twice what the last prune kept: a cache that shrank after a prune left its stale entries for good. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Addressing the OpenAI review's P2 (the live index not shrinking with the cache): fixed in b3a71f3f03. (Written by Claude Opus 5.5) |
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: b3a71f3
No new actionable bugs found in the changes since e47eff3, including the base-merge integration.
- Fixed: the prior P2 live-index retention finding. Pruning now follows the current visible population, and
the_live_index_shrinks_with_the_cacheexercises successive shrinkage while searches keep succeeding at the newest entry. - The independent cross-track index finding is also addressed: indexes use weak track-account identities, and the equivalence test now searches both tracks through each shared window.
Overall direction: these targeted corrections preserve the incremental range-query design without returning to full rescans; the extra track identity is justified by replacement-copy expiry searches. No new public API or wire change identified.
Verification: static GitHub source review of the update and relevant surrounding code. I did not run tests, builds, or benchmarks, or independently reproduce the reported performance.
(Written by OpenAI)
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Re-reviewed the incremental delta e47eff3af6..b3a71f3f03 (both new commits). The delta is limited to the two fixes for prior external findings plus their tests; no new behavioral surface beyond those fixes.
- Live index keyed per searched track (
model/track.rs):Livenow holdsVec<Indexed>, each carrying aWeak<cache::Track>, andLive::indexpicks the entry whose track account matchesstate.cache. A group's expiry that moved to a replacement copy searches its reader's sharedPublishedon the new copy's own state, so it no longer feeds the original track's arrival order and stamps into the index. - Index shrinks with the cache (
model/track.rs):catch_updroppedkeptand prunes whengroups.len() > 2 * state.visible.len().max(LIVE_BATCH), following the current visible population rather than the last prune's retained count. - Tests:
layered_searches_match_a_scan_of_every_live_groupnow searches each window on two tracks, andthe_live_index_shrinks_with_the_cacheexercises successive cache shrinkage while searches keep answering at the newest entry. - Merge/comment only:
Cargo.tomlfuzzcomment andsubscription.rs'sAccountedre-export changed by theb80294751main merge.
I verified that each TrackState gets a fresh cache::Track in TrackState::spawn (track.rs:2093), so the pointer comparison is a valid track identity, and that stale indexes are dropped when a new track is first indexed, keeping lingering dead entries bounded.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…nges/fill-linear Conflicts: - rs/moq-net/Cargo.toml: keep both the `ranges` and main's `backing` bench. - lite/publisher.rs: main's `.parked()` subscription with this branch's `groups` binding. - track.rs probe: this branch's layered `retain` with main's single-lock `(asks, fills)` judgement. - subscription/window.rs: main's time-start lookups (#7310) ported onto the shared and parked walks. A span looking up its start sits in a `looking` list, outside the walks and runs, so `lookups()` costs the lookups rather than every span. `found` places it as a fixed run or a new walk, which walks from where the span reaches as main's did. Integration fixes the merge exposed: - Main's per-reader holdings (a reader's own refused and given-up fills count as gone) collided with `window::Seen`; renamed `ReaderHoldings`. It now reports what the reader's fills newly gave up as changes (`Misses::take_gone`), so a time end parked on a gap its fill refuses wakes. Every window walk reads it, including a replaced window, a pending one, and a lookup's answer, so none parks on a gap already gone. - A handler appearing or leaving (`looks_up`) wakes every parked walk, since it changes which starts count and what is gone. - miss.rs tests: main's `bound` and this branch's `view` both join `judge`. New test: a_time_end_parked_on_a_gap_wakes_when_its_fill_refuses_it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nges/fill-linear Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: 4147d28
No new actionable bugs found in the substantive integration since b3a71f3, accounting for the merged main changes.
The unresolved time-start lookup handling fits the shared-walk model, and reader-local fill refusals/give-ups now wake affected parked time ends even without a track change. The added refusal regression covers that interaction. The previously fixed live-index retention and cross-track identity findings remain addressed.
Overall direction: keeping unresolved lookups separate and forwarding reader-local wake ranges preserves selective traversal rather than restoring full-window rescans; the additional state is justified by those distinct dependencies.
Verification: static GitHub source review, including an independent pass over lookup resolution and parked-walk wakeups. I did not run tests, builds, or benchmarks, or independently verify the reported performance.
(Written by OpenAI)
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Re-reviewed the incremental delta b3a71f3f03..4147d28035 — the two merges of main into the branch (01e4c8296, hand-resolved; 4147d28035, conflict-free). The delta imports main's time-start lookup path and its per-reader "gone" runs, and integrates both with this branch's parked-walk, layered-run, and shared-walk machinery.
- Time-start lookup over the parked walks (
model/subscription/window.rs,model/track.rs):Windowgainslooking/lookups/found/unresolved;Cursor::look_upasks a fill once per distinct time start, parks on each join, andWindow::foundplaces the answer as fixed runs or as a freshWalkwalked from where the span reaches.awaits_fillcountswindow.lookups()so a bounded read cannot report an unresolved start as its end, andsyncclearsfilter.lookupson a range replacement so a stale join cannot shadow a new resolution. - A reader's own gone runs wake it (
model/track.rs,model/track/miss.rs):Missesrecords what its fills newly refuse or give up (newly_gone);Cursor::syncdrains it intoReaderHoldings, whosechangesappends those runs to the track's and whosegonereports them, so a walk never parks on a gap its own fill refused.a_time_end_parked_on_a_gap_wakes_when_its_fill_refuses_itcovers the wake. - A handler appearing or leaving (
model/track.rs):looks_up()joins theSeen.globaltuple so a handler toggle wakes every parked walk, and re-gatesgone/next_heldfor the history a handler serves. - Merge conflicts:
Cargo.tomlkeeps both therangesandbackingbenches, andlite/publisher.rsnow builds the covered subscription with.parked(), leaving the FIN to the wire.
Main's three lookup tests run over the reworked window and pass, and a focused sub-review of the lookup/parked-walk integration found no scenario where a parked walk misses a wake or a lookup is dropped. No public API or wire change.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Re-reviewed the incremental delta 4147d28035..74708ae93 — two merges of main into the branch, with no change to this PR's own added lines.
- Main's request track-name move (
lite/publisher.rs): thefa01c8a66merge resolved its one conflict in thestdimport list by keeping main'sborrow::Cowalongside this branch'scollections::{BTreeMap, HashMap}. Main's hunks (Request::take_track,RequestServe'strack()accessor) sit around lines 1555-2340 and tests near 9061, disjoint from this PR'sFrames/Bounds/TrackRunadditions. - Quest-only merge (
74708ae93): conflict-free merge of #7616, touching only quest files. - Integration check:
SubscribeServe::startandsubscriberead the passedtrack: &strplusmsg.ranges/priority/order, never the now-emptiedmsg.track, soTrackRun.named(cached frombounds.groups().start()) and theFramesmerge are unaffected.
The range-diff against 4147d28035 shows the branch's own lines unchanged apart from that import hunk, so the prior full-diff review carries over.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…nto main's cursors Decided 2026-11-03 (maintainer): re-port in place. Main rewrote the JS cursor paths this PR changed (#7486 clean-close completion, #7597 `Delay { min, max }`, #7609 owed-hole waits, #7542 Rust window walks), so the ranges, window, and lite-07 serving go on top of main's versions. - Bounded reads end, as Rust's do since #7545 and #7625, replacing the 2026-10-31 decision to park: a plain reader judges its window as Rust's `Cursor::is_exhausted` does, and a reader following route copies judges it on the serving copy as Rust's `Reader::exhausted` does. Wire publishers and route copies park through `hooks.parked`. - Main's `delay: { min, max }` replaces this PR's `maxDelay`, with `NO_MAX_DELAY`/`maxDelayOf` as the no-limit value. - The arrival grace is no longer capped at CACHE_EXPIRY: main bounds it by the publisher's max age instead, and an unlimited ordered reader waits for its hole (#7609, #7169). - Owed holes count only inside the reader's ranges. - lite-07: a SUBSCRIBE goes out alongside its TRACK, as on main, unless a time anchor needs the track's timescale, as the Rust `pipeline` does. The covered FIN without SUBSCRIBE_END stays lite-07 only, now that `waitsForSubscriberFin` covers lite-05+. - Rust: the waiting time start is ported onto main's window walks as a placeholder span, as a lookup is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…trand it #7542 gave a cursor's misses one waker for every fill it waits on, replaced by whichever path resolved last. A read parked behind its idle gate and a drop poll resolve with different waiters, so the drop poll's took over: the fill's answer woke only it, the drop poll settled the want, and the read's gate stayed armed on a want already gone. The read never reached the track's end. Register each resolving waiter in a kio::WaiterList instead, so an answer wakes every waiter that resolved since the last one, as polling each fill directly did before #7542. The resume model passes 3000 cases on seeds 1 to 16, 777, 4242, 12345 and its default again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Problem
A lite-07 SUBSCRIBE fits about 32K ranges in its 64 KiB message. After moq-dev/moq#6995, the Range count cost quest listed the walks that still grew with range count on each poll, and a peer can multiply each one by the number of ranges it names.
This PR finishes that quest. Every path in the
rangesbench now costs about the same per step at 16,384 ranges as at 1, and the quest file is deleted.Approach
Earlier rounds, kept as they were:
Framesinlite/publisher.rsis aBTreeMapkeyed by group, with the parts merged as the bounds decode. Each lookup is one search.Windowkeeps anAdmissionindex, four sorted lists for the four pairings of group or time bounds. Left as it was: moq-dev/moq#7575 removes it with stream-track datagrams.Walk, holding their times in aBTreeSet. A group starting between them splits them, and walks that converge merge.find_servablesearches a sortedVecof runs by binary search, not rangemap's iterator.This round (decisions 2 to 4 below):
model/subscription/runs.rs). The window keeps its groups asLayered: shared fixed runs (Arc<Runs>) with the walks' runs layered over them (also anArc). Each query (contains, first or last run reaching into a span, runs from a point) answers from both by binary searches. When a layered run covers many fixed runs, a query grows the run across them in a few searches instead of visiting each one. A move rebuilds only the layer. Settling a time end, and a walk parking or waking (below), rebuild the fixed runs, which happens once per end or per gap event, never per group. Each pass that followed a move now works from the layers:Window::rebuildsets the layer from the walks not parked.Servable::Runsholds the layered set, its cap, and the reader's live index, soupdate_servablecopies twoArcs, andPublishedcompares the fixed runs by reference first.Cursor::admitkeeps the admitted set as a layered snapshot (Admitted). When only the layer moved, it owes what the layer gained, but only where the cache holds a group now, the only placenext_owedcan find one. It also cuts what the layer lost, only from what is owed.Misses::retainandMisses::judgekeep the last missable view. When only the layer moved, they forget what it lost and judge what it gained, within what they hold or judge (from the floor up to the newest group, below the cap), instead of clipping every set to the whole window again.covered_byneeds nothing: it returns early while a time end is unsettled, so its memo only restarts after a settle.Liveintrack.rs). EachServable::Runscarries an index of the live feed's cached groups inside its fixed runs, shared by every servable built on the same fixed runs. It takes in the arrival order on the reader's own search, one lookup in the fixed runs per arrival. An entry whose slot left or was replaced is dropped when a search meets it, or once stale entries could outnumber the live groups.find_servablesearches the index for the fixed runs and leapfrogs only the layer's few runs, short of what the index found. Entries are handed out in batches with the lock released, since an answer reads each group's own state.Window::parked). A walk whose settle stopped at a gap below the group its ends fall before, with nothing between that could still start, parks. Until a sequence from aboveknownup to that group arrives or is given up, it reads the same answers. SoHoldings::changesreports the runs whose arrivals or drops changed (the arrival ledger's and the drops' change indexes), and a settle wakes only the parked walks a change reaches. The declared start, completion, or a fetched group (which the ledger never sees) wakes them all. Parked walks whose waits overlap are kept together, so the parked groups stay disjoint and one search finds each. Their runs live in the fixed runs, so a moving cap beside thousands of them still rebuilds only the walks not parked.miss.rs):poll_driftedjudges all of them. A fresh stretch with no arrival before the next live group is passed in one step, however many runs lie between. A property test checks this against judging each stretch alone.JoinWake), which records its key before waking the task the reader last resolved for.resolvelooks only at the recorded joins and the new ones, so a reader read by another task since still learns of every answer.judgeno longer walks every wanted run to clip what an eviction gave up again: it visits only the runs overlapping what it found.ranges_time_gapsbench. Time ranges each ending between two groups of a sparse history, so each waits on a gap of its own, beside one time end past the live edge whose cap moves twice per group.Tests
New property tests, each comparing the fast path with the walk it replaced, with deterministic generators so a failure replays:
layered_runs_match_their_union,a_moved_layer_gains_exactly_the_new_groups,inserted_runs_merge_as_a_set,a_run_minus_runs_is_its_gaps: the layered set against the set of its runs merged.layered_searches_match_a_scan_of_every_live_group:find_servablethrough the index and layer against a scan of every live group, in both directions, as groups arrive, start, abort, leave the cache, or arrive again between searches that share one index. It found an early return the suite missed (the index finding group 0 ended the whole search).expiring_by_successor_matches_each_stretch_judged_alone: chunked expiry against the old per-stretch judgement, on both clocks, against one run or several.a_moved_layer_is_judged_as_the_whole_window_would_be:retainandjudgeon a moved layer against a second reader handed an unshared copy of the same fixed runs, which judges the whole window each time. Removing either incremental cut makes it fail.a_moving_time_cap_owes_what_it_newly_admits:admitowes, of every cached group, exactly what was owed and is still admitted, plus what is newly admitted. Dropping one gained run makes it fail.an_answered_fill_wakes_the_task_that_read_last: four fills, one answers. Only its join is looked at again, and the later task wakes.shared_walks_match_each_end_walked_alonenow exercises parking. Its test holdings report changes as a track does: arrivals and runs given up, never a start. It caught two wrong parking rules while I wrote this (overlapping waits, and a late arrival below the gap). It passes 8,000 rounds on two seeds, with about 69K parks.Measurements
Criterion's time per step, pinned to P-core 6. "Before" is this PR's previous head, a625ae96e4, run with this round's bench file. Earlier rounds reported some rows in instructions; these are all times, from the same run.
ranges_time_capranges_fills_outranges_sparseranges_time_gapsranges_time_endranges_datagramranges_framesAt one range most rows cost the same, within 3 to 8%.
ranges_time_capsteps from 4.4 µs at one range to 6.5 µs at 64: one range is a single run, kept off the heap, and several runs allocate the layered servable on each move. From 64 to 16,384 it is flat up to the binary searches.ranges_framesgrows with the depth of itsBTreeMap, as in the earlier rounds.Impact
fuzz::FrameBoundsis only exposed through the hiddenfuzzmodule.Decisions
Live, indexing the fixed runs, with the layer's few runs leapfrogged.live_edgeper cursor. Not chosen: it leavessuccessorandarrival.Window,Servable/Published,admit, andMisses.Window::rebuildincremental. Not chosen: it leaves the other passes.The quest is finished and deleted in this PR, so its Plan isn't updated: these records are the paper trail, with git history keeping the quest's last Plan.
Choices made without asking
What a moved layer owes is cut to the cached groups.
admitowes what the layer gained only between the lowest and highest cached sequence. A live group arriving later in that span comes from the arrival walk, as one arriving above it always did, not ahead of it throughnext_owed. The property test checks every cached group.Parked walks drop out of
watch_startsandspeculative. A parked walk waits on an arrival, never a start, and an arrival changes the track a read parks on anyway.A fetched copy wakes every parked walk. Fetched backfill, and a fetched head joining a live group (which can give a group received past frame 0 its start), are never in the arrival ledger. So the track counts them (
fetched), and a change in the count wakes all parked walks rather than adding a third change index.Rebuilt fixed runs keep the old
Arcwhen unchanged, so a walk that wakes and parks again on the same gap keeps every reader's diff on the layer.The test holdings model the track's change reporting, so a starting group reports nothing there either. The parking rules are tested against what the track reports, not what a richer holdings could.
Every window walk reads the reader's own gone runs, where main used the track's alone when replacing or first placing a window and taking in a lookup's answer. Main's walks redo each settle, so this cost nothing there. A parked walk wouldn't redo it, and could stay parked on a gap already gone for its reader.
Remaining cost, by design
Merging main
Main landed every PR this one expected to conflict with (moq-dev/moq#7545, moq-dev/moq#7499, moq-dev/moq#7310, moq-dev/moq#7493); moq-dev/moq#7207 closed unmerged. Merged in 01e4c8296b:
lookinglist inWindow, outside the walks and runs, solookups()(read on every probe) costs the lookups, not every span.Window::foundplaces the span as fixed runs or as a new walk that walks from where the span reaches, as main's did. Main's three lookup tests run on the branch's window.Seenholdings, renamedReaderHoldingssince it collided withwindow::Seen). A walk parked on a gap its fill then refuses would never wake, since the track's change indexes don't see it.Missesnow records what it newly gives up (take_gone), and the holdings report those runs as changes. Every window walk reads these holdings: a replaced window, a pending one, a settle, and a lookup's answer. None parks on a gap already gone for it. New test:a_time_end_parked_on_a_gap_wakes_when_its_fill_refuses_it, which fails without the wake.looks_upchanges which starts count and what is gone, so the track's change marks include it.Cargo.toml(both therangesandbackingbenches),lite/publisher.rs(main's.parked()subscription), and theprobejudgement (the layeredretaininside main's single lock). Main's newjudgetests take both main'sboundand this branch'sview.4147d28035 then merged main again, conflict-free.
fa01c8a663 merged main again, up to
5e327b55f8. Its one conflict was thestdimport list inlite/publisher.rs: main's moq-dev/moq#7500 addedborrow::Cow, and this branch addedBTreeMapforFrames, so the merge keeps both. Main's request changes (the track name moved out of the message) touch no code this branch changes. The branch's added and removed lines against main are the same as on the reviewed 01e4c8296b; only context lines differ. 74708ae931 then merged main's quest-only moq-dev/moq#7616, conflict-free.Checks
On fa01c8a663, the last merge that touches code:
just check: passed, 9501 tests.quest check --base origin/main: passed, also on 74708ae931.On 4147d28035:
a_time_end_parked_on_a_gap_wakes_when_its_fill_refuses_itfails with the reader's newly gone runs left out of the changes.rangesbench (3 s measurement, P-core 6, loaded host) stays flat in the range count:ranges_time_cap4.97 / 9.81 µs,ranges_fills_out4.58 / 5.24 µs,ranges_sparse2.57 / 2.78 µs,ranges_time_gaps5.91 / 8.41 µs at 1 / 16,384 ranges. The table above is from the earlier full run.Online review findings on e47eff3
track.rs(thread): a group's expiry that moves to a replacement copy (GroupExpiry::for_track) keeps its reader'sPublished, so its searches fed the reader'sLiveindex another track's arrival order and stamps. Fixed in b3a71f3f03:Livekeeps one index per track searched, keyed weakly by the track'scache::Trackaccount.layered_searches_match_a_scan_of_every_live_groupnow searches every window on two tracks.state.visible), not twice what the last prune kept. A live entry is a visible slot, so a prune always drops more than half. New test:the_live_index_shrinks_with_the_cache.Review
gpt-6.1-sol, medium effort (quest review --base origin/main).track.rswas fixed in c81f46d1e8.fdeace96bc): no findings.Follow-ups
Admissionindex andranges_datagramalong with stream-track datagrams.🤖 Generated with Claude Code
(Written by Claude Opus 5.5)