Skip to content

perf(net): seek instead of scan for the next in-range group - #3088

Open
kixelated wants to merge 1 commit into
mainfrom
claude/trusting-tharp-6f9ac7
Open

perf(net): seek instead of scan for the next in-range group#3088
kixelated wants to merge 1 commit into
mainfrom
claude/trusting-tharp-6f9ac7

Conversation

@kixelated

Copy link
Copy Markdown
Collaborator

Summary

  • Root cause: TrackState::lookup was a HashMap<u64, Slot>. A hash map has no order, so poll_next_in_range had to scan every cached slot to find the lowest sequence at or above the subscriber's cursor. Subscriber::next_group calls it once per delivery, so draining N cached groups was O(N * cache_size).
  • This is the path the media consumers use (moq-mux's container and MSF catalog readers, moq-json, moq-ffi, moq-transcode's feed, and moq-net's resume), and cache depth is the retained group count. A track publishing one group per frame (hang audio, any write_frame track) at the default 5s retention holds ~250 groups, so every delivery scanned ~250 entries.
  • Fix: make lookup a BTreeMap and seek with range(next_sequence..). The end_sequence cap becomes a take_while, which is only correct because iteration is now ascending; the old scan had to continue past it. Aborted slots (awaiting the next eviction scan to reclaim them) are still stepped over, and the early return that parks rather than ends the stream when end < next_sequence is unchanged, as is final_sequence termination.
  • Trade: lookup get/insert/remove go from O(1) to O(log n). Those are the eviction and fetch paths, which touch one entry at a time, so it is a good trade against removing a full scan per delivery. There is a single insert site and it keys on group.sequence, so the seek key is exact.

Numbers

New track_recv_groups bench arm, sweeping cache depth across both delivery orders. arrival is recv_group (an arrival-order index walk, already flat); sequence is next_group.

cached groups arrival sequence (before) sequence (after)
64 1.79 Melem/s 720 Kelem/s 1.64 Melem/s
512 1.72 Melem/s 110 Kelem/s 1.89 Melem/s
4096 1.76 Melem/s 16.7 Kelem/s 1.79 Melem/s

Flat across a 64x depth increase, ~107x at the top end. The "before" column is from the original report; the "after" column and the arrival baseline are from this branch.

Public API changes

None. TrackState and its lookup field are pub(crate); no pub item in rs/moq-* or js/* is added, renamed, removed, or resignatured. The only additions are private helpers in the benchmark. Targets main accordingly.

Test plan

  • just fix (no changes beyond the two files here), just check, just test: 2799 tests pass, 1 skipped.
  • Added the track_recv_groups arm to rs/moq-net/benches/group.rs as the regression guard. nextest runs criterion benches in test mode, so the arm is exercised in CI rather than rotting until someone next runs cargo bench.
  • Ran cargo bench -p moq-net --bench group -- track_recv_groups for the table above.

Cross-Package Sync

No row applies: this is an internal data-structure change in moq-net with no wire, catalog, config, or CLI surface touched, so js/net and the drafts are unaffected.

Follow-up

#3086 covers making the delivery order a handle (ordered()) rather than a method choice, and moving moq-mux's timestamp-based group skipping down into moq-net. Worth noting for reviewers: this fix removes the performance argument for treating sequence order as the slower opt-in path, but the API argument stands on its own, since recv_datagram bumps next_sequence and read_frame bumps both cursors, making the two orders quietly interact on one Subscriber.


(Written by Claude Opus 5)

`TrackState::lookup` was a `HashMap<u64, Slot>`, which has no order, so
`poll_next_in_range` had to scan every cached slot to find the lowest
sequence at or above the subscriber's cursor. `Subscriber::next_group`
calls it once per delivery, making a drain of N cached groups
O(N * cache_size).

That is the path the media consumers use (moq-mux's container and MSF
catalog readers, moq-json, moq-ffi, moq-transcode's feed, and resume),
and cache depth is the retained group count: a track publishing one
group per frame at the default 5s retention holds ~250 groups, so every
delivery scanned ~250 entries.

Make `lookup` a `BTreeMap` and seek with `range(next_sequence..)`. The
`end_sequence` cap becomes a `take_while` now that iteration is
ascending, and only aborted slots (awaiting the next eviction scan) are
stepped over. Lookups by sequence go from O(1) to O(log n), but those
paths touch one entry at a time.

Measured with the new `track_recv_groups` bench arm, sweeping cache
depth with both delivery orders. Sequence order was 720 Kelem/s at depth
64 and collapsed to 16.7 Kelem/s at 4096; it is now flat at ~1.8
Melem/s across the sweep, matching the arrival-order walk. nextest runs
criterion benches in test mode, so the arm is exercised in CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bbdef98d-7ab4-4991-99b3-da04022c0dc4

📥 Commits

Reviewing files that changed from the base of the PR and between 5ddaed0 and f2081d6.

📒 Files selected for processing (2)
  • rs/moq-net/benches/group.rs
  • rs/moq-net/src/model/track.rs

Included review availability: Your plan provides up to 4 included reviews per hour; 1 remains after this review.


Walkthrough

The track cache now uses BTreeMap instead of HashMap. Sequence delivery seeks directly from the subscriber cursor and skips out-of-range or aborted groups. The group benchmark now builds tracks with cached groups, sweeps cache depths, measures recv_group and next_group, and registers the new benchmark.

Merge Risk: ⚪ Minimal · up to f2081

This changes internal group lookup to seek efficiently in sequence order without changing the public API or intended delivery behavior. No actionable merge-blocking risk remains beyond normal checks and review.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: replacing a scan with a seek to find the next in-range group.
Description check ✅ Passed The description directly explains the performance problem, the BTreeMap-based fix, benchmark results, preserved behavior, and test coverage.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 2 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch claude/trusting-tharp-6f9ac7

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

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant