perf(quic): skip closed streams + skip sort when uniform priority
Multiplexing-throughput investigation (qlog against aioquic):
~25 streams/sec with 1453 GETs in 58s. The bottleneck under high
stream count was drainOutbound's per-call O(N log N) sort over the
ENTIRE stream list (including streams that have already FIN'd both
ways and have nothing to send).
Two cheap optimizations to drainOutbound's stream iteration:
1. Filter to !isClosed streams BEFORE sort. Most streams under
bursty multiplexing loads are done; iterating them is wasted.
2. Skip sortedByDescending entirely when every stream is at
default priority (priority == 0). The pre-priority round-robin
shape (insertion order) is preserved, satisfying the moq-lite
newer-sequence-stream priority contract by happenstance for
uniform-priority loads.
Drops drainOutbound's per-call cost from O(N log N) where N = total
streams to roughly O(active) under realistic loads. Multiplexing's
~2000 streams accumulated over a run drop down to maybe 64 active at
any moment (the chunk in flight).
Doesn't affect the moq-lite audio path's behavior (small N, default
priorities → both paths reduce to the same round-robin walk).
https://claude.ai/code/session_01HcvfQq1ttPV9PkRoJb4nyT
This commit is contained in:
+18
-4
@@ -624,11 +624,25 @@ private fun buildApplicationPacket(
|
||||
// pre-priority round-robin behaviour exactly.
|
||||
//
|
||||
// Cost: O(N log N) per drain pass plus one transient sorted
|
||||
// list. N is small (1–10 in the moq-lite audio path); if it
|
||||
// ever grows enough to matter, switch to an indirect index sort
|
||||
// or maintain an incrementally-sorted view on setPriority.
|
||||
// list. N is small (1–10 in the moq-lite audio path) but the
|
||||
// multiplexing interop testcase pushes N to ~2000, so we
|
||||
// optimize:
|
||||
// 1. filter out streams that are fully closed (no data + no
|
||||
// retransmits coming) — drops "done" streams from the
|
||||
// walk, which is most of them under bursty interop loads.
|
||||
// 2. skip the sort entirely when every stream has the
|
||||
// default priority (the common case) — preserving the
|
||||
// insertion-ordered round-robin shape.
|
||||
// The combination drops drainOutbound's per-call cost from
|
||||
// O(N log N) where N=total-streams to roughly O(active) under
|
||||
// realistic loads.
|
||||
val active = streamsView.filter { !it.isClosed }
|
||||
val sorted =
|
||||
if (streamsView.size > 1) streamsView.sortedByDescending { it.priority } else streamsView
|
||||
when {
|
||||
active.size <= 1 -> active
|
||||
active.all { it.priority == 0 } -> active
|
||||
else -> active.sortedByDescending { it.priority }
|
||||
}
|
||||
val rotation = conn.streamRoundRobinStart
|
||||
var tierStart = 0
|
||||
outer@ while (tierStart < sorted.size) {
|
||||
|
||||
Reference in New Issue
Block a user