#!/usr/bin/env bash
# pibench-sweep.sh — sweep packets-per-second across the three capture backends over the direct
# ethernet link and emit an averaged CSV for plotting.
#
# Runs FROM the workstation: floods the Pi with UDP via iperf3 (the Pi runs a persistent
# `iperf3 -s`), while the Pi captures on its NIC with `pibench capture`. For each frame size it
# records what the capture side actually saw — measured pps, throughput, per-frame CPU, drops.
#
# WHY SWEEP FRAME SIZE (not offered bitrate): on a fixed 100 Mbit link the achievable pps is set
# by line rate, so a 64-byte frame saturates at ~96 kpps no matter how hard you offer — extra
# bitrate or extra iperf streams can't exceed it. The lever for *more* pps is *smaller* frames.
# So we hold the link saturated and step the frame size: big frames -> low pps, tiny frames ->
# high pps (~8 kpps at 1472 B up to ~145 kpps near the 64 B minimum).
#
# Parallel streams (-P) are used to *reach* saturation reliably — a single sender thread can't
# always fill the link with very small frames (it goes pps-bound before the wire does).
#
# Capture is started BEFORE the flood: binding the AF_PACKET socket under an in-flight flood
# causes a one-time burst of kernel ring drops (a setup artifact, not the backend falling behind).
#
# Usage:
#   ./pibench-sweep.sh
#   SIZES="1472 256 64 18" REPS=5 STREAMS=8 SECS=8 ./pibench-sweep.sh
#   PI=johns@192.168.1.147 PI_TARGET=10.10.0.2 IFACE=eth0 ./pibench-sweep.sh
set -uo pipefail

PI=${PI:-johns@192.168.1.147}        # ssh target (management path, over wifi)
PI_TARGET=${PI_TARGET:-10.10.0.2}    # iperf3 destination (direct ethernet link)
IFACE=${IFACE:-eth0}                 # Pi NIC to capture on
PIBENCH=${PIBENCH:-\~/pibench}       # pibench path on the Pi
SIZES=${SIZES:-"1472 512 256 128 64 32 18"}  # UDP payload bytes, big->small == low->high pps
STREAMS=${STREAMS:-4}                # parallel iperf streams, to saturate the link
BMBIT=${BMBIT:-60}                   # per-stream offered Mbit/s (× STREAMS must exceed link)
SECS=${SECS:-6}                      # capture window per rep
REPS=${REPS:-3}                      # reps per (size, backend); reported values are the mean
BACKENDS=${BACKENDS:-"ring basic pnet"}
OUT=${OUT:-pibench-sweep.csv}

ssh_pi() { ssh -o BatchMode=yes "$PI" "$@"; }

# One rep: capture-first, flood covering the window, print "pps mbps proc usf drops" ("drops"=NA for pnet).
# A backend spec may carry a ":verify" suffix (e.g. "ring:verify") to add --verify (etherparse
# parse/classify per frame) — used to measure etherparse overhead as the verify-minus-plain delta.
one_rep() { # $1=backend-spec  $2=len
  local spec="$1" len="$2" be vflag="" tmp line
  be="${spec%%:*}"
  [ "$spec" = "${be}:verify" ] && vflag="--verify"
  tmp=$(mktemp)
  ssh_pi "sudo $PIBENCH capture --backend $be --iface $IFACE --secs $SECS $vflag" >"$tmp" 2>/dev/null &
  local cap=$!
  sleep 2                                            # let the socket bind on an idle wire
  iperf3 -u -b "${BMBIT}M" -l "$len" -P "$STREAMS" -t $((SECS + 4)) -c "$PI_TARGET" >/dev/null 2>&1 &
  local ipf=$!
  wait "$cap" 2>/dev/null || true
  line=$(cat "$tmp"); rm -f "$tmp"
  wait "$ipf" 2>/dev/null || true
  sleep 1
  local pps mbps proc usf drops
  pps=$(grep -oP 'pps=\K[0-9]+'           <<<"$line"); mbps=$(grep -oP 'Mb/s=\K[0-9.]+'        <<<"$line")
  proc=$(grep -oP 'proc_cpu=\K[0-9.]+'    <<<"$line"); usf=$(grep -oP 'cpu_us/frame=\K[0-9.]+' <<<"$line")
  drops=$(grep -oP 'drops=\K([0-9]+|n/a)' <<<"$line")
  [ "$drops" = "n/a" ] && drops=NA          # normalize: backends with no drop counter -> NA
  echo "${pps:-} ${mbps:-} ${proc:-} ${usf:-} ${drops:-NA}"
}

echo "backend,len,streams,reps,pps,mbps,proc_cpu_pct,cpu_us_frame,drops" > "$OUT"
echo "sweep: sizes=[$SIZES] backends=[$BACKENDS] streams=$STREAMS reps=$REPS secs=${SECS}s -> $OUT" >&2

for len in $SIZES; do
  for be in $BACKENDS; do
    rows=""
    for ((r = 1; r <= REPS; r++)); do
      rows+="$(one_rep "$be" "$len")"$'\n'
    done
    # Mean each numeric column across reps (fields: pps mbps proc usf drops).
    # drops averages only reps that reported a number (pnet has none -> NA).
    means=$(printf '%s' "$rows" | awk '
      NF >= 4 { for (i=1;i<=4;i++){ s[i]+=$i; n[i]++ }
                if ($5 != "NA") { s[5]+=$5; n[5]++ } }
      END {
        printf "%.0f,%.1f,%.2f,%.3f,", s[1]/n[1], s[2]/n[2], s[3]/n[3], s[4]/n[4]
        if (n[5]) printf "%.0f", s[5]/n[5]; else printf "NA"
      }')
    echo "${be/:/_},$len,$STREAMS,$REPS,$means" | tee -a "$OUT" >&2   # ring:verify -> ring_verify
  done
done
echo "done -> $OUT" >&2
