build(gmd): cap managed-device emulators to 1 concurrent (serial) to avoid local VT-x contention #267

Merged
JMR-dev merged 1 commits from build-gmd-serial-emulators into main 2026-07-03 21:48:57 +00:00
JMR-dev commented 2026-07-03 21:45:51 +00:00 (Migrated from github.com)

Problem

On a single local machine, Gradle Managed Devices (GMD) can launch multiple
emulators concurrently: the e2e device group in app/build.gradle.kts spans
API levels 29-36, and gradle.properties already has org.gradle.parallel=true.
Running the whole group (./gradlew e2eGroupDebugAndroidTest) or even the
/preflight skill's api36DebugAndroidTest alongside another in-flight GMD run
can therefore try to boot several emulators at once. They contend for the same
hardware virtualization slot (VT-x/HAXM) and hang at 0% CPU with errors like
"another emulator instance is running". Emulator tests need to run serially
(one emulator at a time) on a local machine.

Fix

Added to gradle.properties:

android.experimental.testOptions.managedDevices.maxConcurrentDevices=1

This is AGP's Gradle Managed Device "max concurrent devices" option. I verified
the exact key (rather than trusting memory) by extracting
com/android/build/gradle/options/IntegerOption.class from the
com.android.tools.build:gradle:9.2.1 jar that this repo's root
build.gradle.kts buildscript classpath pins — the enum constant
GRADLE_MANAGED_DEVICE_MAX_CONCURRENT_DEVICES maps to the property string
android.experimental.testOptions.managedDevices.maxConcurrentDevices, typed as
an IntegerOption under ApiStage.Experimental. A short comment explains why
(VT-x contention on single-machine local runs; keeps GMD emulator runs serial).

CI safety

This does not reduce CI's parallelism, for two independent reasons:

  1. Each CI runner already runs exactly one emulator. The e2e matrix in
    .github/workflows/ci.yml (api-level: [29, 30, 31, 32, 33, 34, 35, 36])
    fans out across separate GitHub Actions runners, one API level per runner —
    there's no cross-level concurrency on a single machine to cap.
  2. CI doesn't invoke Gradle Managed Device tasks at all. The e2e and
    e2e-preview jobs boot their emulator via
    reactivecircus/android-emulator-runner (or manual avdmanager provisioning
    for the API 37 preview job) and then run ./gradlew connectedDebugAndroidTest
    against that externally-provisioned emulator — never apXXDebugAndroidTest
    or e2eGroupDebugAndroidTest. managedDevices.maxConcurrentDevices only
    governs Gradle's own orchestration of testOptions.managedDevices-declared
    devices, so it has no effect on CI's E2E jobs regardless of runner topology.

Validation

No emulator run and no full build, per instructions (an emulator run may be in
progress elsewhere). Validated with a light Gradle invocation only, using JDK 21
(JAVA_HOME pointed at JDK 25 by default in this environment, which AGP 9.2
doesn't support):

$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.11.10-hotspot"
.\gradlew :app:help --warning-mode all

Result: BUILD SUCCESSFUL, with:

WARNING: The option setting 'android.experimental.testOptions.managedDevices.maxConcurrentDevices=1' is experimental.

That's AGP's standard acknowledgment for a known experimental option (naming
it back verbatim) — not an "unknown property" warning — confirming the key
parses and is recognized. No unrelated regressions: the only other warning in
the output is a pre-existing, unrelated Gradle 10 dependency-notation
deprecation notice already present on main.

🤖 Generated with Claude Code

## Problem On a single local machine, Gradle Managed Devices (GMD) can launch multiple emulators concurrently: the `e2e` device group in `app/build.gradle.kts` spans API levels 29-36, and `gradle.properties` already has `org.gradle.parallel=true`. Running the whole group (`./gradlew e2eGroupDebugAndroidTest`) or even the `/preflight` skill's `api36DebugAndroidTest` alongside another in-flight GMD run can therefore try to boot several emulators at once. They contend for the same hardware virtualization slot (VT-x/HAXM) and hang at 0% CPU with errors like "another emulator instance is running". Emulator tests need to run **serially** (one emulator at a time) on a local machine. ## Fix Added to `gradle.properties`: ```properties android.experimental.testOptions.managedDevices.maxConcurrentDevices=1 ``` This is AGP's Gradle Managed Device "max concurrent devices" option. I verified the exact key (rather than trusting memory) by extracting `com/android/build/gradle/options/IntegerOption.class` from the `com.android.tools.build:gradle:9.2.1` jar that this repo's root `build.gradle.kts` buildscript classpath pins — the enum constant `GRADLE_MANAGED_DEVICE_MAX_CONCURRENT_DEVICES` maps to the property string `android.experimental.testOptions.managedDevices.maxConcurrentDevices`, typed as an `IntegerOption` under `ApiStage.Experimental`. A short comment explains why (VT-x contention on single-machine local runs; keeps GMD emulator runs serial). ## CI safety This does **not** reduce CI's parallelism, for two independent reasons: 1. **Each CI runner already runs exactly one emulator.** The `e2e` matrix in `.github/workflows/ci.yml` (`api-level: [29, 30, 31, 32, 33, 34, 35, 36]`) fans out across separate GitHub Actions runners, one API level per runner — there's no cross-level concurrency on a single machine to cap. 2. **CI doesn't invoke Gradle Managed Device tasks at all.** The `e2e` and `e2e-preview` jobs boot their emulator via `reactivecircus/android-emulator-runner` (or manual `avdmanager` provisioning for the API 37 preview job) and then run `./gradlew connectedDebugAndroidTest` against that externally-provisioned emulator — never `apXXDebugAndroidTest` or `e2eGroupDebugAndroidTest`. `managedDevices.maxConcurrentDevices` only governs Gradle's own orchestration of `testOptions.managedDevices`-declared devices, so it has no effect on CI's E2E jobs regardless of runner topology. ## Validation No emulator run and no full build, per instructions (an emulator run may be in progress elsewhere). Validated with a light Gradle invocation only, using JDK 21 (`JAVA_HOME` pointed at JDK 25 by default in this environment, which AGP 9.2 doesn't support): ``` $env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.11.10-hotspot" .\gradlew :app:help --warning-mode all ``` Result: `BUILD SUCCESSFUL`, with: ``` WARNING: The option setting 'android.experimental.testOptions.managedDevices.maxConcurrentDevices=1' is experimental. ``` That's AGP's standard acknowledgment for a *known* experimental option (naming it back verbatim) — not an "unknown property" warning — confirming the key parses and is recognized. No unrelated regressions: the only other warning in the output is a pre-existing, unrelated Gradle 10 dependency-notation deprecation notice already present on `main`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.