#!/bin/bash # Multi-run iperf3 + ping bench for FIPS mesh. Repeats each test N times # (default 3), reports median + min/max + flags outliers >20% from # median. Adds round-trip latency (ping) and a TCP-retransmit count # (proxy for packet loss across the FIPS overlay) per path. # # Usage: # ./bench-multirun.sh [mesh|chain] # # Environment: # FIPS_BENCH_RUNS=3 Number of iperf3 repetitions per path # FIPS_BENCH_DURATION=15 Seconds per iperf3 run # FIPS_BENCH_PARALLEL=1 Parallel TCP streams (1 = single-stream) # FIPS_BENCH_PING_COUNT=30 ICMP probes per path # FIPS_BENCH_OUTLIER_PCT=20 Flag run as outlier if Δ from median > N% # FIPS_BENCH_OUTPUT=json|text (default text; json emits NDJSON) set -e trap 'echo ""; echo "Bench interrupted"; exit 130' INT PROFILE="${1:-mesh}" RUNS="${FIPS_BENCH_RUNS:-5}" DURATION="${FIPS_BENCH_DURATION:-15}" PARALLEL="${FIPS_BENCH_PARALLEL:-1}" PING_COUNT="${FIPS_BENCH_PING_COUNT:-30}" OUTLIER_PCT="${FIPS_BENCH_OUTLIER_PCT:-20}" OUTPUT="${FIPS_BENCH_OUTPUT:-text}" if ! [[ "$RUNS" =~ ^[1-9][0-9]*$ ]] || [ "$RUNS" -lt 1 ]; then echo "FIPS_BENCH_RUNS must be ≥ 1" >&2; exit 1 fi SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ENV_FILE="$SCRIPT_DIR/../generated-configs${FIPS_CI_NAME_SUFFIX:-}/npubs.env" if [ ! -f "$ENV_FILE" ]; then echo "Error: $ENV_FILE not found. Run generate-configs.sh first." >&2 exit 1 fi # shellcheck source=../generated-configs/npubs.env source "$ENV_FILE" # ── Helpers ──────────────────────────────────────────────────────────────── # Run iperf3 once, print " " on success or "FAIL" on # failure. Uses JSON output so we don't have to scrape human-readable # SI prefixes. iperf_once() { local client="$1" dest_npub="$2" local out if ! out=$(docker exec "fips-${client}${FIPS_CI_NAME_SUFFIX:-}" iperf3 -c "${dest_npub}.fips" \ -t "$DURATION" -P "$PARALLEL" -J 2>&1); then echo FAIL return fi python3 - "$out" <<'PYEOF' import json, sys try: d = json.loads(sys.argv[1]) bps = d['end']['sum_received']['bits_per_second'] # iperf3 reports TCP retransmits in 'sum_sent.retransmits' on # the client side; missing on UDP or for the server-summary view. retr = d.get('end', {}).get('sum_sent', {}).get('retransmits', 0) print(f"{bps/1e6:.2f} {retr}") except Exception as e: print("FAIL") PYEOF } # Median + min + max + CoV% + outlier indices over a whitespace- # separated list of floats. CoV% = (stddev / mean) * 100, i.e. the # coefficient of variation in percent — directly comparable across # paths regardless of absolute throughput. Single-value runs print # CoV=0. # Prints: " " stats() { python3 - "$OUTLIER_PCT" "$@" <<'PYEOF' import math import sys pct = float(sys.argv[1]) vals = [float(x) for x in sys.argv[2:]] n = len(vals) s = sorted(vals) median = s[n // 2] if n % 2 else (s[n // 2 - 1] + s[n // 2]) / 2 lo, hi = s[0], s[-1] mean = sum(vals) / n if n else 0.0 if n > 1 and mean > 0: variance = sum((v - mean) ** 2 for v in vals) / (n - 1) # sample stddev cov_pct = math.sqrt(variance) / mean * 100 else: cov_pct = 0.0 outliers = [] if median > 0: for i, v in enumerate(vals): if abs(v - median) / median * 100 > pct: outliers.append(str(i + 1)) print( f"{median:.2f} {lo:.2f} {hi:.2f} {cov_pct:.1f}% " f"{','.join(outliers) if outliers else '-'}" ) PYEOF } # Ping once, print " " # or "FAIL". ping_path() { local client="$1" dest_npub="$2" local out if ! out=$(docker exec "fips-${client}${FIPS_CI_NAME_SUFFIX:-}" \ ping -c "$PING_COUNT" -i 0.2 -w "$((PING_COUNT * 2))" \ -q "${dest_npub}.fips" 2>&1); then echo FAIL return fi # "min/avg/max/mdev = 0.123/0.456/0.789/0.012 ms" local rtt loss rtt=$(echo "$out" | awk -F' = ' '/min\/avg\/max\/mdev/ {print $2}' | awk '{print $1}') loss=$(echo "$out" | awk -F', ' '/packet loss/ {for (i=1;i<=NF;i++) if ($i ~ /packet loss/) print $i}' | awk '{print $1}') if [ -z "$rtt" ]; then echo FAIL return fi echo "${rtt//\// } ${loss:-N/A}" return 0 } # ── Path definitions ─────────────────────────────────────────────────────── # # Each entry is `