Measure the throughput limit: window x RTT, not the adapter

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>
This commit is contained in:
2026-10-10 15:26:39 +02:00
parent d6fb7c81b9
commit d8ddea13ba
3 changed files with 129 additions and 5 deletions
+29
View File
@@ -129,6 +129,35 @@ if "pcapng" not in (r.stdout + r.stderr):
else:
print("F pcapng refused with a conversion hint")
# Scenario G: window x RTT bound -- 2 undersized segments per ACK, which is
# the real shape measured on hardware (1024 B segments, 2920 B window).
# The tool must call it window-bound, and must warn that RX interrupt
# COALESCING would make it worse rather than better.
pkts, seq, t = [], 1, 6000.0
for i in range(200):
pkts.append((t, tcp_frame(GC, PC, MAC_GC, MAC_PC, seq, b"\0" * 1024, win=2920)))
seq += 1024
t += 0.0002
pkts.append((t, tcp_frame(GC, PC, MAC_GC, MAC_PC, seq, b"\0" * 1024, win=2920)))
seq += 1024
t += 0.0002
pkts.append((t, tcp_frame(PC, GC, MAC_PC, MAC_GC, 1 + i, b"", win=65535)))
t += 0.0024 # ~2.5 ms per round trip
pG = os.path.join(tmp, "g.pcap")
write_pcap(pG, pkts)
out = run(pG, "--gc-ip", "10.0.0.64")
if "2.0 data frames per ACK" not in out:
fails.append(f"G: frames-per-ACK not derived\n{out}")
elif "WINDOW x RTT BOUND" not in out:
fails.append(f"G: window-bound flow not identified\n{out}")
elif "COALESCING would make this WORSE" not in out:
fails.append(f"G: missing the coalescing counter-warning\n{out}")
elif "full-MSS segments" not in out:
fails.append(f"G: did not suggest full-MSS segments\n{out}")
else:
print("G 1024B segments + 2920B window -> window x RTT bound,")
print(" full-MSS suggested, coalescing warned against")
print("\nFAILURES:\n " + "\n ".join(fails) if fails else
"\nAll analyse-pcap self-tests passed.")
sys.exit(1 if fails else 0)