From ac5ed162aefc1160c23eb8067fcdeabb8185c285 Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Fri, 3 Jul 2026 16:31:41 -0500 Subject: [PATCH] ci(preflight): use host-GPU auto-no-window for local api37 emulator Change the api37_e2e.py emulator launch from `-gpu swiftshader_indirect` to `-gpu auto-no-window`. For a LOCAL run the host GPU is faster and auto-no-window is the mode that boots cleanly on this machine; CI's e2e-preview keeps swiftshader_indirect for headless-runner determinism. This is now the single deliberate divergence from e2e-preview; the image string, provisioning, boot sequence, and every other emulator flag stay in lockstep. Updated the script comments/docstring, SKILL.md, CLAUDE.md, and the build.gradle.kts managed-devices comment to document it. Syntax-only change (python -m py_compile clean); emulator not run. Co-Authored-By: Claude Opus 4.8 --- .claude/skills/preflight/SKILL.md | 15 +++++++++------ .claude/skills/preflight/api37_e2e.py | 18 ++++++++++++------ CLAUDE.md | 5 +++-- app/build.gradle.kts | 7 ++++--- 4 files changed, 28 insertions(+), 17 deletions(-) diff --git a/.claude/skills/preflight/SKILL.md b/.claude/skills/preflight/SKILL.md index 056494b..47a8343 100644 --- a/.claude/skills/preflight/SKILL.md +++ b/.claude/skills/preflight/SKILL.md @@ -64,17 +64,20 @@ Managed Device — its only published system image is the nonstandard `android-3 `google_apis_ps16k` pairing, which neither `ManagedVirtualDevice`'s `apiLevel` (Int) nor `apiPreview` (codename) DSL resolves (see the comment above `testOptions.managedDevices` in `app/build.gradle.kts`), so there is no `api37DebugAndroidTest` task. The script hand-provisions -it exactly the way CI's `e2e-preview` job does — installs the image with `sdkmanager`, creates -the AVD with `avdmanager`, cold-boots it headless with the same emulator flags, waits for -`sys.boot_completed`, runs `:app:connectedDebugAndroidTest`, then kills the emulator and deletes -the AVD. Because it mirrors `e2e-preview`, local preflight == CI on API 37. +it essentially the way CI's `e2e-preview` job does — installs the image with `sdkmanager`, +creates the AVD with `avdmanager`, cold-boots it headless, waits for `sys.boot_completed`, runs +`:app:connectedDebugAndroidTest`, then kills the emulator and deletes the AVD. The emulator flags +match `e2e-preview` with one deliberate local exception: the **GPU mode**. CI uses +`-gpu swiftshader_indirect` (software rendering, deterministic on a headless CI runner); the +local run uses `-gpu auto-no-window`, which renders on the host GPU — faster, and the mode that +boots cleanly on a dev machine. All three levels are the E2E that preflight runs locally and all three must pass; CI fans the same suite out across the whole matrix (API 29–36 in `e2e`, plus API 37 in `e2e-preview`). Keep `api35DebugAndroidTest` / `api36DebugAndroidTest` in lockstep with the top of the managed-device list in `app/build.gradle.kts`, and keep `api37_e2e.py` in lockstep with the `e2e-preview` job in -`.github/workflows/ci.yml` (same image string + emulator flags) — when a newer API level is added -there, run the new top levels instead. +`.github/workflows/ci.yml` (same image string + emulator flags, apart from the intentional GPU-mode +difference noted above) — when a newer API level is added there, run the new top levels instead. ## Reporting diff --git a/.claude/skills/preflight/api37_e2e.py b/.claude/skills/preflight/api37_e2e.py index a3ee5c1..6a72e89 100644 --- a/.claude/skills/preflight/api37_e2e.py +++ b/.claude/skills/preflight/api37_e2e.py @@ -11,9 +11,11 @@ API 37's only published system image is the nonstandard "android-37.0" / google_ "android-" package id (apiLevel = 37 -> "android-37") or an "android-" one -- neither resolves to "android-37.0" -- so there is NO api37DebugAndroidTest task to run. This script custom-provisions the emulator with -sdkmanager / avdmanager / emulator directly, mirroring CI's `e2e-preview` job in -.github/workflows/ci.yml EXACTLY (same system image string, same emulator flags, same -provisioning/boot sequence) so that local preflight == CI. Keep the two in lockstep: when a +sdkmanager / avdmanager / emulator directly, closely mirroring CI's `e2e-preview` job in +.github/workflows/ci.yml (same system image string, same provisioning/boot sequence, same +emulator flags EXCEPT the GPU mode -- CI uses `-gpu swiftshader_indirect` for headless +determinism, while this local run uses `-gpu auto-no-window` to render on the host GPU, which +is faster; see start_emulator). Keep the two in lockstep on everything but that GPU flag: when a stable, GMD-compatible API 37 image ships, delete this script, fold api37 into testOptions.managedDevices, and fold 37 into CI's `e2e` matrix (dropping the `e2e-preview` job). @@ -117,12 +119,16 @@ def create_avd(avdmanager: str, emulator: str) -> None: def start_emulator(emulator: str, emu_log: Path, attempt: int) -> subprocess.Popen: print(f"Starting API 37 emulator (attempt {attempt})...") - # Flags mirror .github/workflows/ci.yml e2e-preview EXACTLY (cold headless boot, SwiftShader - # GPU, hardware accel required, no cameras). Keep in lockstep with that job. + # Flags mirror .github/workflows/ci.yml e2e-preview (cold headless boot, hardware accel + # required, no cameras), with ONE deliberate LOCAL exception -- the GPU mode. CI uses + # `-gpu swiftshader_indirect` (software rendering, deterministic on a headless CI runner); + # locally we use `-gpu auto-no-window`, which renders on the host GPU: faster, and the mode + # that boots cleanly on a dev machine. Keep everything except the GPU mode in lockstep with + # that job. flags = [ "-avd", AVD_NAME, "-no-window", "-no-audio", "-no-boot-anim", "-no-snapshot", "-accel", "on", - "-gpu", "swiftshader_indirect", "-camera-back", "none", "-camera-front", "none", + "-gpu", "auto-no-window", "-camera-back", "none", "-camera-front", "none", ] log = open(emu_log, "wb") # noqa: SIM115 - handed to the child; closed in the parent below try: diff --git a/CLAUDE.md b/CLAUDE.md index e0ea68d..1acc90b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,8 +39,9 @@ top two stable API levels in the E2E matrix, each via its own Gradle Managed Dev and tears down each emulator automatically). API 37 (preview) has no Gradle Managed Device — its only image is the nonstandard `android-37.0` / `google_apis_ps16k` pairing (see the comment above `testOptions.managedDevices` in `app/build.gradle.kts`) — so `api37_e2e.py` hand-provisions it, -mirroring CI's `e2e-preview` job (same image + emulator flags), boots it headless, runs -`connectedDebugAndroidTest`, and tears it down. Running all three levels locally is required; the +mirroring CI's `e2e-preview` job (same image + emulator flags, except it uses host-GPU +`-gpu auto-no-window` locally vs CI's headless `-gpu swiftshader_indirect`), boots it headless, +runs `connectedDebugAndroidTest`, and tears it down. Running all three levels locally is required; the rest of the multi-API matrix (API 29–34) stays CI's job. Emulators need a free hardware hypervisor (VT-x/WHPX), so shut down VirtualBox/other VMs first or the AVD hangs at 0% CPU. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 40108f3..1f390ac 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -159,9 +159,10 @@ android { // (docs/perf/issue-124-unified-inbox-paging.md) independently corroborates this: its API 37 // cross-check used a physical Pixel, not an emulator/AVD. Locally, preflight covers API 37 // by hand-provisioning it with .claude/skills/preflight/api37_e2e.py, which mirrors the - // e2e-preview job (same image + emulator flags). Once a managed-device-compatible image is - // published, add `api37` here (and to the CI matrix), delete that script, and drop the - // e2e-preview job. + // e2e-preview job (same image + emulator flags, except it renders on the host GPU via + // `-gpu auto-no-window` locally instead of CI's headless `-gpu swiftshader_indirect`). Once + // a managed-device-compatible image is published, add `api37` here (and to the CI matrix), + // delete that script, and drop the e2e-preview job. managedDevices { localDevices { listOf(29, 30, 31, 32, 33, 34, 35, 36).forEach { api ->