ui-trace, the adb wrapper and its device recorder were loose files under ~/.local, and each Android checkout carried its own copy of the same emulator boot sequence. This puts them together, with an install script that symlinks them back so editing the repo is editing what runs. Two things are new rather than moved. `emu` is the emulator lifecycle -- name, serial, list, up, down -- keyed on the AVD named after the enclosing checkout, which is the rule that lets several sessions work here at once; and `adb` now fills in `-s` from that same rule, because with two emulators attached a bare `adb shell pm list packages` comes back empty rather than failing, which reads as the app being uninstalled rather than the question being ambiguous. `emu up` refuses when the machine has no room. 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 emulator first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
188 lines
7.2 KiB
Bash
Executable File
188 lines
7.2 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
|
|
port=${serial#emulator-}
|
|
rss=$(ps -eo rss,args | awk -v p="-port $port" '$0 ~ p && $0 ~ /qemu-system/ {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
|