#!/usr/bin/env bash # bench-server.sh — benchmark a Minecraft server: startup + /reload + bot load # # Usage: ./bench-server.sh [bot-count] [--play] # # Tiers: # unpatched control — vanilla jar, depth-16 datapack (defect present) # mitigated fix applied — patched jar, depth-16 datapack (same game, fast) # enriched new territory — patched jar, depth-24 300-ns datapack (impossible before patch) # # Aliases: vanilla=unpatched, patched=mitigated # # --play: after benchmarks, keep server up for human play testing. # Prints connection info and waits for Ctrl-C. set -euo pipefail export PYTHONUNBUFFERED=1 TIER="${1:-unpatched}" BOTS="${2:-20}" # --play flag: accept anywhere in args PLAY_MODE=0 for _arg in "$@"; do [ "$_arg" = "--play" ] && PLAY_MODE=1; done # ── Tier configuration ──────────────────────────────────────────────────────── case "$TIER" in unpatched|vanilla) TIER=unpatched DIR=/tmp/mc-bench/unpatched PORT=25565 RCON=25575 SERVER_JAR=/home/fox/Downloads/server.jar DATAPACK_DEPTH=16 DATAPACK_NS=200 DATAPACK_XREFS=6 TIER_NOTE="control — DependencySorter defect present" ;; mitigated|patched) TIER=mitigated DIR=/tmp/mc-bench/mitigated PORT=25566 RCON=25576 SERVER_JAR=/tmp/mc-bench/server-patched.jar DATAPACK_DEPTH=16 DATAPACK_NS=200 DATAPACK_XREFS=6 TIER_NOTE="fix applied — same game, defect eliminated" ;; enriched) DIR=/tmp/mc-bench/enriched PORT=25567 RCON=25577 SERVER_JAR=/tmp/mc-bench/server-patched.jar DATAPACK_DEPTH=48 DATAPACK_NS=1000 DATAPACK_XREFS=32 TIER_NOTE="enriched — D=48 / 1000 NS / 32 xrefs: impossible before patch" ;; *) echo "Usage: $0 unpatched|mitigated|enriched [bot-count] [--play]" exit 1 ;; esac JAVA=/tmp/jdk25/bin/java LOG="$DIR/bench.log" FIFO="$DIR/server.stdin" # ── System metrics helpers ──────────────────────────────────────────────────── # cpu_snap: echo "user nice sys irq softirq intr" from /proc/stat cpu_snap() { local cpu intr read -r cpu user nice sys idle iowait irq softirq steal guest gnest < /proc/stat intr=$(awk '/^intr / {print $2}' /proc/stat) echo "$user $nice $sys $irq $softirq $intr" } # cpu_delta → prints table row # Computes jiffies burned per field; divides by total to get %. # /proc/stat ticks at CONFIG_HZ (usually 100 Hz). cpu_delta() { local before="$1" after="$2" elapsed_ms="$3" read -r bu bn bs br bsoft bi <<< "$before" read -r au an as_ ar asoft ai <<< "$after" local du=$(( au - bu )) local dn=$(( an - bn )) local ds=$(( as_ - bs )) local dr=$(( ar - br )) local dsoft=$(( asoft - bsoft )) local dintr=$(( ai - bi )) local total=$(( du + dn + ds + dr + dsoft )) if [ "$total" -gt 0 ]; then awk -v u=$du -v n=$dn -v s=$ds -v r=$dr -v soft=$dsoft -v intr=$dintr \ -v tot=$total -v ms=$elapsed_ms 'BEGIN { printf " cpu user=%.1f%% nice=%.1f%% sys=%.1f%% irq=%.1f%% sirq=%.1f%% intr=%d (over %dms)\n", u/tot*100, n/tot*100, s/tot*100, (r+soft)/tot*100, 0, intr, ms }' else echo " cpu (no delta)" fi } # mem_snap: echo free_mb available_mb from /proc/meminfo mem_snap() { awk '/^MemFree:/{f=$2}/^MemAvailable:/{a=$2} END{printf "%d %d", f/1024, a/1024}' /proc/meminfo } # Print and store a labelled memory reading mem_print() { local label="$1" snap="$2" read -r free_mb avail_mb <<< "$snap" echo " mem free=${free_mb}MB available=${avail_mb}MB ($label)" } pkill -f "port $PORT " 2>/dev/null || true sleep 1 rm -f "$LOG" "$FIFO" rm -rf "$DIR/world" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" python3 "$SCRIPT_DIR/gen-modpack-tags.py" "$DIR" "$DATAPACK_DEPTH" "$DATAPACK_NS" "$DATAPACK_XREFS" mkfifo "$FIFO" echo "==========================================" echo " $TIER — Minecraft Server Benchmark" echo " $TIER_NOTE" echo "==========================================" echo " depth-${DATAPACK_DEPTH} ${DATAPACK_NS} namespaces ${DATAPACK_XREFS} xrefs · $BOTS bots" echo "" # ── §1 Startup ──────────────────────────────────────────────────────────────── echo "§1 Starting server..." CPU_BEFORE_STARTUP=$(cpu_snap) MEM_BEFORE_STARTUP=$(mem_snap) START_MS=$(date +%s%3N) exec 3<>"$FIFO" (cd "$DIR" && exec $JAVA -Xmx3G -Xms512m \ --enable-native-access=ALL-UNNAMED \ -DbundlerRepoDir=. \ -jar "$SERVER_JAR" \ --nogui --port "$PORT" \ <"$FIFO" >>"$LOG" 2>&1) & SERVER_PID=$! for i in $(seq 1 180); do sleep 1 if grep -q "Done (" "$LOG" 2>/dev/null; then STARTUP_MS=$(( $(date +%s%3N) - START_MS )) CPU_AFTER_STARTUP=$(cpu_snap) MEM_AFTER_STARTUP=$(mem_snap) DONE_LINE=$(grep "Done (" "$LOG" | tail -1) echo " startup: ${STARTUP_MS} ms" echo " server: $DONE_LINE" cpu_delta "$CPU_BEFORE_STARTUP" "$CPU_AFTER_STARTUP" "$STARTUP_MS" mem_print "before" "$MEM_BEFORE_STARTUP" mem_print "after" "$MEM_AFTER_STARTUP" break fi if ! kill -0 $SERVER_PID 2>/dev/null; then echo "ERROR: server exited prematurely" tail -12 "$LOG" exit 1 fi [ $(( i % 10 )) -eq 0 ] && echo " ...${i}s" done if ! grep -q "Done (" "$LOG" 2>/dev/null; then echo "ERROR: did not start in 180s" kill $SERVER_PID 2>/dev/null exit 1 fi sleep 2 # let RCON bind # ── §2 First bot wave ───────────────────────────────────────────────────────── echo "" echo "§2 First bot wave: $BOTS bots joining..." node "$SCRIPT_DIR/raw-connect.mjs" localhost "$PORT" "$BOTS" 2>&1 sleep 2 # ── §3 /reload timing ───────────────────────────────────────────────────────── echo "" echo "§3 /reload timing..." RELOAD_LINE=$(wc -l < "$LOG") CPU_BEFORE_RELOAD=$(cpu_snap) MEM_BEFORE_RELOAD=$(mem_snap) RELOAD_START=$(date +%s%3N) python3 "$SCRIPT_DIR/rcon.py" localhost "$RCON" benchpass "reload" 2>&1 || \ { echo "RCON failed, sending via stdin"; echo "reload" >&3; } RELOAD_MS="" for i in $(seq 1 180); do sleep 1 if tail -n +"$RELOAD_LINE" "$LOG" 2>/dev/null | grep -qE "Loaded [0-9]+ recipes"; then RELOAD_MS=$(( $(date +%s%3N) - RELOAD_START )) CPU_AFTER_RELOAD=$(cpu_snap) MEM_AFTER_RELOAD=$(mem_snap) echo " /reload: ${RELOAD_MS} ms" tail -n +"$RELOAD_LINE" "$LOG" | grep -E "Loaded [0-9]+ (recipes|advancements)" | head -3 | while IFS= read -r l; do echo " $l"; done cpu_delta "$CPU_BEFORE_RELOAD" "$CPU_AFTER_RELOAD" "$RELOAD_MS" mem_print "before" "$MEM_BEFORE_RELOAD" mem_print "after" "$MEM_AFTER_RELOAD" break fi done [ -z "$RELOAD_MS" ] && { echo " /reload: did not complete in 180s"; RELOAD_MS="-1"; CPU_AFTER_RELOAD=$(cpu_snap); MEM_AFTER_RELOAD=$(mem_snap); } # ── §4 Second bot wave (post-reload) ───────────────────────────────────────── echo "" echo "§4 Post-reload bot wave: $BOTS bots..." node "$SCRIPT_DIR/raw-connect.mjs" localhost "$PORT" "$BOTS" 2>&1 # ── §5 Play test mode (human) ───────────────────────────────────────────────── if [ "$PLAY_MODE" = "1" ]; then echo "" echo "==========================================" echo " PLAY TEST MODE — server staying up" echo "==========================================" echo " Connect to: localhost:$PORT" echo " Tier: $TIER ($TIER_NOTE)" echo " Log: $LOG" echo "" echo " Press Ctrl-C to stop the server and finish benchmark." echo "" # Wait for Ctrl-C; trap cleans up trap 'echo ""; echo "Stopping server..."; echo "stop" >&3; exec 3>&-; wait $SERVER_PID 2>/dev/null || true; rm -f "$FIFO"; echo "Server stopped."; exit 0' INT TERM while kill -0 $SERVER_PID 2>/dev/null; do sleep 5; done echo "Server exited on its own." exec 3>&- 2>/dev/null || true rm -f "$FIFO" exit 0 fi # ── Stop ───────────────────────────────────────────────────────────────────── echo "" echo "Stopping server..." echo "stop" >&3 exec 3>&- wait $SERVER_PID 2>/dev/null || true rm -f "$FIFO" # ── Results ────────────────────────────────────────────────────────────────── # Derive per-phase CPU/mem deltas for results.txt (machine-readable) _cpu_fields() { local before="$1" after="$2" read -r bu bn bs br bsoft bi <<< "$before" read -r au an as_ ar asoft ai <<< "$after" local du=$(( au-bu )) dn=$(( an-bn )) ds=$(( as_-bs )) local dr=$(( ar-br )) dsoft=$(( asoft-bsoft )) dintr=$(( ai-bi )) local tot=$(( du+dn+ds+dr+dsoft )) [ "$tot" -eq 0 ] && tot=1 awk -v u=$du -v n=$dn -v s=$ds -v r=$dr -v soft=$dsoft -v intr=$dintr -v tot=$tot \ -v pfx="$3" 'BEGIN { printf "%scpu_user_pct=%.1f\n%scpu_nice_pct=%.1f\n%scpu_sys_pct=%.1f\n%scpu_irq_pct=%.1f\n%scpu_intr=%d\n", pfx, u/tot*100, pfx, n/tot*100, pfx, s/tot*100, pfx, (r+soft)/tot*100, pfx, intr }' } _mem_fields() { read -r free_mb avail_mb <<< "$(mem_snap)" echo "${1}mem_free_mb=$free_mb" echo "${1}mem_avail_mb=$avail_mb" } echo "" echo "==========================================" echo " $TIER RESULTS" echo "==========================================" echo " startup: ${STARTUP_MS} ms" echo " reload: ${RELOAD_MS} ms" echo " bots: $BOTS" echo " depth: $DATAPACK_DEPTH ns: $DATAPACK_NS xrefs: $DATAPACK_XREFS" echo "" echo " --- CPU/mem during startup ---" cpu_delta "$CPU_BEFORE_STARTUP" "$CPU_AFTER_STARTUP" "$STARTUP_MS" mem_print "before" "$MEM_BEFORE_STARTUP" mem_print "after" "$MEM_AFTER_STARTUP" echo " --- CPU/mem during /reload ---" cpu_delta "$CPU_BEFORE_RELOAD" "$CPU_AFTER_RELOAD" "$RELOAD_MS" mem_print "before" "$MEM_BEFORE_RELOAD" mem_print "after" "$MEM_AFTER_RELOAD" echo "" { cat << EOF label=$TIER startup_ms=$STARTUP_MS reload_ms=$RELOAD_MS bots=$BOTS datapack_depth=$DATAPACK_DEPTH datapack_ns=$DATAPACK_NS datapack_xrefs=$DATAPACK_XREFS EOF _cpu_fields "$CPU_BEFORE_STARTUP" "$CPU_AFTER_STARTUP" "startup_" _mem_fields "startup_before_" read -r f a <<< "$MEM_BEFORE_STARTUP"; echo "startup_before_mem_free_mb=$f"; echo "startup_before_mem_avail_mb=$a" read -r f a <<< "$MEM_AFTER_STARTUP"; echo "startup_after_mem_free_mb=$f"; echo "startup_after_mem_avail_mb=$a" _cpu_fields "$CPU_BEFORE_RELOAD" "$CPU_AFTER_RELOAD" "reload_" read -r f a <<< "$MEM_BEFORE_RELOAD"; echo "reload_before_mem_free_mb=$f"; echo "reload_before_mem_avail_mb=$a" read -r f a <<< "$MEM_AFTER_RELOAD"; echo "reload_after_mem_free_mb=$f"; echo "reload_after_mem_avail_mb=$a" } > "$DIR/results.txt"