Swiss dumping a disc over the adapter measures 850 KB/s (6.97 Mbit/s, 25.8% of
the 27 Mbit/s EXI ceiling). The capture decomposes it completely:
18395 data frames / 9200 ACKs = 2.0 frames per ACK
18.84 MB / 9200 = 2048 B per round trip
22.78 s / 9200 = 2.48 ms per round trip
2048 B / 2.48 ms = 826 KB/s ~= the measured 850
So the flow is window x RTT bound and nothing else. Payload per frame is
exactly 1024 B (wire 1078 minus 54 of headers) against a 2920 B window, so two
segments fit in flight. Zero retransmissions means the 15-page ring never
overflowed; 0.2% stall time means nothing waits on the disc; our W5100 bus
engine sits at ~17% utilisation. The adapter is not the constraint.
Levers, and the first two are not ours: a larger lwIP TCP_SND_BUF (scales
almost linearly while window-bound), and full-MSS segments -- Swiss writes
1024 B at a time, so the same window and RTT at 1460 B would give ~1150 KB/s,
a 1.43x gain for free. Third is a shorter round trip, which is GC-side
interrupt dispatch plus lwIP, plus EXI lock contention: libogc2's
bba_event_handler defers when EXI_Lock fails, so RX queues behind TX on the
single channel.
⚠️ This REVERSES an earlier recommendation in the TODO. RX interrupt
coalescing (NCRB.RXINTC, RXINTT) was listed as a throughput lever; for a
window-bound flow it makes things worse, because delaying the ACK's interrupt
lengthens the round trip and throughput is bytes-per-round-trip / RTT. The
measurement showed the guess was backwards. Noted in item 3 as "implement for
fidelity, not for speed".
analyse-pcap.py now derives frames-per-ACK, bytes-per-round-trip and the
implied RTT itself, identifies a window-bound flow, quantifies the full-MSS
gain, and warns against coalescing -- so the next person gets the conclusion
rather than the raw numbers. Self-test G replays this exact shape.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>