Files
Joseph MattielloandClaude Opus 5.5 4841d260f7 perf(netlink): wait for unprompted link bytes, add on-device hop diagnostic (#860)
A Doom link tic is 12 one-way single-byte hops (6 pairs, ping-pong).  Two
parts of the old behaviour made the tic rate depend on the vsync phase
between the two devices, collapsing to 5 tics/s at bad phases even on a
fast link: JLinkAwaitReply only engaged after we transmitted, so a side that
only ANSWERS spun on ASISTAT with no wait, finished its frame in ~2 ms and
sat in the frontend while the partner's byte arrived; every such hop then
cost a whole video frame.

uart.c now counts consecutive ASISTAT reads with nothing to send or receive
and, past 24 in a frame, calls JLinkAwaitIdle(), which blocks like the reply
wait (same frame budget and timeout) so the byte is seen the moment it
arrives.  A silent partner backs off exponentially (1..32 frames without idle
waits); any received byte resets it.  Real transports on the JagLink device
only (not loopback, not the voice modem).

Simulator, Doom 2P, tics/s averaged over 4 vsync phases, before -> after
(emu 2 ms, 5 us poll_receive):
  2 ms one-way 27.5 -> 27.5   2.5 ms 19.8 -> 26.0   3 ms 16.3 -> 20.8
  4 ms 7.5 -> 19.2   Wi-Fi A (2 ms+3 jitter+2% spikes) 8.9 -> 16.0
  Wi-Fi B (3 ms+8 jitter+3% spikes) 5.4 -> 9.0   3 ms with emu 6 ms 10 -> 15

Also: a once-per-5-s INFO log line while a link carries traffic (hops/s, mean
hop time, wait per hop, slipped-hop share, timeouts, budget cuts, emulation
cost, idle-wait hits) so the numbers can be read from the RetroArch log on
iOS; netlink_sim gains --btrace/--log/--loss-pct/--rto-ms/--udp-rto-ms/
--corrupt and the clock patch gains a byte-event hook; netlink_btrace_report.py
breaks hop time down by cause and analyses the Doom tic structure.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 23:01:35 -04:00

153 lines
5.4 KiB
C

/* test_jlink.c — unit test for the JagLink byte-transport seam. */
#include <stdio.h>
#include <stdint.h>
#include "src/jerry/jlink.h"
/* #552: jlink.c's negotiation state machine calls into uart.c to read the
config intent and write the negotiated effective divisor. This test
deliberately does not link uart.c (or the event.c/JERRY-stub chain it
drags in) -- it tests the transport seam in isolation -- so stub the
two entry points minimally. Real behavior is exercised end-to-end by
test/test_jlink_negotiate.c and unit-pinned by test/test_uart_loopback.c. */
unsigned UARTWireSpeedupIntent(void) { return 0; }
void UARTSetWireSpeedupEffective(unsigned divisor) { (void)divisor; }
static int failures = 0;
#define CHECK(cond, msg) \
do { if (cond) printf("PASS %s\n", msg); \
else { printf("FAIL %s\n", msg); failures++; } } while (0)
static void test_disabled_mode(void)
{
uint8_t b = 0xEE;
JLinkClose();
CHECK(JLinkMode() == JLINK_MODE_DISABLED, "closed -> mode disabled");
CHECK(!JLinkConnected(), "disabled -> not connected");
JLinkSendByte(0x41);
CHECK(JLinkRxPending() == 0, "disabled send discards");
CHECK(JLinkRecvByte(&b) == 0 && b == 0xEE, "disabled recv returns none");
}
static void test_loopback_roundtrip(void)
{
uint8_t b = 0;
CHECK(JLinkOpen(JLINK_MODE_LOOPBACK) == 1, "open loopback");
CHECK(JLinkConnected(), "loopback -> connected");
JLinkSendByte(0x11);
JLinkSendByte(0x22);
JLinkSendByte(0x33);
CHECK(JLinkRxPending() == 3, "three bytes pending");
CHECK(JLinkRecvByte(&b) == 1 && b == 0x11, "recv 1st in order");
CHECK(JLinkRecvByte(&b) == 1 && b == 0x22, "recv 2nd in order");
CHECK(JLinkRecvByte(&b) == 1 && b == 0x33, "recv 3rd in order");
CHECK(JLinkRxPending() == 0, "drained");
JLinkClose();
}
static void test_overflow_drops_newest(void)
{
int i;
uint8_t b = 0;
JLinkOpen(JLINK_MODE_LOOPBACK);
for (i = 0; i < 300; i++)
JLinkSendByte((uint8_t)i);
CHECK(JLinkRxPending() == 256, "ring capped at 256");
CHECK(JLinkRecvByte(&b) == 1 && b == 0, "oldest byte survives overflow");
JLinkClose();
}
static void test_state_roundtrip(void)
{
uint8_t buf[1024];
uint8_t b = 0;
size_t n;
JLinkOpen(JLINK_MODE_LOOPBACK);
JLinkSendByte(0xAB);
JLinkSendByte(0xCD);
n = JLinkStateSave(buf);
CHECK(n > 0, "state save wrote bytes");
(void)JLinkRecvByte(&b);
(void)JLinkRecvByte(&b);
CHECK(JLinkRxPending() == 0, "drained before load");
CHECK(JLinkStateLoad(buf) == n, "state load consumed same size");
CHECK(JLinkRxPending() == 2, "load restored 2 pending bytes");
CHECK(JLinkRecvByte(&b) == 1 && b == 0xAB, "restored byte order");
JLinkClose();
}
/* jlink.h documents the traffic counters as per-session, reset by
JLinkClose(). They used to survive a close and accumulate across
sessions, which made the netlink diagnostics report traffic the current
session never carried. */
static void test_counters_reset_on_close(void)
{
JLinkOpen(JLINK_MODE_LOOPBACK);
JLinkSendByte(0x5A);
JLinkSendByte(0x5B);
CHECK(JLinkTxTotal() == 2, "tx counted while open");
JLinkClose();
CHECK(JLinkTxTotal() == 0, "close resets tx total");
CHECK(JLinkRxTotal() == 0, "close resets rx total");
}
/* RX is counted where the byte ARRIVES, not where the game drains the ring
— otherwise a peer's traffic stays invisible in the counters until the
game happens to read it, and tx/rx are never comparable. */
static void test_rx_counted_on_arrival(void)
{
uint8_t b = 0;
JLinkOpen(JLINK_MODE_LOOPBACK);
JLinkSendByte(0x77);
CHECK(JLinkRxTotal() == 1, "rx counted on arrival, before any read");
CHECK(JLinkRxPending() == 1, "byte still queued");
(void)JLinkRecvByte(&b);
CHECK(JLinkRxTotal() == 1, "draining the ring does not re-count it");
JLinkClose();
}
/* A dropped byte never entered the ring, so it must not be counted as
received: rx total has to stay consistent with what a reader can see. */
static void test_dropped_bytes_not_counted(void)
{
int i;
JLinkOpen(JLINK_MODE_LOOPBACK);
for (i = 0; i < 300; i++)
JLinkSendByte((uint8_t)i);
CHECK(JLinkTxTotal() == 300, "all 300 sends counted as tx");
CHECK(JLinkRxTotal() == 256, "only the 256 that fit counted as rx");
JLinkClose();
}
/* The link-health log line (JLinkStatsPoll) must stay silent when there is no
link and must never fire in the first call of a window or without traffic:
it runs once per video frame, so a spurious line would be a log flood. */
static void test_stats_quiet(void)
{
char buf[64];
buf[0] = 'x';
buf[1] = '\0';
JLinkClose();
CHECK(JLinkStatsPoll(buf, sizeof(buf)) == 0, "stats: disabled -> no line");
JLinkOpen(JLINK_MODE_LOOPBACK);
CHECK(JLinkStatsPoll(buf, sizeof(buf)) == 0, "stats: first call only starts the window");
JLinkSendByte(0x42);
CHECK(JLinkStatsPoll(buf, sizeof(buf)) == 0, "stats: window not elapsed -> no line");
CHECK(buf[0] == 'x', "stats: buffer untouched when silent");
JLinkClose();
}
int main(void)
{
test_disabled_mode();
test_loopback_roundtrip();
test_overflow_drops_newest();
test_state_roundtrip();
test_counters_reset_on_close();
test_rx_counted_on_arrival();
test_dropped_bytes_not_counted();
test_stats_quiet();
printf(failures ? "FAILED (%d)\n" : "OK\n", failures);
return failures ? 1 : 0;
}