#!/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 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