From d56179d876120fe479dd49b962616d118eb387ac Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 7 Oct 2026 09:18:21 -0400 Subject: [PATCH] [Tools] (P14): pinned-clock perf matrix with a per-thread CPU and GPU-busy sampler. --- tools/device_bench/disagg/cpusampler.c | 190 ++++++++++++++++++++++++ tools/device_bench/disagg/matrix_p14.sh | 189 +++++++++++++++++++++++ tools/device_bench/disagg/p14_report.py | 175 ++++++++++++++++++++++ tools/device_bench/disagg/pin_clocks.sh | 29 ++++ 4 files changed, 583 insertions(+) create mode 100644 tools/device_bench/disagg/cpusampler.c create mode 100644 tools/device_bench/disagg/matrix_p14.sh create mode 100644 tools/device_bench/disagg/p14_report.py create mode 100644 tools/device_bench/disagg/pin_clocks.sh diff --git a/tools/device_bench/disagg/cpusampler.c b/tools/device_bench/disagg/cpusampler.c new file mode 100644 index 00000000..28470c76 --- /dev/null +++ b/tools/device_bench/disagg/cpusampler.c @@ -0,0 +1,190 @@ +// Device-side per-thread CPU sampler for the perf matrix (P14). +// +// Usage (as root, pinned to a little core): +// taskset 01 cpusampler [max_s] +// +// Every interval it reads /proc//task//schedstat (on-CPU ns, not the 10 ms tick +// counters of stat) for every thread of every process whose cmdline starts with +// (the replay process and, for spawn/tcp, the render-server process), and keeps the last +// kRingSeconds of samples in memory. When (benchmark.json) appears it dumps the +// ring to and exits; the host cuts the benchmark's tail window out of it with the +// file's mtime as the window end (make_report.py / cpu_window.py). Timestamps are +// CLOCK_REALTIME so they compare with st_mtim. +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAXP 8 +#define MAXT 512 +#define RING 4096 + +struct Thread { + int pid, tid; + char comm[24]; +}; +struct Sample { + long long t; // CLOCK_REALTIME ns + int index; // into threads[] + long long cpu; // schedstat on-CPU ns +}; + +static struct Thread threads[MAXT]; +static int threadCount; +static struct Sample* ring; +static long long ringCap, ringHead, ringSize; + +static long long NowNs(void) { + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + return (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec; +} + +static int ReadSmall(const char* path, char* buf, int n) { + int fd = open(path, O_RDONLY); + if (fd < 0) return -1; + int r = (int)read(fd, buf, n - 1); + close(fd); + if (r < 0) return -1; + buf[r] = 0; + return r; +} + +// Pseudo-threads: pid -1 carries the Adreno kgsl cumulative busy time and the GPU / CPU clocks, +// so the host can say whether a window was GPU-bound and that the pin held. +static int Pseudo(const char* name) { + for (int i = 0; i < threadCount; ++i) + if (threads[i].pid == -1 && strcmp(threads[i].comm, name) == 0) return i; + if (threadCount >= MAXT) return -1; + threads[threadCount].pid = -1; + threads[threadCount].tid = -1; + snprintf(threads[threadCount].comm, sizeof threads[threadCount].comm, "%s", name); + return threadCount++; +} + +static int FindThread(int pid, int tid) { + for (int i = 0; i < threadCount; ++i) + if (threads[i].tid == tid && threads[i].pid == pid) return i; + if (threadCount >= MAXT) return -1; + char path[96], buf[64]; + snprintf(path, sizeof path, "/proc/%d/task/%d/comm", pid, tid); + threads[threadCount].pid = pid; + threads[threadCount].tid = tid; + threads[threadCount].comm[0] = 0; + if (ReadSmall(path, buf, sizeof buf) > 0) { + buf[strcspn(buf, "\n,")] = 0; + snprintf(threads[threadCount].comm, sizeof threads[threadCount].comm, "%s", buf); + } + return threadCount++; +} + +static int ScanPids(const char* pkg, int* pids) { + int n = 0; + DIR* d = opendir("/proc"); + if (!d) return 0; + struct dirent* e; + size_t len = strlen(pkg); + char path[64], buf[256]; + while ((e = readdir(d)) && n < MAXP) { + if (e->d_name[0] < '0' || e->d_name[0] > '9') continue; + snprintf(path, sizeof path, "/proc/%s/cmdline", e->d_name); + if (ReadSmall(path, buf, sizeof buf) <= 0) continue; + if (strncmp(buf, pkg, len) == 0 && (buf[len] == 0 || buf[len] == ':')) pids[n++] = atoi(e->d_name); + } + closedir(d); + return n; +} + +static void Push(long long t, int index, long long cpu) { + struct Sample* s = &ring[(ringHead + ringSize) % ringCap]; + if (ringSize == ringCap) ringHead = (ringHead + 1) % ringCap; + else ++ringSize; + s->t = t; + s->index = index; + s->cpu = cpu; +} + +int main(int argc, char** argv) { + if (argc < 5) { + fprintf(stderr, "usage: cpusampler [max_s]\n"); + return 2; + } + const char* pkg = argv[1]; + const int intervalMs = atoi(argv[2]) > 0 ? atoi(argv[2]) : 10; + const char* watch = argv[3]; + const char* out = argv[4]; + const long long maxNs = (argc > 5 ? atoll(argv[5]) : 1800) * 1000000000LL; + const long long ringSeconds = 30; + ringCap = (ringSeconds * 1000 / intervalMs + 1) * 128; + ring = calloc((size_t)ringCap, sizeof *ring); + if (!ring) return 1; + const long long start = NowNs(); + int pids[MAXP], pidCount = 0; + long long iteration = 0; + char path[96], buf[512]; + struct stat st; + long long mtime = -1; + for (;;) { + const long long now = NowNs(); + if (now - start > maxNs) break; + if (iteration % 25 == 0 || pidCount == 0) pidCount = ScanPids(pkg, pids); + for (int p = 0; p < pidCount; ++p) { + snprintf(path, sizeof path, "/proc/%d/task", pids[p]); + DIR* d = opendir(path); + if (!d) continue; + struct dirent* e; + while ((e = readdir(d))) { + if (e->d_name[0] == '.') continue; + const int tid = atoi(e->d_name); + snprintf(path, sizeof path, "/proc/%d/task/%d/schedstat", pids[p], tid); + if (ReadSmall(path, buf, sizeof buf) <= 0) continue; + const int index = FindThread(pids[p], tid); + if (index >= 0) Push(now, index, atoll(buf)); + } + closedir(d); + } + if (iteration % 10 == 0) { + // gpu_clock_stats: cumulative busy time per power level; its sum is busy-since-boot. + // (gpubusy is a sliding ~1 s window, not a counter.) + if (ReadSmall("/sys/class/kgsl/kgsl-3d0/gpu_clock_stats", buf, sizeof buf) > 0) { + long long sum = 0; + for (char* q = buf; *q;) { + char* end = q; + const long long v = strtoll(q, &end, 10); + if (end == q) break; + sum += v; + q = end; + } + Push(now, Pseudo("gpubusy_us"), sum); + } + if (ReadSmall("/sys/class/kgsl/kgsl-3d0/devfreq/cur_freq", buf, sizeof buf) > 0) + Push(now, Pseudo("gpufreq"), atoll(buf)); + if (ReadSmall("/sys/devices/system/cpu/cpufreq/policy7/scaling_cur_freq", buf, sizeof buf) > 0) + Push(now, Pseudo("cpu7freq"), atoll(buf)); + if (ReadSmall("/sys/devices/system/cpu/cpufreq/policy2/scaling_cur_freq", buf, sizeof buf) > 0) + Push(now, Pseudo("cpu2freq"), atoll(buf)); + } + ++iteration; + if (stat(watch, &st) == 0 && (long long)st.st_mtim.tv_sec * 1000000000LL + st.st_mtim.tv_nsec > start) { + mtime = (long long)st.st_mtim.tv_sec * 1000000000LL + st.st_mtim.tv_nsec; + break; + } + usleep((useconds_t)intervalMs * 1000); + } + FILE* f = fopen(out, "w"); + if (!f) return 1; + fprintf(f, "# watch_mtime_ns=%lld interval_ms=%d\n", mtime, intervalMs); + fprintf(f, "t_ns,pid,tid,comm,cpu_ns\n"); + for (long long i = 0; i < ringSize; ++i) { + const struct Sample* s = &ring[(ringHead + i) % ringCap]; + const struct Thread* th = &threads[s->index]; + fprintf(f, "%lld,%d,%d,%s,%lld\n", s->t, th->pid, th->tid, th->comm, s->cpu); + } + fclose(f); + return mtime >= 0 ? 0 : 3; +} diff --git a/tools/device_bench/disagg/matrix_p14.sh b/tools/device_bench/disagg/matrix_p14.sh new file mode 100644 index 00000000..6f65d37a --- /dev/null +++ b/tools/device_bench/disagg/matrix_p14.sh @@ -0,0 +1,189 @@ +#!/usr/bin/env bash +# P14 perf matrix: dev (pull monolith, baseline APK) vs feat (monolith / inproc / spawn+shm / +# spawn+tcp-localhost) x {Espryt, Magma} x {openra, rd12}, on a rooted device with pinned clocks. +# +# SER=HA27Q3LQ PKG_FEAT=top.mobilegl.plugin.p14.trace PKG_DEV=top.mobilegl.plugin.p14dev.trace \ +# ARMS="dev monolith inproc shm tcp" BACKENDS="DirectGLES DirectVulkan" WLS="openra rd12" REPS=3 \ +# TAG=base bash matrix_p14.sh prep|run|ssim +# +# Per run: force-stop, clear outputs, start cpusampler (per-thread schedstat + kgsl gpubusy, 10 ms) +# watching benchmark.json, launch the replay, wait, pull result/benchmark/logcat/sampler CSV, and +# verify the arm and backend markers. Rounds interleave arms (and backends) so drift spreads evenly. +# Results: $PERF_WORK////-r/ ; reduce with p14_report.py. +set -u +export MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +SER="${SER:-HA27Q3LQ}" +PKG_FEAT="${PKG_FEAT:-top.mobilegl.plugin.p14.trace}" +PKG_DEV="${PKG_DEV:-top.mobilegl.plugin.p14dev.trace}" +ARMS=(${ARMS:-dev monolith inproc shm tcp}) +BACKENDS=(${BACKENDS:-DirectGLES DirectVulkan}) +WLS=(${WLS:-openra rd12}) +REPS="${REPS:-3}" +TAG="${TAG:-base}" +FIXTURES="${FIXTURES:-$REPO_ROOT/tools/trace_replay/fixtures}" +ROOT="${PERF_WORK:-$REPO_ROOT/.trace-work/perf-p14}/$TAG" +TIMEOUT="${TIMEOUT:-900}" +EXTRA_ENV="${EXTRA_ENV:-}" +COOL_C="${COOL_C:-38}" # wait until battery temperature is below this before a run +LOG="$ROOT/matrix.log" +SAMPLER=/data/local/tmp/cpusampler + +shq() { adb -s "$SER" shell "$1"; } +log() { local line; line="[$(date '+%F %T')] $*"; mkdir -p "$ROOT"; echo "$line" >>"$LOG"; echo "$line"; } +pkg_of() { [ "$1" = dev ] && echo "$PKG_DEV" || echo "$PKG_FEAT"; } +app_of() { echo "/data/user/0/$1/files/trace-replay"; } + +wl_args() { # trace golden w h target crop + case "$1" in + openra) echo "openra openra.0000031249.png 640 480 31249 1 1 638 478" ;; + rd12) echo "rd12 minecraft-1.21.4-rd12-odinlite-in-world.0004660351.png 854 480 4660351 0 0 0 0" ;; + esac +} + +arm_env() { + case "$1" in + dev|monolith) echo "" ;; + inproc) echo "MOBILEGL_TRANSPORT=inproc;MOBILEGL_IPC_RUN_AHEAD=1" ;; + shm) echo "MOBILEGL_TRANSPORT=spawn;MOBILEGL_IPC_CONTROL=fork;MOBILEGL_IPC_DATA=auto;MOBILEGL_IPC_RUN_AHEAD=1" ;; + tcp) echo "MOBILEGL_TRANSPORT=spawn;MOBILEGL_IPC_CONTROL=tcp://127.0.0.1:40613;MOBILEGL_IPC_DATA=stream;MOBILEGL_IPC_RUN_AHEAD=1" ;; + esac +} + +temp_dc() { adb -s "$SER" shell dumpsys battery 2>/dev/null | tr -d '\r' | sed -n 's/.*temperature: \([0-9]*\).*/\1/p' | head -1; } + +cool_down() { + local t; t=$(temp_dc); local waited=0 + while [ -n "$t" ] && [ "$t" -ge $((COOL_C * 10)) ] && [ "$waited" -lt 900 ]; do + sleep 15; waited=$((waited + 15)); t=$(temp_dc) + done + [ "$waited" -gt 0 ] && log "cooled ${waited}s to ${t}" +} + +prep_pkg() { + local pkg="$1" app; app=$(app_of "$pkg") + local tmp="$ROOT/fixtures"; mkdir -p "$tmp" + for wl in openra rd12; do + local tgz="$FIXTURES/$( [ $wl = openra ] && echo openra.tgz || echo minecraft-1.21.4-rd12-odinlite-in-world.tgz)" + set -- $(wl_args $wl) + local golden="$FIXTURES/$2" + if [ ! -f "$tmp/$wl.trace" ]; then + rm -rf "$tmp/x"; mkdir -p "$tmp/x"; tar xzf "$tgz" -C "$tmp/x" || exit 1 + mv "$(find "$tmp/x" -name '*.trace' | head -1)" "$tmp/$wl.trace"; rm -rf "$tmp/x" + fi + adb -s "$SER" push "$(cygpath -w "$tmp/$wl.trace")" /data/local/tmp/p14-$wl.trace >/dev/null || exit 1 + adb -s "$SER" push "$(cygpath -w "$golden")" /data/local/tmp/p14-$wl.png >/dev/null || exit 1 + shq "am force-stop $pkg; run-as $pkg sh -c 'mkdir -p $app/input-$wl $app/output'" + shq "cat /data/local/tmp/p14-$wl.trace | run-as $pkg sh -c 'cat > $app/input-$wl/trace.trace'" + shq "cat /data/local/tmp/p14-$wl.png | run-as $pkg sh -c 'cat > $app/input-$wl/golden.png'" + local size host; size=$(shq "run-as $pkg sh -c 'wc -c < $app/input-$wl/trace.trace'" | tr -d '\r ') + host=$(wc -c <"$tmp/$wl.trace") + [ "$size" = "$host" ] || { log "FATAL: $pkg $wl size $size != $host"; exit 1; } + log "prep $pkg $wl ok ($size bytes)" + done + shq "rm -f /data/local/tmp/p14-*.trace /data/local/tmp/p14-*.png; chmod 755 $SAMPLER" +} + +verify_run() { + local dir="$1" arm="$2" backend="$3" all="$1/markers.txt" v="$1/verify.txt" + grep -aE "Config: MOBILEGL_TRANSPORT|spawn ARMED|control=tcp data=stream|run-ahead ARMED|running lockstep|DISARMED|Active backend type|debug.mobilegl.backend|Fatal\{" \ + "$dir/logcat.txt" "$dir"/mgl.*.log 2>/dev/null >"$all" + { + echo "arm=$arm backend=$backend" + grep -aq "Active backend type set to $backend" "$all" && echo "BACKEND=OK" || echo "BACKEND=MISSING" + case "$arm" in + dev|monolith) grep -aqE "Config: MOBILEGL_TRANSPORT=(inproc|spawn)" "$all" && echo "ARM=UNEXPECTED_SPLIT" || echo "ARM=OK(monolith)" ;; + inproc) grep -aq "Config: MOBILEGL_TRANSPORT=inproc - the MGPipe record stream" "$all" && echo "ARM=OK(inproc)" || echo "ARM=MISSING" ;; + shm) grep -aq "spawn ARMED - the server role runs in pid" "$all" && echo "ARM=OK(spawn)" || echo "ARM=MISSING" ;; + tcp) grep -aq "control=tcp data=stream server=" "$all" && echo "ARM=OK(tcp)" || echo "ARM=MISSING" ;; + esac + case "$arm" in inproc|shm|tcp) grep -aq "run-ahead ARMED" "$all" && echo "RUN_AHEAD=OK" || echo "RUN_AHEAD=MISSING" ;; esac + grep -aq "running lockstep" "$all" && echo "LOCKSTEP=PRESENT" + echo "FATAL_LINES=$(grep -ac 'Fatal{' "$all")" + } >"$v" +} + +# run_one +run_one() { + local wl="$1" backend="$2" arm="$3" rep="$4" mode="$5" + local pkg app dir; pkg=$(pkg_of "$arm"); app=$(app_of "$pkg") + dir="$ROOT/$wl/$backend/$arm-r$rep"; rm -rf "$dir"; mkdir -p "$dir" + set -- $(wl_args "$wl"); local w="$3" h="$4" target="$5" cx="$6" cy="$7" cw="$8" ch="$9" + cool_down + local t0 epoch0 status=""; t0=$(temp_dc); epoch0=$(date +%s) + log "BEGIN $TAG $wl/$backend/$arm r$rep $mode temp=$t0" + shq "am force-stop $PKG_FEAT; am force-stop $PKG_DEV" >/dev/null 2>&1 + shq "run-as $pkg sh -c 'rm -f $app/output/*.json $app/output/*.log $app/output/*.png /data/user/0/$pkg/files/mgl.*.log'" >/dev/null 2>&1 + shq "su -c 'pkill -f cpusampler; rm -f /data/local/tmp/p14cpu.csv'" >/dev/null 2>&1 + adb -s "$SER" logcat -c >/dev/null 2>&1 + if [ "$arm" = tcp ]; then + python "$(cygpath -w "$REPO_ROOT/tools/trace_replay/tcp_device_server.py")" start --serial "$SER" --package "$pkg" \ + --backend "$backend" --listen tcp://127.0.0.1:40613 >"$dir/server-start.log" 2>&1 || { echo server_start_failed >"$dir/STATUS"; log "FAIL tcp server"; return 1; } + local waited=0 + until adb -s "$SER" logcat -d 2>/dev/null | grep -q "listening on tcp://127.0.0.1:40613"; do + sleep 1; waited=$((waited + 1)); [ $waited -ge 30 ] && { echo server_not_listening >"$dir/STATUS"; log "FAIL tcp listen"; return 1; } + done + fi + local passflag="" + [ "$mode" = benchmark ] && passflag="--ez benchmark true --es benchmark_result_path $app/output/benchmark.json" + local env; env="$(arm_env "$arm")"; [ -n "$EXTRA_ENV" ] && env="${env:+$env;}$EXTRA_ENV" + if [ "$mode" = benchmark ]; then + shq "su -c 'nohup taskset 01 $SAMPLER $pkg 10 $app/output/benchmark.json /data/local/tmp/p14cpu.csv $TIMEOUT >/dev/null 2>&1 &'" >/dev/null 2>&1 + fi + local am_out + am_out=$(shq "am start -a top.mobilegl.plugin.TRACE_REPLAY -n $pkg/top.mobilegl.plugin.trace.TraceReplayActivity --es trace_path $app/input-$wl/trace.trace --es golden_path $app/input-$wl/golden.png --es output_dir $app/output --es backend $backend --el target_call $target --ei width $w --ei height $h --es ssim_threshold 0.99 --ez use_pbuffer true $passflag --ei crop_x $cx --ei crop_y $cy --ei crop_width $cw --ei crop_height $ch --es mobilegl_env '$env'" 2>&1) + printf '%s\n' "$am_out" >"$dir/am-start.txt" + local deadline=$((epoch0 + TIMEOUT)) got=0 + while [ "$(date +%s)" -lt "$deadline" ]; do + if adb -s "$SER" exec-out "run-as $pkg cat $app/output/result.json" 2>/dev/null | head -c 400 | grep -q '"passed"'; then got=1; break; fi + if [ $(( $(date +%s) - epoch0 )) -gt 25 ] && [ -z "$(shq "pidof $pkg" 2>/dev/null | tr -d '\r ')" ]; then status=process_died; break; fi + sleep 2 + done + [ $got -eq 1 ] && { status=finished; sleep 2; } || [ -n "$status" ] || status=timeout + local t1; t1=$(temp_dc) + adb -s "$SER" logcat -d >"$dir/logcat.txt" 2>/dev/null + for f in result.json benchmark.json; do + adb -s "$SER" exec-out "run-as $pkg cat $app/output/$f" >"$dir/$f" 2>/dev/null + { [ -s "$dir/$f" ] && ! head -c 200 "$dir/$f" | grep -q "No such file"; } || rm -f "$dir/$f" + done + if [ "$arm" = tcp ]; then + for f in mgl.client.log mgl.server.log; do adb -s "$SER" exec-out "run-as $pkg cat /data/user/0/$pkg/files/$f" >"$dir/$f" 2>/dev/null; done + python "$(cygpath -w "$REPO_ROOT/tools/trace_replay/tcp_device_server.py")" stop --serial "$SER" --package "$pkg" >/dev/null 2>&1 + fi + if [ "$mode" = benchmark ]; then + sleep 1 + adb -s "$SER" exec-out "su -c 'cat /data/local/tmp/p14cpu.csv'" >"$dir/cpu.csv" 2>/dev/null + shq "su -c 'pkill -f cpusampler'" >/dev/null 2>&1 + fi + shq "am force-stop $pkg" >/dev/null 2>&1 + local passed="none"; [ -f "$dir/result.json" ] && passed=$(sed -n 's/.*"passed": *\([a-z]*\).*/\1/p' "$dir/result.json" | head -1) + echo "$status" >"$dir/STATUS" + echo "tag=$TAG wl=$wl backend=$backend arm=$arm rep=$rep mode=$mode status=$status passed=$passed dur=$(( $(date +%s) - epoch0 )) temp0=$t0 temp1=$t1" >"$dir/meta.txt" + verify_run "$dir" "$arm" "$backend" + local fps="-"; [ -f "$dir/benchmark.json" ] && fps=$(sed -n 's/.*"fps": *\([0-9.]*\).*/\1/p' "$dir/benchmark.json" | head -1) + log "END $TAG $wl/$backend/$arm r$rep $mode status=$status passed=$passed fps=$fps temp=$t1 :: $(tr '\n' ' ' <"$dir/verify.txt")" +} + +main() { + log "=== matrix_p14 $1 tag=$TAG arms=${ARMS[*]} backends=${BACKENDS[*]} wls=${WLS[*]} reps=$REPS ===" + shq "input keyevent KEYCODE_WAKEUP; svc power stayon usb" >/dev/null 2>&1 + case "$1" in + prep) prep_pkg "$PKG_FEAT"; prep_pkg "$PKG_DEV" ;; + run) + for wl in "${WLS[@]}"; do + for ((r = 1; r <= REPS; r++)); do + for backend in "${BACKENDS[@]}"; do + for arm in "${ARMS[@]}"; do run_one "$wl" "$backend" "$arm" "$r" benchmark || true; done + done + done + done ;; + ssim) + for wl in "${WLS[@]}"; do for backend in "${BACKENDS[@]}"; do for arm in "${ARMS[@]}"; do + run_one "$wl" "$backend" "$arm" ssim ssim || true + done; done; done ;; + esac + log "=== done ===" +} +main "${1:-run}" diff --git a/tools/device_bench/disagg/p14_report.py b/tools/device_bench/disagg/p14_report.py new file mode 100644 index 00000000..3ac26297 --- /dev/null +++ b/tools/device_bench/disagg/p14_report.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +"""Reduce matrix_p14.sh results: steady-state fps, per-thread CPU ms/frame, GPU busy share. + + python p14_report.py [--md out.md] [--json out.json] + +Steady state drops the first SKIP[wl] frames (the load frames: openra's 270/736 ms set-up +frames, rd12's single multi-second upload frame plus three) and averages the rest. The CPU window is the steady frames' +wall span ending at benchmark.json's mtime (cpusampler.c records that mtime and per-thread +schedstat on-CPU ns every 10 ms); per-thread CPU is interpolated at the window edges and divided +by the steady frame count. GPU busy = delta(kgsl gpu_clock_stats sum) / window. +""" +import argparse +import bisect +import json +import re +import statistics +import sys +from collections import defaultdict +from pathlib import Path + +SKIP = {"openra": 8, "rd12": 4} + + +def load_cpu(path): + meta = {} + series = defaultdict(list) # (pid, tid, comm) -> [(t, v)] + if not path.exists(): + return meta, series + with path.open(encoding="utf-8", errors="replace") as f: + for line in f: + if line.startswith("#"): + for k, v in re.findall(r"(\w+)=(-?\d+)", line): + meta[k] = int(v) + continue + if line.startswith("t_ns"): + continue + parts = line.rstrip("\n").split(",") + if len(parts) != 5: + continue + t, pid, tid, comm, v = parts + series[(int(pid), int(tid), comm)].append((int(t), int(v))) + return meta, series + + +def at(points, t): + ts = [p[0] for p in points] + i = bisect.bisect_left(ts, t) + if i <= 0: + return points[0][1] if points and points[0][0] - t < 50_000_000 else None + if i >= len(points): + return points[-1][1] if t - points[-1][0] < 50_000_000 else None + (t0, v0), (t1, v1) = points[i - 1], points[i] + return v0 + (v1 - v0) * (t - t0) / max(1, t1 - t0) + + +def reduce_run(run_dir, wl): + bench_path = run_dir / "benchmark.json" + if not bench_path.exists(): + return None + bench = json.loads(bench_path.read_text(encoding="utf-8", errors="replace")) + frames = bench.get("frameTimesMs") or [] + skip = SKIP.get(wl, 0) + steady = frames[skip:] + if not steady: + return None + out = { + "frames": len(frames), + "steady_frames": len(steady), + "steady_mean_ms": statistics.mean(steady), + "steady_median_ms": statistics.median(steady), + "steady_p95_ms": sorted(steady)[max(0, int(len(steady) * 0.95) - 1)], + "harness_fps": bench.get("fps"), + } + out["steady_fps"] = 1000.0 / out["steady_mean_ms"] + meta, series = load_cpu(run_dir / "cpu.csv") + end = meta.get("watch_mtime_ns", -1) + if end > 0 and series: + span = sum(steady) * 1e6 + start = end - span + threads = [] + procs = defaultdict(float) + for (pid, tid, comm), pts in series.items(): + if pid < 0: + continue + a, b = at(pts, start), at(pts, end) + if a is None or b is None: + continue + ms = (b - a) / 1e6 / len(steady) + if ms <= 0.005: + continue + threads.append({"pid": pid, "tid": tid, "comm": comm, "ms_per_frame": ms, + "util": (b - a) / span}) + procs[pid] += ms + threads.sort(key=lambda r: -r["ms_per_frame"]) + out["threads"] = threads[:8] + out["cpu_ms_per_frame_total"] = sum(t["ms_per_frame"] for t in threads) + out["process_ms_per_frame"] = dict(procs) + gpu = [v for k, v in series.items() if k[2] == "gpubusy_us"] + if gpu: + a, b = at(gpu[0], start), at(gpu[0], end) + if a is not None and b is not None: + out["gpu_busy"] = (b - a) * 1e3 / span + for name in ("gpufreq", "cpu7freq", "cpu2freq"): + pts = [v for k, v in series.items() if k[2] == name] + if pts: + vals = [v for t, v in pts[0] if start <= t <= end] + if vals: + out[name] = (min(vals), max(vals)) + return out + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("root", type=Path) + ap.add_argument("--md", type=Path) + ap.add_argument("--json", type=Path) + ap.add_argument("--threads", action="store_true", help="print per-run top threads") + args = ap.parse_args() + runs = defaultdict(list) + for bench in sorted(args.root.glob("*/*/*-r*/benchmark.json")): + run_dir = bench.parent + wl, backend = run_dir.parent.parent.name, run_dir.parent.name + m = re.fullmatch(r"(.+)-r(\d+)", run_dir.name) + if not m: + continue + verify = (run_dir / "verify.txt").read_text() if (run_dir / "verify.txt").exists() else "" + if "MISSING" in verify or "UNEXPECTED" in verify: + print(f"skip {run_dir}: {verify.strip()}", file=sys.stderr) + continue + r = reduce_run(run_dir, wl) + if r: + r["rep"] = int(m.group(2)) + r["dir"] = str(run_dir) + runs[(wl, backend, m.group(1))].append(r) + if args.threads: + tops = ", ".join(f"{t['comm']}({t['pid']})={t['ms_per_frame']:.2f}" for t in r.get("threads", [])[:5]) + print(f"{wl:7} {backend:12} {m.group(1):9} r{r['rep']} fps={r['steady_fps']:.1f} " + f"cpu={r.get('cpu_ms_per_frame_total', float('nan')):.2f} gpu={r.get('gpu_busy', float('nan')):.2f} :: {tops}") + order = {"dev": 0, "monolith": 1, "inproc": 2, "shm": 3, "tcp": 4} + rows = [] + for (wl, backend, arm), rs in sorted(runs.items(), key=lambda kv: (kv[0][0], kv[0][1], order.get(kv[0][2], 9), kv[0][2])): + fps = sorted(r["steady_fps"] for r in rs) + best = max(rs, key=lambda r: r["steady_fps"]) + med = statistics.median(fps) + main_thread = best.get("threads", [{}])[0] if best.get("threads") else {} + rows.append({ + "wl": wl, "backend": backend, "arm": arm, "n": len(rs), + "best_fps": fps[-1], "median_fps": med, + "spread_pct": 100.0 * (fps[-1] - fps[0]) / med if len(fps) > 1 else 0.0, + "best_mean_ms": best["steady_mean_ms"], "best_median_ms": best["steady_median_ms"], + "cpu_total": best.get("cpu_ms_per_frame_total"), + "top_thread": f"{main_thread.get('comm', '?')} {main_thread.get('ms_per_frame', float('nan')):.2f}" if main_thread else "-", + "top_util": main_thread.get("util"), + "second_thread": (f"{best['threads'][1]['comm']} {best['threads'][1]['ms_per_frame']:.2f}" + if len(best.get("threads", [])) > 1 else "-"), + "gpu_busy": best.get("gpu_busy"), + "procs": best.get("process_ms_per_frame"), + }) + lines = ["| workload | backend | arm | n | best fps | median fps | spread | mean ms | CPU ms/frame (all threads) | top thread ms/frame (util) | 2nd thread | GPU busy |", + "|---|---|---|---|---|---|---|---|---|---|---|---|"] + fmt = lambda v, f="{:.1f}": "-" if v is None else f.format(v) + for r in rows: + lines.append(f"| {r['wl']} | {r['backend']} | {r['arm']} | {r['n']} | {r['best_fps']:.1f} | {r['median_fps']:.1f} | " + f"{r['spread_pct']:.1f}% | {r['best_mean_ms']:.2f} | {fmt(r['cpu_total'], '{:.2f}')} | {r['top_thread']} " + f"({fmt(r['top_util'] and 100 * r['top_util'], '{:.0f}')}%) | {r['second_thread']} | {fmt(r['gpu_busy'] and 100 * r['gpu_busy'], '{:.0f}')}% |") + text = "\n".join(lines) + print(text) + if args.md: + args.md.write_text(text + "\n", encoding="utf-8") + if args.json: + args.json.write_text(json.dumps({"rows": rows, "runs": {"/".join(k): v for k, v in runs.items()}}, indent=1), encoding="utf-8") + + +if __name__ == "__main__": + main() diff --git a/tools/device_bench/disagg/pin_clocks.sh b/tools/device_bench/disagg/pin_clocks.sh new file mode 100644 index 00000000..66d6265d --- /dev/null +++ b/tools/device_bench/disagg/pin_clocks.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Pin / restore CPU and GPU clocks on a rooted bench device (P14). +# SER= bash pin_clocks.sh pin|restore|show +# pin: every cpufreq policy gets scaling_min = scaling_max = its PIN_ value (default: +# the highest available frequency <= PIN_CPU_KHZ, 2.0 GHz), the Adreno devfreq min = max = +# PIN_GPU_HZ. The pre-pin values are saved on the device (/data/local/tmp/p14-clocks.saved) +# and restore puts them back. Never pin a hot device: check the battery temperature first. +set -u +export MSYS_NO_PATHCONV=1 +SER="${SER:-HA27Q3LQ}" +PIN_CPU_KHZ="${PIN_CPU_KHZ:-2035200}" +PIN_LITTLE_KHZ="${PIN_LITTLE_KHZ:-1574400}" +PIN_GPU_HZ="${PIN_GPU_HZ:-903000000}" +KGSL=/sys/class/kgsl/kgsl-3d0/devfreq +SAVED=/data/local/tmp/p14-clocks.saved + +su_sh() { adb -s "$SER" shell "su -c '$1'"; } + +case "${1:-show}" in + pin) + su_sh "test -f $SAVED || { for p in /sys/devices/system/cpu/cpufreq/policy*; do echo \$p \$(cat \$p/scaling_min_freq) \$(cat \$p/scaling_max_freq); done; echo $KGSL \$(cat $KGSL/min_freq) \$(cat $KGSL/max_freq); } > $SAVED" + su_sh "for p in /sys/devices/system/cpu/cpufreq/policy*; do want=$PIN_CPU_KHZ; [ \$p = /sys/devices/system/cpu/cpufreq/policy0 ] && want=$PIN_LITTLE_KHZ; f=0; for a in \$(cat \$p/scaling_available_frequencies); do [ \$a -le \$want ] && f=\$a; done; echo \$f > \$p/scaling_max_freq; echo \$f > \$p/scaling_min_freq; echo \$f > \$p/scaling_max_freq; done; echo $PIN_GPU_HZ > $KGSL/max_freq; echo $PIN_GPU_HZ > $KGSL/min_freq; echo $PIN_GPU_HZ > $KGSL/max_freq" + "$0" show ;; + restore) + su_sh "test -f $SAVED && while read p mn mx; do if [ \$p = $KGSL ]; then echo \$mx > \$p/max_freq; echo \$mn > \$p/min_freq; else echo \$mx > \$p/scaling_max_freq; echo \$mn > \$p/scaling_min_freq; fi; done < $SAVED; rm -f $SAVED" + "$0" show ;; + show) + su_sh "for p in /sys/devices/system/cpu/cpufreq/policy*; do echo \$p cur=\$(cat \$p/scaling_cur_freq) min=\$(cat \$p/scaling_min_freq) max=\$(cat \$p/scaling_max_freq); done; echo gpu cur=\$(cat $KGSL/cur_freq) min=\$(cat $KGSL/min_freq) max=\$(cat $KGSL/max_freq); dumpsys battery | grep temperature" ;; +esac