Android Mac Mini POC โ
Objective โ
Evaluate the capacity of a self-hosted Mac Mini for running Android emulators and GitHub Actions runners for E2E testing, with the goal of running multiple test suites in parallel to reduce PR feedback time.
Machine spec: Mac Mini M4 ยท 10 cores ยท 32GB RAM
Emulator config: API 34 ยท google_apis ยท arm64-v8a ยท Pixel 6 profile (1080x2400) ยท 2048MB RAM (hw.ramSize) ยท 2 cores
GPU rendering: -gpu host
Emulator capacity on the current machine โ
Android emulators are fundamentally different from iOS simulators โ they run a full guest OS under QEMU with a separate ADB server:
- Constant QEMU overhead โ each emulator consumes ~4.5GB real RAM regardless of configured heap size
- Separate ADB server per runner โ required to prevent cross-talk between emulators
Actual Android emulator capacity on the current Mac Mini (10 cores, 32GB):
| Runners | Peak RAM | Viability |
|---|---|---|
| 2 | 64% | Stable โ confirmed viable, 0 pageouts |
| 3 | 98% | Not viable โ over-committed from the start |
| 4 | โ | Not viable โ OOM kills during startup |
Each runner slot carries an emulator, ADB server, and Appium server. 3 emulators ร ~4.5GB + ~20GB baseline exceeds the machine's 31GB โ the 3-runner config was over-committed from the start.
What Was Tried โ
RAM consistently hits 98โ99% under 3-runner load โ the combined QEMU, rendering, and ADB overhead saturates 32GB. The key variable is whether suites are actually completing cleanly or being held together by retry logic.
| Metric | 2 runners | 3 runners | 4 runners |
|---|---|---|---|
| Suites passed | 4/4 | 6/6* | 0/6 |
| Peak RAM | 64% | 98% | โ |
| Avg CPU | ~71% | ~92% | โ |
| Pageouts | 0 | +16k | โ |
| OOM kills | 0 | Yes (hidden) | Yes |
| Duration | ~22 min | ~19 min | โ |
2 runners โ all 4 suites passed in 22 min post-reboot. 64% RAM, zero pageouts. Confirmed stable.
3 runners โ appeared to pass but was running in a degraded state throughout. Emulators were crashing and restarting mid-test; retry logic masked the failures. CPU was below 5% idle for 58% of the day. Not viable.
4 runners โ OOM killed during startup. Not viable on 32GB.
Observed Resource Usage โ
3 runners (2026-03-19, 10:32โ10:51) โ
| Metric | Peak | Sustained | Notes |
|---|---|---|---|
| System RAM | 98% (31G / 31G) | 95โ98% for entire run | Effectively no headroom |
| System CPU | 100% (0% idle) | Hit 0% idle multiple times | Both user and sys saturated |
| Per-emulator CPU | 696.9% (~7 cores) | 100โ600% range | Bursts during app install/boot |
| Per-emulator RAM | ~14% each (~4.3GB) | ~12โ14% | 2x configured 2048MB due to QEMU/rendering overhead |
| Pageouts | +18,230 during run | Active throughout | System was continuously paging to disk |
During this run, emulator PID 47179 (runner_1) was OOM-killed mid-test and replaced by a new process (PID 55082) โ visible in the logs as a PID change partway through.
2 runners (2026-03-20, 12:52โ13:14) โ
| Metric | Peak | Sustained | Notes |
|---|---|---|---|
| System RAM | 64% (20GB) | 53โ64% | ~11GB headroom throughout |
| System CPU | 100% (0% idle) | ~29% avg idle | Bursts at emulator boot only |
| Per-emulator CPU | 779% (~8 cores) | 317โ659% range | Spikes during boot/install |
| Per-emulator RAM | ~4.5GB each | Consistent | Same QEMU overhead as 3-runner config |
| Pageouts | 0 | 0 | No paging at any point |
One emulator (S22_runner_3) crashed and restarted once during a CPU spike when both emulators were booting concurrently. It recovered and the suite completed.
Limitations Discovered โ
3 runners was never actually viable โ
- Each emulator consumes ~4.5GB real RAM regardless of the configured
hw.ramSizeโ 3 emulators + ~20GB baseline = ~33.5GB on a 31GB machine. The setup was always over-committed. - Tests appeared to pass during the POC but were doing so by brute force โ emulators would crash, restart, and retry until the test eventually got through. The logs confirm this was happening continuously.
- 10 CPU cores are fully saturated under parallel load. Individual emulators burst to 600-700% CPU (6-7 cores) during app install and rendering, leaving nothing for the other runners.
Memory compressor drift requires regular reboots โ
macOS accumulates compressed page data from emulator launches and OOM-kills in the memory compressor. This memory is not released until reboot. After 8 days of uptime, the compressor had consumed 7.4GB of RAM โ enough to push even a 2-runner config into failure. A scheduled weekly reboot is required to keep the baseline stable.
Screen recording is unlikely to be feasible โ
Both RAM and CPU are at their ceiling even at 2 runners โ adding ffmpeg or any video capture process per runner would push the machine further into resource pressure.
Things to Try โ
- GitHub Actions hosted runners โ GHA's
macos-latestand Linux runners support Android emulation without the overhead of managing self-hosted hardware. This is what we ended up adopting โ see the Android GHA hosted POC for details. - Linux runners โ macOS is not well-suited to Android emulation at scale. A Linux-based runner (KVM-accelerated, hardware GPU passthrough) could support more parallel runners at a lower cost.
Maintenance โ
- Runner logs are stored at
~/Desktop/e2e-logs/and retained for 30 days. - A weekly reboot (e.g. Sunday 3 AM) is required to clear macOS memory compressor drift โ after 8 days of uptime, the compressor had consumed 7.4GB of RAM, destabilising even the 2-runner config.
Incident โ
Throughout the POC, 3 runners was at the limit but appeared to be holding โ controlled test runs were completing and the hope was that it would be stable enough for general use. Attempt 6 (dropping to 2 runners) triggered a full collapse: the emulators, Appium, and ADB processes all crashed. The Mac Mini itself was restarted to recover, and a post-mortem was run against the monitor logs.
The logs revealed that 3 emulators was never actually stable โ it had been running in a degraded state throughout the POC. CPU was below 5% idle for 58% of the day, 4.2GB was paged out to disk, and RAM hit 96-99% for approximately 2 hours of wall time.
A contributing factor was memory compressor drift โ the machine had been running for 8 days without a reboot, and the macOS memory compressor had accumulated 7.4GB of RAM holding compressed page data from previous emulator launches and OOM-kills. This is not released until reboot. It pushed the idle baseline from the normal ~20GB up to ~25GB, meaning even 2 emulators exceeded available memory.
The underlying root cause: each emulator consumes ~4.5GB real RAM regardless of configured heap size. 3 ร 4.5GB + ~20GB baseline = ~33.5GB on a 31GB machine. The machine was over-committed from the start.
Final Recommendation โ
The Mac Mini is not recommended for Android E2E testing. 2 runners is the maximum viable configuration, costs โฌ125/month, and requires ongoing maintenance (weekly reboots, memory compressor management) to stay stable. GitHub Actions hosted runners are a more reliable and cost-effective alternative โ see the Android GHA hosted POC.