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:
@@ -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
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
Reference in New Issue
Block a user