Skip to content

Optimize Android build performance with parallel builds and caching#4

Merged
gmaclennan merged 1 commit into
claude/document-codebase-agents-Mi9Dqfrom
claude/speed-up-ci-builds-ecNnx
Mar 11, 2026
Merged

Optimize Android build performance with parallel builds and caching#4
gmaclennan merged 1 commit into
claude/document-codebase-agents-Mi9Dqfrom
claude/speed-up-ci-builds-ecNnx

Conversation

@gmaclennan

Copy link
Copy Markdown
Member

Summary

This PR improves Android build performance by enabling Gradle parallel builds and build caching, and specifies the x86_64 architecture for all Gradle test tasks in the CI pipeline.

Key Changes

  • Gradle Configuration (example/android/gradle.properties):

    • Enabled org.gradle.parallel=true to allow Gradle to run tasks in parallel
    • Enabled org.gradle.caching=true to cache build outputs and speed up incremental builds
  • CI Pipeline (.github/workflows/android-tests.yml):

    • Added -PreactNativeArchitectures=x86_64 flag to all Gradle test commands:
      • JVM unit tests task
      • APK assembly task
      • Connected Android tests task
    • This ensures consistent architecture targeting across all test runs and prevents architecture-related build issues

Implementation Details

The architecture specification (-PreactNativeArchitectures=x86_64) is particularly important for CI environments where the emulator runs on x86_64 architecture. This explicit configuration prevents potential architecture mismatch issues and ensures reproducible builds across different environments.

The parallel builds and caching settings in gradle.properties apply globally to all Gradle invocations in the Android project, reducing overall build times without requiring changes to individual build commands.

https://claude.ai/code/session_019Lbxh6CTzAuSh5YvP6tkGj

- Enable org.gradle.parallel=true and org.gradle.caching=true in gradle.properties
- Pass -PreactNativeArchitectures=x86_64 to all gradlew invocations in CI,
  reducing native build targets from 4 to 1 (~67% native build reduction)

https://claude.ai/code/session_019Lbxh6CTzAuSh5YvP6tkGj
@gmaclennan gmaclennan merged commit 2b08163 into claude/document-codebase-agents-Mi9Dq Mar 11, 2026
2 checks passed
gmaclennan added a commit that referenced this pull request Mar 12, 2026
* Add agents.md documenting codebase architecture and structure

Comprehensive documentation covering the dual-process architecture
(React Native + embedded Node.js), IPC protocol over Unix domain
sockets, Android foreground service lifecycle, directory structure,
data flow, and platform status.

https://claude.ai/code/session_01JvQt9SCcroodTwtW4NhiC7

* Add CoMapeo ecosystem context and IPC integration details to agents.md

Documents what CoMapeo is, the relationship to Mapeo, and how this
module fits into the broader ecosystem (@comapeo/core, @comapeo/ipc,
comapeo-mobile, comapeo-desktop, comapeo-cloud). Explains how the
messagePort connects to @comapeo/ipc's createMapeoServer/Client pattern.

https://claude.ai/code/session_01JvQt9SCcroodTwtW4NhiC7

* Add Maestro e2e testing infrastructure for Android emulator

- Add testID props to example app for element targeting
- Create 5 Maestro flow files testing: app launch, state transitions,
  Node.js process startup, IPC round-trip (1000 messages), and
  multi-round messaging stability
- Add e2e runner script with emulator management, APK build/install,
  and single-flow or full-suite execution modes
- Add e2e:android script to example/package.json
- Document setup, prerequisites, and CI integration

https://claude.ai/code/session_01JvQt9SCcroodTwtW4NhiC7

* Add Android instrumented tests and JVM unit tests

Three-layer test infrastructure:

1. JVM unit tests (android/src/test/) - no device needed:
   - MessageFramingTest: length-prefix protocol encoding/decoding,
     buffer reuse correctness, unicode handling, multi-frame sequences

2. Android instrumented tests (android/src/androidTest/) - requires device:
   - NodeJSIPCTest: IPC protocol against mock LocalServerSocket (connect,
     send, receive, echo round-trip, 100-message burst, 64KB messages,
     delayed socket creation, server disconnect handling)
   - ServiceLifecycleTest: foreground service via intents (start, stop,
     separate process verification, START_STICKY restart after kill,
     socket file lifecycle, notification presence)
   - ShutdownPathTest: graceful shutdown, process kill recovery, stop
     during startup race, 3x start/stop cycles, force-stop cleanup
   - WatchForFileTest: FileObserver suspension, cancellation, TOCTOU
     scenarios, wrong-file filtering, parent directory creation

