bc19e90c17
Diagnosis from qlog timeline pattern: send packets clustered ~37ms
apart (sim RTT) but each cluster contained only ONE stream's data
(80-byte packets despite 1452-byte capacity). 1457 GETs in 58s, ~25
streams/sec.
Root cause: race between client.get()'s per-call driver.wakeup() and
the dispatcher scheduling the OTHER 63 coroutines. Sequence:
1. c1 acquires conn lock, enqueues request, wakes send loop, releases
2. Send loop wakes, queues for lock — other 63 coroutines haven't
started yet (dispatcher hasn't picked them up)
3. Send loop acquires lock alone, drains c1's data into one tiny
packet, releases
4. c2 finally starts, acquires, enqueues, wakes...
→ one stream per packet, no coalescing
Fix: split GetClient.get() into prepareRequest (open + enqueue + FIN,
synchronous, no wake) and awaitResponse (collect, async). Multiplex
chunk loop now:
Phase 1: serial prepareRequest for every URL in the chunk (64 in
sequence, each adding to send buffers)
Phase 2: SINGLE driver.wakeup() — by now all 64 streams have data
queued; send loop drains them all in coalesced packets
Phase 3: parallel awaitResponse with per-stream timeout
Predicted throughput jump: 25 streams/sec → ~1000+/sec (sim RTT-bound
at ~30ms per round trip = 64 streams per RTT = 2100/sec ceiling).
Single-request paths (transfer / chacha20 / etc) keep using the
default GetClient.get() which still wraps prepare+await; no behavioral
change there.
https://claude.ai/code/session_01HcvfQq1ttPV9PkRoJb4nyT