Skip to content

quest: borrow-free QUIC state, mechanism prototypes and measurements - #7531

Open
kixelated wants to merge 4 commits into
mainfrom
quest/m2/perf/quic-borrow-free
Open

kixelated wants to merge 4 commits into
mainfrom
quest/m2/perf/quic-borrow-free

Conversation

@kixelated

@kixelated kixelated commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

Finishes quest/m2/perf/quic-borrow-free.md and deletes it. This mechanism report is the quest's deliverable and its evidence; the perf epic README has no findings section, so the report lives here.

Summary

This is the borrow-free report. I built three prototypes, all on real moq-quic connections, and measured them against today's lock with deterministic instruction counts. The maintainer chose to stop at this mechanism report (see Decisions), so the driver-level prototypes in moq-tokio and moq-uring and the fanout-bench sweep are not built, and adoption is planned separately.

  • Prototype branch: proto/quic-borrow-free (c368f72). Per the Plan it does not land. It contains rs/moq-quic/benches/share.rs, plus three moq-quic credit queries that only the channel driver uses: Streams::write_limit, Streams::initial_send_credit, and send::Stream::writable.
  • This PR's diff: quest-only. It deletes the quest and its epic link, and rewrites QUIC lock contention's pointer to it so that quest cites these measured costs instead.

Method

share.rs runs a relay, the server, that opens streams new uni streams per round on each of conns subscriber connections. It makes writes writes of 1200 bytes to each stream and then finishes it. Each subscriber leases each stream to its end, releases the lease, and stops the stream. A round ends once every stream has been read and the path has settled. The harness uses the benches' plaintext crypto, an unlimited controller, and virtual time. Every shape makes the same moq-quic calls, using the poll_* forms that moq-dev/moq#7391 landed. Every task wake is the same lock-free run-queue push. The shapes differ only in how a handle reaches the connection:

shape stands for handle holds access
context context passing stream ID the owner's &mut Connection
refcell moq-uring today Rc + ID RefCell::borrow_mut, plus a driver flag
mutex moq-tokio today Arc + ID Mutex::lock, plus a driver wake per change
channel driver-owned state Arc<cell> see below

How the channel shape works:

  • Send. The handle appends Bytes to its own kio::log::Writer. Before appending it reserves stream credit and connection credit, which the driver publishes as monotonic atomics. It then pushes its cell onto the driver's dirty stack.
  • Driver. The driver alone owns the connection. It opens the stream lazily, so a handle can append up to the peer's initial stream credit before it has an ID. It moves the log's entries into moq-quic, and publishes the consumed index so the handle can evict.
  • Receive. The driver's waiter on poll_lease wakes the cell. The driver hands the Lease to the reader through the cell, and the reader hands it back for release.

Measurement. perf stat -e instructions:u, with taskset pinning a core. The steady-state cost is the difference between two round counts, divided by the extra streams. Two full sweeps agreed within 0.6% per cell.

Results: instructions per stream, single core

writes conns streams context refcell mutex channel mutex − context channel − context
1 1 1 33017 33217 34008 36510 +991 (+3.0%) +3493 (+10.6%)
1 1 16 15891 15944 16163 18937 +272 (+1.7%) +3046 (+19.2%)
1 1 128 16350 16436 16630 20957 +280 (+1.7%) +4607 (+28.2%)
1 16 1 32815 32886 33847 35926 +1032 (+3.1%) +3111 (+9.5%)
1 16 16 16569 16632 16854 20689 +285 (+1.7%) +4120 (+24.9%)
1 16 128 16602 16687 16869 21387 +267 (+1.6%) +4785 (+28.8%)
1 128 1 35755 36127 36994 39556 +1239 (+3.5%) +3801 (+10.6%)
1 128 16 16819 16864 17113 20985 +294 (+1.7%) +4166 (+24.8%)
1 128 128 16561 16631 16862 21377 +301 (+1.8%) +4816 (+29.1%)
16 1 1 141578 142779 144560 148458 +2982 (+2.1%) +6880 (+4.9%)
16 1 16 138550 138861 140205 146135 +1655 (+1.2%) +7585 (+5.5%)
16 1 128 138848 139117 140435 148298 +1587 (+1.1%) +9450 (+6.8%)
16 16 1 142921 143308 144920 151183 +1999 (+1.4%) +8262 (+5.8%)
16 16 16 138860 139287 140603 148382 +1743 (+1.3%) +9522 (+6.9%)
16 16 128 138068 138429 139655 148437 +1587 (+1.1%) +10369 (+7.5%)
16 128 1 152300 152097 154025 160837 +1725 (+1.1%) +8537 (+5.6%)
16 128 16 140453 140748 142309 150305 +1856 (+1.3%) +9852 (+7.0%)
16 128 128 138109 138427 139696 147558 +1587 (+1.1%) +9449 (+6.8%)

