Repository navigation
perf(quic): send groups leave their round robin without a scan - #7527
Conversation
Quest-Claim: ac29367135c4df01
Each priority level's round robin is now a circular list threaded through the group slab, so moving a group to another priority or freeing it while it holds a round-robin place is O(1) instead of a search of every queued group at its level. Order, deficits, and the lazy removal of groups that run dry away from the front are unchanged. Adds scheduler cases moving and retiring queued groups from the front, tail, and spread across the round robin, and a connection case churning send groups under traffic. 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>
…survey/quic-schedule-churn
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 2 minutes. View limit details
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. |
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: 1077a17ef620323b2c55627778ea0942b9114001.
No actionable bugs found. Checked ring insertion/removal (including singleton and head removal), deficit rotation, reprioritization, lazy dry-group retention, slab retirement, and the new scheduler/connection benchmarks. The slab-linked ring directly removes the linear scan while preserving queue order; its localized complexity is proportionate, with no public API or wire change.
Verification limits: static source review, including an independent scheduler pass. I did not run tests, benchmarks, or the reported differential trace, and have not independently reproduced the performance figures.
(Written by OpenAI)
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- O(1) round-robin removal:
schedule.rsreplaces each priority level'sVecDeque<usize>with a circular doubly linkedRing { prev, next }on each group, withlevels: BTreeMap<i64, usize>holding the head.unringsplices a group out from anywhere instead of scanning, soset_group_priorityand retirement no longer cost O(queued). - Preserved policy:
deactivatestill only unlinks the level head, so a group that runs dry away from the front keeps its place;free_groupunrings beforegroups.remove, andset_group_priorityunrings before mutatingpriority, keeping the ring and head map consistent. - Tests: new
groups_leave_the_round_robin_from_anywherecovers mid-ring removal and a reprioritized group returning to the tail. - Benchmarks:
schedule/group_priority,schedule/group_retire(queued × changed × front/tail/mixed), and real-packetgroup_churn. - Quest docs: deletes
quic-schedule-churn.mdand its README line.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
|
codex / Codex land fixer note:
(written by codex / Codex) |

Finishes QUIC scheduling churn from the performance survey.
Summary
The QUIC send scheduler kept each priority level's round robin of send groups in a
VecDeque. Moving a group to another priority (Groups::set_priority, used by moq-lite subscription updates) or freeing a group that still held a round-robin place searched that deque linearly. On a session with many queued subscriptions at one level, each update or cancel cost about 5 instructions per queued group ahead of it.Each level's round robin is now a circular doubly linked list threaded through the group slab (
Ring { prev, next }on each group, with the level map holding its head). Leaving the round robin from anywhere is O(1). Push-back, pop-front, and the deficit rotation keep the same order. Groups that run dry away from the front still keep their place until the round robin reaches them. No scheduling policy, public API, or wire change.rs/moq-quic/src/connection/streams/schedule.rs: the linked ring, plus a unit test covering removal from the middle of a ring and a reprioritized group going back to the tail.rs/moq-quic/benches/streams.rs:schedule/group_priorityandschedule/group_retire(queued × changed groups, front/tail/mixed), andgroup_churn(send groups churned through the public API under real packet exchange).streamsalready runs nightly injust gh nightly rust-sharedand on CodSpeed, so no workflow change is needed.Measurements
All measurements are on an Intel Core Ultra 7 270K Plus (Linux 7.0), Rust from
nix develop. Each process is pinned to P-core 6 (taskset -c 6), and user-space instruction counts come fromperf_event_open/perf stat -e instructions:u. Base isd42abb2a86. Base and candidate runs alternated, two rounds each. The A/A control (base against itself) matched exactly in every scheduler case.Scheduler alone. A scratch harness compiled both
schedule.rsversions from source into one binary. N groups at one priority each had a queued stream. Each run moved M of them to another level, or retired M of them (close the group, then its last stream leaves). Instructions per operation:pop_front.repeat, 256 flips) went from 10,701 to 288 instructions per flip at N = 4,096. It also went from 128 allocations to 0, because a level no longer allocates a deque.Dispatch during churn. N groups were kept backlogged, with 100,000 pop/sent dispatches. Every 8th dispatch also cancelled a random group and opened a replacement, and every 8th (offset) moved a random group to the other level. Instructions per dispatch, churn included:
Connection, real packets. The
group_churnbench gives one send group per subscription on one connection. Each round writes a 256-byte stream in every group. With that data queued, it moveschurngroups to another priority and back and cancelschurnothers (reset, close, reopen), then drives until the server has seen every stream. The bench was built twice (base and candidateschedule.rs). Each figure is one--testpass of the case minus a pass whose filter matches nothing, so setup cancels out. Instructions per round:(Rounds agree within about 1.5k instructions. With an unlimited controller the backlog drains every transmit, so only groups queued between writes and the next transmit get scanned. A congested session keeps groups backlogged across transmits, which makes the scheduler-alone case the closer model.)
Thread-CPU timing of the same cases on this shared host (load average about 13) stayed within criterion's noise for 16 churned groups. The instruction counts are the evidence here.
Equivalent work. A differential trace ran 200 seeds × 50,000 random operations on both versions: open, close, and reprioritize groups; join, leave, and reprioritize streams; push, evict, and send datagrams; pop/sent with fair and unfair requeue. Every seed produced an identical hash of every pop result and eviction outcome. A temporary counter confirmed each trace removes a group from mid-ring about 2,300 times.
Memory.
Groupgrows from 96 to 120 bytes. Each level'sVecDeque(a 24-byte value, plus a heap buffer of 8–16 bytes per ringed group, allocated per level) becomes oneusizehead. The net is about +8 to +16 bytes per group.Nightly. The new cases run in
just gh nightly rust-shared(--bench streams). The new benches add 24 scheduler cases and 4 connection cases. Locally,cargo bench -p moq-quic --features fuzz --bench streams -- --test group_runs each case once, and all of them pass.Checks
just check origin/mainpassed rs (clippy, then nextest over moq-quic and its dependents), gh, drill, shell, toml, nix, and justfile. Markdown passed on a re-run: the first run raced the quest deletion.just rs checkskips the fuzz-gated benches, so I also rancargo clippy -p moq-quic --features fuzz --all-targets -D warningsandcargo clippy -p moq-net --features async --bench schedule -D warnings(that bench compilesschedule.rsfrom source), plus a--testpass of the moq-netschedulebench (60 cases).Choices made without asking
deactivatenow gives that as the reason instead of the old scan cost.priority::group). So the publisher axis decides how groups spread across levels, and the scan only touched groups at the same level. The connection bench uses the worst case: every subscription at one level. A full relay run would add relay costs on top of the same scheduler path.Coordination
moq-dev/moq#7490 (draft, quic-api/waits) touches moq-quic's
poll_transmit,streams/{mod,send,state}.rs, andbenches/common/mod.rs. This PR changes onlystreams/schedule.rsandbenches/streams.rs, so the two don't overlap. moq-dev/moq#7388 touchesstreams/state.rsonly.Measured no-win
I tried trimming the front-retirement regression by unlinking through a single
BTreeMap::entrylookup. It saved 9 instructions there but cost 24 per priority move, 34 per mid-ring retirement, and 10 per dispatch, so I reverted it.Follow-ups
None.
Review
Round 1: Codex (
quest review), gpt-6.1-sol, medium effort, againstorigin/mainba421c30d3. Reviewed head1077a17ef620323b2c55627778ea0942b9114001. No findings.🤖 Generated with Claude Code
(Written by Claude Opus 5.5)