Repository navigation
fix(net): a fired deadline costs its driver one poll, not two - #5393
Conversation
`Driver::poll` advances its clock, which wakes the due deadlines. A deadline owned by a `kio::Tasks` child wakes the child's set, and the set wakes its owner: the driver task, already mid-poll. The executor then polled the driver again for a wake the same pass had taken. The deadline's re-arm did the same through the driver's own clock registration, still live from the last poll. `Waiter::batch` runs one driver poll; a `Tasks` set woken from inside it holds its wake of the owner until the batch returns, and drops it if the set was polled since. Wakes from other threads are never held. The clock drops the driver's registration on `advance` and re-registers it with the timeout read, under one lock, when the poll ends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
55e5f60 to
5927253
Compare
A `Tasks` wake from another thread-local's destructor at thread exit can run after the batch scope's own thread-local is destroyed; `with` then panicked inside a destructor and aborted. `try_with` delivers the wake unheld. Tighten `idle_session` to one driver poll per PROBE refresh now that #5382 is in, and bench the batch's own cost next to the plain sparse-wake pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
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 11 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. 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. |
|
Queuing for merge at
(Written by Claude Opus 5.5) |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
Every
time::Deadlinethat fired cost its session driver two polls instead of one.Driver::pollcallsClock::advance(now), which wakes the due deadlines. When a deadline belongs to akio::Taskschild (the lite publisher's control streams, for example), the child's wake sets its bit and wakes the set's owner. That owner is the driver task, which is already being polled. So the executor polls the driver again once it returns, and that poll finds nothing, because theTasks::polllater in the first pass already ran the child.A second wake hid behind the first. The child usually re-arms its deadline, and
Deadline::setwakes the driver's clock registration when the new instant is the earliest. That registration was still live from the previous poll. Removing either wake alone changes nothing: the other still costs the second poll.Fix
kio::Waiter::batch(|| ..)runs one poll of the waiter's task. Inside it, aTasksset whose owner is that task holds its wake of the owner. When the batch returns, the wake goes out only if the set is still pending, meaning nothing polled it after the wake. The child's own ready bit is set immediately, as before. Wakes from other threads, and wakes of any other task, are never held. No latency is added: the executor cannot re-poll a task before its current poll returns. The scope is thread-local (loom::thread_local!under loom) and is read withtry_with, so a wake from another thread-local's destructor at thread exit is delivered rather than aborting the process.Clock::advancedrops the driver's registration. It usestake()so the list keeps its serial; a freshWaiterListwould invalidate the driver waiter's records and make it re-register on every list.Clock::parkre-registers it and reads the earliest deadline under one lock at the end of the poll. A deadline armed during the poll is reported bypark; one armed elsewhere afterparkwakes the driver.time::Driverimpls (the sessionDriverand the originDriver) wrap their poll inwaiter.batch.Why opt-in, not the default for every
kio::waitpollMeasured with the same workloads as below:
kio::waitpollTaskschild pollMost
kio::waitpolls own noTasksset, so a default scope is pure overhead there. A batch nested inside another also shadows the outer one, because only the innermost batch holds wakes. A child's wake of a sibling in the driver's set then reaches the executor, which is why scoping children added lite polls back. The batch pays at a task's outermost poll that wakes its own children before polling them, which is what a driver advancing its clock does. Thebatchdoc says to use it once, there.Measurements
Deterministic: simulated time (
moq_net_sim), lite-06,--profile bench, pinned to one P-core,perf stat -e instructions:u. Every variant ran at least twice with identical counts. Instructions cover the whole process, including setup, the client sessions, the mock transport and the executor.The
session_delivery_pacedroom frombenches/session.rs(lite-06, 16 publishers), run for a fixed 256 rounds:Criterion (
cargo bench -p moq-net --bench session -- 'session_idle|session_delivery_paced/moq-lite-06'), run as before / after / before / after on a heavily loaded 24-core host (load average 25 to 40). Medians:The paced viewers=256 criterion row disagrees with the fixed-round count for the same room (−4.0% instructions, 524 ms → 513 ms wall). Treat the criterion numbers as noise on this host; the instruction counts are the measurement.
The batch's own cost, from the new
kiobench (cargo bench -p kio --bench tasks -- wake_16_of_1000):kio_tasks752.7 ns,kio_tasks_batch771.6 ns per pass, so about 19 ns per driver poll against a whole saved poll.The moq-transport sessions' extra polls come from a different path than
Clock::advance. Their driver is a boxed future, so its children's own sets are woken inside nested polls. That is left for another PR.Tests and benchmarks
kiotask::tests::a_wake_from_the_owners_own_poll_is_not_redelivered: the owner's batch wakes a task in its own set, then polls the set. The owner must not be woken. Fails without the hold.kiotask::tests::a_held_wake_lands_when_the_set_is_not_polled: the same wake, with the batch returning before polling the set, must still reach the owner.kiotask::tests::a_wake_during_thread_teardown_reaches_the_owner: a task woken from a thread-local's destructor after the batch scope's thread-local is destroyed. Aborts the process (SIGABRT, TLS access after destruction) withwithin place oftry_with.kiolooma_held_wake_never_hides_a_remote_wake: a held wake races a wake from another thread, with the batch either consuming its wake in the same pass or returning first. Makingreleasea no-op deadlocks the model.moq-nettime::tests::a_deadline_costs_the_driver_one_poll: aTaskschild re-arming a periodic deadline under the driver's advance, poll and park sequence. Three ticks wake the driver zero times, and an earlier deadline armed outside the poll still wakes it once. Fails with either half of the fix removed.moq-nettests/idle_session.rs: with fix(net): stop idle viewer sessions from waking every 100ms #5382 merged, the bound tightens from2 * refreshestorefreshesdriver polls per 60 s idle. Main: 12 polls, fails. This branch: 6. Withbatchmade a passthrough (the Clock half alone): still 12.kiowake_16_of_1000/kio_tasks_batch: the existing sparse-wake bench with the gates fired inside the owner's batch, so the hold and release cost is tracked. The session-level win stays tracked bymoq-net'ssession_idlebench and the deterministicidle_sessionbound.just rs loomfor kio passes, including the new model.Decisions
Settled during
/quest-iterateby an agent that could not prompt; each took the recommended option. The maintainer confirmed decision 1.kioAPI? (Maintainer confirmed: public, namedbatch.)Waiter::batchpublic, under the namebatch(recommended). The kio half is required (batchas a passthrough leavesidle_sessionat 12 polls), only the owner knows it is mid-poll, and the owner (moq-net's drivers) is in another crate.Driver::pollis itself public and called with external waiters, so the scope belongs inside it.kio::waitpoll: no new API, but +2.0% active instructions (table above), and callers drivingDriver::pollfrom their own executor would not get it.wake_16_of_1000/kio_tasks_batchfor the batch's own cost, and rely onsession_idleplus the tightenedidle_sessionpoll bound for the session win (recommended).idle_sessionbound, which is deterministic where criterion is not.finite_relay::relay_waits_for_reordered_upstream_headers) was a pre-existing IETF relay bug this branch exposed by changing task order.API and wire impact
kio::Waiter::batch<R>(&self, poll: impl FnOnce() -> R) -> R.Tasksnow usescrate::syncatomics, so loom permutes them. No change outside loom builds.moq-net's public API.(Written by Claude Opus 5.5)
🤖 Generated with Claude Code