test(multiplayer): report transport health on prediction-quality failures

A percentile alone cannot tell "the predictor regressed" from "the client
never received the data". The client gate now prints snapshot_loss /
snapshot_age / rtt on every run, and on a quality failure with >20% loss
says explicitly that the run was transport-starved. It deliberately does
not convert the failure into a pass: a client that cannot receive
snapshots is still a failed run, just a differently-diagnosed one.

Both directions of the new branch verified non-vacuously (forced true so
it fires and formats; restored so it stays quiet on a healthy run while
the INFO line still prints).

Records the investigation behind it in multiplayer-todo.md: the reviewer's
3-process p95 0.688 did not reproduce. An idle third process costs nothing
(p99 0.094), a spectator costs a small but real amount (p99 0.094-0.146),
and snapshot loss held at 0.0% even under 2x CPU oversubscription - all an
order of magnitude inside the 0.5/2.0 gates. Also notes that a previously
working class_name can silently drop out of the .godot class cache, which
surfaces as a bogus parse error with nothing in git status to explain it.
This commit is contained in:
Josh Creek
2026-08-21 15:24:58 +01:00
parent 7a1668c902
commit e51dc765a2
2 changed files with 21 additions and 1 deletions
+18
View File
@@ -290,6 +290,18 @@ func run_client_check(settle_seconds: float, drive_seconds: float, exercise_ball
# server consumes, so both the same-sequence raw residual and the exposed
# render discontinuity are meaningful free-flight gates. Hard corrections
# remain separately gated by cohort.
# Transport health, printed alongside the quality numbers and asserted
# separately below. Without this a p95 failure is undiagnosable: "the
# predictor got worse" and "the client never received the data" look
# identical in a percentile. An adversarial review hit exactly that — a
# 3-process run failed at p95 0.688 with roughly a third of snapshots
# missing, and it could not be told apart from a real regression.
var snapshot_loss_pct := float(net_stats.get("snapshot_loss_pct", 0.0))
var snapshot_age_ms := float(net_stats.get("snapshot_age_ms", 0.0))
print("SMOKE INFO: transport snapshot_loss=%.1f%% snapshot_age=%.1fms rtt=%.1fms" % [
snapshot_loss_pct, snapshot_age_ms, NetworkManager.rtt_ms
])
var raw_quality_p95: float = float(prediction_stats.get("free_flight_position_error_p95", INF))
var raw_quality_p99: float = float(prediction_stats.get("free_flight_position_error_p99", INF))
var raw_rotation_p95: float = float(prediction_stats.get("free_flight_rotation_error_p95", INF))
@@ -438,6 +450,12 @@ func run_client_check(settle_seconds: float, drive_seconds: float, exercise_ball
])
var success := verification_movement > 1.0 and local_prediction_ok and prediction_quality_ok and ball_contact_ok and match_state_ok
# A run starved of snapshots has not measured prediction quality at all, so
# say so explicitly instead of blaming the predictor. Deliberately does NOT
# convert the failure into a pass — a client that cannot receive snapshots
# is still a failed run, just a differently-diagnosed one.
if not prediction_quality_ok and snapshot_loss_pct > 20.0:
print("SMOKE FAIL: transport-starved, not a prediction regression (snapshot_loss=%.1f%%) — check host CPU contention before suspecting the predictor" % snapshot_loss_pct)
print("SMOKE %s: client locally predicted %.2fm horizontal, local_prediction_ok=%s prediction_quality_ok=%s" % [
"PASS" if success else "FAIL", moved_horizontal, str(local_prediction_ok), str(prediction_quality_ok)
])