Folds the proven 2026-07-06 cold-fetch pause-hook A/B flow into scripts/device-testing/ as a first-class, reproducible cold-fetch-ab scenario (issue #405), upstreaming the scratchpad driver so the connection-reuse / throttle A/B can be re-run on demand to catch perf regressions.
What's folded in
Pause-hook helpers — new fetchgate.py:
FETCH_GATEpause / resume / query for scope backfill,prefetch through the guarded adb.py wrapper (the broadcast targets our own package/component, so assert_safe accepts it — no guard changes needed).
Parses the ordered-broadcast read-back the debug receiver returns (Broadcast completed: result=0, data="paused=[backfill,prefetch]") into a typed BroadcastResult.
resume → measure warm (cached) re-opens of the same messages.
report.render_cold_fetch_ab reports the cold-vs-warm median delta, the connect=0ms connection-reuse proof, and any server-side throttle signature (a very high cold IMAP work).
Restore semantics: the gate is always cleared on exit (finally), even on error/exception — a device is never left with fetch paused.
Portability / robustness (subsumes #392): file-based uiautomator dump (/sdcard/window_dump.xml + cat, not /dev/tty), UTF-8 adb decode + PYTHONUTF8/console I/O, openMessage-breadcrumb readiness (not just UI polling), and row-selection hardening (skip tappable rows with no sender/subject label). These are shared improvements, so the existing scenarios benefit too.
Debug-build-only
The FETCH_GATE pause hook (FetchGateReceiver / DebugFetchGate, issues #393/#395) is compiled only into app/src/debug and R8-stripped from release, so cold-fetch-ab requires a debug APK. A full on-device run is a follow-up; the automated validation here is the mocked unit tests + --dry-run.
Validation
python -m unittest discover -s scripts/device-testing/tests — 103 tests pass (26 new across test_fetchgate.py + test_scenarios_coldfetch.py): read-back parsing, FetchGate through the real safety guard, the sign-in / halt-confirm matchers, the wait helpers, the full A/B scenario happy path, the always-resume-on-error restore, row-selection hardening, the report renderer, and the --dry-run end-to-end through perf_harness.main.
perf_harness.py cold-fetch-ab --dry-run — emits the full auditable command plan (pause → file-based dump → cold open → resume → dump → warm open → query → resume) and writes a valid timing-tables.md.
Scope
Dev-tooling only — all changes are under scripts/device-testing/. No app/src (Kotlin) changes.
Folds the proven 2026-07-06 cold-fetch pause-hook A/B flow into `scripts/device-testing/` as a first-class, reproducible `cold-fetch-ab` scenario (issue #405), upstreaming the scratchpad driver so the connection-reuse / throttle A/B can be re-run on demand to catch perf regressions.
## What's folded in
**Pause-hook helpers** — new `fetchgate.py`:
- `FETCH_GATE` `pause` / `resume` / `query` for scope `backfill,prefetch` through the guarded `adb.py` wrapper (the broadcast targets our own package/component, so `assert_safe` accepts it — no guard changes needed).
- Parses the **ordered-broadcast read-back** the debug receiver returns (`Broadcast completed: result=0, data="paused=[backfill,prefetch]"`) into a typed `BroadcastResult`.
**Cold-fetch A/B scenario** — `scenarios.cold_fetch_ab`:
1. **Pre-arm the halt** (`pause backfill,prefetch`) before sign-in, so proactive fetch is gated the moment sync starts.
2. **Detect sign-in** via the `MailSyncer: sync all: N accounts` breadcrumb (sign-in is manual — OAuth can't be automated).
3. **Confirm the halt** via `prefetch skipped: fetch-gate paused`.
4. **Wait for header sync** (uncached bodies).
5. **Measure cold opens** (`ImapPerf` connect/work + `MailReader openMessage fetchedBody=true`).
6. **`resume` → measure warm** (cached) re-opens of the same messages.
`report.render_cold_fetch_ab` reports the cold-vs-warm median delta, the **`connect=0ms`** connection-reuse proof, and any **server-side throttle signature** (a very high cold IMAP `work`).
**Restore semantics:** the gate is **always cleared on exit** (`finally`), even on error/exception — a device is never left with fetch paused.
**Portability / robustness (subsumes #392):** file-based `uiautomator dump` (`/sdcard/window_dump.xml` + `cat`, not `/dev/tty`), UTF-8 adb decode + `PYTHONUTF8`/console I/O, `openMessage`-breadcrumb readiness (not just UI polling), and row-selection hardening (skip tappable rows with no sender/subject label). These are shared improvements, so the existing scenarios benefit too.
## Debug-build-only
The `FETCH_GATE` pause hook (`FetchGateReceiver` / `DebugFetchGate`, issues **#393/#395**) is compiled **only** into `app/src/debug` and R8-stripped from release, so **`cold-fetch-ab` requires a debug APK**. A full **on-device run is a follow-up**; the automated validation here is the **mocked unit tests + `--dry-run`**.
## Validation
- **`python -m unittest discover -s scripts/device-testing/tests`** — **103 tests pass** (26 new across `test_fetchgate.py` + `test_scenarios_coldfetch.py`): read-back parsing, `FetchGate` through the real safety guard, the sign-in / halt-confirm matchers, the wait helpers, the full A/B scenario happy path, the **always-resume-on-error** restore, row-selection hardening, the report renderer, and the `--dry-run` end-to-end through `perf_harness.main`.
- **`perf_harness.py cold-fetch-ab --dry-run`** — emits the full auditable command plan (pause → file-based dump → cold open → resume → dump → warm open → query → resume) and writes a valid `timing-tables.md`.
## Scope
Dev-tooling only — all changes are under `scripts/device-testing/`. **No `app/src` (Kotlin) changes.**
Relates: #370 (harness), #393/#395 (pause hook), #392 (subsumed), the perf drilldown.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Folds the proven 2026-07-06 cold-fetch pause-hook A/B flow into
scripts/device-testing/as a first-class, reproduciblecold-fetch-abscenario (issue #405), upstreaming the scratchpad driver so the connection-reuse / throttle A/B can be re-run on demand to catch perf regressions.What's folded in
Pause-hook helpers — new
fetchgate.py:FETCH_GATEpause/resume/queryfor scopebackfill,prefetchthrough the guardedadb.pywrapper (the broadcast targets our own package/component, soassert_safeaccepts it — no guard changes needed).Broadcast completed: result=0, data="paused=[backfill,prefetch]") into a typedBroadcastResult.Cold-fetch A/B scenario —
scenarios.cold_fetch_ab:pause backfill,prefetch) before sign-in, so proactive fetch is gated the moment sync starts.MailSyncer: sync all: N accountsbreadcrumb (sign-in is manual — OAuth can't be automated).prefetch skipped: fetch-gate paused.ImapPerfconnect/work +MailReader openMessage fetchedBody=true).resume→ measure warm (cached) re-opens of the same messages.report.render_cold_fetch_abreports the cold-vs-warm median delta, theconnect=0msconnection-reuse proof, and any server-side throttle signature (a very high cold IMAPwork).Restore semantics: the gate is always cleared on exit (
finally), even on error/exception — a device is never left with fetch paused.Portability / robustness (subsumes #392): file-based
uiautomator dump(/sdcard/window_dump.xml+cat, not/dev/tty), UTF-8 adb decode +PYTHONUTF8/console I/O,openMessage-breadcrumb readiness (not just UI polling), and row-selection hardening (skip tappable rows with no sender/subject label). These are shared improvements, so the existing scenarios benefit too.Debug-build-only
The
FETCH_GATEpause hook (FetchGateReceiver/DebugFetchGate, issues #393/#395) is compiled only intoapp/src/debugand R8-stripped from release, socold-fetch-abrequires a debug APK. A full on-device run is a follow-up; the automated validation here is the mocked unit tests +--dry-run.Validation
python -m unittest discover -s scripts/device-testing/tests— 103 tests pass (26 new acrosstest_fetchgate.py+test_scenarios_coldfetch.py): read-back parsing,FetchGatethrough the real safety guard, the sign-in / halt-confirm matchers, the wait helpers, the full A/B scenario happy path, the always-resume-on-error restore, row-selection hardening, the report renderer, and the--dry-runend-to-end throughperf_harness.main.perf_harness.py cold-fetch-ab --dry-run— emits the full auditable command plan (pause → file-based dump → cold open → resume → dump → warm open → query → resume) and writes a validtiming-tables.md.Scope
Dev-tooling only — all changes are under
scripts/device-testing/. Noapp/src(Kotlin) changes.Relates: #370 (harness), #393/#395 (pause hook), #392 (subsumed), the perf drilldown.
🤖 Generated with Claude Code