[Tools] (P14): the profiler records the whole run and the analysis cuts the steady tail.

This commit is contained in:
2026-10-07 10:50:39 -04:00
parent 4ef9705ee0
commit eafe1179b3
3 changed files with 74 additions and 25 deletions
+23 -1
View File
@@ -17,6 +17,26 @@ sys.path.insert(0, os.environ.get("SIMPLEPERF_DIR", ""))
from simpleperf_report_lib import ReportLib # noqa: E402
def tail_start(data, symfs, tail_ms):
"""Timestamp (ns) where the last `tail_ms` of the recording begins, or 0 for everything."""
if not tail_ms:
return 0
lib = ReportLib()
lib.SetRecordFile(data)
lib.SetSymfs(symfs)
# The end is the replay's last retraced call, not the recording's last sample: the app
# tears its context down after the benchmark ends, and that must not land in the window.
last = 0
while True:
sample = lib.GetNextSample()
if sample is None:
break
chain = lib.GetCallChainOfCurrentSample()
if any("retrace::retraceCall" in chain.entries[i].symbol.symbol_name for i in range(chain.nr)):
last = max(last, sample.time)
return last - int(tail_ms * 1e6)
def short(name):
name = name.split("(")[0]
return name.replace("MobileGL::", "").replace("MG_Backend::DirectVulkan::", "DV::")[-110:]
@@ -30,10 +50,12 @@ def main():
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("--tail-ms", type=float, default=0.0, help="only the last N ms of the recording")
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()
start = tail_start(args.data, args.symfs, args.tail_ms)
lib = ReportLib()
lib.SetRecordFile(args.data)
lib.SetSymfs(args.symfs)
@@ -44,7 +66,7 @@ def main():
sample = lib.GetNextSample()
if sample is None:
break
if args.thread and args.thread not in sample.thread_comm:
if (args.thread and args.thread not in sample.thread_comm) or sample.time < start:
continue
total += 1
# frames leaf first: the sample's own symbol, then callchain entries (callers)
+27 -4
View File
@@ -2,7 +2,7 @@
"""Inclusive share of fixed components on one thread of a simpleperf profile, converted to
milliseconds per frame with the thread's measured CPU ms/frame.
python components.py <perf.data> <binary_cache> <thread comm> [cpu_ms_per_frame]
python components.py <perf.data> <binary_cache> <thread comm> [cpu_ms_per_frame] [--tail-ms=N]
A sample counts toward a component when any frame of its call chain matches the component's
pattern (first match in COMPONENTS order wins for the 'exclusive' column, so the rows of that
@@ -15,6 +15,26 @@ from collections import Counter
sys.path.insert(0, os.environ.get("SIMPLEPERF_DIR", ""))
from simpleperf_report_lib import ReportLib # noqa: E402
def tail_start(data, symfs, tail_ms):
"""Timestamp (ns) where the last `tail_ms` of the recording begins, or 0 for everything."""
if not tail_ms:
return 0
lib = ReportLib()
lib.SetRecordFile(data)
lib.SetSymfs(symfs)
# The end is the replay's last retraced call, not the recording's last sample: the app
# tears its context down after the benchmark ends, and that must not land in the window.
last = 0
while True:
sample = lib.GetNextSample()
if sample is None:
break
chain = lib.GetCallChainOfCurrentSample()
if any("retrace::retraceCall" in chain.entries[i].symbol.symbol_name for i in range(chain.nr)):
last = max(last, sample.time)
return last - int(tail_ms * 1e6)
COMPONENTS = [
("trace parse (harness)", ("trace::Parser::", "BrotliFile", "trace::Call::~Call", "trace::File::")),
("validate point", ("MGPipeValidateForVerb",)),
@@ -29,8 +49,11 @@ COMPONENTS = [
def main():
data, symfs, comm = sys.argv[1], sys.argv[2], sys.argv[3]
cpu = float(sys.argv[4]) if len(sys.argv) > 4 else None
args = [a for a in sys.argv[1:] if not a.startswith("--tail-ms=")]
tail = next((float(a.split("=", 1)[1]) for a in sys.argv[1:] if a.startswith("--tail-ms=")), 0.0)
data, symfs, comm = args[0], args[1], args[2]
cpu = float(args[3]) if len(args) > 3 else None
start = tail_start(data, symfs, tail)
lib = ReportLib()
lib.SetRecordFile(data)
lib.SetSymfs(symfs)
@@ -41,7 +64,7 @@ def main():
sample = lib.GetNextSample()
if sample is None:
break
if comm not in sample.thread_comm:
if comm not in sample.thread_comm or sample.time < start:
continue
total += 1
frames = [lib.GetSymbolOfCurrentSample().symbol_name]
+24 -20
View File
@@ -1,21 +1,22 @@
#!/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).
# One simpleperf profile of a matrix_p14 arm.
# ARM=monolith BACKEND=DirectVulkan WL=rd12 TAG=prof SYMDIR=<unstripped libs> bash profile_p14.sh
# Records the whole run (every thread of the app's processes, the spawn/tcp render server
# included, via --app) with frame-pointer call graphs, stops when benchmark.json appears, and
# builds a binary cache against the unstripped libraries in SYMDIR. The steady window is cut out
# afterwards: components.py / callee_split.py take --tail-ms (the steady frames' wall span from
# benchmark.json), so no launch-to-steady delay has to be guessed. 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}"
ARM="${ARM:-monolith}" BACKEND="${BACKEND:-DirectVulkan}" WL="${WL:-rd12}" TAG="${TAG:-prof}"
FREQ="${FREQ:-2000}"
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")
@@ -31,21 +32,24 @@ case "$WL" in
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 "su -c 'pkill -INT simpleperf; rm -f /data/local/tmp/p14perf.data'" >/dev/null 2>&1
# --app follows the package's processes and their new threads from the moment they exist.
adb -s "$SER" shell "su -c 'simpleperf record --app $pkg -e cpu-clock --call-graph fp -f $FREQ -o /data/local/tmp/p14perf.data'" >"$OUT/record.log" 2>&1 &
sp_pid=$!
sleep 2
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 $pkg.*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
waited=0
until adb -s "$SER" exec-out "run-as $pkg cat $app/output/benchmark.json" 2>/dev/null | grep -q '"fps"'; do
sleep 1; waited=$((waited + 1)); [ "$waited" -ge 600 ] && break
done
adb -s "$SER" shell "su -c 'pkill -INT simpleperf'" >/dev/null 2>&1
wait "$sp_pid"
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")
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"
tail -1 "$OUT/record.log"
echo "wrote $OUT (steady tail: $(python -c "import json,sys; f=json.load(open(sys.argv[1]))['frameTimesMs']; print(round(sum(f[4:]),1))" "$(cygpath -w "$OUT/benchmark.json")") ms)"