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:
Claude
2026-05-07 01:50:32 +00:00
parent 4f52027ccb
commit 0cc577f0fb
@@ -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 (110 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 (110 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) {