#!/usr/bin/env bash
# test_e2e_benchmark.sh — Automated network benchmark for hyper-tps.
#
# Starts two headless instances with --benchmark flag. Both players walk in
# circles while the game collects RTT, position error, and sync stats.
# After --duration seconds, collects final diagnostics and prints a summary.
#
# Usage:
# bash scripts/test_e2e_benchmark.sh # 60s benchmark
# bash scripts/test_e2e_benchmark.sh --duration 30 # 30s benchmark
# bash scripts/test_e2e_benchmark.sh --config /path.json # custom config
# bash scripts/test_e2e_benchmark.sh --room "The Lobby" # export config first
# bash scripts/test_e2e_benchmark.sh --timeout 90 # connection timeout
#
# Prerequisites:
# - hyper-tps installed via pgh app install
# - jq installed
# - keet-daemon running
set -euo pipefail
APP="hyper-tps"
CONFIG_DIR="$HOME/.config/hyper-tps"
CONFIG_PATH="$CONFIG_DIR/headless.json"
ROOM=""
TIMEOUT=60
DURATION=60
EXPORT_CONFIG=false
while [[ $# -gt 0 ]]; do
case $1 in
--config) CONFIG_PATH="$2"; shift 2 ;;
--room) ROOM="$2"; EXPORT_CONFIG=true; shift 2 ;;
--timeout) TIMEOUT="$2"; shift 2 ;;
--duration) DURATION="$2"; shift 2 ;;
--export-config) EXPORT_CONFIG=true; shift ;;
--help|-h)
echo "Usage: $0 [--duration <secs>] [--timeout <secs>] [--config <path>]"
echo " $0 --room \"The Lobby\" # export config for specific room"
echo ""
echo "Options:"
echo " --duration <secs> Benchmark duration after connection (default: 60)"
echo " --timeout <secs> Connection timeout (default: 60)"
echo " --config <path> Path to headless config JSON"
echo " --room <name> Export config for this room before testing"
echo " --export-config Re-export config before testing"
exit 0 ;;
*) echo "Unknown arg: $1"; exit 1 ;;
esac
done
# Check deps
if ! command -v "$APP" &>/dev/null; then
echo "ERROR: $APP not found. Install with:"
echo " pgh app install pear://p9d1sizcqe4f8oepymqsp6o4bgbqs57aammb5sg716cirdh8aysy --name hyper-tps"
exit 1
fi
if ! command -v jq &>/dev/null; then
echo "ERROR: jq required. Install with: sudo apt install jq"
exit 1
fi
# ---- Step 1: Export config if needed ----
if [[ ! -f "$CONFIG_PATH" ]]; then
EXPORT_CONFIG=true
fi
if [[ "$EXPORT_CONFIG" == "true" ]]; then
echo "=== Exporting Config ==="
mkdir -p "$CONFIG_DIR"
EXPORT_ARGS=(--headless --export-config)
if [[ -n "$ROOM" ]]; then
EXPORT_ARGS+=(--room "$ROOM")
fi
echo "Running: $APP ${EXPORT_ARGS[*]}"
"$APP" "${EXPORT_ARGS[@]}" 2>&1 | tee /tmp/hypertps-export.log
if [[ ! -f "$CONFIG_PATH" ]]; then
echo "ERROR: Config was not created at $CONFIG_PATH"
exit 1
fi
echo ""
fi
# ---- Step 2: Confirm config ----
echo "=== Config ==="
jq '.' "$CONFIG_PATH"
echo ""
ROOM_ID=$(jq -r '.room_id' "$CONFIG_PATH")
ROOM_NAME=$(jq -r '.room_name' "$CONFIG_PATH")
if [[ -z "$ROOM_ID" || "$ROOM_ID" == "null" ]]; then
echo "ERROR: No room_id in config. Re-export with: $0 --export-config --room <name>"
exit 1
fi
echo "Room: $ROOM_NAME"
echo "Duration: ${DURATION}s"
echo "Timeout: ${TIMEOUT}s"
echo ""
# ---- Step 3: Run benchmark ----
DIAG_A="/tmp/hypertps-bench-A.diag.json"
DIAG_B="/tmp/hypertps-bench-B.diag.json"
PID_A=""
PID_B=""
kill_all() {
[[ -n "$PID_A" ]] && kill -- -"$PID_A" 2>/dev/null || true
[[ -n "$PID_B" ]] && kill -- -"$PID_B" 2>/dev/null || true
sleep 1
[[ -n "$PID_A" ]] && kill -9 -- -"$PID_A" 2>/dev/null || true
[[ -n "$PID_B" ]] && kill -9 -- -"$PID_B" 2>/dev/null || true
PID_A=""; PID_B=""
}
cleanup() {
echo ""
echo "=== Cleaning up ==="
kill_all
}
trap cleanup EXIT
rm -f "$DIAG_A" "$DIAG_B"
echo "=== Starting Benchmark ==="
echo ""
# Start Instance A (admin) with --benchmark
echo "Starting Instance A (admin + benchmark)..."
setsid "$APP" --no-updates --headless --benchmark --config "$CONFIG_PATH" --role admin --diag-path "$DIAG_A" \
> /tmp/hypertps-bench-A.log 2>&1 &
PID_A=$!
echo " pid=$PID_A log=/tmp/hypertps-bench-A.log diag=$DIAG_A"
# Wait for A mesh_active
echo "Waiting for A mesh_active..."
MESH_UP=false
for i in $(seq 1 "$TIMEOUT"); do
if [[ -f "$DIAG_A" ]]; then
MESH_A=$(jq -r '.mesh_active // false' "$DIAG_A" 2>/dev/null || echo "false")
if [[ "$MESH_A" == "true" ]]; then
echo " A mesh active after ${i}s"
MESH_UP=true
break
fi
fi
if ! kill -0 "$PID_A" 2>/dev/null; then
echo "ERROR: Instance A exited unexpectedly. Log:"
tail -20 /tmp/hypertps-bench-A.log
exit 1
fi
sleep 1
done
if [[ "$MESH_UP" != "true" ]]; then
echo "ERROR: A did not become mesh active within ${TIMEOUT}s"
tail -30 /tmp/hypertps-bench-A.log
exit 1
fi
# Start Instance B (peer) with --benchmark
echo ""
echo "Starting Instance B (peer + benchmark)..."
setsid "$APP" --no-updates --headless --benchmark --config "$CONFIG_PATH" --role peer --diag-path "$DIAG_B" \
> /tmp/hypertps-bench-B.log 2>&1 &
PID_B=$!
echo " pid=$PID_B log=/tmp/hypertps-bench-B.log diag=$DIAG_B"
# Wait for connection (both have 2+ players spawned)
echo ""
echo "Waiting for connection (max ${TIMEOUT}s)..."
CONNECTED=false
for i in $(seq 1 "$TIMEOUT"); do
PEERS_A=0; PEERS_B=0; SPAWN_A=0; SPAWN_B=0
if [[ -f "$DIAG_A" ]]; then
PEERS_A=$(jq -r '.godot_peers | length' "$DIAG_A" 2>/dev/null || echo "0")
SPAWN_A=$(jq -r '.players_spawned // 0' "$DIAG_A" 2>/dev/null || echo "0")
fi
if [[ -f "$DIAG_B" ]]; then
PEERS_B=$(jq -r '.godot_peers | length' "$DIAG_B" 2>/dev/null || echo "0")
SPAWN_B=$(jq -r '.players_spawned // 0' "$DIAG_B" 2>/dev/null || echo "0")
fi
echo " [${i}s] A: peers=$PEERS_A players=$SPAWN_A B: peers=$PEERS_B players=$SPAWN_B"
if [[ "$PEERS_A" -gt 0 ]] && [[ "$PEERS_B" -gt 0 ]] && \
[[ "$SPAWN_A" -ge 2 ]] && [[ "$SPAWN_B" -ge 2 ]]; then
CONNECTED=true
break
fi
if ! kill -0 "$PID_A" 2>/dev/null; then
echo "ERROR: Instance A died during connection phase"
tail -20 /tmp/hypertps-bench-A.log
exit 1
fi
if ! kill -0 "$PID_B" 2>/dev/null; then
echo "ERROR: Instance B died during connection phase"
tail -20 /tmp/hypertps-bench-B.log
exit 1
fi
sleep 1
done
if [[ "$CONNECTED" != "true" ]]; then
echo ""
echo "FAILURE: Players did not connect within ${TIMEOUT}s"
for label in A B; do
echo "--- Instance $label (last 20 lines) ---"
tail -20 "/tmp/hypertps-bench-${label}.log" 2>/dev/null || echo "(no log)"
done
exit 1
fi
# ---- Step 4: Benchmark phase — walk in circles ----
echo ""
echo "=== Benchmark Running (${DURATION}s) ==="
echo "Both players walking in circles. Collecting stats..."
echo ""
for elapsed in $(seq 1 "$DURATION"); do
# Progress every 10s
if (( elapsed % 10 == 0 )); then
RTT_A=""; RTT_B=""
if [[ -f "$DIAG_A" ]]; then
RTT_A=$(jq -r '.benchmark.peers | to_entries[0].value.rtt_avg_ms // "-"' "$DIAG_A" 2>/dev/null || echo "-")
fi
if [[ -f "$DIAG_B" ]]; then
RTT_B=$(jq -r '.benchmark.peers | to_entries[0].value.rtt_avg_ms // "-"' "$DIAG_B" 2>/dev/null || echo "-")
fi
echo " [${elapsed}/${DURATION}s] A rtt_avg=${RTT_A}ms B rtt_avg=${RTT_B}ms"
fi
# Check processes alive
if ! kill -0 "$PID_A" 2>/dev/null; then
echo "ERROR: Instance A died during benchmark at ${elapsed}s"
tail -10 /tmp/hypertps-bench-A.log
break
fi
if ! kill -0 "$PID_B" 2>/dev/null; then
echo "ERROR: Instance B died during benchmark at ${elapsed}s"
tail -10 /tmp/hypertps-bench-B.log
break
fi
sleep 1
done
# ---- Step 5: Collect and print results ----
echo ""
echo "========================================"
echo " BENCHMARK RESULTS"
echo "========================================"
echo ""
for label in A B; do
DIAG="/tmp/hypertps-bench-${label}.diag.json"
echo "--- Instance $label ---"
if [[ -f "$DIAG" ]]; then
BENCH=$(jq '.benchmark // empty' "$DIAG" 2>/dev/null)
if [[ -n "$BENCH" ]]; then
ELAPSED=$(echo "$BENCH" | jq -r '.elapsed_s // 0')
echo " Elapsed: ${ELAPSED}s"
echo ""
echo "$BENCH" | jq -r '.peers | to_entries[] | " Peer \(.key):\n RTT avg=\(.value.rtt_avg_ms // "-")ms min=\(.value.rtt_min_ms // "-")ms max=\(.value.rtt_max_ms // "-")ms jitter=\(.value.rtt_jitter_ms // "-")ms\n Samples: \(.value.rtt_sample_count // 0) Syncs: \(.value.sync_count // 0)\n Position error avg=\(.value.position_error_avg_m // "-")m max=\(.value.position_error_max_m // "-")m"' 2>/dev/null || echo " (no peer data)"
else
echo " (no benchmark data in diagnostics)"
fi
else
echo " (no diagnostic file)"
fi
echo ""
done
# Quick pass/fail summary
echo "--- Summary ---"
PASS=true
for label in A B; do
DIAG="/tmp/hypertps-bench-${label}.diag.json"
if [[ -f "$DIAG" ]]; then
POS_MAX=$(jq -r '.benchmark.peers | to_entries[0].value.position_error_max_m // 999' "$DIAG" 2>/dev/null || echo "999")
RTT_AVG=$(jq -r '.benchmark.peers | to_entries[0].value.rtt_avg_ms // 999' "$DIAG" 2>/dev/null || echo "999")
SYNC_N=$(jq -r '.benchmark.peers | to_entries[0].value.sync_count // 0' "$DIAG" 2>/dev/null || echo "0")
STATUS="OK"
if (( $(echo "$POS_MAX > 2.0" | bc -l 2>/dev/null || echo "0") )); then
STATUS="WARN: pos_error_max=${POS_MAX}m > 2m"
PASS=false
fi
if [[ "$SYNC_N" -eq 0 ]]; then
STATUS="WARN: no sync data"
PASS=false
fi
echo " Instance $label: rtt_avg=${RTT_AVG}ms sync_count=${SYNC_N} pos_max=${POS_MAX}m [$STATUS]"
fi
done
echo ""
if [[ "$PASS" == "true" ]]; then
echo "BENCHMARK PASSED"
exit 0
else
echo "BENCHMARK COMPLETED WITH WARNINGS"
exit 0
fi