Restores the in-flight wedge-capture diagnostics on the matrix e2e legs (API 29-36), mirroring the API-37 e2e-preview job. Unblocked + de-risked by #454, which replaced reactivecircus/android-emulator-runner on the matrix with the same hand-provisioned manual boot e2e-preview uses — so the capture_wedge mechanism that already runs on that manual-boot structure in e2e-preview (without hanging) can now be brought to the matrix.
What was mirrored from e2e-preview
Everything is copied identical in shape from e2e-preview (no new wrapper invented), adapted only for per-API-level naming (api${{ matrix.api-level }} instead of api37-...-shard${{ matrix.shard }}):
WEDGE_LOG + WEDGE_TIMEOUT=1200 variables in the "Boot emulator and run E2E" step.
capture_wedge() — byte-for-byte the same probe sequence as preview: running/last test (TestRunner logcat), sys.boot_completed, init.svc.*, service list + service check input/window/activity (the binder-publish crux), app/test pids, SIGQUIT (kill -3) thread dumps of the app + instrumentation processes, /data/anr/*, dumpsys activity, dumpsys window, logcat -d tail, and accel/kvm/mem/disk.
run_e2e() wrapped in timeout -k 30s "$WEDGE_TIMEOUT" — mirrors preview's run_shard(): on exit 124 it calls capture_wedge then returns 124 so the retry/gate still see a failure. Retry-once parity preserved (attempt 1 / attempt 2 (retry)).
"Upload wedge diagnostics" step — if: always(), if-no-files-found: ignore (healthy runs produce no file and stay quiet), per-API-level artifact name wedge-diagnostics-api${{ matrix.api-level }}.
The stale comment (NO #404 wedge-capture wrapper here ...) above the boot step was corrected to explain the restore.
Why it cannot re-hang the legs (the structural reason)
The #404/#406 revert (90dfb18) happened because that wrapper wrapped reactivecircus/android-emulator-runner's emulator boot — killing that via timeout orphaned the emulator and hung all 8 legs. Here the timeout wraps only the foreground ./gradlew connectedDebugAndroidTest client, never the backgrounded emulator (which stays alive so capture_wedge can query it via adb). This is exactly e2e-preview's proven run_shard structure. WEDGE_TIMEOUT=1200 (20 min) reuses preview's value: healthy matrix legs run the whole job in ~8.3-12.0 min (per docs/perf/api37-e2e-sharding-spike.md §4; API 30 is the ~12.0 min ceiling) and the wrapper covers only the gradle invocation (a subset of that), so 20 min is generous no-false-trip headroom and sits well under this job's 50-min cap — which itself backstops any pathological hang.
Self-validating: this PR's own CI run exercises the wrapper on all 8 matrix legs. A false trip or a hang would fail the run (won't merge, won't reach the queue).
Scope guarantee
#454's boot logic is untouched — boot_emulator(), adb start-server, the 2-attempt cold-boot loop, timeout 300 adb wait-for-device, the non-fatal keyevent 82 unlock, and the animation settings are all unchanged (diff hunks land only in the comment, the variable block, and after the boot loop).
#460's path-filter block (the changes job) is not touched at all.
Verification
python -c "yaml.safe_load(...)" → parses cleanly.
actionlint .github/workflows/ci.yml → exit 0.
Reviewed the full diff (+94/-6, confined to the e2e matrix job).
Closes #421.
Restores the in-flight **wedge-capture diagnostics** on the matrix `e2e` legs (API 29-36), mirroring the API-37 `e2e-preview` job. Unblocked + de-risked by #454, which replaced `reactivecircus/android-emulator-runner` on the matrix with the same hand-provisioned manual boot `e2e-preview` uses — so the `capture_wedge` mechanism that already runs on that manual-boot structure in `e2e-preview` (without hanging) can now be brought to the matrix.
## What was mirrored from `e2e-preview`
Everything is copied **identical in shape** from `e2e-preview` (no new wrapper invented), adapted only for per-API-level naming (`api${{ matrix.api-level }}` instead of `api37-...-shard${{ matrix.shard }}`):
1. **`WEDGE_LOG` + `WEDGE_TIMEOUT=1200`** variables in the "Boot emulator and run E2E" step.
2. **`capture_wedge()`** — byte-for-byte the same probe sequence as preview: running/last test (TestRunner logcat), `sys.boot_completed`, `init.svc.*`, `service list` + `service check input/window/activity` (the binder-publish crux), app/test pids, **SIGQUIT (`kill -3`) thread dumps** of the app + instrumentation processes, `/data/anr/*`, `dumpsys activity`, `dumpsys window`, `logcat -d` tail, and accel/kvm/mem/disk.
3. **`run_e2e()` wrapped in `timeout -k 30s "$WEDGE_TIMEOUT"`** — mirrors preview's `run_shard()`: on exit 124 it calls `capture_wedge` then returns 124 so the retry/gate still see a failure. Retry-once parity preserved (`attempt 1` / `attempt 2 (retry)`).
4. **"Upload wedge diagnostics" step** — `if: always()`, `if-no-files-found: ignore` (healthy runs produce no file and stay quiet), per-API-level artifact name `wedge-diagnostics-api${{ matrix.api-level }}`.
The stale comment (`NO #404 wedge-capture wrapper here ...`) above the boot step was corrected to explain the restore.
## Why it cannot re-hang the legs (the structural reason)
The #404/#406 revert (90dfb18) happened because that wrapper wrapped `reactivecircus/android-emulator-runner`'s **emulator boot** — killing that via `timeout` orphaned the emulator and hung all 8 legs. Here the `timeout` wraps **only the foreground `./gradlew connectedDebugAndroidTest` client**, never the backgrounded emulator (which stays alive so `capture_wedge` can query it via adb). This is exactly `e2e-preview`'s proven `run_shard` structure. `WEDGE_TIMEOUT=1200` (20 min) reuses preview's value: healthy matrix legs run the whole job in ~8.3-12.0 min (per `docs/perf/api37-e2e-sharding-spike.md` §4; API 30 is the ~12.0 min ceiling) and the wrapper covers only the gradle invocation (a subset of that), so 20 min is generous no-false-trip headroom and sits well under this job's 50-min cap — which itself backstops any pathological hang.
**Self-validating:** this PR's own CI run exercises the wrapper on all 8 matrix legs. A false trip or a hang would fail the run (won't merge, won't reach the queue).
## Scope guarantee
- **#454's boot logic is untouched** — `boot_emulator()`, `adb start-server`, the 2-attempt cold-boot loop, `timeout 300 adb wait-for-device`, the non-fatal `keyevent 82` unlock, and the animation settings are all unchanged (diff hunks land only in the comment, the variable block, and *after* the boot loop).
- **#460's path-filter block (the `changes` job) is not touched at all.**
## Verification
- `python -c "yaml.safe_load(...)"` → parses cleanly.
- `actionlint .github/workflows/ci.yml` → exit 0.
- Reviewed the full diff (+94/-6, confined to the `e2e` matrix job).
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.
Closes #421.
Restores the in-flight wedge-capture diagnostics on the matrix
e2elegs (API 29-36), mirroring the API-37e2e-previewjob. Unblocked + de-risked by #454, which replacedreactivecircus/android-emulator-runneron the matrix with the same hand-provisioned manual boote2e-previewuses — so thecapture_wedgemechanism that already runs on that manual-boot structure ine2e-preview(without hanging) can now be brought to the matrix.What was mirrored from
e2e-previewEverything is copied identical in shape from
e2e-preview(no new wrapper invented), adapted only for per-API-level naming (api${{ matrix.api-level }}instead ofapi37-...-shard${{ matrix.shard }}):WEDGE_LOG+WEDGE_TIMEOUT=1200variables in the "Boot emulator and run E2E" step.capture_wedge()— byte-for-byte the same probe sequence as preview: running/last test (TestRunner logcat),sys.boot_completed,init.svc.*,service list+service check input/window/activity(the binder-publish crux), app/test pids, SIGQUIT (kill -3) thread dumps of the app + instrumentation processes,/data/anr/*,dumpsys activity,dumpsys window,logcat -dtail, and accel/kvm/mem/disk.run_e2e()wrapped intimeout -k 30s "$WEDGE_TIMEOUT"— mirrors preview'srun_shard(): on exit 124 it callscapture_wedgethen returns 124 so the retry/gate still see a failure. Retry-once parity preserved (attempt 1/attempt 2 (retry)).if: always(),if-no-files-found: ignore(healthy runs produce no file and stay quiet), per-API-level artifact namewedge-diagnostics-api${{ matrix.api-level }}.The stale comment (
NO #404 wedge-capture wrapper here ...) above the boot step was corrected to explain the restore.Why it cannot re-hang the legs (the structural reason)
The #404/#406 revert (
90dfb18) happened because that wrapper wrappedreactivecircus/android-emulator-runner's emulator boot — killing that viatimeoutorphaned the emulator and hung all 8 legs. Here thetimeoutwraps only the foreground./gradlew connectedDebugAndroidTestclient, never the backgrounded emulator (which stays alive socapture_wedgecan query it via adb). This is exactlye2e-preview's provenrun_shardstructure.WEDGE_TIMEOUT=1200(20 min) reuses preview's value: healthy matrix legs run the whole job in ~8.3-12.0 min (perdocs/perf/api37-e2e-sharding-spike.md§4; API 30 is the ~12.0 min ceiling) and the wrapper covers only the gradle invocation (a subset of that), so 20 min is generous no-false-trip headroom and sits well under this job's 50-min cap — which itself backstops any pathological hang.Self-validating: this PR's own CI run exercises the wrapper on all 8 matrix legs. A false trip or a hang would fail the run (won't merge, won't reach the queue).
Scope guarantee
boot_emulator(),adb start-server, the 2-attempt cold-boot loop,timeout 300 adb wait-for-device, the non-fatalkeyevent 82unlock, and the animation settings are all unchanged (diff hunks land only in the comment, the variable block, and after the boot loop).changesjob) is not touched at all.Verification
python -c "yaml.safe_load(...)"→ parses cleanly.actionlint .github/workflows/ci.yml→ exit 0.e2ematrix job).Merge Queue Status
2026-07-08 22:27 UTC· Rule:default· triggered by merge protections2026-07-08 22:50 UTC· at9407b20914119d4724983e2fe65f11d892c4b7ae· mergeThis pull request spent 22 minutes 52 seconds in the queue, including 1 second running CI.
Required conditions to merge
-conflict-draftbase = maincheck-success = CI passedgithub-review-approved[🛡 GitHub repository ruleset rulemain]label != brokencheck-success = Debug buildcheck-neutral = Debug buildcheck-skipped = Debug buildcheck-success = Unit testscheck-neutral = Unit testscheck-skipped = Unit testscheck-success = CI passedcheck-neutral = CI passedcheck-skipped = CI passedmain]:check-success = @github-actions/CI passedcheck-neutral = @github-actions/CI passedcheck-skipped = @github-actions/CI passed