From 6e0e0270e2cb9c54b1b7e63d9e67d8ac16792cef Mon Sep 17 00:00:00 2001 From: Luke Hoersten Date: Sat, 20 Jun 2026 11:40:26 -0500 Subject: phase 6: performance review playbook in TESTING.md + UDP-vs-TCP rationale in stream_server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two pieces of documentation, neither changes behavior: 1. TESTING.md gains a "Performance review playbook" section: per- session capture commands for firmware serial + /state poll + Scrypted console, an annotated walkthrough of what each log shape means, an investigation-threshold table mapping user-facing symptoms to likely causes and the first thing to check, and a one-line tools list. Replaces the implicit "ask the maintainer how to debug" loop with a reproducible workflow. 2. stream_server.c gains a "Why TCP and not UDP" header comment documenting the analysis from the design phase. JPEGs are ~123 IP datagrams at 1500 MTU; on hardwired Gigabit LAN switch-fabric loss is < 1e-9/packet → per-frame corruption ≈ 1.2e-7. UDP's theoretical wins (no Nagle, latest-wins semantics) don't apply because TCP_NODELAY is on, socket.write p50 < 1ms, and the FIONREAD trick already implements latest-wins on the receive side. UDP's costs (200-400 LOC of app-layer fragmentation, loss of nc/curl debug, FIONREAD trick stops working under fragmentation) are real. Documented as reference so a future contributor doesn't re-derive the analysis from scratch. --- main/stream_server.c | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) (limited to 'main/stream_server.c') diff --git a/main/stream_server.c b/main/stream_server.c index c417e4a..1321f22 100644 --- a/main/stream_server.c +++ b/main/stream_server.c @@ -18,6 +18,29 @@ static const char *TAG = "stream"; +// Why TCP and not UDP. +// +// JPEGs at panel-native q:v 1 are ~180 KB → ~123 IP datagrams at 1500 +// MTU. On hardwired Gigabit LAN with a single managed switch, fabric +// loss is < 1e-9/packet, so per-frame corruption is ≈1.2e-7 (~1 bad +// frame every 95 days at 30 fps). UDP's theoretical wins don't apply +// to this deployment: +// +// - Nagle/SACK/window-scaling overhead: socket.write p50 < 1ms in +// steady state (see Scrypted-side log). TCP_NODELAY is on. No +// per-frame ACK round-trip exists in either direction. +// - "Latest wins" semantics: implemented for free above via the +// FIONREAD check below. UDP would have to reassemble fragments +// first before deciding to skip, defeating the latency win. +// +// UDP's costs would be real: 200-400 LOC of app-layer fragmentation +// + reassembly + partial-frame timeout, loss of `nc localhost 81 | +// xxd` for debug, and the FIONREAD-skip trick stops working because +// UDP recvmsg counts fragments not whole messages. +// +// Revisit only if a Wi-Fi-only variant ships (loss rate 1e-3..1e-5 +// would make TCP retransmits painful enough to consider the rewrite). + #define HEADER_V0_BYTES 8 // legacy: jpeg_len + seq #define HEADER_V1_BYTES 16 // current: magic + jpeg_len + seq + event_us_low #define HEADER_BYTES HEADER_V0_BYTES // used for FIONREAD threshold — -- cgit v1.2.3