diff --git a/tools/perf/fx-telem-2026-07-11.md b/tools/perf/fx-telem-2026-07-11.md index 04b828a633..647b4b322a 100644 --- a/tools/perf/fx-telem-2026-07-11.md +++ b/tools/perf/fx-telem-2026-07-11.md @@ -154,8 +154,30 @@ Raw logs: `fx-telem-logs/*.emulog.txt` (session scratchpad). dirty subset, not worth an ABI bump. Footnote: VI is the only churning channel (5–7× VF) — future mVU perf work should look at VI pool sizing, not eviction policy. -- **FX-15 → pending iTLB A/B** (same binary, `YAPS2_NO_THP=1` toggle, - fan pinned). +- **FX-15 → applied, kept default-ON** (faaf117bf). SD865 iTLB A/B + (fx15-ab-1: 4 titles × 4 ABBA rounds, same eerunner, `YAPS2_NO_THP=1` + toggle, fan pinned, `tools/perf/fx15_thp_ab.sh` + `fx15_thp_report.py`): + + | title | ITLB_WALK | L1I_TLB_REFILL | cycles | wall | + |---|---|---|---|---| + | SotC | −35.4% | −8.7% | +0.10% | +0.36% | + | Katamari | −24.9% | −33.8% | +0.08% | +0.00% | + | OutRun | −22.5% | −4.3% | −0.46% | −0.39% | + | UYA | −14.6% | −58.9% | +0.11% | +1.09% | + + Mechanism confirmed, throughput neutral: baseline A77 iTLB pressure is + already negligible (ITLB_WALK 0.008–0.025/kinsn — one walk per 40k–125k + insns; the 1280-entry L2 TLB covers the ~8MB live JIT footprint even in + 4K pages) and STALL_FRONTEND is unchanged, so frontend stalls are + icache/branch, not TLB. Kept: zero cost, no regression, and the + in-order A53 (RK3562, far smaller TLBs) should benefit more — optional + RK wallclock A/B if that device returns to rotation. THP probes: 8 MB + of hugepages materialized per title, exactly in the two madvised rec + ranges (EE+IOP at 0x111d00000, mVU0+1 at 0x118d00000). + +Campaign CLOSED 2026-07-12. Remaining FX queue: FX-05 (mVU digest DX), +FX-14/FX-09 (someday). Device left on the clean (census-off) binary; +build-rocknix flags reverted. ## Smoke result (SotC gameplay savestate, 75s, 2026-07-11) diff --git a/tools/perf/fx15_thp_ab.sh b/tools/perf/fx15_thp_ab.sh new file mode 100644 index 0000000000..61a2309bd7 --- /dev/null +++ b/tools/perf/fx15_thp_ab.sh @@ -0,0 +1,117 @@ +#!/bin/bash +# fx15_thp_ab.sh — interleaved A/B for FX-15 (MADV_HUGEPAGE on the JIT code caches). +# +# ONE pcsx2-eerunner binary, two arms: A = YAPS2_NO_THP=1 (4K pages, baseline), +# B = default (THP madvise on). Same savestate-anchored --liverun workloads as +# callret_ab.sh, ABBA-interleaved so thermal/clock drift cancels, perf stat +# around each run. Throughput truth = wall seconds per N frames (unlimited +# limiter, headless --renderer null); mechanism check = ITLB_WALK (r35) and +# L1I_TLB_REFILL (r02) per kinsn. +# +# During the first B round of each game a background probe snapshots +# AnonHugePages from /proc//smaps for the JIT arena (VA >= 0x100000000) +# into /.thp — if those are all zero the A/B is null by construction +# (madvise didn't take) and the numbers mean nothing. +# +# Usage: +# fx15_thp_ab.sh -b -g [-f frames] [-r rounds] +# [-c cpulist] [-o outdir] +# games.cfg lines: name|savestate|iso +# +# Device notes (SD865/Rocknix): run with -c 4-7 (A77 cluster). Fan pinned to +# max and RESTORED to auto on exit. EERUNNER_SYNCMTGS=0 always (bare --liverun +# wedges at frame 1 without it). +set -u + +FRAMES=2000 +ROUNDS=4 +CPUS="" +OUT="fx15-thp-ab-$(date +%m%d-%H%M%S)" +BIN="" GAMES="" + +while getopts "b:g:f:r:c:o:" opt; do + case $opt in + b) BIN=$OPTARG ;; + g) GAMES=$OPTARG ;; + f) FRAMES=$OPTARG ;; + r) ROUNDS=$OPTARG ;; + c) CPUS=$OPTARG ;; + o) OUT=$OPTARG ;; + *) exit 2 ;; + esac +done +[ -x "$BIN" ] && [ -f "$GAMES" ] || { + echo "usage: $0 -b -g [-f frames] [-r rounds] [-c cpus] [-o outdir]" >&2 + exit 2 +} +mkdir -p "$OUT" + +# --- device pinning (no-op off-device) -------------------------------------- +FAN="" +restore() { + if [ -n "$FAN" ]; then + echo 2 > "$FAN/pwm1_enable" 2>/dev/null + echo "fan restored to auto ($FAN)" + fi +} +trap restore EXIT +for h in /sys/class/hwmon/hwmon*; do + [ -f "$h/name" ] && [ "$(cat "$h/name" 2>/dev/null)" = "pwmfan" ] || continue + FAN=$h + echo 1 > "$FAN/pwm1_enable" && echo 255 > "$FAN/pwm1" + echo "fan pinned to max ($FAN)" +done +for g in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do + echo performance > "$g" 2>/dev/null +done +echo "global THP mode: $(cat /sys/kernel/mm/transparent_hugepage/enabled 2>/dev/null)" | tee "$OUT/thp-mode.txt" + +# Rocknix device: the eerunner binaries need the bundled libs (libjpeg/lz4). +[ -d /storage/pcsx2/lib ] && export LD_LIBRARY_PATH=/storage/pcsx2/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} + +# TLB event set (A77: 6 programmable + fixed cycles, no multiplexing): +# r02 L1I_TLB_REFILL, r35 ITLB_WALK, r34 DTLB_WALK, r23 STALL_FRONTEND +EVENTS="cycles,instructions,r02,r35,r34,r23" +TASKSET="" +[ -n "$CPUS" ] && TASKSET="taskset -c $CPUS" + +probe_thp() { # $1=outfile — sample JIT-arena AnonHugePages of the live run + sleep 25 + local pid + pid=$(pgrep -n -f pcsx2-eerunner) + [ -n "$pid" ] || { echo "no pid" > "$1"; return; } +# JIT arena = 0x1_00000000..: 9-hex-digit VMA start beginning with 1. + # (busybox awk: no strtonum) + awk '/^[0-9a-f]+-[0-9a-f]+ /{vma=$1; keep=(index($1,"-")==10 && substr($1,1,1)=="1")} + /^AnonHugePages:/{if (keep && $2 > 0) print vma, $0}' \ + "/proc/$pid/smaps" > "$1" 2>/dev/null + echo "total-kB $(awk '{s+=$3} END{print s+0}' "$1")" >> "$1" +} + +run_one() { # $1=arm(A|B) $2=name $3=state $4=iso $5=round + local tag="$2-$1-r$5" envA="" + [ "$1" = A ] && envA="YAPS2_NO_THP=1" + echo "=== $tag ($(date +%H:%M:%S)) ===" + if [ "$1" = B ] && [ "$5" = 1 ]; then + probe_thp "$OUT/$tag.thp" & + fi + env $envA EERUNNER_SYNCMTGS=0 $TASKSET perf stat -e "$EVENTS" \ + -o "$OUT/$tag.stat" -- \ + "$BIN" --liverun --renderer null --savestate "$3" --iso "$4" \ + --frames "$FRAMES" > "$OUT/$tag.log" 2>&1 + echo "exit=$? $(grep -o 'PerfLog session:.*' "$OUT/$tag.log" | tail -1)" + sleep 5 # cool-down between runs +} + +for round in $(seq 1 "$ROUNDS"); do + # ABBA: odd rounds run A first, even rounds B first. + if [ $((round % 2)) -eq 1 ]; then order="A B"; else order="B A"; fi + for arm in $order; do + while IFS='|' read -r name state iso; do + case $name in ''|\#*) continue ;; esac + run_one "$arm" "$name" "$state" "$iso" "$round" + done < "$GAMES" + done +done +wait +echo "done — stats in $OUT/*.stat, THP probes in $OUT/*-B-r1.thp" diff --git a/tools/perf/fx15_thp_report.py b/tools/perf/fx15_thp_report.py new file mode 100644 index 0000000000..bc0d8526da --- /dev/null +++ b/tools/perf/fx15_thp_report.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# fx15_thp_report.py — summarize an fx15_thp_ab.sh output dir. +# +# Per (game, arm): mean/stdev of wall (PerfLog session line — the throughput +# truth), cycles, instructions, IPC, and the TLB mechanism events per kinsn +# (r02 L1I_TLB_REFILL, r35 ITLB_WALK, r34 DTLB_WALK, r23 STALL_FRONTEND). +# Then B (THP) vs A (YAPS2_NO_THP=1) deltas. Counters are whole-process; +# ABBA interleave + identical boot work make the delta meaningful. +# +# usage: fx15_thp_report.py + +import re +import statistics +import sys +from collections import defaultdict +from pathlib import Path + +EVENTS = ["cycles", "instructions", "r02", "r35", "r34", "r23"] +EVENT_LABEL = { + "r02": "L1I_TLB_REFILL", + "r35": "ITLB_WALK", + "r34": "DTLB_WALK", + "r23": "STALL_FRONTEND", +} + + +def parse_stat(path): + vals = {} + for line in path.read_text().splitlines(): + m = re.match(r"\s*([\d,]+)\s+(\S+)", line) + if m and m.group(2) in EVENTS: + vals[m.group(2)] = int(m.group(1).replace(",", "")) + m = re.match(r"\s*([\d.]+)\s+seconds time elapsed", line) + if m: + vals["elapsed"] = float(m.group(1)) + return vals + + +def parse_wall(logpath): + if not logpath.exists(): + return None + m = None + for line in logpath.read_text(errors="replace").splitlines(): + mm = re.search(r"PerfLog session: (\d+) frames in ([\d.]+)s wall", line) + if mm: + m = float(mm.group(2)) + return m + + +def main(): + outdir = Path(sys.argv[1]) + runs = defaultdict(list) # (game, arm) -> [dict] + for stat in sorted(outdir.glob("*.stat")): + m = re.match(r"(.+)-([AB])-r(\d+)\.stat", stat.name) + if not m: + continue + game, arm = m.group(1), m.group(2) + vals = parse_stat(stat) + wall = parse_wall(stat.with_suffix(".log")) + if wall is not None: + vals["wall"] = wall + if "cycles" in vals: + runs[(game, arm)].append(vals) + + games = sorted({g for g, _ in runs}) + for game in games: + print(f"\n=== {game} ===") + agg = {} + for arm in "AB": + rr = runs.get((game, arm), []) + if not rr: + continue + a = {} + for k in ["wall", "elapsed", *EVENTS]: + xs = [r[k] for r in rr if k in r] + if xs: + a[k] = (statistics.mean(xs), + statistics.stdev(xs) if len(xs) > 1 else 0.0) + agg[arm] = a + label = "A thp-off" if arm == "A" else "B thp-on " + ki = a["instructions"][0] / 1e3 + per_ki = " ".join( + f"{EVENT_LABEL[e]}={a[e][0] / ki:.3f}" + for e in ["r02", "r35", "r34"] if e in a) + stallpct = 100.0 * a["r23"][0] / a["cycles"][0] if "r23" in a else 0 + wall = f"wall={a['wall'][0]:.2f}s±{a['wall'][1]:.2f}" if "wall" in a else "" + print(f" {label} n={len(rr)} {wall} cycles={a['cycles'][0]/1e9:.3f}G " + f"IPC={a['instructions'][0]/a['cycles'][0]:.3f} " + f"| per-kinsn: {per_ki} | stall_fe={stallpct:.1f}%cyc") + if "A" in agg and "B" in agg: + def delta(k): + return 100.0 * (agg["B"][k][0] - agg["A"][k][0]) / agg["A"][k][0] + parts = [f"wall {delta('wall'):+.2f}%"] if "wall" in agg["A"] and "wall" in agg["B"] else [] + parts += [f"cycles {delta('cycles'):+.2f}%"] + parts += [f"{EVENT_LABEL[e]} {delta(e):+.1f}%" + for e in ["r02", "r35", "r34", "r23"] + if e in agg["A"] and e in agg["B"]] + print(f" B-vs-A: {' '.join(parts)}") + + thps = sorted(outdir.glob("*.thp")) + if thps: + print("\nTHP probes (arm B, round 1):") + for t in thps: + total = [l for l in t.read_text().splitlines() if l.startswith("total-kB")] + print(f" {t.name}: {total[0] if total else 'no data'}") + + +if __name__ == "__main__": + main()