refactor(scripts): fold cold-fetch pause-hook A/B into device-testing harness (#405)

Bakes the proven 2026-07-06 cold-vs-warm pause-hook flow into
scripts/device-testing/ as a first-class, reproducible `cold-fetch-ab`
scenario, upstreaming the scratchpad driver.

- fetchgate.py: FETCH_GATE pause/resume/query helpers through the guarded
  adb wrapper, with ordered-broadcast read-back parsing (paused=[...]).
- scenarios.cold_fetch_ab: pre-arm halt -> detect sign-in (sync all
  breadcrumb) -> confirm halt (prefetch skipped) -> wait for header sync ->
  measure cold opens -> resume -> measure warm opens. ALWAYS resumes on exit
  (finally), even on error -- never leaves fetch paused.
- report.render_cold_fetch_ab: gate summary, cold/warm tables, cold-vs-warm
  delta, connect=0ms reuse proof, throttle signature.
- Portability (subsumes #392): file-based uiautomator dump (not /dev/tty),
  UTF-8 adb decode + PYTHONUTF8/console I/O, openMessage-breadcrumb readiness,
  row-selection hardening (skip non-message rows).

The pause hook is debug-build-only (#393/#395), so the scenario needs a debug
APK. Automated validation: mocked unittest coverage (adb/breadcrumbs/gate) for
the helpers and the A/B scenario incl. restore-on-error, plus a --dry-run path
exercised end-to-end through perf_harness.main. A full on-device run is a
follow-up. Dev-tooling only; no app/src changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 05:07:11 -05:00
co-authored by Claude Opus 4.8
parent 582d1f3077
commit 55f1f59e3d
10 changed files with 1310 additions and 8 deletions
+40
View File
@@ -102,6 +102,46 @@ def is_perf_line(line: str) -> bool:
return parsed is not None and parsed.tag in PERF_TAGS
# --------------------------------------------------------------------------- #
# Control-signal line matchers (not perf metrics -- flow signals for the harness)
# --------------------------------------------------------------------------- #
# These are *not* perf breadcrumbs (they carry no timing), so they are deliberately kept out
# of PERF_TAGS / the Event dispatch. The cold-fetch scenario tails the live log for them to
# know when sign-in/sync has started and whether the pre-armed fetch-gate halt took effect.
_SYNC_ALL_RE = re.compile(r"^sync all:\s+(?P<n>\d+)\s+accounts?$")
# Verbatim from MailSyncer.kt / MailBackfiller.kt when a paused FETCH_GATE skips prefetch.
FETCH_GATE_SKIP_MESSAGE = "prefetch skipped: fetch-gate paused"
def match_sync_all(line: str) -> Optional[int]:
"""If ``line`` is the ``MailSyncer: sync all: N accounts`` breadcrumb, return N.
N is the account count logged at the start of a sync pass -- the harness's sign-in /
sync-start signal (a fresh sign-in triggers the first ``syncAll``). Returns ``None`` for
any other line.
"""
log = parse_logcat_line(line)
if log is None or log.tag != "MailSyncer":
return None
m = _SYNC_ALL_RE.match(log.message)
return int(m.group("n")) if m else None
def is_fetch_gate_skip(line: str) -> bool:
"""True if ``line`` is the debug ``prefetch skipped: fetch-gate paused`` breadcrumb.
Emitted by ``MailSyncer``/``MailBackfiller`` when a pre-armed FETCH_GATE pause is honoured
-- the harness's proof the halt actually took effect (bodies will stay uncached).
"""
log = parse_logcat_line(line)
return (
log is not None
and log.tag in ("MailSyncer", "MailBackfiller")
and log.message == FETCH_GATE_SKIP_MESSAGE
)
# --------------------------------------------------------------------------- #
# Typed breadcrumb events
# --------------------------------------------------------------------------- #