What the table shows:

  • Context passing and the existing locks. The Mutex costs about 290 instructions per stream, plus about 100 per extra write. The RefCell costs 0.2–1.0%. The 1-stream rows are higher because per-round costs, mostly ACKs and timers, are spread over a single stream.
  • The channel. It costs about 4.2–4.8k instructions per stream plus about 320 per extra write. Fanout's shape (one write per new stream) is the worst case, at +25–29%. A profile normalized to these totals attributes the extra at 16×128 roughly as follows:
    • about 1.3k to kio::log: its epoch pin, flush, and collect, and its segment retire on drop;
    • about 1.1k to allocation (send cell, log state and segment, receive cell, reader task, two lease boxes, the cell's waiter);
    • about 0.5k of extra moq-quic work, from eager leasing (lease rank and discard);
    • the rest to credit reservation, the dirty and ready stacks, and the extra wake hop on receive (moq-quic → driver → reader, where the lock shapes go moq-quic → reader).

Results: threaded, wall ns per stream (median of 5, noisy host, load 12–20)

Each relay connection's streams are spread over writers threads. The main thread drives every connection and runs every subscriber. These numbers are indicative only: the main thread is the bottleneck, and in the channel shape it also does the moq-quic writes that the mutex shape leaves on the writer threads.

writes conns writers mutex (min–max) channel (min–max)
1 1 1 2349 (1879–2925) 2933 (2394–3286)
1 1 2 2762 (2245–3008) 2354 (2097–2442)
1 1 4 3803 (3354–4618) 3106 (2547–3534)
1 1 7 3004 (2489–4831) 3099 (2840–3941)
1 16 1 1651 (1310–1889) 2332 (2294–3086)
1 16 2 1588 (1432–2018) 1922 (1789–2220)
1 16 4 1452 (1295–2017) 2017 (1735–3187)
1 16 7 1469 (1186–1643) 1873 (1652–2443)
16 1 1 11156 (10017–17060) 14276 (12734–16118)
16 1 2 11137 (8908–13567) 14958 (12783–16764)
16 1 4 14861 (10505–25773) 14219 (11067–14788)
16 1 7 16523 (13484–28691) 17500 (14151–19374)
16 16 1 13536 (12790–14394) 14985 (13349–15664)
16 16 2 12046 (11562–12322) 14423 (14257–14788)
16 16 4 13330 (13036–14046) 14555 (13799–15361)
16 16 7 12078 (11933–13234) 14996 (13949–16367)

The mutex shape had the lower median in 13 of 16 cases. The channel was ahead only when 2–4 writers shared a single connection, which is the most contended case.

API each implies

Context passing

  • moq-quic: no change. Its API already is context passing: Connection::send_stream(id) and recv_stream(id) borrow the connection mutably and take an ID.
  • Drivers: handles become owned, non-Clone tokens. Every operation takes the connection (conn.write(&mut stream, buf)) from one owner task that polls its children (kio::Tasks).
  • moq-net: transport::poll's &self session methods and its &mut self stream methods gain a connection context. Per-subscription tasks become children of the session task.
  • Runtime cost: on tokio, one connection's streams no longer run in parallel across workers. moq-uring already works this way, so there it is a type change rather than a scheduling change.

Driver-owned channel

  • Handles: keep &self and Send.
  • moq-quic: must publish credit (the three queries on the prototype branch). The Plan's full form, packetizing straight from each handle's log by offset, would also need a send buffer backed by an external log, plus an acked-eviction path back to the writer. The prototype instead moves log entries into moq-quic's own buffer, which costs a refcount per write, not a copy.
  • Opening a stream becomes asynchronous: a stream has no ID until its driver opens it. Priority, group, reset, and finish all become commands to the driver.
  • Shared connection credit: across streams it needs a CAS on a shared counter.
  • Receive: works with today's one-lease-at-a-time API, at the cost of an extra wake hop.

Today's lock: no change.

Recommendation (mine; adoption is decided through /quest-plan)

Keep the lock and RefCell on the data path, and do not build the channel design.

  • The lock's measured cost is small. On one core the Mutex costs 1.1–3.5% more than context passing, and the RefCell under 1%.
  • The channel costs more than it saves. It adds 5–29%, and the most on fanout's shape. It also moves every moq-quic write onto the driver's core. In this harness it came out ahead only with several writers on one connection.
  • Context passing is the cheapest design, but its cost is structural. It means restructuring moq-net's session tasks and transport::poll, and giving up per-connection parallelism on tokio. A prototype of that is effectively the adoption itself.
  • If the lock's API wart should go regardless, I would plan context passing as its own adoption quest, starting with moq-uring, where the connection already has one owner per core. I would wait for the lock-contention profile before touching moq-tokio.

Decisions

  1. ✅ (a) Stop here (maintainer, 2026-11-01). This mechanism report is the deliverable: the quest is deleted, and the report stays in this PR body. The driver-level prototypes and the fanout sweep are not built.
  2. ✅ Adoption goes through /quest-plan (maintainer, 2026-11-01), run by the parent session. This PR plans nothing; the adoption follow-up is below.

Choices made without asking

  • Prototypes are at the mechanism level (a moq-quic bench over real connections), not in moq-tokio's and moq-uring's drivers. moq-dev/moq#7490 is rewriting both drivers' waiter tables right now, so driver prototypes would conflict with it and would be built on a shape that is about to change.
  • Deterministic numbers are instruction counts on one pinned core, as briefed. The threaded run is reported separately and labelled as noisy.
  • Channel details not settled by the Plan:
    • It uses kio::log, as the Plan names.
    • A handle appends before its stream is opened, against the peer's initial stream credit.
    • Receive is a lease handoff.
    • The windows never fill, so parking on credit and the acked-eviction path back to the writer are not exercised.
  • Workload: the relay sends one stream per group, which is fanout's group_size = 0 shape. The 16-write rows cover longer groups.
  • Branch name: the prototypes are on proto/quic-borrow-free, outside the quest/ namespace.

Overlaps

  • moq-dev/moq#7490 (waits). The prototypes use moq-quic's waits (poll_lease, poll_accept, poll_write). Any driver-level prototype should come after moq-quic owns its waits #7490.
  • moq-dev/moq#7527 (schedule churn). No overlap with this diff. The prototype branch's moq-quic additions are separate from the scheduler.
  • moq-dev/moq#7528 (quic-receive-reuse). No overlap. The lease handoff relies only on lease and release semantics, which that quest keeps.
  • Lock-contention quest. Its staging-queue lever is this PR's channel send path. Measured here, that costs about 4.5k instructions per stream and about 320 per extra write on a single core.

Suggested follow-ups

  • Adopt a borrow-free design, or keep the lock (for /quest-plan). Recommended goal: a recorded verdict on how moq-quic's connection is shared with its stream handles. Options: (1) keep the Mutex and RefCell (recommended: they cost 1.1–3.5% and under 1% over context passing on one core); (2) context passing, starting in moq-uring where each connection already has one owner per core, after waits and the lock-contention profile; (3) the driver-owned channel (not recommended: +5–29%, worst on fanout's one-write-per-stream shape, ahead only with 2–4 writers on one connection).
  • kio::log costs about 1.3k instructions for a short-lived log, in epoch pin, flush, and collect plus segment retirement. Already planned: kio waiter costs cites this PR.
  • A threaded fanout harness should put the relay's driver on its own thread, apart from the subscribers, so that lock contention is not hidden behind the subscriber work.

