[Tools] (P14): pinned-clock perf matrix with a per-thread CPU and GPU-busy sampler.

This commit is contained in:
2026-10-07 09:18:21 -04:00
parent f8a9466b93
commit d56179d876
4 changed files with 583 additions and 0 deletions
+190
View File
@@ -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 <package> <interval_ms> <watch_file> <out.csv> [max_s]
//
// Every interval it reads /proc/<pid>/task/<tid>/schedstat (on-CPU ns, not the 10 ms tick
// counters of stat) for every thread of every process whose cmdline starts with <package>
// (the replay process and, for spawn/tcp, the render-server process), and keeps the last
// kRingSeconds of samples in memory. When <watch_file> (benchmark.json) appears it dumps the
// ring to <out.csv> 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 <dirent.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#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 <package> <interval_ms> <watch_file> <out.csv> [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;
}
+189
View File
@@ -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/<TAG>/<wl>/<backend>/<arm>-r<n>/ ; 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 <wl> <backend> <arm> <rep> <mode:benchmark|ssim>
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}"
+175
View File
@@ -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 <results_dir> [--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()
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Pin / restore CPU and GPU clocks on a rooted bench device (P14).
# SER=<serial> bash pin_clocks.sh pin|restore|show
# pin: every cpufreq policy gets scaling_min = scaling_max = its PIN_<policy> 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