revert(quic-interop): drop explicit QPACK SETTINGS — made multiplexing worse
The QPACK_MAX_TABLE_CAPACITY=0 advertisement worsened the result (1 file → 0 files written). Empirical: empty SETTINGS = spec defaults (both 0) and aioquic was already sending literal-encoded responses under that condition. Real bug isn't QPACK — diagnostic showed 1359 GETs processed in 58s (~23 streams/sec). Throughput is hard-bottlenecked by :quic's single conn.lock serializing send loop + read loop + openBidiStream across 1999 waiting coroutines. Even at maximum throughput we'd only complete ~1400 of 1999 in 60s. The 1 file we managed previously was just the first response landing before lock contention spiked. Filed multiplexing as a known-throughput-limit follow-up. Possible fixes (per-level lock split / Semaphore-bounded concurrency / QPACK dynamic-table support) are all non-trivial and the testcase is a stress test, not a real-world load shape. For now: revert to empty SETTINGS, accept multiplexing as the lone deferred testcase. Aioquic + picoquic stay at 6/7. https://claude.ai/code/session_01HcvfQq1ttPV9PkRoJb4nyT
This commit is contained in:
@@ -181,13 +181,25 @@ the marquee result.
|
||||
Implementing v2 is its own project (different Initial-secret
|
||||
derivation, different transport-parameter encoding, different
|
||||
long-header type bits — not just a version-number swap).
|
||||
- **Multiplexing throughput on Mac+Rosetta** — the timeout bump
|
||||
unblocks tests that were finishing in time. The deeper fix is
|
||||
per-stream backpressure (parser suspends when channel near
|
||||
capacity instead of tearing down or dropping). See `:quic`'s
|
||||
`Audit-4 #3` decision: prefer fail-fast over silent-drop on
|
||||
per-stream channel saturation. Needs a re-think for
|
||||
multi-stream-heavy workloads.
|
||||
- **Multiplexing throughput on Mac+Rosetta** — measured at ~23 streams/sec
|
||||
(aioquic processed 1359 GETs in 58s). The runner's multiplexing test
|
||||
generates 1999 files; even with a 60s budget we only get 70% of them
|
||||
open before the test ends, with most coroutines failing to surface a
|
||||
status=200 in time for the file-write loop. Throughput is hard-bottlenecked
|
||||
by `:quic`'s single `conn.lock` serializing the send loop, the read
|
||||
loop, and `openBidiStream` across all 1999 awaiting coroutines. Under
|
||||
this lock contention pattern, parallelism doesn't help — coroutines
|
||||
queue on the lock anyway.
|
||||
Possible fixes (non-trivial):
|
||||
- Per-level / per-stream lock split (big refactor; touches almost
|
||||
everything in `:quic/connection`).
|
||||
- Batch concurrency via a `Semaphore` capping in-flight `client.get()`
|
||||
calls to e.g. 64 at a time. Doesn't lift the throughput ceiling but
|
||||
reduces lock thrash and probably lets MORE files complete in 60s.
|
||||
- QPACK dynamic-table support so aioquic doesn't need to fall back
|
||||
to literal encoding (would help on the response-decode side).
|
||||
None are urgent; this is a stress-test scenario, not a normal-traffic
|
||||
one. Filed for follow-up.
|
||||
- **Server role** — entire stack is client-only. Implementing the
|
||||
server side would unlock `handshakeloss` against aioquic
|
||||
(currently `?` because aioquic-server doesn't support it), and
|
||||
|
||||
+8
-19
@@ -27,7 +27,6 @@ import com.vitorpamplona.quic.http3.Http3Frame
|
||||
import com.vitorpamplona.quic.http3.Http3FrameReader
|
||||
import com.vitorpamplona.quic.http3.Http3FrameType
|
||||
import com.vitorpamplona.quic.http3.Http3Settings
|
||||
import com.vitorpamplona.quic.http3.Http3SettingsId
|
||||
import com.vitorpamplona.quic.http3.Http3StreamType
|
||||
import com.vitorpamplona.quic.qpack.QpackDecoder
|
||||
import com.vitorpamplona.quic.qpack.QpackEncoder
|
||||
@@ -63,27 +62,17 @@ class Http3GetClient(
|
||||
private val conn: QuicConnection,
|
||||
) : GetClient {
|
||||
suspend fun init(scope: CoroutineScope) {
|
||||
// Control stream: type-0x00 prefix followed by a SETTINGS frame.
|
||||
// Explicitly advertise QPACK_MAX_TABLE_CAPACITY=0 and
|
||||
// QPACK_BLOCKED_STREAMS=0 — our QpackDecoder is literal-only
|
||||
// (no dynamic-table support, RFC 9204 Required Insert Count = 0).
|
||||
// Empty SETTINGS would let the spec default kick in (also 0),
|
||||
// but aioquic's behavior on multiplexing showed that without
|
||||
// explicit signal it still emits dynamic-table-referenced fields
|
||||
// in response HEADERS, which our decoder silently mis-parses
|
||||
// (status=0, file not written). Sending the explicit zeros
|
||||
// forces aioquic onto the literal-only QPACK path.
|
||||
// Control stream: type-0x00 prefix followed by a SETTINGS frame
|
||||
// (empty body is legal — RFC 9114 §7.2.4). Empty body = spec
|
||||
// defaults: QPACK_MAX_TABLE_CAPACITY=0, QPACK_BLOCKED_STREAMS=0,
|
||||
// MAX_FIELD_SECTION_SIZE=unlimited. We deliberately do NOT
|
||||
// explicitly send QPACK_MAX_TABLE_CAPACITY=0 — empirically
|
||||
// (aioquic 2026-05-07) that worsened the multiplexing case
|
||||
// from 1 file to 0 files.
|
||||
val control = conn.openUniStream()
|
||||
val w = QuicWriter()
|
||||
w.writeVarint(Http3StreamType.CONTROL)
|
||||
w.writeBytes(
|
||||
Http3Settings(
|
||||
mapOf(
|
||||
Http3SettingsId.QPACK_MAX_TABLE_CAPACITY to 0L,
|
||||
Http3SettingsId.QPACK_BLOCKED_STREAMS to 0L,
|
||||
),
|
||||
).encodeFrame(),
|
||||
)
|
||||
w.writeBytes(Http3Settings(emptyMap()).encodeFrame())
|
||||
control.send.enqueue(w.toByteArray())
|
||||
// Control stream stays open for the lifetime of the H3 connection;
|
||||
// do NOT call finish() — peers treat that as H3_CLOSED_CRITICAL_STREAM.
|
||||
|
||||
Reference in New Issue
Block a user