docs: record the QEMU clock-dilation lead for the FGS video

Chasing the video on the emulator found one lever that actually attacks the failure, and it is
worth writing down even though it did not hold:

  emulator -avd phone ... -qemu -icount shift=1,sleep=off

Without -icount the guest clock is tied to the host's wall clock, so Android's five-second
startForegroundService() deadline means five real seconds however slowly TCG executes - which is
what kills the app here every time. With -icount the guest clock advances per executed
instruction, and it measured 2.30x slow (30.9 s host to 13.4 s guest uptime). On that boot a scan
start succeeded: the app's notification appeared five seconds after the tap and there was no
am_crash, the only success recorded on this machine.

It then stopped reproducing - later attempts produced neither a notification nor a crash. Two
possibilities, and the second is an app-side diagnosability gap: either the tap never reached the
app, or ScanService.start() swallowed a ForegroundServiceStartNotAllowedException, which it does
silently (it catches IllegalStateException and does nothing with it, so logcat shows nothing at
all). Notes say so, and point at SETTLE_SECONDS / WARM_SECONDS for scaling the waits, because a
dilated guest is slower at everything - uiautomator dumps included.

The script also retries a tap (up to three times, re-reading the coordinates) before falling back
to a cold restart, since on a slow emulator the window can still be settling while the
accessibility tree already lists the button - and a restart costs another cold start. The
recommendation stands: record the take on the phone.
This commit is contained in:
2026-10-14 12:25:02 +02:00
parent 179bc2cc33
commit ddc7fb44b0
2 changed files with 40 additions and 9 deletions
+14 -6
View File
@@ -308,12 +308,20 @@ action except the items marked **[code]**.
`startForeground()` inside Android's budget — while on the phone this app scans a `startForeground()` inside Android's budget — while on the phone this app scans a
5,363-photo library in 20 s with the notification up throughout. 5,363-photo library in 20 s with the notification up throughout.
An earlier version of this note claimed a second, unexplained mid-scan death. That was One emulator-side lever is worth knowing, because it is the only thing that ever got this
this same failure misread: the script's old "is a scan running?" check matched the app's foreground service started on this machine: QEMU's virtual instruction counter
service *record*, which exists from the moment `startForegroundService()` is called, so dilates the **guest** clock, so Android's deadline is measured against time that passes
it reported success on runs that were already dying. `tools/record-fgs-demo.sh` now waits more slowly than the host's. Booting with `-qemu -icount shift=1,sleep=off` made the guest
for the app's notification instead, which is the only honest proof the service started, clock run **2.30× slow** (measured: 30.9 s of host time to 13.4 s of guest uptime), and on
and reports the `am_crash` line when it never does. that boot a scan start *worked* — the app's notification appeared five seconds after the
tap, with no `am_crash`, the only success recorded here. It then stopped working: later
attempts produced neither a notification nor a crash, which means the tap never reached
the app, or `ScanService.start()` silently swallowed a
`ForegroundServiceStartNotAllowedException` (it catches `IllegalStateException` and does
nothing with it, so that path leaves no trace in logcat at all — a diagnosability gap
worth a log line if this is ever chased further). So treat `-icount` as a lead to retry
with patience rather than a recipe to rely on, and use `SETTLE_SECONDS` / `WARM_SECONDS`
to scale this script's waits for a dilated clock.
The declaration is **per type**: if a new foreground-service type is ever added to The declaration is **per type**: if a new foreground-service type is ever added to
the manifest, it has to be declared here too. the manifest, it has to be declared here too.
+23
View File
@@ -29,6 +29,15 @@
# #
# It also writes a checkpoint PNG at each milestone, so the recording can be checked without # It also writes a checkpoint PNG at each milestone, so the recording can be checked without
# decoding the video. `ffprobe` is used for the duration when it is installed. # decoding the video. `ffprobe` is used for the duration when it is installed.
#
# On a host with no KVM the one lever that ever got the service started here was QEMU's clock
# dilation, which makes Android's deadline be measured in slower time than the host's:
#
# emulator -avd <name> ... -qemu -icount shift=1,sleep=off # guest clock ran 2.30x slow
#
# That worked once and then not again, so treat it as a lead rather than a recipe: scale the two
# timing knobs up for a dilated clock (the guest is slower at everything, dumps included) and expect
# to iterate. docs/PLAY_RELEASE_CHECKLIST.md step 5 has the long version of this note.
set -euo pipefail set -euo pipefail
here="$(cd "$(dirname "$0")" && pwd)" here="$(cd "$(dirname "$0")" && pwd)"
@@ -209,10 +218,24 @@ while [ "$attempt" -lt 4 ]; do
started=no started=no
# dumpsys only while the service starts: every `uiautomator dump` spawns a JVM on the device and # dumpsys only while the service starts: every `uiautomator dump` spawns a JVM on the device and
# competes for exactly the CPU that the five-second deadline is measured against. # competes for exactly the CPU that the five-second deadline is measured against.
for tap_try in 1 2 3; do
for _ in $(seq 1 12); do for _ in $(seq 1 12); do
if fgs_started; then started=yes; break; fi if fgs_started; then started=yes; break; fi
sleep 3 sleep 3
done done
if [ "$started" = yes ]; then break; fi
printf ' tap %s did not start the service' "$tap_try"
# On a slowed emulator the window can still be settling even though the accessibility tree
# already lists the button, so the tap goes nowhere. Tapping again is far cheaper than the
# cold restart the outer loop would otherwise resort to.
if start="$(wait_for 'Start scan' 60)"; then
printf ' - tapping again\n'
"${adb[@]}" shell input tap $start
else
printf ' - the intro is gone, so restarting instead\n'
break
fi
done
if [ "$started" = yes ]; then if [ "$started" = yes ]; then
printf ' attempt %s: the service started (notification posted)\n' "$attempt" printf ' attempt %s: the service started (notification posted)\n' "$attempt"
break break