3. Runner script (e2e/run-instrumented-tests.sh) with --class filtering,
   --unit-only mode, and automatic emulator boot

https://claude.ai/code/session_01JvQt9SCcroodTwtW4NhiC7

* Add CI workflow, nodejs-mobile download script, and fix test issues

- GitHub Actions workflow (.github/workflows/android-tests.yml) with two jobs:
  - JVM unit tests: runs without emulator, caches gradle + nodejs-mobile
  - Instrumented tests: uses reactivecircus/android-emulator-runner with KVM
- Both jobs: expo prebuild, download nodejs-mobile binaries, auto-discover
  the Expo-autolinked Gradle module name

- Download script (scripts/download-nodejs-mobile.sh):
  - Fetches nodejs-mobile prebuilt Android binaries from GitHub releases
  - Supports version override via argument or NODEJS_MOBILE_VERSION env
  - Caches with .version marker to skip redundant downloads
  - Verifies expected structure (libnode.so per ABI + node headers)

- Fix NodeJSIPCTest: LocalServerSocket(String) uses abstract namespace but
  NodeJSIPC connects to FILESYSTEM namespace. Fixed by binding a LocalSocket
  to the filesystem address and passing its FD to LocalServerSocket.

- Remove unused imports across test files

https://claude.ai/code/session_01JvQt9SCcroodTwtW4NhiC7

* Fix find-module step failing due to grep exit code 1

When no comapeo module is found by the initial grep, the fallback
diagnostic grep also returns exit code 1 (no matches), causing the
script to fail under bash -e before the fallback MODULE_NAME is set.
Add || true to both grep pipelines so the fallback path works.

https://claude.ai/code/session_01JvQt9SCcroodTwtW4NhiC7

* Add Gradle setup steps to generate wrapper jar in CI

The gradle-wrapper.jar is gitignored and not available in CI, causing
"Could not find or load main class org.gradle.wrapper.GradleWrapperMain".
Use gradle/actions/setup-gradle and generate the wrapper before running
any ./gradlew commands.

https://claude.ai/code/session_01JvQt9SCcroodTwtW4NhiC7

* Generate Gradle wrapper jar in temp dir to avoid Kotlin version conflict

Running `gradle wrapper` inside example/android causes Gradle 9.3.1 to
compile the project's Expo/RN Kotlin plugins against its bundled Kotlin
2.2.0 stdlib, which is incompatible with the older Kotlin versions those
plugins expect. Generate the wrapper in an empty temp dir instead and
copy just the jar file.

https://claude.ai/code/session_01JvQt9SCcroodTwtW4NhiC7

* Fix Android tests and CI workflow

