Files
vfio-native/scripts/verify-perf.sh

110 lines
5.8 KiB
Bash
Executable File

#!/bin/bash
# Check that a corrected guest is actually performing as it should.
#
# @@SELFNAME@@ [user@ip] [ssh-key]
#
# Copies the benchmark in if it is missing, runs it, and grades the numbers that
# matter against the thresholds a correctly configured guest meets. Read-only:
# it changes nothing on the host or in the guest.
set -uo pipefail
GUEST="${1:-User@192.168.122.222}"
KEY="${2:-}"
[ -n "$KEY" ] || for k in "$HOME/.ssh/win11-native" "$HOME/.ssh/win11"; do [ -f "$k" ] && { KEY="$k"; break; }; done
# Locate the project data (patches, benchmark sources), whether running from a
# checkout or installed as a package.
for d in "$(cd "$(dirname "$0")/.." 2>/dev/null && pwd)" /usr/share/vfio-native; do
if [ -d "$d/bench" ] || [ -d "$d/patches" ]; then SELF="$d"; break; fi
done
SELF="${SELF:-$(cd "$(dirname "$0")/.." && pwd)}"
BENCH="$SELF/bench/vmbench.exe"
[ -w "$SELF/bench" ] 2>/dev/null || BENCH="${XDG_CACHE_HOME:-$HOME/.cache}/vfio-native/vmbench.exe"
SSH=(ssh -i "$KEY" -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=10)
command -v ssh >/dev/null || { echo "ssh not found"; exit 1; }
[ -n "$KEY" ] && [ -f "$KEY" ] || { echo "no ssh key for the guest - pass it: $(basename "$0") user@guest-ip ~/.ssh/key"; exit 1; }
if [ ! -f "$BENCH" ]; then
echo "building vmbench.exe..."
command -v x86_64-w64-mingw32-gcc >/dev/null || { echo "need mingw-w64-gcc to build it"; exit 1; }
mkdir -p "$(dirname "$BENCH")"
x86_64-w64-mingw32-gcc -O2 -o "$BENCH" "$SELF/bench/vmbench.c" || exit 1
fi
"${SSH[@]}" "$GUEST" 'exit' 2>/dev/null || { echo "cannot reach $GUEST over ssh"; exit 1; }
scp -q -i "$KEY" -o BatchMode=yes -o StrictHostKeyChecking=no \
"$BENCH" "$GUEST:C:/Users/User/vmbench.exe" 2>/dev/null
# A guest that has just booted is still indexing, patching and starting services,
# and that shows up entirely in the stall counts. Let it settle first.
SETTLE="${SETTLE:-60}"
if [ "$SETTLE" -gt 0 ]; then
echo "letting the guest settle for ${SETTLE}s (SETTLE=0 to skip)..."
sleep "$SETTLE"
fi
echo "running benchmark in the guest, this takes a few minutes..."
OUT=$("${SSH[@]}" "$GUEST" 'C:\Users\User\vmbench.exe all' 2>/dev/null)
[ -n "$OUT" ] || { echo "benchmark produced no output"; exit 1; }
echo "$OUT"
echo
val() { echo "$OUT" | sed -n "s/.*$1=\([0-9.]*\).*/\1/p" | head -1; }
pass=0; fail=0
check() { # name value op limit explanation
local n="$1" v="$2" op="$3" lim="$4" why="$5" ok
if [ -z "$v" ]; then printf ' ?? %-22s (not measured)\n' "$n"; return; fi
ok=$(awk -v a="$v" -v b="$lim" "BEGIN{print (a $op b)?1:0}")
if [ "$ok" = "1" ]; then printf ' OK %-22s %-10s\n' "$n" "$v"; pass=$((pass+1))
else printf ' ?? %-22s %-10s <- %s\n' "$n" "$v" "$why"; fail=$((fail+1)); fi
}
if [ -f /sys/module/kvm_amd/parameters/cpuid_passthrough ]; then
echo "kvm_amd cpuid_passthrough=$(cat /sys/module/kvm_amd/parameters/cpuid_passthrough) brand='$(cat /sys/module/kvm_amd/parameters/brand_string)'"
fi
# A guest can silently run on 4 KiB pages. With one qemu process on the host its
# AnonHugePages should be close to the guest's RAM; with several, name the domain.
pids=$(pgrep -f "[q]emu-system-x86_64" | tr '\n' ' ')
[ -n "${VMDOMAIN:-}" ] && pids=$(pgrep -f "[q]emu-system-x86_64.*guest=$VMDOMAIN" | tr '\n' ' ')
if [ "$(echo $pids | wc -w)" = 1 ] && thp=$(sudo -n awk '/AnonHugePages/ {s+=$2} END {print int(s/1024)}' /proc/$pids/smaps 2>/dev/null); then
ram=$(awk '/VmRSS/ {print int($2/1024)}' /proc/$pids/status 2>/dev/null)
echo "hugepages: ${thp} MiB of ${ram} MiB resident are transparent hugepages$([ "${thp:-0}" -lt $(( ${ram:-1} / 2 )) ] && echo ' <- LOW: check memoryBacking and /sys/kernel/mm/transparent_hugepage/enabled')"
elif [ -n "$pids" ]; then
# another user's smaps needs root; the host-wide counter does not, and with one guest it is close enough
echo "hugepages: host AnonHugePages $(awk '/AnonHugePages/ {print int($2/1024)}' /proc/meminfo) MiB across $(echo $pids | wc -w) guest(s) (sudo for a per-guest figure; THP $(cat /sys/kernel/mm/transparent_hugepage/enabled 2>/dev/null | grep -o '\[.*\]'))"
fi
echo
echo "results:"
check "QPC cost (ns)" "$(val qpc_ns)" "<" 50 "should be ~15. Over ~1000 means Windows lost the boot TSC race - leave the host 4+ free cores, then reboot to re-measure"
check "rdtsc cost (ns)" "$(val rdtsc_ns)" "<" 20 "unexpectedly slow TSC read"
check "1-thread (Mops)" "$(val 'threads=1 aggregate_Mops')" ">" 5000 "single-thread throughput low - check the host governor is 'performance'"
check "L3 latency (ns)" "$(echo "$OUT" | sed -n 's/memlat L3_8M ns=\([0-9.]*\)/\1/p')" "<" 13 "high L3 latency suggests the vCPUs are spread across both CCDs"
check "jitter p99.99 (us)" "$(val 'p99.99')" "<" 20 "scheduling tail is long - check <cputune> pinning and that the emulator is off the vCPU cores"
check "stalls >100us" "$(echo "$OUT" | sed -n 's/.*over_100us=\([0-9]*\).*/\1/p')" "<" 20 "frequent long stalls. If the guest booted recently, let it idle and re-run"
echo
if [ "$fail" -eq 0 ]; then
echo "All $pass checks in range - the guest is configured correctly."
else
echo "$pass in range, $fail out of range. See the notes above each one."
fi
cat <<'EOF'
For reference, a correctly configured guest on a 7950X measures roughly:
QPC cost ~15 ns (a 32-vCPU guest measures ~1300 ns instead)
rdtsc cost ~7 ns
1 thread ~5300 Mops
L1 / L2 / L3 ~0.8 / ~3.4 / ~10 ns
DRAM ~80-90 ns
memory read ~50 GB/s
jitter p99.99 ~3 us
stalls >100us 0-2 over a 10 second run
The two that matter most are QPC cost and the jitter tail. Software that polls the
clock in a tight loop calls QPC thousands of times a second, and the jitter tail is
what shows up as hitching in an interactive session. Mean throughput shows neither.
EOF