Store sent packets in a ring buffer instead of a BTreeMap#2730
Conversation
c0145f7 to
b29f633
Compare
|
I have to run before I can give this a thorough review, but I'll note for starters that this is precisely the data structure I had in mind. Thanks! |
great :) - no rush on my end; hopefully this helps |
I would like this in the same PR, as a separate commit. |
Yes, and with positive results. I ran quinn-perf over loopback, comparing the branch tip against its parent commit. Both binaries built --release from the same toolchain; before/after runs interleaved to cancel drift; 2 GiB transfer, 12 s/run, warmup interval dropped. Median loopback throughput on an 8-core machine:
In agreement with the profiling: removing per-packet allocator churn, so it shows up as CPU headroom / throughput rather than RSS. |
|
@djc I've pushed some changes related to your comments, each as a single commit. I'm going to run the throughput tests again to confirm the performance gains remain; I'll update after that's done. |
|
We'll want all of your intermediate commits to be squashed -- we rebase on merge so we want the PR history to be clean, with review feedback addressed in the originating commit. |
|
Re-ran the loopback throughput (same method as before) after the review changes, comparing the branch head against the pre-PR state (BTreeMap):
So the refactors and the O(1) has_in_flight haven't regressed throughput; the improvement over the BTreeMap baseline still holds. |
(To be clear, still good to keep the O(1) changes in a separate commit.) |
a0242a7 to
c941b77
Compare
|
Two commits:
Also rebased onto the current upstream base. |
c941b77 to
9d90509
Compare
`PacketSpace::sent_packets` is inserted on every sent packet and removed on ACK/loss. Because packet numbers are monotonic, a `BTreeMap` spends most of its cost allocating and freeing a tree node per entry: heap profiling a loopback bulk transfer attributes ~36% of all allocations (and ~99% of BTreeMap-node allocations) to this one map, even though only a few dozen entries are live at once — it is allocation churn, not residency. Replace it with `SentPackets`, a sparse ring buffer indexed by `packet number - offset`. Insert and lookup are O(1) with no per-packet allocation; skipped packet numbers leave vacant slots, and vacant slots at the front are reclaimed as the low end is acknowledged. Iteration order and range/gap semantics match the previous `BTreeMap`, so ACK and loss detection are unchanged. Fixes quinn-rs#2720.
`PacketSpace::has_in_flight` scanned `sent_packets` on every PTO computation. Track the number of in-flight (`size != 0`) entries on insert and remove instead, so the query is a single comparison. This was the last per-poll scan of the sent-packet window, so `values` is no longer needed.
9d90509 to
ea5a7a4
Compare
cool :) I've resolved the nit, so once CI is green again, it should be OK for a merge. |
Will wait for a review from @Ralith first. |
Ralith
left a comment
There was a problem hiding this comment.
This is great, thank you!
Fixes #2720.
Problem
PacketSpace::sent_packetsis inserted on every sent packet and removed on ACK/loss. Because packet numbers are monotonic, aBTreeMapspends most of its cost allocating and freeing a tree node per entry.Heap-profiling a 200 MiB loopback
benchbulk transfer withdhatand deep backtraces (see the confirmation in #2720) attributes 35.8% of all allocations — 24,288 of 67,766 blocks — and ~99% of all BTreeMap-node allocations to this one map, while only ~46 entries are live at peak. It's allocation churn, not residency: CPU spent in the allocator, invisible in RSS, and felt most on edge devices at high packet rates.Change
Replace the
BTreeMapwithSentPackets, a sparse ring buffer indexed bypacket number - offset(aVecDeque<Option<T>>plus a base offset):PacketNumberFilter) leave vacant slots.The type keeps a
BTreeMap-compatible API, so the call sites are unchanged apart from&pn→pnin two loops. Crucially,range()yields present entries in ascending packet-number order, so the loss-detection loop's gap detection (prev_packet != Some(pn - 1)) and the ACK range walk behave identically — a removed or skipped packet number is absent either way.Result
Re-profiling the same transfer after the change:
sent_packetsallocationsThe 18 remaining are the amortized
VecDequegrowth reallocations (a handful of buffer resizes vs ~24k per-packet node allocs).lost_packetswas already negligible (~0.5%) and is left as aBTreeMap.Testing
SentPackets: monotonic-with-gaps insert, out-of-order removal, front reclamation, range bounds (including the excluded-start "first entry after x" query used bysent()), drain,mem::takereuse, out-of-range access, and index panic.quinn-protosuite green (283 tests), including the sim-IO ACK / loss / persistent-congestion tests that depend on iteration order and gap detection.cargo fmtandclippyclean.The
dhatharness used for the numbers above is a small feature-gated addition tobench; happy to submit that separately if it'd be useful for future profiling.Possible follow-ups
Out of scope here, but noting them so they're on record:
sent_packetsto answer "is anything in flight?" — with the offset-indexed layout these could become O(1) (a live-entry count maintained on insert/remove), removing the last per-poll walks over the window. Happy to do this as a separate PR if wanted.dhatharness. As above — the feature-gatedbenchaddition could land on its own for future allocation profiling.This change was written with AI assistance; I've reviewed every line and stand behind it.