fix(kio): split waiters by condition so writes don't churn closed/consumer waiters#1739
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (6)
WalkthroughThe change replaces a single unified 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches✨ Simplify code
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 |
…sumer waiters A single waiter list meant every event woke every parked waiter. A per-modification write (the hot path) woke `closed()`, `used()`, and `unused()` waiters that don't care about value changes, and consumer create/drop woke the value waiters. Those spurious wakes re-register and ping-pong, which is the root cause behind the per-poll consume() churn. Split `State` into three lists keyed by what each waiter waits on: - waiters_value -> poll/wait, woken on every modification (+ close) - waiters_closed -> closed(), woken only on close - waiters_consumer -> used()/unused(), woken on consumer 0<->1 (+ close) Close still wakes all three, since every waiter resolves on close. `used`/`unused` share one list: every call site uses them sequentially (used().await, then a loop selecting unused()), so they're never parked on the same channel at once and a combined list never cross-wakes. `closed()` gets its own list because it's heavily used (87 call sites) and parked long-term, so sharing the value list churned it on every frame write. Also drop `[lib] test = false`, which silently skipped the inline tests on a plain `cargo test -p kio` (CI's --all-targets ran them, so this was a local footgun only), and add waker-counting regression tests that assert each event wakes only the waiters that care. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
a66a3a9 to
f7d4fa8
Compare
…sumer waiters (#1739) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reconciles main's new features with dev's reshaped APIs: - moq-mux codec imports: combined main's `TrackProvider` (import onto an existing track, #1684) with dev's per-track timescale. The timescale is baked into `TrackProvider::create()` so unique tracks publish at the native container timescale; fixed tracks keep the caller's info. Added a `FixedTrackReconfigured` error variant per codec to replace the anyhow::bail in the crate::Result paths. - kio: dev's new `Weak::poll_closed` now registers on main's split `waiters_closed` list (#1739) instead of the removed single `waiters`. - js/watch: re-implemented main's distance-based `visible` download control (#1744) on top of dev's input/output signal architecture. The renderer takes `visible: Visible` as an input and emits the boolean `output.visible`; the backend keeps owning the paused/enabled gating. - noq/websocket: bumped web-transport-noq to 0.2 (#1741, #1743) and qmux to 0.1.3, then mapped qmux's new `Error::Http(status)` to `ConnectError::Unauthorized` so the WebSocket fallback surfaces auth rejections like the QUIC backend. - Cargo: took the newer of each shared dep (web-async 0.1.4, iroh 0.6, noq 0.2); Cargo.lock based on dev's to avoid gratuitous transitive bumps. - Brought the test code main introduced (and dev's stale integration tests) up to dev's current API (create_track(name, info), announced().next(), track.name(), with_publisher/with_subscriber, moq_net::Timestamp). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
kio used a single waiter list, so every event woke every parked waiter. The expensive case is a per-modification write (the hot path): it woke
closed(),used(), andunused()waiters that don't care about value changes, and conversely consumer create/drop woke the value waiters. Those spurious wakes just re-register and ping-pong, which is the root cause behind the per-pollconsume()churn.This splits
Stateinto three lists, keyed by what each waiter actually waits on:waiters_valuepoll/waitwaiters_closedclosed()waiters_consumerused()/unused()Close still wakes all three, since every waiter resolves on close. No other event over-wakes.
Why these groupings
used/unusedshare one list. Every call site uses them sequentially (used().await, then a loop selecting onunused()), so they're never parked on the same channel simultaneously and a combined list never cross-wakes in practice. A separateunusedlist would add cost for no benefit.closed()gets its own list. It's heavily used (87 call sites) and parked long-term inselect!blocks. Sharing the value list meant it ate a spurious wake on every frame write — a bigger churn source than used/unused.Also
[Unreleased]CHANGELOG entry.closed()/unused(), consumer churn doesn't wake value/closed()waiters, and close wakes all three.Relation to #1735
Rebased on top of #1735 (read-only-predicate
poll/waitrework). That PR fixed the "a no-op mutation during a pending poll wakes the poller's own waiter" footgun by making the predicate read-only; this PR fixes the orthogonal "every event wakes every waiter" problem by splitting the list. The two compose:poll/waitnow register inwaiters_value, and a write through the returnedMutwakes only that list (plus closed/consumer on close).Public API
No public API change —
Stateand its waiter lists arepub(crate). Internal behavior-preserving fix, hence targetingmain.Tradeoff
Statenow holds threeWaiterLists (each ~256 B inline viaINLINE_WAITERS = 32), so it grows ~512 B vs. the single-list original.waiters_value/waiters_closedboth scale with subscriber count, butwaiters_consumeris almost always 0–1 waiters, where inline-32 is waste. A clean follow-up is makingWaiterListgeneric over its inline size and giving the consumer list a tiny one; left out here to keep the change focused.Test plan
cargo test -p kio— 6 passed (4 new +is_last+ kio: rework Producer::poll/wait to a read-only predicate that returns a Mut #1735'spoll_gates)moq-netbuilds; its 143 model tests pass (exercise theunused()path)(Written by Claude)