[Tools] (P14): sampler sees server processes and GPU busy every tick, arms can name a second build, and a simpleperf profile helper.

This commit is contained in:
2026-10-07 10:07:44 -04:00
parent a9108e1a0b
commit 2b0857b016
4 changed files with 173 additions and 24 deletions
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""Split one function's inclusive samples by its direct callees (and by malloc/atomic leaves).
python callee_split.py <perf.data> <binary_cache> <function substring> [--depth 1] [--thread NAME]
Uses the NDK's simpleperf_report_lib (set SIMPLEPERF_DIR to <ndk>/simpleperf). For every sample
whose call chain contains a frame matching <function substring>, attributes the sample to the
frame directly below it (its callee), or to "<self>" when it is the leaf. Prints the percentage
of all samples of the selected threads.
"""
import argparse
import os
import sys
from collections import Counter
sys.path.insert(0, os.environ.get("SIMPLEPERF_DIR", ""))
from simpleperf_report_lib import ReportLib # noqa: E402
def short(name):
name = name.split("(")[0]
return name.replace("MobileGL::", "").replace("MG_Backend::DirectVulkan::", "DV::")[-110:]
def main():
ap = argparse.ArgumentParser()
ap.add_argument("data")
ap.add_argument("symfs")
ap.add_argument("function")
ap.add_argument("--depth", type=int, default=1)
ap.add_argument("--thread", default=None)
ap.add_argument("--top", type=int, default=40)
ap.add_argument("--leaf", action="store_true",
help="match <function> against the LEAF only and attribute to the nearest caller "
"outside the allocator/libc/atomics (who called malloc?)")
args = ap.parse_args()
lib = ReportLib()
lib.SetRecordFile(args.data)
lib.SetSymfs(args.symfs)
total = 0
hit = 0
callees = Counter()
while True:
sample = lib.GetNextSample()
if sample is None:
break
if args.thread and args.thread not in sample.thread_comm:
continue
total += 1
# frames leaf first: the sample's own symbol, then callchain entries (callers)
frames = [lib.GetSymbolOfCurrentSample().symbol_name]
chain = lib.GetCallChainOfCurrentSample()
for i in range(chain.nr):
frames.append(chain.entries[i].symbol.symbol_name)
if args.leaf:
if args.function not in frames[0]:
continue
hit += 1
noise = ("scudo", "malloc", "free", "operator new", "operator delete", "__aarch64_", "@plt",
"memmove", "memcpy", "memset", "__emutls", "pthread_", "shared_count", "__release_shared",
"!!!", "std::__ndk1::")
owner = next((f for f in frames[1:] if not any(n in f for n in noise)), "<none>")
callees[short(owner)] += 1
continue
# find the outermost (closest to root) matching frame so recursion counts once
idx = None
for i in range(len(frames) - 1, -1, -1):
if args.function in frames[i]:
idx = i
break
if idx is None:
continue
hit += 1
if idx == 0:
callees["<self>"] += 1
continue
key = " > ".join(short(frames[j]) for j in range(idx - 1, max(idx - 1 - args.depth, -1), -1))
callees[key] += 1
print(f"samples={total} in '{args.function}'={hit} ({100.0 * hit / max(1, total):.1f}%)")
for key, n in callees.most_common(args.top):
print(f"{100.0 * n / max(1, total):6.2f}% {key}")
if __name__ == "__main__":
main()
+20 -16
View File
@@ -5,7 +5,7 @@
//
// 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
// (the replay process) and of every libMobileGLServer.so process (spawn/tcp), 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
@@ -20,7 +20,7 @@
#include <time.h>
#include <unistd.h>
#define MAXP 8
#define MAXP 16
#define MAXT 512
#define RING 4096
@@ -94,7 +94,11 @@ static int ScanPids(const char* pkg, int* pids) {
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);
// The replay process (and its :services), plus the render-server processes the spawn and
// tcp arms exec (their cmdline is the server library's name, not the package's).
if ((strncmp(buf, pkg, len) == 0 && (buf[len] == 0 || buf[len] == ':')) ||
strncmp(buf, "libMobileGLServer.so", 20) == 0)
pids[n++] = atoi(e->d_name);
}
closedir(d);
return n;
@@ -148,20 +152,20 @@ int main(int argc, char** argv) {
}
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);
// 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 (iteration % 10 == 0) {
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)
+17 -8
View File
@@ -33,7 +33,16 @@ 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"; }
# An arm is <kind>[@<tag>]: the tag picks another installed build of the trace APK
# (top.mobilegl.plugin.<tag>.trace), so two builds can be interleaved in one matrix.
arm_kind() { echo "${1%@*}"; }
pkg_of() {
case "$1" in
*@*) echo "top.mobilegl.plugin.${1#*@}.trace" ;;
dev) echo "$PKG_DEV" ;;
*) echo "$PKG_FEAT" ;;
esac
}
app_of() { echo "/data/user/0/$1/files/trace-replay"; }
wl_args() { # trace golden w h target crop
@@ -44,7 +53,7 @@ wl_args() { # trace golden w h target crop
}
arm_env() {
case "$1" in
case "$(arm_kind "$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" ;;
@@ -93,13 +102,13 @@ verify_run() {
{
echo "arm=$arm backend=$backend"
grep -aq "Active backend type set to $backend" "$all" && echo "BACKEND=OK" || echo "BACKEND=MISSING"
case "$arm" in
case "$(arm_kind "$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
case "$(arm_kind "$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"
@@ -114,11 +123,11 @@ run_one() {
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 "am force-stop $PKG_FEAT; am force-stop $PKG_DEV; am force-stop $pkg" >/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
if [ "$(arm_kind "$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
@@ -148,7 +157,7 @@ run_one() {
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
if [ "$(arm_kind "$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
@@ -170,7 +179,7 @@ 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" ;;
prep) for p in ${PREP_PKGS:-$PKG_FEAT $PKG_DEV}; do prep_pkg "$p"; done ;;
run)
for wl in "${WLS[@]}"; do
for ((r = 1; r <= REPS; r++)); do
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# One simpleperf profile of a matrix_p14 arm, recorded inside the steady window.
# ARM=monolith BACKEND=DirectVulkan WL=rd12 DELAY=30 DUR=6 TAG=prof bash profile_p14.sh
# DELAY is the seconds from launch to the start of recording (past the load frame), DUR the
# recording length. Records every thread of the replay process (and of the server process for
# spawn arms) with frame-pointer call graphs, then reports on the host against the unstripped
# libraries in SYMDIR. Clocks are expected to be pinned by the caller (pin_clocks.sh).
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}"
ARM="${ARM:-monolith}" BACKEND="${BACKEND:-DirectVulkan}" WL="${WL:-rd12}"
DELAY="${DELAY:-30}" DUR="${DUR:-6}" TAG="${TAG:-prof}"
PKG_FEAT="${PKG_FEAT:-top.mobilegl.plugin.p14.trace}" PKG_DEV="${PKG_DEV:-top.mobilegl.plugin.p14dev.trace}"
SYMDIR="${SYMDIR:?unstripped library directory}"
NDK="${NDK:-/c/Users/yello/AppData/Local/Android/Sdk/ndk/27.3.13750724}"
SP_HOST="$NDK/simpleperf/bin/windows/x86_64/simpleperf.exe"
OUT="${PERF_WORK:-$REPO_ROOT/.trace-work/perf-p14}/$TAG/$WL-$BACKEND-$ARM"
mkdir -p "$OUT"
pkg=$([ "$ARM" = dev ] && echo "$PKG_DEV" || echo "$PKG_FEAT")
app=/data/user/0/$pkg/files/trace-replay
case "$ARM" in
dev|monolith) env="" ;;
inproc) env="MOBILEGL_TRANSPORT=inproc;MOBILEGL_IPC_RUN_AHEAD=1" ;;
shm) env="MOBILEGL_TRANSPORT=spawn;MOBILEGL_IPC_CONTROL=fork;MOBILEGL_IPC_DATA=auto;MOBILEGL_IPC_RUN_AHEAD=1" ;;
esac
[ -n "${EXTRA_ENV:-}" ] && env="${env:+$env;}$EXTRA_ENV"
case "$WL" in
openra) args="--el target_call 31249 --ei width 640 --ei height 480" ;;
rd12) args="--el target_call 4660351 --ei width 854 --ei height 480" ;;
esac
adb -s "$SER" shell "am force-stop $PKG_FEAT; am force-stop $PKG_DEV; run-as $pkg sh -c 'rm -f $app/output/*.json'" >/dev/null 2>&1
adb -s "$SER" shell "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 $args --ez use_pbuffer true --ez benchmark true --es benchmark_result_path $app/output/benchmark.json --es mobilegl_env '$env'" >/dev/null
sleep "$DELAY"
pids=$(adb -s "$SER" shell "su -c 'echo \$(pidof $pkg) \$(pgrep -f ^libMobileGLServer.so)'" | tr -d '\r' | tr -s ' ' ',' | sed 's/^,//; s/,$//')
echo "pids=$pids" | tee "$OUT/pids.txt"
adb -s "$SER" shell "su -c 'simpleperf record -e cpu-clock -p $pids --call-graph fp -f 4000 --duration $DUR -o /data/local/tmp/p14perf.data'" >"$OUT/record.log" 2>&1
adb -s "$SER" exec-out "run-as $pkg cat $app/output/benchmark.json" >"$OUT/benchmark.json" 2>/dev/null
until adb -s "$SER" exec-out "run-as $pkg cat $app/output/benchmark.json" 2>/dev/null | grep -q '"fps"'; do sleep 2; done
adb -s "$SER" exec-out "run-as $pkg cat $app/output/benchmark.json" >"$OUT/benchmark.json" 2>/dev/null
adb -s "$SER" shell "am force-stop $pkg" >/dev/null 2>&1
adb -s "$SER" shell "su -c 'chmod 644 /data/local/tmp/p14perf.data'"
adb -s "$SER" pull /data/local/tmp/p14perf.data "$(cygpath -w "$OUT/perf.data")" >/dev/null
W_OUT=$(cygpath -w "$OUT"); W_SYM=$(cygpath -w "$SYMDIR")
# binary_cache/ mirrors the device paths: our unstripped libraries by build id, the rest pulled.
(cd "$OUT" && ANDROID_SERIAL="$SER" python "$(cygpath -w "$NDK/simpleperf/binary_cache_builder.py")" -i perf.data -lib "$W_SYM" >binary_cache.log 2>&1)
"$SP_HOST" report -i "$W_OUT\perf.data" --symfs "$W_OUT\binary_cache" --sort comm,tid --percent-limit 1 >"$OUT/threads.txt" 2>/dev/null
"$SP_HOST" report -i "$W_OUT\perf.data" --symfs "$W_OUT\binary_cache" --sort comm,symbol --percent-limit 0.3 >"$OUT/self.txt" 2>/dev/null
"$SP_HOST" report -i "$W_OUT\perf.data" --symfs "$W_OUT\binary_cache" --children --sort comm,symbol --percent-limit 1.5 >"$OUT/children.txt" 2>/dev/null
echo "wrote $OUT"; head -30 "$OUT/threads.txt"