diff --git a/tools/device_bench/disagg/callee_split.py b/tools/device_bench/disagg/callee_split.py index 9d64f082..914881ed 100644 --- a/tools/device_bench/disagg/callee_split.py +++ b/tools/device_bench/disagg/callee_split.py @@ -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 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) diff --git a/tools/device_bench/disagg/components.py b/tools/device_bench/disagg/components.py index c960e3ce..1edf7306 100644 --- a/tools/device_bench/disagg/components.py +++ b/tools/device_bench/disagg/components.py @@ -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 [cpu_ms_per_frame] + python components.py [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] diff --git a/tools/device_bench/disagg/profile_p14.sh b/tools/device_bench/disagg/profile_p14.sh index d08bba9d..acf52a08 100644 --- a/tools/device_bench/disagg/profile_p14.sh +++ b/tools/device_bench/disagg/profile_p14.sh @@ -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= 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)"