#!/bin/bash # This checkout's emulator: which one it is, whether there is room for it, # and getting it up and down. # # The AVD is named after the enclosing git checkout and nobody has to say so # (see `project_avd`). That is the whole point: several agent sessions work on # this machine at once, and an emulator named after the checkout is one that # cannot be somebody else's to install onto or take the foreground from. # # `up` refuses rather than tries when memory is short, which is not caution # for its own sake. On 2026-08-30 an emulator started with 2.8 GB available # invoked the OOM killer, and what it took was not the emulator that had just # started: it walked the user slice and killed pipewire, dbus-broker and # another session's running emulator first. The cost of one too many lands on # somebody else's work, minutes later, looking like an unrelated crash. set -euo pipefail . "$(dirname "$(readlink -f "$0")")/../lib/project-avd.sh" # What one costs, measured: 3.8 GB resident for a headless x86_64 AVD, plus # what it grows into while an app builds and installs. EMU_EXPECTED_MB=${EMU_EXPECTED_MB:-4200} # What must be left afterwards. A machine at exactly zero swaps itself to a # halt rather than failing cleanly. EMU_HEADROOM_MB=${EMU_HEADROOM_MB:-1200} # Two are comfortable on 23 GB; three is what invoked the OOM killer. EMU_MAX_RUNNING=${EMU_MAX_RUNNING:-2} DEVICE_PROFILE=${DEVICE_PROFILE:-pixel_10} SYSTEM_IMAGE=${SYSTEM_IMAGE:-system-images;android-36;google_apis;x86_64} sdk=$(android_sdk) || { echo "emu: no Android SDK found; set ANDROID_HOME" >&2 exit 127 } adb=$(android_adb) avd=$(project_avd) export ANDROID_AVD_HOME="${ANDROID_AVD_HOME:-$HOME/.android/avd}" usage() { cat >&2 <<'USAGE' usage: emu name the AVD this directory means serial its adb serial, if it is running (exit 1 if not) list every emulator attached, with its AVD and what it is costing up start it, refusing if the machine has no room down stop it Environment: AVD_NAME overrides the name, EMU_FORCE=1 overrides the memory refusal, DEVICE_PROFILE and SYSTEM_IMAGE decide what `up` creates. USAGE exit 2 } # Every emulator's serial, AVD and resident size -- the last being the number # the refusal below is about, so it is worth seeing before asking for another. cmd_list() { found=false while IFS=$'\t' read -r serial name; do found=true # Matched on the AVD name the emulator was started with, which is # what its process actually carries -- there is no -port in that # command line, and the console port in the serial does not appear # in it. Filtered on the process *name* rather than on the whole # line, so this cannot match the shell running it: that shell's # command line contains this pattern, and a `ps | awk` that matches # itself reports the size of the shell. rss=$(ps -eo rss=,comm=,args= | awk -v want="-avd $name" '$2 ~ /^qemu-system/ && index($0, want) {print int($1/1024); exit}') printf ' %-16s %-24s %s\n' "$serial" "$name" "${rss:+$rss MB}" done < <(running_avds "$adb") [ "$found" = true ] || echo " (none attached)" } # Whether starting one now is likely to cost somebody else their work. # # Reported in full rather than as a bare refusal: the caller cannot see this # machine, and "not enough memory" without the numbers or a way forward is a # message that gets overridden blind. check_room() { available=$(mem_available_mb) needed=$((EMU_EXPECTED_MB + EMU_HEADROOM_MB)) running=$(running_avds "$adb" | wc -l) problem="" if [ "$running" -ge "$EMU_MAX_RUNNING" ]; then problem="$running emulator(s) are already running, and $EMU_MAX_RUNNING is as many as this machine takes" elif [ "$available" -lt "$needed" ]; then problem="$available MB available, and one of these needs about $EMU_EXPECTED_MB MB plus $EMU_HEADROOM_MB MB of headroom" fi [ -z "$problem" ] && return 0 if [ "${EMU_FORCE:-}" = 1 ]; then echo "emu: $problem -- starting anyway because EMU_FORCE=1" >&2 return 0 fi { echo "emu: not starting '$avd' -- $problem." echo echo " Emulators attached:" cmd_list echo echo " Biggest processes:" ps -eo rss,comm --sort=-rss | awk 'NR>1 && NR<=6 {printf " %-24s %6.0f MB\n", $2, $1/1024}' echo echo " What usually frees enough:" echo " gradle --stop (an idle Gradle daemon holds 1-2 GB between builds)" echo " emu down (in the checkout that owns an emulator you are done with)" echo " ask the session that owns one -- they are named after their checkout" echo " EMU_FORCE=1 emu up (if you have decided the numbers above are wrong)" } >&2 exit 1 } cmd_up() { if serial=$(avd_serial "$adb" "$avd"); then echo "emu: '$avd' is already running ($serial)" >&2 echo "$serial" return 0 fi check_room if [ ! -f "$ANDROID_AVD_HOME/$avd.ini" ]; then echo "emu: creating AVD '$avd' ($DEVICE_PROFILE, $SYSTEM_IMAGE)" >&2 "$sdk/cmdline-tools/latest/bin/sdkmanager" "emulator" "$SYSTEM_IMAGE" >/dev/null || echo "emu: sdkmanager could not confirm the system image; trying anyway" >&2 echo no | "$sdk/cmdline-tools/latest/bin/avdmanager" create avd \ -n "$avd" -k "$SYSTEM_IMAGE" --device "$DEVICE_PROFILE" --sdcard 512M >&2 fi # The host keyboard, so anything with a text field can be typed into # rather than tapped out on the on-screen one. config="$ANDROID_AVD_HOME/$avd.avd/config.ini" if [ -f "$config" ] && ! grep -qx 'hw.keyboard=yes' "$config"; then grep -v '^hw\.keyboard=' "$config" >"$config.tmp" echo "hw.keyboard=yes" >>"$config.tmp" mv "$config.tmp" "$config" fi # A stray process for this AVD that never registered with adb. The # bracketed first character keeps the pattern from matching the shell # running this script, which would kill it mid-flight. pkill -f "[e]mulator.*-avd $avd" >/dev/null 2>&1 || true log="/tmp/$avd-emulator.log" : >"$log" if [ -n "${DISPLAY:-}" ] || [ -n "${WAYLAND_DISPLAY:-}" ]; then echo "emu: starting '$avd' with GPU acceleration" >&2 "$sdk/emulator/emulator" -avd "$avd" -gpu host -no-audio >"$log" 2>&1 & else echo "emu: no display -- starting '$avd' headless" >&2 "$sdk/emulator/emulator" -avd "$avd" -gpu swiftshader_indirect -no-audio -no-window \ >"$log" 2>&1 & fi pid=$! for _ in $(seq 150); do if ! kill -0 "$pid" 2>/dev/null; then echo "emu: the emulator exited before it finished booting:" >&2 tail -20 "$log" >&2 exit 1 fi if serial=$(avd_serial "$adb" "$avd"); then booted=$("$adb" -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') if [ "$booted" = 1 ]; then echo "emu: '$avd' booted ($serial)" >&2 echo "$serial" return 0 fi fi sleep 2 done echo "emu: '$avd' did not finish booting in five minutes; log at $log" >&2 exit 1 } cmd_down() { if ! serial=$(avd_serial "$adb" "$avd"); then echo "emu: '$avd' is not running" >&2 return 0 fi "$adb" -s "$serial" emu kill >/dev/null rm -f "${XDG_RUNTIME_DIR:-/tmp}/emulator-tools/$avd.serial" echo "emu: stopped '$avd' ($serial)" >&2 } case "${1:-}" in name) echo "$avd" ;; serial) avd_serial "$adb" "$avd" || { echo "emu: '$avd' is not running" >&2; exit 1; } ;; list) cmd_list ;; up) cmd_up ;; down) cmd_down ;; *) usage ;; esac