#!/usr/bin/env bash # # basic-benchmark / run-benchmarks.sh # Run the SAME benchmark set before and after the CPU/platform upgrade. # # The suite is split in two parts so it can be driven from another device: # # ./run-benchmarks.sh before headless # no display needed -> run over SSH # ./run-benchmarks.sh before headed # needs the KDE session -> run locally # ./run-benchmarks.sh before # legacy: headless + headed (if display) # # The part is appended to the tag so the two parts never overwrite each other: # # before headless -> results/{power,phases}-before-headless.csv # before headed -> results/{power,phases}-before-headed.csv # # Compare: # python3 power-report.py --tags before-headless after-headless # python3 power-report.py --tags before-headless before-headed after-headless after-headed # # Primary cross-platform score: PassMark PerformanceTest (cpubenchmark.net). # GPU tests use glmark2/vkmark. # # Env options: # INSTALL_DEPS=1 auto-install missing tools via sudo dnf (fresh Fedora) # default is 'ask' (prompts); 0 disables # RUN_GPU=0 skip the headed GPU tests in 'all' mode # TOOLS=/path where PassMark lives (default ./tools) # FIOFILE=/path fio scratch file (must be on the target filesystem) # FIO_SIZE=1G fio file size # FIO_RUNTIME=30 seconds per fio job # GPU_PCI=0000:03:00.0 pin GPU tests to this PCI device (auto-detected) # GPU_VD=0x1002:0x73df vendor:device hex for Vulkan selection (auto-detected) # LLAMA_MODEL=/path GGUF for the LLM test (default: bundled model) # LLAMA_NGL=0 GPU layers to offload (0 = CPU/RAM, the default) # RUN_APPS=1 app workloads: LibreOffice, GEGL, Inkscape # CORPUS=/path where the sample inputs are cached (default ./tools/corpus) # CORPUS_URL=... base URL the sample corpora are fetched from # INKSCAPE_FILES=25 how many SVGs the Inkscape step exports # GOVERNOR=powersave CPU governor to pin (default performance); an explicit # value is appended to the tag, e.g. after-powersave-headless # GLMARK2_DURATION=10 seconds for the glmark2 scene (headed part) # VKMARK_DURATION=10 seconds for the vkmark scene (headed part) # VKMARK_BENCH=texture vkmark scene to run (e.g. texture, shading, vertex) # # Run the headless part with a TTY so sudo can prompt for power logging: # ssh -t '~/basic-benchmark/run-benchmarks.sh before headless' set -u # User-space tools (e.g. a source-built tool) live here; llama.cpp ships prebuilt. export PATH="$HOME/.local/bin:$PATH" # PassMark is a curses app; it needs a real terminal type even when piped. 'dumb' # and unset both fail ("cannot initialize terminal type"), so force xterm then. case "${TERM:-}" in ''|dumb|unknown) export TERM=xterm ;; esac usage() { printf 'usage: %s [headless|headed|all]\n' "$(basename "$0")" >&2 printf ' e.g. %s before headless # over SSH, no display needed\n' "$(basename "$0")" >&2 printf ' %s before headed # at the machine, in a graphical session\n' "$(basename "$0")" >&2 printf ' %s before # both parts (all)\n' "$(basename "$0")" >&2 printf ' %s doctor # check readiness without benchmarking\n' "$(basename "$0")" >&2 } TAG_BASE="${1:-}" PART="${2:-all}" DOCTOR=0 [[ "$TAG_BASE" == "doctor" || "$TAG_BASE" == "--check" ]] && DOCTOR=1 if [[ "$DOCTOR" == "0" ]]; then if [[ -z "$TAG_BASE" || "$TAG_BASE" == "-h" || "$TAG_BASE" == "--help" ]]; then usage [[ "$TAG_BASE" == "-h" || "$TAG_BASE" == "--help" ]] && exit 0 exit 1 fi case "$PART" in headless|headed|all) ;; *) printf 'bad part: %s\n' "$PART" >&2; usage; exit 1 ;; esac fi DIR="$(cd "$(dirname "$0")" && pwd)" RES="$DIR/results" mkdir -p "$RES" cd "$RES" || exit 1 # CPU governor to pin for the timed runs (the original is restored on exit). # Set GOVERNOR=... to choose one; an explicit choice is appended to the tag so # several profiles never overwrite each other: # GOVERNOR=powersave ./run-benchmarks.sh after headless -> after-powersave-headless # GOVERNOR=performance ./run-benchmarks.sh after headless -> after-performance-headless # ./run-benchmarks.sh after headless -> after-headless (performance) GOVERNOR_SET=0 [[ -n "${GOVERNOR:-}" ]] && GOVERNOR_SET=1 GOVERNOR="${GOVERNOR:-performance}" TAG="$TAG_BASE" [[ "$GOVERNOR_SET" == "1" ]] && TAG="$TAG-$GOVERNOR" [[ "$PART" != "all" ]] && TAG="$TAG-$PART" RUN_GPU="${RUN_GPU:-1}" TOOLS="${TOOLS:-$DIR/tools}" if [[ -z "${PT:-}" ]]; then for cand in "$TOOLS/pt/pt_linux_x64" \ "$TOOLS/pt/PerformanceTest/PerformanceTest_Linux_x86-64"; do [[ -x "$cand" ]] && { PT="$cand"; break; } done fi PT="${PT:-$TOOLS/pt/pt_linux_x64}" # Bundled LLM bench (self-contained under tools/llama/): token generation on CPU/RAM. LLAMA_DIR="$TOOLS/llama" LLAMA_BIN="$LLAMA_DIR/bin/llama-bench" LLAMA_MODEL="${LLAMA_MODEL:-$LLAMA_DIR/models/MiniCPM5-2B-Q8_0.gguf}" LLAMA_NGL="${LLAMA_NGL:-0}" # 0 = CPU/RAM only (no GPU offload) LLAMA_TG="${LLAMA_TG:-128}" # generated tokens LLAMA_REPS="${LLAMA_REPS:-2}" # App workloads: stock distro applications run over a # fixed sample corpus, fetched once and checksum-pinned, then cached under # CORPUS and reused by every run so both sides process identical input bytes. RUN_APPS="${RUN_APPS:-1}" CORPUS="${CORPUS:-$TOOLS/corpus}" CORPUS_URL="${CORPUS_URL:-http://phoronix-test-suite.com/benchmark-files}" # sample-file CDN SAMPLER="$DIR/powerlog.py" REPORT="$DIR/power-report.py" PHASES="$RES/phases-$TAG.csv" FIOFILE="${FIOFILE:-$HOME/.cache/basic-benchmark-fio.bin}" FIO_SIZE="${FIO_SIZE:-1G}" FIO_RUNTIME="${FIO_RUNTIME:-30}" section() { printf '\n\033[1m=== %s ===\033[0m\n' "$*"; } have() { command -v "$1" >/dev/null 2>&1; } phase() { printf '%s,%s\n' "$(date +%s)" "$1" >> "$PHASES"; } # --- dependency provisioning (Fedora / dnf) ----------------------------------- # The kit targets a fresh Fedora 44 with internet. Map each benchmark tool to the # Fedora package that provides it; on request, install what's missing. Tools that # are not packaged (or already bundled) are listed separately. PKG_FOR_TOOL_CMD="7z:7zip openssl:openssl fio:fio stress-ng:stress-ng \ sensors:lm_sensors turbostat:kernel-tools cpupower:cpupowerutils \ glmark2:glmark2 vkmark:vkmark glxinfo:mesa-demos vulkaninfo:vulkan-tools \ sysbench:sysbench" # App workloads map to their own packages; included in the missing/pkg checks. PKG_FOR_APP_CMD="libreoffice:libreoffice-writer inkscape:inkscape gegl:gegl04-tools" missing_tools() { # prints tool names whose command is absent local pair cmd for pair in $PKG_FOR_TOOL_CMD; do cmd="${pair%%:*}" have "$cmd" || printf '%s\n' "$cmd" done } missing_apps() { local pair cmd for pair in $PKG_FOR_APP_CMD; do cmd="${pair%%:*}" have "$cmd" || printf '%s\n' "$cmd" done } pkgs_for_missing() { # prints the packages needed for the missing commands local pkgs="" pair cmd for pair in $PKG_FOR_TOOL_CMD $PKG_FOR_APP_CMD; do cmd="${pair%%:*}"; pkg="${pair#*:}" have "$cmd" || pkgs="$pkgs $pkg" done python3 -c 'import matplotlib' >/dev/null 2>&1 || pkgs="$pkgs python3-matplotlib" # PassMark needs the ncurses compat libs (rpm check, these aren't commands) if [ -x "$PT" ]; then rpm -q ncurses-libs >/dev/null 2>&1 || pkgs="$pkgs ncurses-libs" rpm -q ncurses-compat-libs >/dev/null 2>&1 || pkgs="$pkgs ncurses-compat-libs" fi printf '%s' "$pkgs" } install_deps() { # INSTALL_DEPS=1 (or 'ask') local mode="${INSTALL_DEPS:-ask}" pkgs pkgs="$(pkgs_for_missing)" if [ -z "${pkgs// /}" ]; then printf 'all dnf-provided tools present\n'; return 0; fi printf 'missing (dnf):%s\n' "$pkgs" case "$mode" in 0|no|off) printf 'INSTALL_DEPS=%s -> not installing; some tests will skip\n' "$mode"; return 0 ;; ask) if [ -t 0 ]; then printf 'install now with sudo dnf? [y/N] ' read -r ans; case "$ans" in [yY]*) ;; *) printf 'skipped\n'; return 0 ;; esac else printf 'non-interactive; set INSTALL_DEPS=1 to auto-install, else tests skip\n'; return 0 fi ;; esac if ! have dnf; then printf 'dnf not found (non-Fedora?); install manually\n'; return 0; fi if ! sudo -n true 2>/dev/null; then printf 'need sudo for dnf; run in a terminal (ssh -t)\n'; return 0 fi section "installing missing tools (dnf)" # shellcheck disable=SC2086 sudo dnf install -y $pkgs || printf 'dnf install failed; tests may skip\n' } # Idle baseline: let the machine settle and log >=15 s of true idle so the # report has a real idle window (the 'env' phase alone was only ~2 s). run_idle() { local secs="${IDLE_SECONDS:-15}" section "IDLE baseline (${secs}s, CPU: $CPU_MODEL)" phase "idle" # settle: give the governor/load from startup time to calm down sleep 3 # keep the phase open for at least $secs of sampling sleep "$secs" } # Model string for titles (env capture, graphs, stdout headers). cpu_model() { local m="" m="$(sed -n 's/^model name[[:space:]]*:[[:space:]]*//p' /proc/cpuinfo | head -1)" [ -z "$m" ] && m="$(lscpu 2>/dev/null | sed -n 's/^Model name:[[:space:]]*//p' | head -1)" [ -z "$m" ] && m="$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_driver 2>/dev/null)" printf '%s' "${m:-unknown CPU}" } CPU_MODEL="$(cpu_model)" # Register one score (phase,value) into scores-.csv for the efficiency table. reg_score() { # $1 = phase name (must match a phase), $2 = numeric score [ -n "${2:-}" ] || return 0 printf '%s,%s\n' "$1" "$2" >> "scores-$TAG.csv" } # 7-Zip "Avr:" row = "Avr: | "; the # compressing MIPS is the Rating, field 5 (the "Tot:" row's first field is usage %). 7z_mips() { awk '/^Avr:/{print $5; exit}' "$1" 2>/dev/null; } # sysbench prints "events per second: ". sysbench_eps() { awk -F: '/events per second/{gsub(/ /,"",$2); print $2; exit}' "$1" 2>/dev/null; } # sysbench memory prints "... MiB transferred ( MiB/sec)". sysbench_mbs() { sed -n 's/.*(\([0-9.]*\) MiB\/sec).*/\1/p' "$1" 2>/dev/null | head -1; } # openssl speed ends each algorithm with a "...k" row; take its last (largest) value. openssl_gbs() { awk 'NF && $NF ~ /k$/ {gsub(/k$/,"",$NF); v=$NF} END{ if (v!="") printf "%.3f", v/1000000 }' "$1" 2>/dev/null; } # fio JSON: read one number from the first job. fio_json() { python3 -c 'import json,sys; j=json.load(open(sys.argv[1]))["jobs"][0]; print((j.get(sys.argv[2]) or {}).get(sys.argv[3]) or "")' "$1" "$2" "$3" 2>/dev/null; } fio_reg() { # $1=job $2=read|write $3=bw_bytes|iops local v; v="$(fio_json "fio-$1-$TAG.json" "$2" "$3")" [ -n "$v" ] || return 0 if [ "$3" = "bw_bytes" ]; then reg_score "fio-$1" "$(awk -v v="$v" 'BEGIN{printf "%.1f", v/1000000}')" else reg_score "fio-$1" "$(awk -v v="$v" 'BEGIN{printf "%.0f", v}')" fi } # glmark2/vkmark print a final " Score: ". gpu_score() { awk -v tool="$1" 'index($0, tool " Score:"){print $NF; exit}' "$2" 2>/dev/null; } # PassMark's .txt is a curses screen capture (unreadable); the numbers live in # a results_*.yml file. Write scores-.csv so power-report.py can compute # score/W efficiency. Each PassMark suite is its own phase (PassMark-cpu / # PassMark-mem) so the power window matched to a score is the suite that made it. # The single-thread mark rides in the CPU suite and shares its window. parse_passmark() { # $1 = yml, $2 = phase name, $3 = suite ("cpu"|"mem") local yml="$1" ph="$2" suite="$3" [ -f "$yml" ] || return 0 local out="scores-$TAG.csv" awk -v out="$out" -v ph="$ph" -v suite="$suite" ' /SUMM_CPU:/ { cpu = $2 } /SUMM_ME:/ { mem = $2 } /CPU_SINGLETHREAD:/{ st = $2 } END { if (suite == "cpu" && cpu != "") { print ph "," cpu > out if (st != "") print ph "-single," st >> out } if (suite == "mem" && mem != "") print ph "," mem >> out }' "$yml" } # --- app workloads: fixed corpora + timing ------------------------------------ # Each app benchmark runs the stock command-line tool over a fixed input set and # reports a rate (items/s, higher is better) so the score/W table stays meaningful. fetch_corpus() { # $1=file $2=url $3=sha256 -> ensures $CORPUS/ is present local f="$CORPUS/$1" sha if [ -s "$f" ]; then sha="$(sha256sum "$f" 2>/dev/null | cut -d' ' -f1)" [ "$sha" = "$3" ] && return 0 fi mkdir -p "$CORPUS" if have curl; then curl -fsSL --retry 2 -o "$f.part" "$2" || { rm -f "$f.part"; return 1; } elif have wget; then wget -q -O "$f.part" "$2" || { rm -f "$f.part"; return 1; } else printf 'need curl or wget to fetch %s\n' "$1" >&2; return 1 fi mv -f "$f.part" "$f" sha="$(sha256sum "$f" | cut -d' ' -f1)" if [ "$sha" != "$3" ]; then printf 'checksum mismatch for %s (got %s)\n' "$1" "$sha" >&2 rm -f "$f"; return 1 fi printf 'fetched %s -> %s\n' "$1" "$f" } extract_zip() { # $1=zip $2=destdir mkdir -p "$2" if have unzip; then unzip -oq "$1" -d "$2" elif have 7z; then 7z x -y -o"$2" "$1" >/dev/null else printf 'need unzip or 7z to extract %s\n' "$1" >&2; return 1 fi } setup_corpus() { # $1=zip $2=url $3=sha256 $4=subdir -> extracted once under $CORPUS/$4 local d="$CORPUS/$4" fetch_corpus "$1" "$2" "$3" || return 1 if [ -z "$(ls -A "$d" 2>/dev/null)" ]; then extract_zip "$CORPUS/$1" "$d" || return 1 fi } now() { date +%s.%N; } secs() { awk -v a="$1" -v b="$2" 'BEGIN{printf "%.3f", b-a}'; } per_s() { awk -v n="$1" -v s="$2" 'BEGIN{ if (s>0) printf "%.3f", n/s }'; } # --- sudo: acquire once, then keep the timestamp alive for the whole run ------ SUDO="" KEEPID="" acquire_sudo() { if sudo -n true 2>/dev/null; then SUDO="sudo"; return 0; fi if [[ -t 0 || -t 1 ]]; then if sudo -v 2>/dev/null; then SUDO="sudo"; return 0; fi fi SUDO="" } start_sudo_keepalive() { [[ -n "$SUDO" ]] || return 0 ( while :; do sudo -n true 2>/dev/null || exit 0; sleep 50; done ) & KEEPID=$! } # --- CPU governor: pin and restore ------------------------------------------- GOV_ORIG="" pin_governor() { [[ -n "$SUDO" ]] || { printf 'governor not pinned (no sudo)\n'; return 0; } GOV_ORIG="$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null || true)" section "pin CPU governor: ${GOVERNOR} (was ${GOV_ORIG:-unknown})" $SUDO cpupower frequency-set -g "${GOVERNOR}" >/dev/null 2>&1 || \ for f in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do printf '%s' "${GOVERNOR}" | $SUDO tee "$f" >/dev/null 2>&1 done } restore_governor() { [[ -n "$SUDO" && -n "$GOV_ORIG" && "$GOV_ORIG" != "$GOVERNOR" ]] || return 0 $SUDO cpupower frequency-set -g "$GOV_ORIG" >/dev/null 2>&1 || \ for f in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do printf '%s' "$GOV_ORIG" | $SUDO tee "$f" >/dev/null 2>&1 done printf 'governor restored to %s\n' "$GOV_ORIG" } # --- discrete GPU detection & pinning (for the headed GL/Vulkan tests) -------- # Picks the GPU that owns no connected display connector (the headless dGPU on # this box: the RX 6700 XT, while both monitors hang off the Cezanne iGPU). detect_dgpu() { local c pci conn v d best_pci="" best_vd="" for c in /sys/class/drm/card[0-9]*; do [[ "$(basename "$c")" =~ ^card[0-9]+$ ]] || continue [[ -r "$c/device/vendor" && -r "$c/device/device" ]] || continue conn=0 for s in "$c"/card*-*/status; do [[ -e "$s" ]] || continue [[ "$(cat "$s" 2>/dev/null)" == "connected" ]] && conn=$((conn + 1)) done v="$(cat "$c/device/vendor" 2>/dev/null)" d="$(cat "$c/device/device" 2>/dev/null)" pci="$(basename "$(readlink -f "$c/device")")" if [[ "$conn" -eq 0 ]]; then best_pci="$pci"; best_vd="$v:$d"; break fi [[ -z "$best_pci" ]] && { best_pci="$pci"; best_vd="$v:$d"; } done GPU_PCI="${GPU_PCI:-$best_pci}" GPU_VD="${GPU_VD:-$best_vd}" } pin_gpu_env() { detect_dgpu if [[ -n "${GPU_PCI:-}" ]]; then export DRI_PRIME="pci-$GPU_PCI"; fi if [[ -n "${GPU_VD:-}" ]]; then local vd="${GPU_VD//0x/}" export MESA_VK_DEVICE_SELECT="$vd" export VK_LOADER_DEVICE_SELECT="$vd" fi printf 'GPU pinning: DRI_PRIME=%s MESA_VK_DEVICE_SELECT=%s\n' \ "${DRI_PRIME:-}" "${MESA_VK_DEVICE_SELECT:-}" } # --- readiness check (no benchmarks run) ------------------------------------- doctor() { local problems=() warnings=() section "basic-benchmark doctor ($(hostname))" printf 'kit: %s\n' "$DIR" printf 'user: %s\n' "$(id -un)" printf 'TERM: %s\n' "${TERM:-}" printf 'display: %s\n' "${WAYLAND_DISPLAY:-}${DISPLAY:-}" if sudo -n true 2>/dev/null; then printf 'sudo: cached (ok)\n' else printf 'sudo: will prompt for a password\n' warnings+=("run the headless part as 'ssh -t ...' so sudo can prompt") fi detect_dgpu printf 'dGPU: pci=%s vd=%s\n' "${GPU_PCI:-}" "${GPU_VD:-}" printf 'RAPL: %s\n' "$(ls -d /sys/class/powercap/intel-rapl:* 2>/dev/null | tr '\n' ' ')" printf 'governor: %s (pinned for timed runs; set GOVERNOR=... to choose)\n' "$GOVERNOR" printf 'corpus: %s\n' "$CORPUS" # --- dnf-provisioned tools and apps --- printf '\ntools (dnf-provisioned):\n' local pair cmd pkg missing_pkgs="" for pair in $PKG_FOR_TOOL_CMD; do cmd="${pair%%:*}"; pkg="${pair#*:}" if have "$cmd"; then printf ' %-14s ok\n' "$cmd" else printf ' %-14s MISSING -> %s\n' "$cmd" "$pkg"; missing_pkgs="$missing_pkgs $pkg"; fi done printf 'apps (dnf-provisioned):\n' for pair in $PKG_FOR_APP_CMD; do cmd="${pair%%:*}"; pkg="${pair#*:}" if have "$cmd"; then printf ' %-14s ok\n' "$cmd" else printf ' %-14s MISSING -> %s\n' "$cmd" "$pkg"; missing_pkgs="$missing_pkgs $pkg"; fi done # --- bundled binaries / data (not dnf) --- printf 'bundled:\n' if [ -x "$PT" ]; then printf ' %-14s ok (%s)\n' PassMark "$PT" else printf ' %-14s MISSING -> see README section 4\n' PassMark; problems+=("PassMark binary not found at $PT"); fi if [ -x "$LLAMA_BIN" ]; then printf ' %-14s ok\n' llama-bench else printf ' %-14s MISSING -> see README section 4\n' llama-bench; problems+=("llama-bench not found at $LLAMA_BIN"); fi if [ -f "$LLAMA_MODEL" ]; then printf ' %-14s ok (%s)\n' model "$(du -h "$LLAMA_MODEL" 2>/dev/null | cut -f1)" else printf ' %-14s MISSING -> see README section 4\n' model; problems+=("GGUF model not found at $LLAMA_MODEL"); fi if have python3 && python3 -c 'import matplotlib' >/dev/null 2>&1; then printf ' %-14s ok\n' matplotlib else printf ' %-14s MISSING -> python3-matplotlib (graphs)\n' matplotlib; missing_pkgs="$missing_pkgs python3-matplotlib"; fi # --- verdict --- if [ -z "${missing_pkgs// /}" ]; then printf '\ndnf: nothing missing\n' else printf '\ndnf: needed:%s\n' "$missing_pkgs" printf ' install: sudo dnf install -y%s\n' "$missing_pkgs" printf ' (or auto-provision: INSTALL_DEPS=1 ./run-benchmarks.sh headless)\n' fi if [ ${#problems[@]} -eq 0 ] && [ -z "${missing_pkgs// /}" ] && [ ${#warnings[@]} -eq 0 ]; then printf '\nRESULT: ready - all benchmark dependencies are present.\n' else printf '\nRESULT: not fully ready:\n' for p in "${problems[@]}"; do printf ' - missing dependency: %s\n' "$p"; done [ -n "${missing_pkgs// /}" ] && printf ' - installable via dnf:%s\n' "$missing_pkgs" for w in "${warnings[@]}"; do printf ' - %s\n' "$w"; done fi printf '\nnotes: headless part needs sudo (power/governor/turbostat); headed part needs a graphical session.\n' } # --- sampler + cleanup -------------------------------------------------------- SPID="" start_sampler() { if [[ -n "$SUDO" ]] && have python3 && [[ -f "$SAMPLER" ]]; then section "start power/thermal sampler (powerlog.py)" $SUDO python3 "$SAMPLER" --tag "$TAG" --interval 1 & SPID=$! sleep 1 printf 'sampler pid %s -> results/power-%s.csv\n' "$SPID" "$TAG" else printf 'power sampler skipped (needs sudo + python3; use "ssh -t" over SSH)\n' fi } stop_sampler() { [[ -n "$SPID" ]] || return 0 kill -INT "$SPID" 2>/dev/null || $SUDO pkill -INT -f "powerlog.py --tag $TAG" 2>/dev/null wait "$SPID" 2>/dev/null SPID="" } CLEANED=0 cleanup() { [[ "$CLEANED" == "1" ]] && return 0 CLEANED=1 stop_sampler [[ -n "$KEEPID" ]] && { kill "$KEEPID" 2>/dev/null; wait "$KEEPID" 2>/dev/null; KEEPID=""; } restore_governor rm -f "$FIOFILE" 2>/dev/null } on_signal() { cleanup; exit 130; } trap 'on_signal' INT TERM trap 'cleanup' EXIT # --- test groups -------------------------------------------------------------- run_env() { section "environment (tag: $TAG)" phase "env" { printf 'CPU_MODEL=%s\n' "$CPU_MODEL" date uname -a lscpu free -h lsblk have stress-ng && stress-ng --version have glxinfo && glxinfo -B 2>/dev/null have vulkaninfo && vulkaninfo --summary 2>/dev/null | head -70 printf 'DRI_PRIME=%s MESA_VK_DEVICE_SELECT=%s VK_LOADER_DEVICE_SELECT=%s\n' \ "${DRI_PRIME:-}" "${MESA_VK_DEVICE_SELECT:-}" "${VK_LOADER_DEVICE_SELECT:-}" [ -x "$PT" ] && sha256sum "$PT" printf 'LLAMA_BIN=%s\nLLAMA_MODEL=%s\n' "$LLAMA_BIN" "$LLAMA_MODEL" printf 'MEM/CAPACITY: MemTotal=%s VRAM(dGPU)=%s\n' \ "$(awk '/MemTotal/{printf "%.1f GiB", $2/1048576}' /proc/meminfo)" \ "$(cat /sys/class/drm/card*/device/mem_info_vram_total 2>/dev/null | awk 'BEGIN{m=0}{if($1>m)m=$1}END{if(m)printf "%.1f GiB",m/1073741824; else print "?"}')" printf 'MODELS:\n' find "$HOME/models" -iname '*.gguf' -printf '%s\t%p\n' 2>/dev/null \ | sort -n | awk '{printf " %.2f GiB\t%s\n", $1/1073741824, $2}' } > "env-$TAG.txt" 2>&1 printf 'wrote results/env-%s.txt\n' "$TAG" } # --- LLM inference (llama.cpp, bundled) --------------------------------------- # Self-contained: the llama-bench runtime + a small model live under tools/llama/. # Targets token GENERATION on CPU/RAM (ngl=0), which is memory-bound, so the tg # number reflects how well this machine's RAM bandwidth serves an LLM. run_llama() { if [ ! -x "$LLAMA_BIN" ]; then printf 'skip (missing %s)\n' "$LLAMA_BIN"; return 0 fi if [ ! -f "$LLAMA_MODEL" ]; then printf 'skip (missing model %s)\n' "$LLAMA_MODEL"; return 0 fi local size; size="$(du -h "$LLAMA_MODEL" 2>/dev/null | cut -f1)" printf 'bench: %s\nmodel: %s (%s)\nngl=%s (CPU/RAM) tg=%s reps=%s\n' \ "$LLAMA_BIN" "$LLAMA_MODEL" "${size:-?}" "$LLAMA_NGL" "$LLAMA_TG" "$LLAMA_REPS" ( cd "$LLAMA_DIR/bin" || exit 1 LD_LIBRARY_PATH="$PWD" ./llama-bench \ -m "$LLAMA_MODEL" -ngl "$LLAMA_NGL" -p 0 -n "$LLAMA_TG" -r "$LLAMA_REPS" ) 2>&1 | tee "llama-$TAG.txt" local tg tg="$(awk -F'|' '/ tg[0-9]/{gsub(/ /,"",$8); print $8; exit}' "llama-$TAG.txt" | sed 's/±.*//')" if [ -z "$tg" ]; then printf 'llama: no t/s parsed — check %s (missing libs?)\n' "llama-$TAG.txt" return 0 fi reg_score "llama-tg" "$tg" } # --- app workloads (stock apps + fixed corpora) ------------------------------- # Phases and score names are the benchmark names: libreoffice gegl inkscape. # Each registers a rate (items/s) so higher stays better for the score/W table. run_libreoffice() { section "APP: LibreOffice — documents to PDF" phase "libreoffice" if ! have libreoffice; then printf 'skip (libreoffice missing) — sudo dnf install -y libreoffice-writer libreoffice-calc\n' return 0 fi setup_corpus lo-sample-documents-1.zip "$CORPUS_URL/lo-sample-documents-1.zip" \ a1e92da67999d8ed91ad6330b47828207285c146a3a4ebd7b95ad39cb2d62dcd libreoffice \ || { printf 'skip (corpus unavailable)\n'; return 0; } local d="$CORPUS/libreoffice" out="$CORPUS/libreoffice-out" log="libreoffice-$TAG.txt" local n t0 t1 el r n=$(find "$d/documents" -type f 2>/dev/null | wc -l) [ "$n" -gt 0 ] || { printf 'skip (no documents in corpus)\n'; return 0; } rm -rf "$out"; mkdir -p "$out" { libreoffice --version; printf 'convert %s documents -> PDF (headless, default filters)\n' "$n"; } > "$log" 2>&1 t0=$(now) libreoffice --headless --convert-to pdf --outdir "$out" "$d"/documents/* >> "$log" 2>&1 t1=$(now) el="$(secs "$t0" "$t1")"; r="$(per_s "$n" "$el")" cat "$log" printf 'libreoffice: %s docs in %ss = %s docs/s\n' "$n" "$el" "$r" reg_score "libreoffice" "$r" } run_gegl() { section "APP: GEGL — image operations" phase "gegl" if ! have gegl; then printf 'skip (gegl missing) — sudo dnf install -y gegl04-tools\n' return 0 fi setup_corpus sample-photo-6000x4000-1.zip "$CORPUS_URL/sample-photo-6000x4000-1.zip" \ cadcd434abcdc82a220e7022c749b6fa5f46bb7a32ec14423ca8aab067b0e989 gegl \ || { printf 'skip (corpus unavailable)\n'; return 0; } local d="$CORPUS/gegl" img log="gegl-$TAG.txt" t0 t1 el r op img="$(find "$d" -maxdepth 1 -iname '*.jpg' 2>/dev/null | head -1)" [ -n "$img" ] || { printf 'skip (no JPEG in corpus)\n'; return 0; } local -a ops=( "rotate-on-center degrees=90" "scale-size x=400 y=400" "antialias" \ "cartoon" "color-enhance" "crop x=100 y=100 width=1920 height=1080" \ "wavelet-blur" "reflect" "tile-glass tile-width=20 tile-height=20" ) { gegl --version 2>&1 | head -1; printf '%s ops x 1 image (%s)\n' "${#ops[@]}" "$(basename "$img")"; } > "$log" t0=$(now) for op in "${ops[@]}"; do printf -- '-- %s --\n' "$op" >> "$log" # shellcheck disable=SC2086 gegl -i "$img" -o "$d/out.png" -- $op >> "$log" 2>&1 done t1=$(now) el="$(secs "$t0" "$t1")"; r="$(per_s "${#ops[@]}" "$el")" cat "$log" printf 'gegl: %s ops in %ss = %s ops/s\n' "${#ops[@]}" "$el" "$r" reg_score "gegl" "$r" } run_inkscape() { section "APP: Inkscape — SVG to PNG" phase "inkscape" if ! have inkscape; then printf 'skip (inkscape missing) — sudo dnf install -y inkscape\n' return 0 fi setup_corpus svg-test-files-1.zip "$CORPUS_URL/svg-test-files-1.zip" \ c373998bebff0a88f0c4e6fec383eaa62690f544c6eb2b1ad7c55b1547e4db08 inkscape \ || { printf 'skip (corpus unavailable)\n'; return 0; } local d="$CORPUS/inkscape" out="$CORPUS/inkscape-out" log="inkscape-$TAG.txt" local n limit t0 t1 el r f local -a svgs mapfile -t svgs < <(find "$d" -maxdepth 1 -name '*.svg' 2>/dev/null | sort) n="${#svgs[@]}" [ "$n" -gt 0 ] || { printf 'skip (no SVG in corpus)\n'; return 0; } limit="${INKSCAPE_FILES:-25}" # cap the corpus so the step stays short [ "$limit" -gt 0 ] 2>/dev/null && [ "$limit" -lt "$n" ] && n="$limit" rm -rf "$out"; mkdir -p "$out" { inkscape --version; printf 'export %s of %s SVG files -> PNG\n' "$n" "${#svgs[@]}"; } > "$log" 2>&1 t0=$(now) for f in "${svgs[@]:0:$n}"; do inkscape --export-filename="$out/$(basename "${f%.svg}").png" "$f" >> "$log" 2>&1 done t1=$(now) el="$(secs "$t0" "$t1")"; r="$(per_s "$n" "$el")" cat "$log" printf 'inkscape: %s SVGs in %ss = %s images/s\n' "$n" "$el" "$r" reg_score "inkscape" "$r" } run_apps() { section "APP WORKLOADS (LibreOffice / GEGL / Inkscape)" run_libreoffice run_gegl run_inkscape } run_headless() { section "PASSMARK PERFORMANCETEST LINUX — CPU suite (cpubenchmark.net)" phase "PassMark-cpu" if [ -x "$PT" ]; then "$PT" -r 1 -d 2 &1 | tail -20 | tee "openssl-aes-$TAG.txt" openssl speed -multi "$(nproc)" -evp sha256 2>&1 | tail -20 | tee "openssl-sha-$TAG.txt" reg_score "openssl-aes" "$(openssl_gbs "openssl-aes-$TAG.txt")" reg_score "openssl-sha" "$(openssl_gbs "openssl-sha-$TAG.txt")" fi section "CPU: sysbench 1 thread / all threads" phase "sysbench-1t" if have sysbench; then sysbench cpu --threads=1 run | tee "sysbench-1t-$TAG.txt" reg_score "sysbench-1t" "$(sysbench_eps "sysbench-1t-$TAG.txt")" phase "sysbench-nt" sysbench cpu --threads="$(nproc)" run | tee "sysbench-nt-$TAG.txt" reg_score "sysbench-nt" "$(sysbench_eps "sysbench-nt-$TAG.txt")" phase "sysbench-mem" sysbench memory --threads="$(nproc)" run | tee "sysbench-mem-$TAG.txt" reg_score "sysbench-mem" "$(sysbench_mbs "sysbench-mem-$TAG.txt")" else printf 'skip (sysbench missing) — sudo dnf install -y sysbench\n' fi section "LLM INFERENCE: llama.cpp (estimate model capacity & tok/s)" phase "llama" run_llama if [[ "$RUN_APPS" == "1" ]]; then run_apps else section "APP WORKLOADS: skipped (RUN_APPS=0)" fi section "STORAGE: fio (same drive, $FIOFILE)" phase "fio" if have fio; then mkdir -p "$(dirname "$FIOFILE")" for job in "seqread read 1M 16" "seqwrite write 1M 16" "randread randread 4k 32" "randwrite randwrite 4k 32"; do set -- $job printf '\n-- fio %s --\n' "$1" phase "fio-$1" fio --name="$1" --rw="$2" --bs="$3" --numjobs=1 --iodepth="$4" \ --size="$FIO_SIZE" --direct=1 --time_based --runtime="$FIO_RUNTIME" \ --filename="$FIOFILE" --group_reporting \ --output-format=json --output="fio-$1-$TAG.json" case "$1" in seqread) fio_reg "$1" read bw_bytes ;; seqwrite) fio_reg "$1" write bw_bytes ;; randread) fio_reg "$1" read iops ;; randwrite) fio_reg "$1" write iops ;; esac rm -f "$FIOFILE" done else printf 'skip (fio missing)\n' fi section "THERMALS / POWER / SUSTAINED CLOCKS" phase "thermal-load" if have stress-ng; then stress-ng --cpu 0 --cpu-method matrixprod --timeout 300 --metrics-brief > "stress-ng-$TAG.txt" 2>&1 & LOADPID=$! sleep 120 have sensors && sensors > "sensors-load-$TAG.txt" cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq >> "sensors-load-$TAG.txt" 2>/dev/null if have turbostat && [[ -n "$SUDO" ]]; then # this turbostat has no --timeout; bound it with coreutils timeout instead timeout 30 $SUDO turbostat --interval 5 --quiet \ --show Core,CPU,Busy%,Bzy_MHz,PkgWatt,PkgTmp \ | tee "turbostat-load-$TAG.txt" fi wait "$LOADPID" 2>/dev/null else printf 'skip (stress-ng missing)\n' fi section "DESKTOP / SYSTEM" phase "systemd" if have systemd-analyze; then systemd-analyze | tee "systemd-analyze-$TAG.txt" systemd-analyze blame | head -20 >> "systemd-analyze-$TAG.txt" fi } run_headed() { section "GPU: glmark2 / vkmark (pinned to ${GPU_PCI:-default})" # One representative scene each by default (fast, comparable). Override the # scene/options with GLMARK2_BENCH / VKMARK_BENCH, or the seconds with # GLMARK2_DURATION / VKMARK_DURATION. phase "glmark2" if have glmark2; then glmark2 -b "terrain:duration=${GLMARK2_DURATION:-10}" 2>&1 | tail -25 | tee "glmark2-$TAG.txt" reg_score "glmark2" "$(gpu_score glmark2 "glmark2-$TAG.txt")" else printf 'skip (glmark2 missing) — sudo dnf install -y glmark2\n' fi phase "vkmark" if have vkmark; then vkmark -b "${VKMARK_BENCH:-texture}:duration=${VKMARK_DURATION:-10}" 2>&1 | tail -30 | tee "vkmark-$TAG.txt" reg_score "vkmark" "$(gpu_score vkmark "vkmark-$TAG.txt")" else printf 'skip (vkmark missing) — sudo dnf install -y vkmark\n' fi have glxinfo && glxinfo -B | tee "glxinfo-$TAG.txt" have vulkaninfo && vulkaninfo --summary 2>/dev/null | tee "vulkan-$TAG.txt" >/dev/null } require_display() { if [[ -z "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ]]; then printf 'ERROR: no display (WAYLAND_DISPLAY/DISPLAY unset).\n' >&2 printf 'The "headed" part must run inside the KDE session (e.g. Konsole),\n' >&2 printf 'not over a plain SSH command. Use "ssh -X"/"ssh -t" only for "headless".\n' >&2 exit 2 fi } # --- main --------------------------------------------------------------------- TITLE="$TAG [$PART]" printf '\033[1mbasic-benchmark — %s\033[0m\n' "$TITLE" if [[ "$DOCTOR" == "1" ]]; then doctor; exit 0; fi # Fail fast: the headed part is defined by having a display. [[ "$PART" == "headed" ]] && require_display acquire_sudo if [[ -n "$SUDO" ]]; then start_sudo_keepalive; else printf 'WARNING: no sudo — power logging, governor and turbostat will be skipped\n' printf ' run over "ssh -t" (allocate a TTY) to let sudo prompt.\n' fi : > "$PHASES" : > "scores-$TAG.csv" printf '\033[1mCPU under test: %s\033[0m\n' "$CPU_MODEL" printf '%s\n' "$CPU_MODEL" > "cpu-$TAG.txt" install_deps pin_gpu_env pin_governor start_sampler run_env run_idle case "$PART" in headless) run_headless ;; headed) run_headed ;; all) run_headless if [[ "$RUN_GPU" == "1" ]] && [[ -n "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ]]; then run_headed else section "GPU: skipped (no desktop session or RUN_GPU=0)" fi ;; esac stop_sampler section "POWER / THERMAL REPORT" if have python3 && [[ -f "$REPORT" ]]; then python3 "$REPORT" --tags "$TAG" || printf 'report failed\n' fi section "DONE ($TAG)" printf 'results in: %s\n' "$RES" printf 'score file (optional, for efficiency): %s/scores-%s.csv\n' "$RES" "$TAG" printf 'compare: python3 power-report.py --tags before-headless after-headless\n' printf 'remember to back up results/ before a reinstall.\n'