The adb wrapper can only aim a call that goes through it, and Gradle's
Android tasks do not: installDebug, uninstallDebug and connectedAndroidTest
ask the adb server for every attached device and act on all of them. One
session ran installDebug with two emulators up and replaced the app on both,
which Gradle reported as success and the other session read as its own build
never landing.
`emu check` answers the question the wrapper already answers for adb --
would a command that reaches every attached device reach a stranger's
emulator from here -- and install.sh links a Gradle init script into
~/.gradle/init.d so every build on this machine asks it, including checkouts
nobody has adopted it in.
It refuses narrowly: a lone emulator, a physical phone and a caller who
named a device are all fine. ANDROID_SERIAL naming another checkout's
emulator is not, because that is what a serial exported into a long-lived
shell decays into once an emulator restarts and another's takes the port.
`emu up` kills a stray process for its own AVD first, with `pkill -f
"[e]mulator.*-avd $avd"`. An AVD name is a prefix of every longer one, so
`-avd ai-app` matched `-avd ai-app-2`: starting one checkout's emulator
silently killed another checkout's. That is the exact interference this tool
exists to prevent, and from the other side it looked like an emulator dying
on its own -- twice on 2026-08-31 before the pattern was the suspect, with
nothing in its log but a graceful shutdown nobody had asked for.
`emu list` had the same hazard in its `index($0, want)`, where the cost is
quieter: with both up it reported ai-app-2's resident size against ai-app,
which is a number that looks entirely plausible.
`running_avds` and `avd_serial` were already exact matches, which is what
made this hard to see -- the rule was right in two places out of four.
Verified with both emulators running: the ai-app pattern now matches only
ai-app, and `emu list` reports the two sizes separately.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This VM sees the host's 7900 XT through virtio-gpu and renderD128 is
readable without the video group, so the software rasteriser was never
necessary. What was in the way is that the emulator's `-gpu host` renderer
speaks GLX: with no DISPLAY it cannot open a context at all
("GlxEnginegetDefaultDisplay: Failed to open display 0") and silently falls
back.
So `emu up` now starts a headless sway whose only job is to provide an
Xwayland, and Xwayland's GLX runs on the render node. The guest's GLES
reports `virgl (AMD Radeon RX 7900 XT)` afterwards. One compositor per
machine, on a named socket so a second `emu up` reuses it; no seat, no DRM
master, no card node.
Scrolling a list in the stock Settings app: 25.5% janky frames and 8 slow
UI-thread frames on software, 3.3% and 0 on the GPU. An emulator also costs
1.1 GB less resident (2.9 GB against 3.9 GB), which is real headroom on a
machine that only takes two.
The reason this is worth the machinery is measurement rather than speed.
Before it, frame timings from in here said nothing about an app at all --
ai-app scrolled *better* than Settings, because both were bounded by the
rasteriser rather than by anything either was doing.
Host Vulkan is off in `GPU_HOST_FEATURES`, and that one is a bug rather than
a preference: gfxstream asks Venus for a memory type for its ColorBuffers
that Venus does not offer, and the emulator dies before adb sees it. GLES is
unaffected. Retry it whenever mesa moves; the failure is loud.
A missing sway, or one that will not start, says so and falls back to
software exactly as before -- verified by moving the config aside.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>