From 6429e99cd94b7f0e3cb784c6a6d06382e4f92423 Mon Sep 17 00:00:00 2001 From: Andy Dixon Date: Fri, 9 Oct 2026 22:03:30 +0100 Subject: [PATCH] Benchmark harness (python3-only); enable MemoryAccounting on pilens.service --- scripts/benchmark.sh | 89 ++++++++++++++++++++++++++++++++++++++++++ systemd/pilens.service | 1 + web/dist/.gitkeep | 0 3 files changed, 90 insertions(+) create mode 100755 scripts/benchmark.sh create mode 100644 web/dist/.gitkeep diff --git a/scripts/benchmark.sh b/scripts/benchmark.sh new file mode 100755 index 0000000..c2ebd8f --- /dev/null +++ b/scripts/benchmark.sh @@ -0,0 +1,89 @@ +#!/bin/bash +# PiLens benchmark. Drives the running appliance over its own API and measures +# capture throughput, loss, filter latency, dissection latency, export and import. +# Numbers are hardware-dependent; this prints exactly what it measured. +# +# ./benchmark.sh https://127.0.0.1:8443 admin <<<"$PASSWORD" +# (reads the admin password from stdin so it never appears in argv or history) +set -euo pipefail +BASE=${1:-https://127.0.0.1:8443} +USER=${2:-admin} +read -r PASS +J=$(mktemp); trap 'rm -f "$J" /tmp/pilens-bench.*' EXIT +CJ() { curl -sk -b "$J" -c "$J" -H 'X-Requested-With: pilens' -H "X-CSRF-Token: ${CSRF:-}" -H 'Content-Type: application/json' "$@"; } +now() { date +%s.%N; } +# arithmetic/formatting via python3 (no bc/numfmt dependency) +calc() { python3 -c "import sys;print(eval(sys.argv[1]))" "$1"; } +iec() { python3 -c " +import re +m=re.search(r'\\d+','$1' or '0'); n=float(m.group() if m else 0) +for u in ['B','KiB','MiB','GiB','TiB']: + if abs(n)<1024: print('%.1f %s'%(n,u)); break + n/=1024 +else: print('%.1f PiB'%n)"; } +CSRF=$(CJ -X POST "$BASE/api/auth/login" -d "{\"username\":\"$USER\",\"password\":\"$PASS\"}" | python3 -c 'import json,sys;print(json.load(sys.stdin)["csrf"])') +jq() { python3 -c "import json,sys;d=json.load(sys.stdin);print($1)"; } + +MB=${BENCH_MB:-300} +echo "# PiLens benchmark $(date -Is)" +echo "host: $(tr -d '\0' /dev/null) · $(nproc) cores · $(awk '/MemTotal/{printf "%.1f GiB", $2/1048576}' /proc/meminfo)" +MEM0=$(systemctl show -p MemoryCurrent --value pilens 2>/dev/null || echo 0) +echo "idle pilens RSS: $(iec ${MEM0:-0})" + +# ---- capture throughput on loopback under a bulk transfer ---- +head -c $((MB*1000000)) /dev/urandom >/tmp/pilens-bench.bin +python3 -m http.server 8998 --bind 127.0.0.1 --directory /tmp >/dev/null 2>&1 & SRV=$! +sleep 1 +SID=$(CJ -X POST "$BASE/api/captures" -d '{"name":"benchmark","interfaces":["lo"],"capture_filter":"tcp port 8998"}' | jq 'd["id"]') +t0=$(now) +for i in $(seq 1 ${BENCH_LOOPS:-4}); do curl -s -o /dev/null "http://127.0.0.1:8998/pilens-bench.bin" 2>/dev/null || true; done +t1=$(now) +sleep 2 +CJ -X POST "$BASE/api/captures/live/stop" >/dev/null +kill $SRV 2>/dev/null || true +S=$(CJ "$BASE/api/captures/$SID") +PK=$(echo "$S" | jq 'd["session"]["packets"]'); BY=$(echo "$S" | jq 'd["session"]["bytes"]') +DROPS=$(echo "$S" | jq 'd["session"].get("drops","")') +secs=$(calc "$t1 - $t0") +echo +echo "## capture (loopback, $MB MB x ${BENCH_LOOPS:-4} transfers)" +printf " packets: %s bytes: %s wall: %.1fs\n" "$PK" "$(iec $BY)" "$secs" +printf " rate: %.0f pkt/s %.0f MB/s\n" "$(calc "$PK/$secs")" "$(calc "$BY/$secs/1000000")" +echo " drops: $DROPS" +MEM1=$(systemctl show -p MemoryCurrent --value pilens); echo " pilens RSS after capture: $(iec $MEM1)" + +# ---- display filter latency over the whole capture ---- +echo +echo "## display filter latency (cold, tshark over all segments)" +for F in "tcp" "tcp.port==8998" "tcp.analysis.retransmission" "frame.len>1000"; do + t0=$(now); TOT=$(CJ "$BASE/api/captures/$SID/packets?filter=$(python3 -c "import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1]))" "$F")&limit=1" | jq 'd["total"]'); t1=$(now) + printf " %-32s %6s matches %.2fs\n" "$F" "$TOT" "$(calc "$t1-$t0")" +done + +# ---- packet dissection latency ---- +echo +echo "## packet detail (PDML dissection)" +MIDN=$(CJ "$BASE/api/captures/$SID/packets?offset=$((PK/2))&limit=1" | jq 'd["rows"][0]["n"]') +t0=$(now); CJ "$BASE/api/captures/$SID/packets/$MIDN" >/dev/null; t1=$(now) +printf " single packet #%s: %.3fs\n" "$MIDN" "$(calc "$t1-$t0")" + +# ---- export ---- +echo +echo "## export (verbatim copy of original packets)" +t0=$(now); CJ -o /tmp/pilens-bench.out "$BASE/api/captures/$SID/export"; t1=$(now) +SZ=$(stat -c%s /tmp/pilens-bench.out); printf " %s in %.2fs (%.0f MB/s)\n" "$(iec $SZ)" "$(calc "$t1-$t0")" "$(calc "$SZ/($t1-$t0)/1000000")" + +# ---- import + index ---- +echo +echo "## import + full index of that file" +t0=$(now); ISID=$(CJ -H 'Content-Type: application/octet-stream' --data-binary @/tmp/pilens-bench.out "$BASE/api/captures/import?name=bench-import" | jq 'd["id"]') +while [ "$(CJ "$BASE/api/captures/$ISID" | jq 'd["session"]["state"]')" = importing ]; do sleep 0.3; done +t1=$(now) +IPK=$(CJ "$BASE/api/captures/$ISID" | jq 'd["session"]["packets"]') +printf " indexed %s packets in %.1fs (%.0f pkt/s)\n" "$IPK" "$(calc "$t1-$t0")" "$(calc "$IPK/($t1-$t0)")" + +echo +echo "cleanup: removing benchmark sessions" +CJ -X DELETE "$BASE/api/captures/$SID?confirm=$SID" >/dev/null +CJ -X DELETE "$BASE/api/captures/$ISID?confirm=$ISID" >/dev/null +echo "done" diff --git a/systemd/pilens.service b/systemd/pilens.service index f9099a8..9d8edf7 100644 --- a/systemd/pilens.service +++ b/systemd/pilens.service @@ -15,6 +15,7 @@ ExecStart=/opt/pilens/bin/pilens serve -data /var/lib/pilens -listen :8443 Restart=on-failure RestartSec=3 WatchdogSec=60 +MemoryAccounting=yes # Shutdown stops any running capture and hashes its last segment. TimeoutStopSec=60 KillSignal=SIGTERM diff --git a/web/dist/.gitkeep b/web/dist/.gitkeep new file mode 100644 index 0000000..e69de29