Files
emulator-tools/bin/emu
T
irisandClaude Opus 5 e379edfc1b Aim at the right emulator, and measure the right process
Two corrections from driving these against a live machine. A bare adb call
made from a checkout whose own emulator is down was still handed through to
whatever single device was attached -- which is another session's emulator,
installing over the app they are looking at and taking their foreground, and
looking like it worked. It now refuses and says which serials belong to whom.
A physical phone belongs to no checkout and is left alone, which is why this
asks for AVD names rather than counting devices.

`emu list` was reporting 6 MB for a 3.8 GB emulator: it matched on a -port
that is not in the process's command line at all, and the line it did match
was the shell running the ps.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 13:09:46 -04:00

195 lines
7.7 KiB
Bash
Executable File

#!/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 <command>
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