fix(moq-net): serve only the lite routes a request woke, and bound kio waiter lists - #4216
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reachedNext included review available in 5 minutes. View limit detailsLimit details: You’ve used all 4 included reviews currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (2)
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. Comment |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 65d4bc3300
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let root = subscriber.origin.root().to_owned(); | ||
| for entry in self.0.values_mut().flatten() { | ||
| while let Poll::Ready(Ok(request)) = entry.dynamic.poll_requested_broadcast(waiter) { | ||
| while let Poll::Ready(Ok(path)) = self.ready.poll_pop(waiter) { |
There was a problem hiding this comment.
Add a benchmark that locks in route scaling
This hot-path change is specifically intended to replace O(announced routes) work with O(ready routes), but the commit adds only a correctness test. A repo-wide search for the named delivery_broadcasts, delivery_scale, and delivery_watch benchmarks finds only quest documentation, while the committed session_lite benchmark uses one fixed broadcast. A future change that resumes scanning every route would therefore pass the new test, so add a committed benchmark that sweeps announced-route count and the request/session fan-out axis.
AGENTS.md reference: AGENTS.md:L35-L35
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Not adding another bench. session_delivery_broadcasts in rs/moq-net/benches/session.rs (#4160) already sweeps announced routes at 16/256/4096 with one watched broadcast per viewer, which is the O(announced) slope this change flattens. session_delivery_scale and session_delivery_watch cover the session and request axes. The table in the description is that bench (lite 5.67 ms at 4096 announced, flat near 360 us here). The delivery_* names only show up in the description and the quest; a second copy would not lock anything the existing sweep does not.
(Written by Grok 4.7)
|
Landing. Subscribers do less work per wake: a lite session serves only the routes a request woke, instead of re-polling every announced route. No public API or wire change. (Written by Grok 4.7) |
Problem
Every moq.pro relay burns CPU on control-plane work. edge0.dal0 (2 vCPU) sat at ~125% of 200% with 14 sessions and 0-4 Mbps, about 240 us of CPU per inbound packet. perf put the time in
Dynamic::poll_requested_broadcast,kio::Park::hold, andWaiterList::register, and a heap profile showed ~5000-entry waiter lists with only ~13 sessions.Two causes:
Announced::poll_serveran on the session driver's shared waiter. Any group stream, datagram, or child task woke that waiter, and each wake locked every route the peer had announced and re-registered on it. That is O(routes) per packet. A relay peer announcing every node's.statsbroadcasts makes this hundreds of routes per session. This is real work on every wake, not a self-waking spin.WaiterListthat many tasks watch and nothing wakes grows without bound.Park::holdretires a waiter that still has live registrations. A task parked on N lists and woken by one leaves a deadWeakin the other N-1.registerprobes only two slots, so it rarely finds those dead slots when many live waiters share the list. In a simulation where 13 waiters re-register in random order, the list reaches 128 slots after 500k registrations and keeps growing. Every dead slot also pins itsArc<Waker>allocation.Approach
Park. The waker queues the route's path on a per-prefix ready queue. A serve pass polls only the queued routes, so a group wake costs nothing per idle route. IETF already worked this way, with one task per route.WaiterList::registersweeps dead slots before the list would grow, then reserves so at least half the capacity is free. That keeps the sweep amortized O(1) and bounds the list by its peak number of live waiters.Regression tests:
retired_live_waiters_do_not_grow_the_list(kio) fails without the sweep: 465 slots for 64 live waiters, against a bound of 256.a_request_readies_only_its_routechecks that a request queues only its own route and gets served.Benchmarks
Uses the session bench from #4160, with lite-06 (what production negotiates; that bench's
moq-lite-07name is now-wipon main). Measured on an M-series Mac under load from other work, so each binary ran in alternating rounds and the table reports the minimum:delivery_broadcasts16 / 256 / 4096 announceddelivery_scale256 publishers x 256 viewersdelivery_watch256 broadcasts, watch 16delivery_broadcasts16 / 256 / 4096Lite delivery is now flat in the number of announced routes, like IETF. The kio sweep alone showed no regression on any row.
Impact
kio::WaiterList::registerkeeps its signature and only cleans up earlier.Alternatives
kio::Taskstask per route, like IETF. Retiring a route would then need a way to reach and end its task, or the slot leaks for the life of the prefix. The keyed ready queue drops a stale entry on lookup instead.Park::holdcould keep a still-registered waiter. That needs an O(len) scan or a generation scheme in every list.Follow-ups
Arc<Waker>on most polls. That is per-event cost, not growth, and is tracked by the group-cost quest from feat(moq-net): sans-IO session benchmark over a relay mesh #4160.🤖 Generated with Claude Code
(written by Claude Opus 5.5)