#!/usr/bin/env bash # Pre-release frame-regression profile for the Pad editor. # # Measures whether the app generates frames WITHOUT a cause. Frame emission # is event-driven (architecture.md §2), so a correct app emits a small # number of frames per user action and NOTHING while idle. This script # drives a fixed sequence of actions on a connected device/emulator and # checks the in-app profiler's CSV for phases that exceed their frame # budget. The headless contract of the same guard is # TestNoFramesWhileIdle (internal/test/e2e), which runs in the regular # go-test suite. # # Usage: # scripts/profile_emulator.sh [-s SERIAL] [-o OUT_DIR] # # Driven sequence (one app run): # launch -> 8s idle (browser) -> open seeded file (debug cmd) -> 16s (settle + # editor idle) -> scroll -> 2.5s -> scroll -> 2.5s -> scroll # # Analysis: the profiler marks rows whose previous frame is >= 100ms away # as gaps, and flushes the CSV when a burst ends, so the CSV is complete # and self-anchoring (no clock correlation). Rows are grouped into PHASES # by splitting at gaps >= 2s (the script's action windows are all >= 2s # apart; late async frames — list load, line-index build — land 100-200ms # after their action and stay in the phase). Expected phases: # 1. browser startup (+ 8s idle) budget 10 frames # 2. open (+ settle + 16s editor idle) budget 20 frames # 3..N. one per driven scroll budget 8 frames each # A phase over budget means frames were emitted without (enough) cause — # a ticker, a re-emission loop, a feedback ping-pong; a 1/s spinner alone # exceeds a 16s idle phase's budget. The PERF / PERF-PRESENT logcat lines # are printed for a human to eyeball live fps and latency. # # Requirements: adb, the app installed, and (Android 11+) the All-files # access appop (set automatically when possible). set -euo pipefail SERIAL="" OUT="/tmp/pad-prof" while getopts ":s:o:h" flag; do case "$flag" in s) SERIAL="$OPTARG" ;; o) OUT="$OPTARG" ;; h) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; *) echo "unknown option" >&2; exit 2 ;; esac done PKG=pad.pad ACT=pad.pad/org.gioui.GioActivity PERFDIR=/storage/emulated/0/PadPerf # The browser opens at the app's start path (cmd/pad/impl_android.go); # seed a known scrollable file there so the tap opens real content — it is # the newest file and the list is date-desc, so it is the first row. NOTES=/storage/emulated/0/Notes PROFILE_FILE=padprofile.txt # Frame budgets per phase index (1-based): startup, open, then per scroll. BUDGET_STARTUP="${BUDGET_STARTUP:-10}" BUDGET_OPEN="${BUDGET_OPEN:-20}" BUDGET_SCROLL="${BUDGET_SCROLL:-8}" if [ -z "$SERIAL" ]; then SERIAL=$(adb devices | awk 'NR>1 && $2=="device" {print $1; exit}') if [ -z "$SERIAL" ]; then echo "no connected device; pass -s SERIAL" >&2 exit 1 fi fi adb() { command adb -s "$SERIAL" "$@"; } echo "== profiling on $SERIAL ==" adb shell "mkdir -p $PERFDIR $NOTES" adb shell "touch $PERFDIR/enable" adb shell appops set "$PKG" MANAGE_EXTERNAL_STORAGE allow 2>/dev/null || true mkdir -p "$OUT" CSV="$OUT/logic_frames.csv" awk 'BEGIN { for (i = 1; i <= 4000; i++) printf "profile line %04d: the quick brown fox jumps over the lazy dog\n", i }' > "$OUT/$PROFILE_FILE" adb push "$OUT/$PROFILE_FILE" "$NOTES/$PROFILE_FILE" >/dev/null cleanup() { adb shell "rm -f $NOTES/$PROFILE_FILE" 2>/dev/null || true } trap cleanup EXIT # parse_csv prints one line per phase: frame count, max in-phase (non-gap) # delta (ms), and the pages covered. Phases are the frame-runs split at # gaps >= 2s (the rows themselves are still counted in their phase). parse_csv() { awk -F, ' function emit() { if (n > 0) printf "frames=%-4d maxDelta=%7.1f ms page=%s\n", n, md, pg n = 0; md = 0; pg = "" } { if ($10 == "true" && $3 + 0 >= 2000) emit() if (n == 0) pg = $4; else if ($4 != pg) pg = pg "+" $4 n++ if ($10 != "true" && $3 + 0 > md) md = $3 + 0 } END { emit() } ' "$1" } fail=0 adb shell am force-stop "$PKG" adb shell "rm -f $PERFDIR/logic_frames.csv $PERFDIR/cmd" adb logcat -c adb shell am start -n "$ACT" >/dev/null echo "-- app launched; driving: 8s idle, open (debug cmd), 16s, scroll x3" sleep 8 adb shell "echo 'open $NOTES/$PROFILE_FILE' > $PERFDIR/cmd" sleep 16 adb shell "echo 'frac 0.5' > $PERFDIR/cmd"; sleep 2.5 adb shell "echo 'frac 0.2' > $PERFDIR/cmd"; sleep 2.5 adb shell "echo 'frac 0.4' > $PERFDIR/cmd"; sleep 2 adb shell am force-stop "$PKG" adb pull "$PERFDIR/logic_frames.csv" "$CSV" >/dev/null echo "-- logcat PERF lines (live view; fps < 1 means no continuous redraw):" adb logcat -d | grep "I $PKG.*PERF" | sed 's/^/ /' || true echo "-- phases (split at idle gaps >= 2s):" PHASES=$(parse_csv "$CSV") echo "$PHASES" | sed 's/^/ /' # The seeded file is 4000 lines: a frame with TotalLines >= 4000 proves the # editor actually opened it (the 'open' cmd goes through the same OpenFile # path as a browser tap). if ! awk -F, '$7 >= 4000 { found = 1 } END { exit !found }' "$CSV"; then echo "FAIL: the seeded file was never opened (no frame with" \ "TotalLines >= 4000) — check the debug 'open' command in logcat." >&2 fail=1 fi i=1 while IFS= read -r line; do n=$(echo "$line" | sed 's/.*frames=//; s/ .*//') if [ "$i" -eq 1 ]; then budget="$BUDGET_STARTUP" elif [ "$i" -eq 2 ]; then budget="$BUDGET_OPEN" else budget="$BUDGET_SCROLL"; fi if [ "$n" -gt "$budget" ]; then echo "FAIL: phase $i has $n frames (budget $budget) — frames are" \ "being generated without a cause: $line" >&2 fail=1 fi i=$((i + 1)) done <<< "$PHASES" echo if [ "$fail" -ne 0 ]; then echo "RESULT: FAIL — unnecessary frame generation detected (details above)." >&2 exit 1 fi echo "RESULT: PASS — every phase is within its frame budget." echo "CSV saved: $CSV"