Files
urus-benches/spin_ab_urus.sh
sandbox-agent 926aba4344 bench: flag void (high non-2xx) runs; add status-code diagnostic
The first multi-core A/B was void: ~100% non-2xx on every row, so 'sustained
rps' was error-path throughput, not store work -- the spin patch's park/wake
path was never exercised. The harness reported it cheerfully anyway.

- spin_ab_urus.sh: per-run output now computes non-2xx vs approx total and
  prints 'VOID: N% non-2xx' instead of a meaningless rps when >2% error.
- loadgen/_diag_status.lua: single-thread wrk script that tallies exact status
  codes (response() + done()) and names the likely cause (401=auth not
  reaching server / 404=store not pre-populated / 503=store actor down).
  The aggregate 'Non-2xx' count can't distinguish these; this can.

Root cause still open: locally the store survives heavy keep-alive load (no
panic), prepop populates, auth+happy-path serve 200s -- so the storm isn't
reproducible in this 1-core sandbox. s3 at ~100% (not ~80%) rules out a plain
404 storm and points at auth or store-call erroring on every request; needs
the diag run on the real box to confirm.
2026-06-14 21:58:27 +00:00

245 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
# spin_ab_urus.sh — A/B the RFC 004 spinning policy on the *realistic* urus
# workload (not the multi_scheduler microbench).
#
# It does exactly what the handoff asks: check out the pre-spin smarm commit,
# run the urus benches; check out the latest smarm commit, run them again;
# then print a before/after comparison.
#
# BEFORE = b0c9685 (pre-spin: no spinning code exists at all)
# AFTER = current smarm HEAD (spin defaults on + N=1 fix; e.g. 2f011f6)
#
# smarm is a *path dependency* of urus, which is a path dependency of
# urus-benches (all three are siblings). So we A/B by checking out smarm at
# each sha and rebuilding urus-benches; urus itself is held fixed — only the
# smarm under it changes between A and B. Each (commit, scenario, cores) run
# goes through the existing runner.sh (saturation probe -> warmup -> wrk2
# latency-honest measure) and lands a result.json; analyze_spin_ab_urus.py
# pairs them up at the end.
#
# Scenarios (per RECON §6.4): s2 (router+auth, no store) and s3 (memory store,
# the headline). Server is urus-mem for both. We do NOT touch s4/sqlite.
#
# Core sweep: the spin gate is active at 2-4 schedulers and inert at 1 and
# >=5. We sweep 1/2/4/6 server cores: 2 and 4 are gate-ACTIVE, 1 and 6 are
# inert controls -- 6 also being a *saturating* control, since the old 8-core
# point couldn't be saturated by an 8-core loadgen (it plateaued ~102k rps,
# loadgen-bound). Server is pinned to cores 0..N-1; the load generator gets
# its own disjoint cores (LOADGEN_CPUS), sized ~>=2x the server cores per the
# saturation goal while leaving cores free for the OS.
#
# Usage:
# ./spin_ab_urus.sh # full A/B, defaults
# ./spin_ab_urus.sh --quick # short timings (15s phases)
# ./spin_ab_urus.sh --dry-run # print the plan, run nothing
# CORES="2 4" SCENARIOS="s3" ./spin_ab_urus.sh
# BEFORE=b0c9685 AFTER=2f011f6 ./spin_ab_urus.sh
# ./spin_ab_urus.sh --with-axum-control # also run axum-mem (smarm-free
# # invariant; should be flat A vs B)
#
# Env knobs (all overridable):
# BEFORE / AFTER smarm commit-ishes (default b0c9685 / HEAD)
# SCENARIOS space-separated (default "s2 s3")
# CORES server core counts to sweep (default "1 2 4 6")
# LOADGEN_CPUS loadgen taskset range (default "8-23" = 16 cores)
# WRK_THREADS loadgen wrk/wrk2 threads (default 16)
# WRK_CONNS loadgen open connections (default 512)
# SERVER memory server backend (default urus-mem)
# PROBE_SEC/WARMUP_SEC/MEASURE_SEC/SAT_RATIO/PREPOP/WRK_CONNS/WRK_THREADS
# passed straight through to runner.sh
# SMARM_DIR override smarm location (default: sibling ../smarm)
# OUTDIR results dir (default: results/spin_ab_urus_<ts>)
set -euo pipefail
# ---------------------------------------------------------------------------
# Locate the pieces
# ---------------------------------------------------------------------------
BENCH_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # urus-benches
RUNNER="${BENCH_DIR}/runner.sh"
ANALYZER="${BENCH_DIR}/analyze_spin_ab_urus.py"
SMARM_DIR="${SMARM_DIR:-$(cd "${BENCH_DIR}/../smarm" 2>/dev/null && pwd || true)}"
if [[ ! -x "$RUNNER" ]]; then
echo "ERROR: runner.sh not found/executable at $RUNNER" >&2; exit 1
fi
if [[ -z "$SMARM_DIR" || ! -d "$SMARM_DIR/.git" ]]; then
echo "ERROR: smarm git repo not found (looked for sibling ../smarm). Set SMARM_DIR." >&2
exit 1
fi
# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------
BEFORE="${BEFORE:-b0c9685}"
# AFTER defaults to whatever smarm is currently on ("the latest").
AFTER_DEFAULT="$(git -C "$SMARM_DIR" rev-parse --short HEAD)"
AFTER="${AFTER:-$AFTER_DEFAULT}"
SCENARIOS="${SCENARIOS:-s2 s3}"
CORES="${CORES:-1 2 4 6}"
SERVER="${SERVER:-urus-mem}"
LOADGEN_CPUS="${LOADGEN_CPUS:-8-23}"
# Loadgen sizing: ~2x loadgen cores per server core. 16 cores covers the
# 6-core server at ~2.7x and the 4-core at 4x. Threads track the core count;
# connections bumped so the probe can actually find the server's peak.
export WRK_THREADS="${WRK_THREADS:-16}"
export WRK_CONNS="${WRK_CONNS:-512}"
QUICK=0
DRY=0
WITH_AXUM=0
for arg in "$@"; do
case "$arg" in
--quick) QUICK=1 ;;
--dry-run|-n) DRY=1 ;;
--with-axum-control) WITH_AXUM=1 ;;
-h|--help) sed -n '2,/^set -euo/p' "$0" | sed 's/^# \{0,1\}//; $d'; exit 0 ;;
*) echo "unknown arg: $arg" >&2; exit 2 ;;
esac
done
if [[ "$QUICK" == "1" ]]; then
export PROBE_SEC="${PROBE_SEC:-15}"
export WARMUP_SEC="${WARMUP_SEC:-15}"
export MEASURE_SEC="${MEASURE_SEC:-15}"
fi
TS="$(date +%Y%m%d-%H%M%S)"
OUTDIR="${OUTDIR:-${BENCH_DIR}/results/spin_ab_urus_${TS}}"
NPROC="$(nproc)"
# Loadgen low core, for an overlap sanity check against the server range.
LG_LO="${LOADGEN_CPUS%%-*}"
SERVERS_TO_RUN=("$SERVER")
[[ "$WITH_AXUM" == "1" ]] && SERVERS_TO_RUN+=("axum-mem")
echo ">>> smarm: $SMARM_DIR"
echo ">>> BEFORE=$BEFORE AFTER=$AFTER"
echo ">>> servers: ${SERVERS_TO_RUN[*]}"
echo ">>> scenarios: $SCENARIOS"
echo ">>> cores: $CORES"
echo ">>> loadgen: cpus=$LOADGEN_CPUS threads=$WRK_THREADS conns=$WRK_CONNS"
echo ">>> machine: $NPROC cores"
echo ">>> outdir: $OUTDIR"
[[ "$QUICK" == "1" ]] && echo ">>> --quick: probe/warmup/measure = ${PROBE_SEC}/${WARMUP_SEC}/${MEASURE_SEC}s"
echo
# server core range for a given count: "0" for 1, else "0-(N-1)"
server_cpus() { (( $1 == 1 )) && echo "0" || echo "0-$(( $1 - 1 ))"; }
# ---------------------------------------------------------------------------
# Safety: refuse a dirty smarm tree; restore original ref on exit
# ---------------------------------------------------------------------------
if ! git -C "$SMARM_DIR" diff-index --quiet HEAD -- 2>/dev/null; then
echo "ERROR: smarm working tree is dirty. Commit/stash before A/B so checkouts are safe." >&2
git -C "$SMARM_DIR" status --short >&2
exit 1
fi
ORIG_REF="$(git -C "$SMARM_DIR" symbolic-ref --quiet --short HEAD || git -C "$SMARM_DIR" rev-parse HEAD)"
restore_smarm() {
echo
echo ">>> restoring smarm to $ORIG_REF"
git -C "$SMARM_DIR" checkout --quiet "$ORIG_REF" 2>/dev/null \
|| echo "WARN: could not restore $ORIG_REF — smarm is on $(git -C "$SMARM_DIR" rev-parse --short HEAD)" >&2
}
[[ "$DRY" == "0" ]] && trap restore_smarm EXIT
# Overlap sanity: server top core must be below the loadgen range.
MAX_SRV=0; for c in $CORES; do (( c > MAX_SRV )) && MAX_SRV=$c; done
if [[ "$LG_LO" =~ ^[0-9]+$ ]] && (( MAX_SRV - 1 >= LG_LO )); then
echo "WARN: server cores reach $((MAX_SRV-1)) which overlaps LOADGEN_CPUS=$LOADGEN_CPUS." >&2
echo " set LOADGEN_CPUS to a disjoint range (two-process discipline)." >&2
fi
if (( MAX_SRV > NPROC )); then
echo "WARN: sweep asks for up to ${MAX_SRV} server cores but machine has ${NPROC}." >&2
fi
mkdir -p "$OUTDIR"
# ---------------------------------------------------------------------------
# Run one commit across the (server x scenario x cores) sub-matrix
# ---------------------------------------------------------------------------
run_commit() {
local sha="$1" label="$2"
echo "============================================================"
echo ">>> $label: checking out smarm $sha and rebuilding urus-benches"
echo "============================================================"
if [[ "$DRY" == "1" ]]; then
echo " [dry] git -C $SMARM_DIR checkout $sha"
echo " [dry] (cd $BENCH_DIR && cargo build --release)"
else
git -C "$SMARM_DIR" checkout --quiet "$sha"
# urus is a path dep of urus-benches and smarm a path dep of urus, so a
# workspace release build picks up the swapped smarm. cargo refingerprints
# on the changed sources.
( cd "$BENCH_DIR" && cargo build --release ) 2>&1 | tail -3
fi
for srv in "${SERVERS_TO_RUN[@]}"; do
for scn in $SCENARIOS; do
for c in $CORES; do
local scpus; scpus="$(server_cpus "$c")"
local run_id="${label}__${srv}__${scn}__c${c}"
local dest="${OUTDIR}/${label}/${srv}-${scn}-c${c}"
echo ">>> $label $srv $scn server-cores=$c (taskset $scpus) loadgen=$LOADGEN_CPUS"
if [[ "$DRY" == "1" ]]; then
echo " [dry] SERVER_CPUS=$scpus LOADGEN_CPUS=$LOADGEN_CPUS $RUNNER $srv $scn $run_id"
continue
fi
mkdir -p "$dest"
if SERVER_CPUS="$scpus" LOADGEN_CPUS="$LOADGEN_CPUS" \
"$RUNNER" "$srv" "$scn" "$run_id" > "${dest}/runner.log" 2>&1; then
if [[ -d "${BENCH_DIR}/results/${run_id}" ]]; then
mv "${BENCH_DIR}/results/${run_id}/"* "$dest/" 2>/dev/null || true
rmdir "${BENCH_DIR}/results/${run_id}" 2>/dev/null || true
fi
local rps p99 non2xx ms errpct
rps=$(awk -F'[:,]' '/"sustained_rps"/ {gsub(/[ "]/,"",$2); print $2; exit}' "${dest}/result.json" 2>/dev/null || echo "?")
p99=$(awk -F'"' '/"p99"/ {print $4; exit}' "${dest}/result.json" 2>/dev/null || echo "?")
non2xx=$(awk -F'[:,]' '/"non_2xx"/ {gsub(/[ "]/,"",$2); print $2; exit}' "${dest}/result.json" 2>/dev/null || echo 0)
ms="${MEASURE_SEC:-60}"
# error rate vs approx total requests (sustained_rps * measure_sec).
errpct=$(awk -v n="$non2xx" -v r="$rps" -v s="$ms" \
'BEGIN { t=r*s; if (t>0) printf "%.1f", 100*n/t; else print "?" }')
if awk -v e="$errpct" 'BEGIN { exit !(e+0 > 2.0) }' 2>/dev/null; then
echo " -> VOID: ${errpct}% non-2xx (${non2xx}) — server is erroring, not serving. rps=${rps} is meaningless."
else
echo " -> sustained=${rps} rps p99=${p99} (non-2xx ${errpct}%)"
fi
else
echo " -> FAILED (see ${dest}/runner.log)"
fi
done
done
done
}
run_commit "$BEFORE" "before"
run_commit "$AFTER" "after"
[[ "$DRY" == "1" ]] && { echo; echo ">>> dry run complete (nothing executed, smarm untouched)."; exit 0; }
# ---------------------------------------------------------------------------
# Analyze
# ---------------------------------------------------------------------------
echo
echo "============================================================"
echo ">>> A/B comparison"
echo "============================================================"
if command -v python3 >/dev/null && [[ -f "$ANALYZER" ]]; then
python3 "$ANALYZER" "$OUTDIR" --before before --after after | tee "${OUTDIR}/comparison.txt"
else
echo "(python3 or analyzer missing; raw result.json files are under $OUTDIR)"
fi
echo
echo ">>> DONE. results: $OUTDIR"