Accepted
Some platform automation backends are expensive to start but cheap to reuse. iOS already uses a
long-lived XCTest runner session with an HTTP transport. That model avoids paying xcodebuild,
runner boot, and XCTest readiness costs for every command, while still allowing the daemon to
invalidate the runner when the device, app, bundle, or runner process changes.
Android snapshot capture initially used a one-shot instrumentation helper. Every snapshot launched
adb shell am instrument, connected UiAutomation, captured the tree, emitted XML, and exited.
Recent Android snapshot optimizations reduced XML size, idle waiting, extra file I/O, and hidden
content hint work, but a throwaway prototype still showed that process/session startup dominates
steady-state latency:
- launcher snapshot: one-shot p50
227ms, persistent socket p505.8ms - React Navigation playground snapshot: one-shot p50
265.7ms, persistent socket p5016.5ms
The same pressure can appear on new platform adapters. HarmonyOS or other device backends may have host tools, test runners, accessibility services, or bridge processes with the same shape: expensive startup, cheap repeated commands, and a need for strict invalidation.
Use persistent platform helper sessions when a backend has high startup cost and a reusable automation context.
A helper session is an optimization layer owned by the daemon, not a replacement for command correctness. It may keep processes, sockets, runner state, accessibility service flags, or device forwards warm. It must still execute each command against fresh platform state unless a separate cache contract has explicit invalidation.
The session pattern is:
- start lazily on the first command that benefits from reuse
- bind the session to a device identity and helper/runner identity
- communicate through a small validated protocol with request ids and version metadata
- reuse the session while the identity and protocol remain valid
- invalidate on device disconnect, helper reinstall/version change, process exit, socket/protocol failure, app/session identity change, or capture options that affect command semantics
- fall back to the existing one-shot path for the current command when reuse fails
- make shutdown best effort and make stale sessions disposable
For Android snapshots, productize a persistent helper mode that keeps UiAutomation alive and
serves fresh snapshot requests over an adb forward socket. Do not add snapshot result caching as
part of that first step. The first reliable win is infrastructure reuse, not data reuse. The current
implementation keeps the existing one-shot instrumentation helper as the fallback for startup,
socket, protocol, and request failures.
Android permits only one reliable instrumentation-owned UiAutomation context per device. Before
starting a different instrumentation helper, such as touch synthesis, the daemon must stop the
persistent snapshot helper session and let the next snapshot restart it lazily. Helper reuse must
never turn process ownership into cross-command interference.
For iOS, keep the XCTest runner session as the reference implementation for lifecycle and invalidation behavior. Android does not need to copy iOS internals, but it should reuse the same daemon-side ideas: per-device session manager, readiness checks, structured protocol errors, fallback/invalidation, and request-scoped observability.
For future platforms such as HarmonyOS, prefer designing adapters around this same helper-session contract when their native automation layer is runner-like. Avoid embedding platform-specific startup assumptions directly in command handlers.
- Keep one-shot helpers only: simplest and robust, but Android measurements show it leaves an order of magnitude of steady-state snapshot performance on the table.
- Cache snapshots in the daemon: faster for repeated reads, but unsafe after mutations, animations, navigation, system dialogs, or app process changes unless a mutation generation contract exists. Cache infrastructure can be added later; it should not be mixed with helper-session reuse.
- Promote an abstract cross-platform runner immediately: tempting, but premature. iOS XCTest, Android instrumentation, macOS helper, Linux AT-SPI, and future HarmonyOS backends have different startup and transport mechanics. Share the daemon lifecycle contract first, then extract common code only where repetition appears.
- Replace Android instrumentation with a normal app service: potentially useful, but Android
UiAutomationaccess is instrumentation-owned. A persistent instrumentation process keeps the required privilege model while removing repeated process startup.
Persistent helper sessions should be measured before being productized. A prototype or benchmark should show meaningful wall-clock improvement on a realistic app state, not just a trivial screen.
Session managers need more lifecycle tests than one-shot helpers: startup, ready protocol, reuse, timeout, malformed response, helper version mismatch, device disconnect, install invalidation, shutdown, exclusive instrumentation handoff, and one-shot fallback.
Observability should report whether a command used a persistent session, started one, reused one, invalidated one, or fell back to one-shot. This keeps CI and user bug reports diagnosable when a fast path fails.
Persistent sessions should not make direct interactive commands unexpectedly slow. Use short connect/request timeouts for the persistent path, then fall back to the existing one-shot timeout budget.
The daemon remains the owner of session lifecycle. Platform modules may expose helper-session operations, but command handlers should not directly manage long-lived helper processes or raw host tool state.
This ADR does not require every backend to implement a persistent session. It defines the preferred shape when the backend has the same startup/reuse economics that iOS and Android snapshots now demonstrate.