Review

Local review skipped under the simple-review rule (moq-dev/quest#364): head 303d25d touches only quests. No online-reviewer findings were posted on the PR.

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

kixelated and others added 2 commits November 1, 2026 01:24
The prototypes live on proto/quic-borrow-free. On a pinned core a Mutex
costs 1.1-3.5% over context passing, a RefCell 0.2-1.0%, and the
driver-owned channel 5-29%. The driver-level prototypes and the fanout
sweep remain, pending whether they are still worth building.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title QUIC connection state without runtime borrow checks quest: borrow-free QUIC state, mechanism prototypes and measurements Nov 1, 2026
kixelated and others added 2 commits November 1, 2026 14:19
The maintainer chose to stop at the mechanism report (2026-11-01). Delete
the quest; the report stays in the PR body, and adoption is planned
separately. The lock-contention quest now cites the measured costs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review November 1, 2026 22:21
@coderabbitai

coderabbitai Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You'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 6 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e33f8e59-1dda-473b-b29d-797e505f8e8c
Commits

Reviewing files that changed from the base of the PR and between 59e12a5 and 303d25d.

Files selected for processing: 3
  • quest/m2/perf/README.md
  • quest/m2/perf/quic-borrow-free.md
  • quest/m2/perf/quic-lock-contention.md
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed commit: 303d25daed61ef545a46e12e2444155521e811dc.

No actionable bugs found in the quest-only changes. Removing the completed quest and pointing the lock-contention plan at the mechanism report is a proportionate, simpler outcome than landing channel machinery without a demonstrated workload win. The remaining plan correctly keeps the contended baseline and adoption decision separate; there is no runtime, public API, or wire change here.

Verification: inspected all three changed documents, the report, surrounding plan, and review history (no prior published review). Static review only: I did not run tests, reproduce the measurements, or validate the non-landing prototype branch. The report's single-core and noisy threaded measurements do not establish production contention performance.

(Written by OpenAI)

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — This run reviewed the quest-only finish of quest/m2/perf/quic-borrow-free.md: the quest file is deleted, its index bullet is removed, and quic-lock-contention.md now cites the measured costs instead of the deleted quest.

  • Quest deleted — quest/m2/perf/quic-borrow-free.md is removed; the mechanism report lives in the PR body per the recorded maintainer decision (2026-11-01).
  • Index bullet removed — quest/m2/perf/README.md drops the borrow-free entry, the standard quest-completion convention.
  • Pointer rewritten — quest/m2/perf/quic-lock-contention.md replaces the borrow-free link with a citation to PR #7531's numbers (Mutex +1.1–3.5% over context passing; driver-owned channel ~4.5k instructions/stream plus ~320/write) and drops it from ## Related.

I verified the cited figures against the report's tables; they agree. A git grep -i for borrow.free leaves only two valid citations, both PR URLs (quic-lock-contention.md:51, kio-waiter-costs.md:13), so no stale quest-path references remain.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

This branch has not been deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant