From 0cc577f0fb71f79f1b014aa8dc47a89c2f5d20e5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 7 May 2026 01:50:32 +0000 Subject: [PATCH] perf(quic): skip closed streams + skip sort when uniform priority MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../quic/connection/QuicConnectionWriter.kt | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/quic/src/commonMain/kotlin/com/vitorpamplona/quic/connection/QuicConnectionWriter.kt b/quic/src/commonMain/kotlin/com/vitorpamplona/quic/connection/QuicConnectionWriter.kt index 675167144..85a4e1107 100644 --- a/quic/src/commonMain/kotlin/com/vitorpamplona/quic/connection/QuicConnectionWriter.kt +++ b/quic/src/commonMain/kotlin/com/vitorpamplona/quic/connection/QuicConnectionWriter.kt @@ -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) {