The on-device profile is demoted to a diagnostic: the run force-stops the app and seeds/removes a file in its storage, and the developer may be using the phone while a release runs. The release gate is now explicitly (1) TestNoFramesWhileIdle in the go-test suite and (2) scripts/profile_emulator.sh on the EMULATOR. - script: auto-select picks an emulator only (previously any connected device); no emulator -> clear error; a physical device passed via -s prints a DIAGNOSTIC (non-gating) banner and a heads-up. - doc: architecture.md §11 states the two gate tiers and the diagnostic use of the physical-device run.
169 lines
6.7 KiB
Bash
Executable File
169 lines
6.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Pre-release frame-regression profile for the Pad editor.
|
|
#
|
|
# RELEASE GATE ON THE EMULATOR ONLY. The frame-regression guard has two
|
|
# tiers (architecture.md §11):
|
|
# 1. TestNoFramesWhileIdle (internal/test/e2e) — headless, in the regular
|
|
# go-test suite, runs on every build;
|
|
# 2. this script, run on the EMULATOR, as the pre-release checklist item.
|
|
# Runs on a PHYSICAL device are DIAGNOSTIC only — never a gate. The script
|
|
# force-stops the app and seeds/removes a file in its storage; the developer
|
|
# may be using the phone while a release runs, so physical devices are
|
|
# never auto-selected. Pass -s <phone serial> explicitly for a diagnostic
|
|
# run (a banner reminds you the result is informational).
|
|
#
|
|
# 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 and checks the in-app profiler's CSV
|
|
# for phases that exceed their frame budget.
|
|
#
|
|
# 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
|
|
# Auto-select an EMULATOR only: physical devices are diagnostic runs and
|
|
# must be requested explicitly (the developer may be using the phone).
|
|
SERIAL=$(adb devices | awk 'NR>1 && $2=="device" && $1 ~ /^emulator-/ {print $1; exit}')
|
|
if [ -z "$SERIAL" ]; then
|
|
echo "no connected emulator; start one (adb emulator / AVD Manager)" >&2
|
|
echo "or pass -s <phone serial> for a DIAGNOSTIC (non-gating) run" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
adb() { command adb -s "$SERIAL" "$@"; }
|
|
|
|
case "$SERIAL" in
|
|
emulator-*) echo "== release-gate profile on emulator $SERIAL ==" ;;
|
|
*) echo "== DIAGNOSTIC (non-gating) profile on physical device $SERIAL =="
|
|
echo " (release gate = emulator; this run force-stops the app and"
|
|
echo " seeds a file in its storage — do not run on a phone in use)" ;;
|
|
esac
|
|
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"
|