Remove ServiceLifecycleTest and ShutdownPathTest which can't work as
library-level instrumented tests (test APK runs as different uid and
can't start non-exported service in example app). Remove unused
IServiceCallback reference left after AIDL removal. Simplify CI by
removing broken Gradle wrapper generation step (expo prebuild already
generates it) and fragile module name discovery.

Remaining tests (16 instrumented + JVM) all pass locally:
- MessageFramingTest: length-prefixed framing protocol
- NodeJSIPCTest: IPC socket communication
- WatchForFileTest: file watching utility

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add app-level service lifecycle tests and check in example/android

Move service lifecycle and shutdown path tests to the example app's
androidTest (same UID as the service) to fix SecurityException when
starting the unexported ComapeoCoreService.

Tests that require a full Node.js backend are @ignore'd for now —
they'll be enabled once the app is fully built with the JS bundle.

Passing tests (4 app + 16 library + JVM):
- ServiceLifecycleTest: start, separate process, stop
- ShutdownPathTest: stop during startup
- NodeJSIPCTest: IPC socket communication (9 tests)
- WatchForFileTest: file watching utility (7 tests)
- MessageFramingTest: framing protocol (JVM)

Also:
- Check in example/android/ (remove from .gitignore) so we can
  add androidTest files without expo prebuild regeneration
- Remove expo prebuild step from CI (no longer needed)
- Add :app:connectedDebugAndroidTest to CI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add gradle-wrapper.jar (was excluded by global gitignore)

The jar is required to bootstrap Gradle builds. It was being excluded
by a global ~/.gitignore_global rule for *.jar files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix CI disk space for emulator and optimize workflow

- Add jlumbroso/free-disk-space to remove dotnet, haskell, large
  packages, and docker images (~15-20GB freed)
- Set disk-size: 2048M on emulator (down from default 7.4GB)
- Remove unused pre-installed NDKs and platform versions
- Use API level 30 for emulator (smaller system image)
- Restore npm install (needed for Gradle's node-based module resolution)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove manual NDK install steps — let Gradle auto-install correct version

Gradle auto-downloads the NDK version specified in build.gradle, so manual
sdkmanager install was redundant and was installing the wrong version
(27.0.12077973 vs 27.1.12297006 that Gradle actually needs).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix gradlew not found in emulator runner script

The android-emulator-runner action runs each line of the script as a
separate shell command, so cd and gradlew must be on the same line.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Skip tests that need service to stay alive without JS bundle

serviceRunsInSeparateProcess and stopActionStopsService both require
the service to remain running, which needs the full JS backend bundle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Enable all service lifecycle tests with Node.js backend deps

- Add Gradle task to auto-install nodejs-project node_modules before
  asset merging, so the Node.js backend can find its dependencies
- Remove all @ignore annotations — all 9 tests now pass
- Fix test cleanup race condition: stop using startServiceWithAction(STOP)
  in stopServiceAndWait() as it starts the service process and queues
  intents that race with the test's USER_FOREGROUND intent

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add stabilization delay after service start for CI emulator

The CI emulator (x86_64 software rendering) is significantly slower
than local ARM emulators. Wait 5 seconds after service registers as
running to let Node.js fully initialize before testing further actions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix critical bugs in service lifecycle, IPC, and file watching

Addresses 11 bugs identified in code review:

- ComapeoCoreService: reset isServiceStarted on stop/destroy, replace
  runBlocking with serviceScope.launch to prevent ANR on main thread
- NodeJSIPC: break send loop on IOException instead of infinite retry,
  allow reconnection from error state, close/recreate sendChannel on
  disconnect/connect, expose connectionState property
- NodeJSService: replace lateinit ipc with CompletableDeferred to prevent
  UninitializedPropertyAccessException race, return early in stop() when
  not running instead of throwing, separate destroy() from finally block
- ComapeoCoreReactActivityLifecycleListener: safe-call nullable activity
- watchForFile: start FileObserver before checking existence to fix TOCTOU
  race, add withTimeout wrapper (default 30s)
- control-rpc.js: declare missing count variable
- ComapeoCoreModule: return actual IPC connection state from getState()

Adds JVM unit tests for CompletableDeferred and timeout patterns, and
instrumented tests for stop-then-restart and waitForFile behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix Process.killProcess race on stop→restart and stabilize CI test

The async onDestroy coroutine could call Process.killProcess() after a
new service instance had already started in the same process, causing
"startForegroundService did not call startForeground" crashes. Track
active instance count so killProcess is skipped when a new instance
is alive.

Also change stopThenRestartWorks test to use stopServiceAndWait()
(process kill) instead of STOP action to avoid race on slow CI emulators.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Always call startForeground on USER_FOREGROUND to satisfy Android contract

Android requires startForeground() to be called within 5 seconds every
time startForegroundService() is invoked, even if the service is already
in the foreground. The old code returned early from startService() when
isServiceStarted was true, skipping the startForeground() call and
causing a RemoteServiceException crash on the second USER_FOREGROUND.

Move startForeground() before the isServiceStarted guard so it is
always called, while still only starting Node.js once.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Speed up CI emulator with AVD caching and ATD images

Two changes to improve instrumented test performance:

1. Switch from google_apis to google_atd (Automated Test Device) target.
   ATD images strip unnecessary apps and services, resulting in faster
   boot and lower resource usage. Available at API 30.

2. Add AVD snapshot caching. On first run, the emulator boots and saves
   a snapshot to the GitHub Actions cache. Subsequent runs boot from
   the cached snapshot instead of cold-booting, saving 1-3 minutes.

Also removed profile and disk-size/heap-size overrides since ATD images
have appropriate defaults and don't need a device profile.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: speed up builds with parallel gradle and x86_64-only native arch (#4)

- Enable org.gradle.parallel=true and org.gradle.caching=true in gradle.properties
- Pass -PreactNativeArchitectures=x86_64 to all gradlew invocations in CI,
  reducing native build targets from 4 to 1 (~67% native build reduction)

https://claude.ai/code/session_019Lbxh6CTzAuSh5YvP6tkGj

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
@gmaclennan gmaclennan deleted the claude/speed-up-ci-builds-ecNnx branch March 12, 2026 12:48
@gmaclennan gmaclennan added the maintenance Refactor / test / chore / ci / build (changelog) label Jun 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

maintenance Refactor / test / chore / ci / build (changelog)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants