b72847b822
With the install watcher answering MIUI's dialog, all four tests ran and three produced numbers: coldStart 574.17 ms P50 (553.95-730.19), opening a 1000-file folder 14 frames P50 (13-17), scrolling it with four swipes 98 frames P50 (93-100). The search benchmark still fails, on the app bar's search action not being in the accessibility tree after twenty seconds - the setup wait is raised from ten to twenty now that the folder render is known to be the slow step. The frame counts are labelled as frame counts and not as jank, because this phone reports no frame durations at all: the same results file contains frameCount and nothing else, so 14 frames says the interaction is short and says nothing about whether a frame was slow. The doc also keeps the measurements that explain the watcher's shape - the dialog is FLAG_SECURE, its lifetime varies between under a second and 8.5 s, and pm install from an app uid fails earlier with MIUI's XSpace SecurityException - along with the honest cost: MIUI keeps mCurrentFocus on the installer after the window is gone, so a coordinate tap can land on the home screen, which it did. The watcher no longer reuses a saved position at all and refuses to tap within two seconds of the previous tap.
206 lines
8.6 KiB
Bash
Executable File
206 lines
8.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Answers MIUI's "Install via USB" dialog while installs are happening on the device.
|
|
#
|
|
# ./tools/benchmark-watch-install.sh # ten minutes, poll every 2 s
|
|
# ./tools/benchmark-watch-install.sh --interval 5 # poll less often when idle
|
|
# ./tools/benchmark-watch-install.sh --burst 30 # keep watching fast for 30 s after a dialog
|
|
# ./tools/benchmark-watch-install.sh --dry-run # report, never tap
|
|
#
|
|
# Run this for *every* install, not just the first: a benchmark run installs the test APK and then, per
|
|
# test, the app under test. Measured the hard way — the first version of this script spent three to four
|
|
# seconds reading one dialog, and the next dialog opened and closed inside that window.
|
|
#
|
|
# How it answers a dialog:
|
|
#
|
|
# 1. If the Install button's position for this dialog was measured before (and saved), tap it — but
|
|
# only after re-checking, immediately beforehand, that an installer window is still focused. That
|
|
# check is what stops a tap from landing on the home screen once the dialog closes.
|
|
# 2. Otherwise read the screen (`uiautomator dump`), find the button by label — refusing anything that
|
|
# means "no" — verify the installer is still focused, then tap its own bounds and save the position
|
|
# for the dialogs that cannot be read in time.
|
|
# 3. If no Install button can be read and no position is known, tap nothing and say so.
|
|
#
|
|
# After every dialog the script watches in "burst" mode (every 0.4 s instead of the idle interval),
|
|
# because installs arrive in pairs and the second one used to slip through while the first was read.
|
|
#
|
|
# Measured on the Xiaomi phone (Android 12, MIUI 14): an adb-started install raises the dialog after
|
|
# about 1.7 s with a ten second deny countdown, so there is time for one dump and one tap; the same
|
|
# dialog is FLAG_SECURE, so a screencap of it shows whatever is behind it.
|
|
# No `-e`: this is a watcher that has to survive for the whole run, and a single failed adb call must not
|
|
# end it. It ended silently once before, in the middle of a benchmark run, which is why it looked like it
|
|
# had "stopped running" — a helper ending in `| head -1` takes a SIGPIPE when `head` exits first, and
|
|
# `pipefail` plus `-e` turned that into an exit.
|
|
set -uo pipefail
|
|
|
|
seconds=600
|
|
interval=2
|
|
burst=30
|
|
wait=10
|
|
dry_run=0
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--seconds) seconds=$2; shift 2 ;;
|
|
--interval) interval=$2; shift 2 ;;
|
|
--burst) burst=$2; shift 2 ;;
|
|
--wait) wait=$2; shift 2 ;;
|
|
--dry-run) dry_run=1; shift ;;
|
|
-h|--help) sed -n '2,26p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
*) echo "usage: $0 [--seconds N] [--interval N] [--burst N] [--wait N] [--dry-run]" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
command -v adb >/dev/null || { echo "benchmark-watch-install: adb is not on PATH" >&2; exit 1; }
|
|
command -v python3 >/dev/null || { echo "benchmark-watch-install: python3 is needed to read the UI dump" >&2; exit 1; }
|
|
|
|
device_dump=/sdcard/.benchmark-watch.xml
|
|
local_dump=$(mktemp)
|
|
trap 'rm -f "$local_dump"' EXIT
|
|
|
|
answered_token=""
|
|
tapped_token=""
|
|
last_tap_at=-100
|
|
opened_windows=0
|
|
taps=0
|
|
misses=0
|
|
|
|
focused_window() {
|
|
adb shell dumpsys window 2>/dev/null | grep -m1 'mCurrentFocus' || true
|
|
}
|
|
|
|
# Only the installer's own windows count; anything else is left alone.
|
|
is_installer() {
|
|
case "$1" in
|
|
*securitycenter*install*|*InstallApp*|*packageinstaller*) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
# The window token, so the same dialog is not answered twice while it closes.
|
|
window_token() {
|
|
sed -n 's/.*mCurrentFocus=Window{\([0-9a-f]*\).*/\1/p' <<<"$1" | head -1 || true
|
|
}
|
|
|
|
# The activity that owns the focused window. Dialogs each get their own window token, so the token
|
|
# cannot be used to tell "the dialog I read is still up" from "another installer dialog replaced it" —
|
|
# which is exactly what happens when a run installs twice in quick succession. The activity name is
|
|
# stable across both, and it is still specific enough that a tap can only ever land on an installer.
|
|
installer_activity() {
|
|
sed -n 's/.*u0 \([^ }]*\).*/\1/p' <<<"$1" | head -1 || true
|
|
}
|
|
|
|
# Prints "<x> <y> <label>" for the dialog's Install button, or nothing at all.
|
|
install_button() {
|
|
adb shell uiautomator dump "$device_dump" >/dev/null 2>&1 || return 0
|
|
adb pull "$device_dump" "$local_dump" >/dev/null 2>&1 || return 0
|
|
python3 - "$local_dump" <<'PY' || true
|
|
import sys
|
|
import xml.etree.ElementTree as ET
|
|
|
|
DENY = ('verweigern', 'deny', 'ablehnen', 'abbrechen', 'cancel', 'nein', 'no')
|
|
ALLOW = ('installieren', 'install', 'erlauben', 'zulassen', 'allow')
|
|
|
|
def centre(node):
|
|
x1, y1, x2, y2 = (int(v) for v in node.get('bounds').strip('[]').replace('][', ',').split(','))
|
|
return (x1 + x2) // 2, (y1 + y2) // 2
|
|
|
|
for node in ET.parse(sys.argv[1]).getroot().iter('node'):
|
|
if 'Button' not in node.get('class', ''):
|
|
continue
|
|
text = (node.get('text') or '').strip()
|
|
low = text.lower()
|
|
if not text or any(low.startswith(d) for d in DENY):
|
|
continue
|
|
if any(low.startswith(a) for a in ALLOW) or node.get('resource-id', '').endswith('button2'):
|
|
x, y = centre(node)
|
|
print(x, y, text.replace(' ', '_'))
|
|
break
|
|
PY
|
|
}
|
|
|
|
# Reads the button, and taps it only if the dialog is *still* the focused window at that moment. Called
|
|
# only while an installer window is up.
|
|
#
|
|
# The re-check is the whole point of this function. A dump takes three to four seconds, which is longer
|
|
# than the dialog can live, so the position read from a dying dialog used to be tapped after it had
|
|
# closed — and the tap landed on whatever the home screen had at those coordinates (a Telegram icon, in
|
|
# the run that made this obvious). Nothing here is remembered between dialogs either, for the same
|
|
# reason: a remembered position is a blind tap waiting to happen.
|
|
answer_dialog() {
|
|
local focus=$1 token activity label now_focus
|
|
token=$(window_token "$focus")
|
|
activity=$(installer_activity "$focus")
|
|
if [ "$token" != "$answered_token" ]; then
|
|
answered_token=$token
|
|
opened_windows=$((opened_windows + 1))
|
|
fi
|
|
|
|
# One tap per dialog. A second would be a tap on whatever the dialog left behind.
|
|
if [ "$token" = "$tapped_token" ]; then
|
|
return 0
|
|
fi
|
|
|
|
local attempt
|
|
for attempt in $(seq 1 "$wait"); do
|
|
read -r button_x button_y label <<<"$(install_button)"
|
|
if [ -n "${button_x:-}" ]; then
|
|
now_focus=$(focused_window)
|
|
if ! is_installer "$now_focus" || [ "$(installer_activity "$now_focus")" != "$activity" ]; then
|
|
echo " $(date +%H:%M:%S) read '$label' at $button_x,$button_y, but the dialog closed before the tap; not tapping"
|
|
return 0
|
|
fi
|
|
# Whatever installer dialog is up *now* is the one being answered; it may be a newer window
|
|
# than the one this function was called for, which is exactly the case that used to be
|
|
# skipped.
|
|
if [ $((SECONDS - last_tap_at)) -lt 2 ]; then
|
|
echo " $(date +%H:%M:%S) a tap went out less than 2 s ago; not tapping again"
|
|
return 0
|
|
fi
|
|
tapped_token=$(window_token "$now_focus")
|
|
last_tap_at=$SECONDS
|
|
if [ "$dry_run" -eq 1 ]; then
|
|
echo " $(date +%H:%M:%S) would tap '$label' at $button_x,$button_y"
|
|
else
|
|
adb shell input tap "$button_x" "$button_y" || true
|
|
taps=$((taps + 1))
|
|
echo " $(date +%H:%M:%S) tapped '$label' at $button_x,$button_y (read from the screen, installer still up)"
|
|
fi
|
|
return 0
|
|
fi
|
|
sleep 0.5
|
|
done
|
|
|
|
misses=$((misses + 1))
|
|
echo " $(date +%H:%M:%S) installer window focused, but no Install button appeared within ${wait}s"
|
|
}
|
|
|
|
deadline=$((SECONDS + seconds))
|
|
burst_until=0
|
|
last_beat=$SECONDS
|
|
echo "benchmark-watch-install: watching for $seconds s (idle poll ${interval}s, burst after a dialog)"
|
|
|
|
while [ $SECONDS -lt $deadline ]; do
|
|
# A heartbeat, so "is it still running?" can be answered by reading the log instead of guessing.
|
|
if [ $((SECONDS - last_beat)) -ge 30 ]; then
|
|
echo " $(date +%H:%M:%S) still watching: $taps answered, $opened_windows window(s) seen"
|
|
last_beat=$SECONDS
|
|
fi
|
|
|
|
focus=$(focused_window)
|
|
if is_installer "$focus"; then
|
|
answer_dialog "$focus"
|
|
burst_until=$((SECONDS + burst))
|
|
sleep 0.7
|
|
continue
|
|
fi
|
|
|
|
if [ $SECONDS -lt $burst_until ]; then
|
|
sleep 0.4
|
|
else
|
|
sleep "$interval"
|
|
fi
|
|
done
|
|
|
|
echo "benchmark-watch-install: done, $taps tap(s), $opened_windows installer window(s) seen, $misses not answerable"
|