Files
emulator-tools/README.md
T
irisandClaude Opus 5 1da445ae32 Re-measure the GPU numbers with an emu list that can be trusted
The table in the README compared our own app on software against the stock
Settings app on the GPU, and labelled both as Settings. The memory saving
beside it -- 1.1 GB -- came from `emu list` before 0984336 fixed its prefix
match, so it was one emulator's resident size reported against another's.
Both numbers were written down as recorded guidance, which is the worst
place for a figure nobody can reproduce.

Taken again on one AVD, same gesture, same app on both sides. Settings
scrolling its own list: 57.4% janky frames and 18 slow UI-thread frames on
software, 2.3% and 0 on the GPU; legacy jank 100% against 5.7%. Memory
twenty seconds after boot: 3537 MB against 2918 MB, so about 620 MB rather
than 1.1 GB.

The conclusion is stronger than the version that was wrong -- the stock
Settings app missed every frame's deadline on the rasteriser -- and the
correction is noted in place rather than quietly swapped, since the old
figure has been quoted between sessions today.

Also records which jank column to read, from the peer session's measurement
of a change whose modern figure barely moved while its legacy figure fell by
five sixths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 00:26:55 -04:00

142 lines
6.7 KiB
Markdown

# emulator-tools
Everything this machine uses to drive an Android emulator, in one place:
which emulator a command means, whether there is room to start it, reading a
screen as text, and taking a screenshot that does not cost a thousand tokens.
./install.sh
symlinks `bin/*` into `~/.local/bin` and `share/ui-trace` into
`~/.local/share`, so editing this repo is editing what runs. It keeps
anything it replaces as `<name>.bak`.
## What is here
| | |
|---|---|
| `emu` | this checkout's emulator: `name`, `serial`, `list`, `up`, `down` |
| `adb` | adb, aimed at this checkout's emulator, with screenshots scaled |
| `ui-trace` | records a screen as text at 60Hz, and drives it |
| `lib/project-avd.sh` | the rules all three share |
| `share/ui-trace/` | the device recorder (`UiTrace.java`) and its build |
## One emulator per checkout, named after it
`emu` and `adb` both work out which AVD you mean from the enclosing git
checkout: in `~/repos/ai-app-2` that is `ai-app-2`. Nothing has to be
configured, and `AVD_NAME` overrides it where the guess is wrong.
This is the rule that lets several agent sessions work here at once. The
emulator used to be the one resource that could not be parallelised: taking
it meant messaging the other sessions, waiting, and handing it back — and
installing an app onto somebody else's running emulator steals the foreground
from whatever they were looking at.
It is also why `adb` fills in `-s`. With two emulators attached, a bare
`adb shell pm list packages` comes back **empty** rather than failing, which
reads as the app having been uninstalled rather than as the question being
ambiguous. An explicit `-s`, `ANDROID_SERIAL`, or a subcommand that is not
about one device (`adb devices`) all turn the defaulting off, and nothing is
guessed when this checkout's emulator is not running: adb's own error is
better than a wrapper picking a stranger's device.
## Rendering on the GPU with no display
This machine has no screen, and for a long time that meant the emulator
rasterised in software. It does not have to: the VM sees the host's GPU
through virtio-gpu, and `/dev/dri/renderD128` is readable without being in
the `video` group.
What was in the way is that the emulator's `-gpu host` renderer speaks GLX,
so with no `DISPLAY` it cannot open a context at all -- it says
`GlxEnginegetDefaultDisplay: Failed to open display 0` and falls back. So
`emu up` 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 then
reports `virgl (AMD Radeon RX 7900 XT)` instead of a software rasteriser.
One compositor is started for the machine and reused, on a named socket
under `XDG_RUNTIME_DIR`; it needs no seat, no DRM master and no card node.
Measured 2026-08-31, scrolling the same list in the stock **Settings** app
on the same AVD with the same gesture -- so this is the platform's own cost,
with no app of ours in it:
| | janky frames | legacy janky | 90th percentile | slow UI-thread frames |
|---|---|---|---|---|
| software | 57.4% | 100% | 73ms | 18 |
| on the GPU | 2.3% | 5.7% | 26ms | 0 |
Read **both** jank columns. The legacy figure counts a frame against the
16.7ms deadline and the modern one against what the compositor actually
needed, so on a fast enough path the modern number can barely move while the
legacy one halves. When comparing two builds of an app in here, the legacy
column is usually the one carrying the signal.
An emulator also costs about **620 MB less** resident this way: measured
twenty seconds after boot on one AVD, 3537 MB on software against 2918 MB on
the GPU. (An earlier draft of this file said 1.1 GB. That number came from
`emu list` before the prefix bug in it was fixed, and was one emulator's size
reported against another's.)
This matters for more than speed. Before it, frame timings measured in here
said nothing at all about an app: every app was bounded by the rasteriser
rather than by anything it was doing, and the stock Settings app missed
*every single frame's* deadline while scrolling its own list. Now the
platform floor is low enough that an app's own jank is visible above it.
`GPU_HOST_FEATURES` carries the one workaround: host Vulkan is switched off,
because 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. It is worth retrying whenever mesa moves -- the failure is loud
and costs one boot to find.
If sway is missing, or the compositor will not start, `emu up` says so and
starts on software rendering as it always did. None of this is essential.
## Refusing to start one
`emu up` checks `MemAvailable` first and refuses if starting an emulator
would leave the machine short, printing what is attached, what is large, and
what usually frees enough. `EMU_FORCE=1` overrides it.
The reason is a real incident rather than tidiness. On 2026-08-30 an emulator
was started on this box with 2.8 GB available; the OOM killer ran, 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. The
cost of one too many lands on somebody else's work, minutes later, looking
like an unrelated crash.
Measured numbers behind the defaults: a headless x86_64 AVD is about 3.8 GB
resident, this VM has 23 GB with a swap that is routinely full, two emulators
are comfortable, and three were not.
## Reading a screen without screenshots
`ui-trace` drives a small resident recorder on the device and samples the
accessibility tree at 60Hz, so an animation or a settling layout is visible
rather than happening between two samples.
ui-trace record -d 3000 --do 'tap 540 800' -o /tmp/t.txt
ui-trace elements /tmp/t.txt # what is on screen, to pick from
ui-trace show /tmp/t.txt # what moved
ui-trace show /tmp/t.txt -m 'Send|Stop' --field box
Bounds come out in **device** pixels, so they feed straight back into a
`--do tap`. Prefer this to a screenshot for anything positional: two buttons
that measured identically at 171x105 looked different in a screenshot,
because the filled one read as larger than the disabled grey one. Screenshots
are for appearance — colour, weight, whether it looks right.
`adb exec-out screencap -p` still works and comes back scaled to 800px on its
long edge, which is ~380 tokens to read instead of ~1460 and still legible
for layout, contrast and small labels. `ADB_SCREENCAP_MAX_EDGE` raises it for
one call.
## Using it from a project
A project's own `run-android.sh` should build and install; the emulator half
belongs here:
SERIAL=$(emu up)
./gradlew :androidApp:assembleDebug
adb install -r <apk> # already aimed at this checkout's emulator