OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / distribute / hypertps-win64 / scripts / terrain-bench.sh

109 lines · bash ref:
#!/usr/bin/env bash
# terrain-bench.sh — DC-terrain generation benchmark harness.
#
# Turns a tee'd game log into objective LOD-generation numbers so "is it fixed?"
# becomes a number, not a screenshot. Parses the [MESHBENCH] (GPU per-stage
# timing + throughput) and [LODDIAG] (backlog/settle) lines emitted by the
# in-game diagnostics.
#
# Usage:
#   # 1. run the game and capture a log (spawn, stand still ~20s, then move):
#   hyper-tps 2>&1 | tee /tmp/tbench.log
#   # 2. report:
#   bash scripts/terrain-bench.sh /tmp/tbench.log
#
# Metrics reported:
#   - throughput: GPU chunks meshed/sec (min / mean / max across intervals)
#   - per-chunk time split: submit vs sync (the CPU<->GPU stall) vs readback
#   - sync fraction: how much of per-chunk time is the serial sync stall (the §E target)
#   - backlog: peak premesh PENDING (queued-but-unmeshed chunks)
#   - convergence: did premesh PENDING ever return to ~0 (terrain settled)?
set -euo pipefail

LOG="${1:-}"
[ -z "$LOG" ] && { echo "usage: $0 <game-log>"; exit 1; }
[ -f "$LOG" ] || { echo "no such log: $LOG"; exit 1; }

echo "=== DC-terrain generation benchmark: $LOG ==="
VER=$(grep -oE "v6[0-9]+-[0-9-]+-[a-z0-9]+" "$LOG" | head -1 || true)
echo "build: ${VER:-unknown}"
echo ""

# --- MESHBENCH: throughput + per-stage timing ---
awk '
  /\[MESHBENCH\] meshed=/ {
    # meshed=+N in Tms => R chunks/s | per-chunk avg: submit=A sync=B readback=C total=D ms | slowest chunk=M ms
    for (i = 1; i <= NF; i++) {
      if ($i ~ /^=>/)         { rate = $(i+1); n++; sum_rate += rate; if (rate>max_rate||n==1) max_rate=rate; if (rate<min_rate||n==1) min_rate=rate }
      if ($i ~ /^submit=/)    { s=$i; sub(/submit=/,"",s); sum_sub += s; ns++ }
      if ($i ~ /^sync=/)      { s=$i; sub(/sync=/,"",s);   sum_syn += s }
      if ($i ~ /^readback=/)  { s=$i; sub(/readback=/,"",s); sum_rb += s }
    }
  }
  END {
    if (n == 0) { print "  (no [MESHBENCH] samples — old build or CPU meshing)"; exit }
    printf "  throughput  : %.0f chunks/s mean  (min %.0f / max %.0f)  over %d intervals\n", sum_rate/n, min_rate, max_rate, n
    if (ns > 0) {
      as=sum_sub/ns; ay=sum_syn/ns; ab=sum_rb/ns; at=as+ay+ab
      printf "  per-chunk   : submit %.2fms | SYNC %.2fms | readback %.2fms | total %.2fms\n", as, ay, ab, at
      if (at > 0) printf "  sync frac   : %.0f%% of per-chunk time is the serial CPU<->GPU sync stall  (<-- the §E target)\n", 100*ay/at
    }
  }
' "$LOG"
echo ""

# --- CHURN: wasted re-meshing (the real deal-breaker) ---
awk '
  /\[CHURN\] discarded=/ {
    for (i = 1; i <= NF; i++) {
      if ($i ~ /^discarded=\+/)  { s=$i; sub(/discarded=\+/,"",s); d_sum+=s; n++ }
      if ($i ~ /^recenters=\+/)  { s=$i; sub(/recenters=\+/,"",s); r_sum+=s }
    }
    # "(P% of meshed wasted)" — the P is field after discarded=+N
    for (i = 1; i <= NF; i++) if ($i ~ /^\(/ ) { p=$i; gsub(/[(%]/,"",p); if (p+0>peak_pct) peak_pct=p+0; pct_sum+=p+0 }
  }
  END {
    if (n==0) { print "  (no [CHURN] samples — old build)"; exit }
    printf "  wasted work : %d meshes discarded over run, %d recenters (avg %.0f%% of meshing wasted, peak %.0f%%)\n", d_sum, r_sum, pct_sum/n, peak_pct
    if (pct_sum/n > 25) print "  >>> CHURN IS THE BOTTLENECK: most meshing is thrown away and redone (fix churn before throughput)"
  }
' "$LOG"
echo ""

# --- EDIT: CSG edit-to-visible latency (demo-critical) ---
awk '
  /\[EDIT\] chunks_remeshed=/ {
    for (i = 1; i <= NF; i++) if ($i ~ /^latency=/) { s=$i; sub(/latency=/,"",s); lat_sum+=s; n++; if (s+0>mx) mx=s+0 }
  }
  END {
    if (n==0) { print "  csg edits   : (no edits during run — do a few CSG edits to benchmark responsiveness)"; exit }
    printf "  csg edits   : %d edit-remesh intervals, avg %.0f frames (~%.0fms) edit->visible, worst %.0f frames (~%.0fms)\n", n, lat_sum/n, (lat_sum/n)*16.6, mx, mx*16.6
    if ((lat_sum/n)*16.6 > 100) print "  >>> EDITS ARE SLOW: edit-to-visible latency is high (likely starved by streaming backlog)"
  }
' "$LOG"
echo ""

# --- LODDIAG: backlog + convergence ---
awk '
  /\[LODDIAG\] leaves=/ {
    pend=0; gen=0
    for (i = 1; i <= NF; i++) {
      if ($i ~ /^PENDING=/) { s=$i; sub(/PENDING=/,"",s); pend=s }
      if ($i ~ /^GEN=/)     { s=$i; sub(/GEN=/,"",s);     gen=s }
    }
    total=pend+gen
    n++
    if (total > peak) peak=total
    if (total <= 2) settled++
  }
  END {
    if (n == 0) { print "  (no [LODDIAG] samples)"; exit }
    printf "  peak backlog: %d premesh chunks queued+in-flight\n", peak
    printf "  convergence : %d/%d intervals at ~0 backlog (%s)\n", settled, n, (settled>0 ? "terrain DID settle at least once" : "NEVER settled — refinement never caught up")
  }
' "$LOG"
echo ""
echo "Interpretation: a high sync fraction + high peak backlog = the single-thread"
echo "serial submit+sync bottleneck (§E). Deferred-readback ring should drop the sync"
echo "fraction and raise throughput; re-run this to confirm the number moved."