Session broker device reuse, reset, and capacity¶
How to trade device startup time against reset work and host capacity. Covers the reuse modes delivered under #14358: iOS erase and keep-booted, Android snapshot and keep-booted, reset-boot admission, and the per-session mutation journal. None has passed real-device contamination qualification, and no matched timing comparison shows reuse beats creating a fresh device.
Current status¶
The broker is opt-in via AUTOMOBILE_SESSION_BROKER_CONFIG; every reuse mode
defaults off.
- iOS:
reuse.ios.modeisoff,erase,keep-booted, orminimal. LegacyreuseCleanDevices: truestill selectseraseand cannot combine with areuse.iosblock choosing another mode. See iOS minimal mode forminimal. - Android:
reuseAndroidCleanDevices: trueplusandroidResetMode(snapshotorkeep-booted). The flag without a mode gives no reuse (release cannot prove unobservable state was restored, so the device is destroyed). iOS settings never enable Android reuse. - Admission: iOS reset boots hold their boot slot across stop, erase and boot;
limits.maxConcurrentResetBootscan queue them; an iOS request can wait for a compatible device about to finish resetting. - Journal: journaled tools record the mutation before acting; a recording failure warns and the tool proceeds. It guides best-effort reverts and is not an admission gate.
Keep-booted is best-effort cleanup, not a guarantee that all state was reset. Known
degradation until #14786: iOS keep-booted reset on iOS 27 always falls back to
erase (the absent-key defaults delete wording is not recognized), so the measured
~10 s keep-booted reuse is not yet reachable there.
Choose an iOS reuse mode¶
| Mode | What it does | Boundary |
|---|---|---|
off |
Destroys the device on release; next request creates one | No idle device retained |
erase |
Stops, erases, then boots the same device before reuse | Erase and boot checks must succeed, else destroyed/unavailable |
keep-booted |
Runs platform cleaning, then a health check, leaving the device booted | Not a claim all state was cleaned (see “What keep-booted does not clean”) |
minimal (#14682) |
Keeps the device booted; uninstalls session apps, resets keychain and privacy, restores appearance, content size and pasteboard | No erase fallback; no baseline diff (see “iOS minimal mode”) |
In keep-booted, maxUses (default 10; 20 in erase mode, where it has no effect) forces every Nth release to erase instead;
on success the same device returns to idle and later releases can use keep-booted
again. It bounds drift rather than capping sessions or retiring the device.
The only iOS fallback is erase.
{
"reuse": { "ios": { "mode": "keep-booted", "maxUses": 10, "fallback": "erase" } },
"limits": {
"idlePoolCap": 2,
"totalDeviceCap": 10,
"sessionCap": 6,
"maxConcurrentResetBoots": 2
}
}
A fragment: include contractVersion and defaults.ios from
the broker experiment; any mode other than off
requires defaults.ios. iosClassifications is optional (default []). The
schema rejects reuseCleanDevices: true alongside a reuse.ios block that does
not select erase.
iOS minimal mode¶
reuse.ios.mode: "minimal" (#14682) keeps the simulator booted and runs only these
steps on release, in order, each with its own time budget (AUTOMOBILE_IOS_RESET_STEP_BUDGET_MS):
- terminate and uninstall each app the session installed (from the mutation journal), and uninstall
the managed Files fixture app when the session staged
user_filesfiles, simctl keychain reset,simctl privacy reset all,simctl ui appearance lightandcontent_size large(the defaults a broker-created simulator enters the pool with),- empty the pasteboard.
It runs no baseline diff and no health probe, and it never falls back to erase or a
reboot. Any failed or timed-out step, an incomplete mutation recording, or the
maxUses-th release (default 5; keep-booted uses 10) returns a pending reset
outcome, so the broker destroys the simulator and the next acquisition creates a
fresh one. A failure is recorded per session: a redrive repeats the destroy outcome
instead of cleaning a half-cleaned simulator again. Lost ownership still aborts the
release rather than destroying.
{ "reuse": { "ios": { "mode": "minimal", "maxUses": 5 } } }
The only tunable is maxUses; fallback is rejected for this mode.
Not reverted (the trade-off for speed): locale, language and calendar, simulated
location, status-bar overrides, increase-contrast, reduce-motion and bold text,
notifications, contacts, calendar and photo stores, trusted certificates and
profiles, edits to preinstalled apps, app-group and shared-container contents of
apps the session did not install, and anything the journal did not record.
Choose keep-booted when those matter; choose minimal when sessions only install
and use their own apps and the cheaper release is worth the weaker isolation.
Device verification is pending (#14786, #14787 describe the keep-booted siblings of
the same simctl behaviours).
Choose an Android reset mode¶
Separate opt-in, default off. androidResetMode requires
reuseAndroidCleanDevices: true and a defaults.android policy.
snapshot: saves a named baseline snapshot for each freshly created or replaced emulator; release loads it, waits for readiness and checks health. On failure it relaunches the same AVD with wipe-data (keeping reserved boot capacity) and recaptures the baseline; otherwise the device is destroyed.wipe-datais the strategy’s internal fallback, not a selectable mode. Broker snapshots are hidden from user snapshot tools; a session making opaque persistent changes (e.g. a user snapshot capture) still forces the wipe-data fallback.keep-booted: runs the cleaning operations below, then health. EveryandroidKeepBootedMaxUses-th (default 20) release goes to the fallback chosen byandroidKeepBootedFallback:"snapshot"(which may itself fall back to wipe-data), or"destroy"/omitted (destroy on failed cleanup, failed health, or themaxUseslimit).
androidResetTimeoutMs (default 600,000 ms) is valid only with snapshot, or
keep-booted plus androidKeepBootedFallback: "snapshot", where it bounds that
fallback. androidKeepBootedMaxUses and androidKeepBootedFallback are rejected
outside keep-booted.
androidIdleTtlMs (optional, positive integer milliseconds; requires an
androidResetMode) destroys an Android device that has sat idle in the pool for that
long. Both modes keep the emulator booted while idle (several GB of RSS each), so it
applies to either. Omitted, an idle device stays booted until it is evicted or drained.
{
"reuseAndroidCleanDevices": true,
"androidResetMode": "snapshot",
"androidResetTimeoutMs": 600000,
"limits": { "idlePoolCap": 1, "totalDeviceCap": 7, "sessionCap": 4 }
}
{
"reuseAndroidCleanDevices": true,
"androidResetMode": "keep-booted",
"androidKeepBootedMaxUses": 20,
"androidKeepBootedFallback": "snapshot",
"androidIdleTtlMs": 900000
}
Both need contractVersion and defaults.android. The strict host schema accepts
the iOS reuse block and the Android keys together.
Admission and fallback order¶
Reuse is limited to an idle device compatible with the requested profile.
- Reset wait (iOS only): a request may wait for a compatible resetting device
only when the same profile has reset p90 and create/boot p50 samples from the last
24 h. The device qualifies when its positive p90 remaining-time estimate is no
greater than the shared cutoff (minimum create p50 + boot p50 across matching
resetting candidates with usable estimates). The wait equals that estimate, capped
by the preparation deadline; one waiter per resetting device; no matching samples
means no wait. If the estimate timer fires first, normal admission resumes; if the
deadline fires first, the request fails with retryable
session_preparation_failed. It is a bounded opportunity, not a guarantee. Otherwise creation is subject to session, total-device and platform boot-gate limits; a cap never makes a busy or incompatible device reusable. - Reset boots: an iOS erase reset reserves its boot slot before stopping and
holds it through erase and boot.
limits.maxConcurrentResetBootsqueues excess iOS reset boots in the broker cleanup queue (not the boot gate); omitted shares the gate’s host limit. Gate admissions are taggedreset/create/restartandlistDevicesreports live iOS admissions by reason. - Keep-booted release: below
maxUses, cleaning runs, then health; a passing health check returns the device to idle only while ownership is valid. ReachingmaxUsesskips cleaning straight to fallback (iOS erase; Android snapshot if configured, else destroy). On iOS, re-running a release that already recorded a failure also skips cleaning; a journal-load failure or unsettled earlier native step fails the release without the erase fallback (resource stays pending or is destroyed); failures during cleaning or health reach the erase fallback. Ownership loss prevents idle publication and cleanup/fallback stay ownership-fenced (pending or destroy, never a reset under a stale fence).offdestroys on release.
When a device is not reused¶
- Owner exit (
session_owner_exited): the broker destroys the device after a delay (about 26 s in batch 20261021e) instead of resetting it to idle. - Graceful daemon restart: active broker sessions are handed off and adopted (#12765, #13849) and keep their device. Android reset baselines live in daemon memory, so an adopted Android session’s device cannot be reset on release and is destroyed; the next session needs a cold create.
Keep-booted cleaning operations¶
No comparison of the result with the captured baseline; unsupported journal reversions may be skipped (no all-kinds gate).
- iOS: replay supported journal reversions; remove non-baseline apps and their removable app-group containers; reset keychain and all privacy grants; clear location and status-bar overrides and pasteboard; restore appearance/content-size/contrast and locale-related global preferences; then check the simulator’s booted target and automation health endpoint. The baseline guides selected operations only.
- Android: replay supported reversions; remove third-party packages absent from the baseline; revoke newly granted runtime permissions for retained third-party apps; restore settings; remove added files under supported shared-storage roots; clear clipboard if supported; then check boot completion, package-manager responsiveness and CtrlProxy health. Non-critical setting/revert errors are logged and skipped; critical cleaning or health failures use fallback or destruction.
- Fallback: iOS erase; Android snapshot reset (itself falling back to wipe-data) when configured, else destroy. Success makes the resource idle-eligible; failure leaves it unavailable or destroys it.
Timings (phase names are implementation details): iOS records journal,
preflight inventory, individual revert, sanitize (incl. trust-store:verify),
health and health-probe phases; native commands bound at 10 s, the health HTTP probe
at 5 s, reset steps default 30 s. Android keep-booted records journal, clean and health
phases. Cleaning uses the remaining androidResetTimeoutMs budget (default 600 s), or
one third of it when snapshot fallback is configured; fallback uses the remaining budget
minus a bounded 2 s command-settlement reserve. Android reset adb attempts derive their
timeouts from the remaining budget and retry transient timeouts with backoff; readback
mismatches still fail closed. Android snapshot phases include identity, load, readiness, health, stop and wipe-data launch. A
keep-booted fallback reports fallback:snapshot even if snapshot itself fell back
to wipe-data.
What keep-booted does not clean¶
A successful run plus health check does not make the device equivalent to a freshly erased or wiped one.
- iOS: Photos, Contacts and Calendar additions remain; baseline app data is not
reset and baseline app-group data can persist in retained shared containers; other
unsupported state may remain. A user-trusted root certificate is never left
behind:
simctl keychain resetdoes not clear the trust store, so the reset checks it and falls back to erase when any user trust setting exists (#14828). - Android: accounts remain; baseline apps’ data is not reset (no default
pm clear); overwritten/deleted baseline files are not restored; added files are removed only under the supported roots; other uninventoried state may remain.
Use iOS erase or Android snapshot when a full reset boundary is required, and
maxUses to bound drift (iOS periodically erases and keeps the device; Android goes
to the snapshot fallback if configured, else destroys).
Size caps independently¶
sessionCap: active or acquiring sessions; size from peak simultaneous workload.totalDeviceCap: all broker-owned native devices (active, creating, idle, resetting, uncertain, still-destroying). Leave recovery headroom; pending destruction stays charged until absence is confirmed. One limit covers every kind: Android emulators of any form factor (phone, Wear OS, TV, Automotive), iPhone/iPad simulators (a CarPlay screen is a display of its iPhone, not a device) and Apple Watch simulators (watchosbindings and runningambroker-watches are charged). A watch binding is created, reused and deleted like an iOS simulator binding and is ready once booted. A released watch followsreuse.ios.mode: erase issimctl eraseplus a gated reboot, keep-booted runs its fallback, minimal destroys; seedocs/decisions/watchos-platform.md.idlePoolCap: devices retained between sessions; more warm devices cost disk, memory and simulator/emulator capacity. Start small, from concurrent bursts and measured reuse demand.- Platform boot-gate limit: concurrent boots (including creation/replacement under that gate); distinct from device-count caps. A watch boot takes a slot in the simulator (iOS) gate, which already counts every booted simulator.
limits.maxConcurrentResetBoots: queues iOS erase resets and keep-booted’s erase fallback, Apple Watch erases included; does not limit Android keep-booted cleanup; unlimited when omitted. Keep it at or below safe iOS boot concurrency.
Starting relationship: totalDeviceCap >= sessionCap + idlePoolCap plus allowance
for in-flight creation/cleanup (a target, not a promise: transitions and uncertain
cleanup also consume the cap). Reducing idlePoolCap does not evict immediately:
trimming runs when a device returns to idle. Separately, admission at the total cap
can evict the oldest idle device, matching or not. Active or resetting devices are
never evicted for an idle target. The daemon/drainBrokerPool operator method
destroys every idle pooled device through the same durable destroy journal, without
an acquisition; each device reports destroyed or cleanup_pending, and a replay
drains only what is still idle.
Idle expiry runs on the broker’s recovery pass (every 5 s), so a device is destroyed between the TTL and the TTL plus one pass after it went idle. The idle clock is held in daemon memory per resource revision: reuse (which changes the revision) restarts it, and a daemon restart restarts every clock from the first pass, so an idle device can outlive one restart by up to one TTL. Expiry claims a device only while it is still idle at the revision the pass observed, in a write transaction serialized with admission; a device an acquisition reserved first, or one that is held, resetting or already destroying, is never expired. Claimed devices are destroyed through the drain’s destroy journal and are joined on shutdown; iOS has no idle TTL.
Worked sizing example¶
No matched comparison shows reuse reduces end-to-end latency; an idle-pool hit
moves boot work from acquisition to release. Illustrative (not measured) creation
p50 ~107 s, p90 ~124 s. Measured iOS erase reset: ~15 s stop, 0.3 s erase, 52 s
boot, ~67.3 s total. For up to six concurrent sessions retaining two warm devices:
sessionCap: 6, idlePoolCap: 2, totalDeviceCap: 10 (if the host can hold ten
devices), and maxConcurrentResetBoots: 2 or lower if the boot gate safely admits
two. At 67.3 s, two reset slots clear ten resets in ~5 waves (337 s), one slot in
~10 waves (673 s), ignoring queueing and variance. If observed reset p90 is well
above the estimate, adjust reset concurrency, idlePoolCap, or the client’s
preparation timeout; more reset concurrency only helps with spare boot-gate and
host capacity.
Read timing and stats¶
Acquire, release and status replies expose timing; correlate with
daemon/sessionBrokerStats (p50/p90). Tune in this order:
- Compare create vs reset p50/p90 for the same platform and profile; if erase does
not beat creation enough, use
offor reduce idle retention. - Compare reset p90 with tolerable wait and the client’s preparation timeout (there is no separate wait-duration setting; do not size from p50 alone).
- Raise
idlePoolCaponly when compatible demand regularly exceeds retained supply and the host has spare capacity. - Compare concurrent reset demand with the boot gate and host pressure before
raising
maxConcurrentResetBoots(queued resets are inactive and cannot be waited on; a high wait p90 under saturated boot capacity is not fixed by the total cap alone). - Re-measure per strategy: iOS erase, iOS keep-booted, Android snapshot and Android keep-booted share no latency assumptions.
p50 is typical work; p90 is a conservative budget, not an upper bound. Use a recent window with enough same-mode, same-profile requests; sparse or mixed samples hide regressions.
Ownership release hold¶
A managed session released while binding ownership is unconfirmed holds device teardown for up to 1.9 s on the daemon’s timer. That is two shared turns at their worst case (700 ms attempt bound + the registry’s 250 ms busy timeout each), covering the turn in flight when the hold is created and the hold’s own expedited attempt. Creating the hold expedites one waiting re-own attempt, still in turn with other attempts; an in-flight attempt continues and recovery admission stays unchanged.
Confirmation starts teardown early. Proven ownership loss skips device writes. At the cap, teardown skips device writes and one warning reports that saved device state was dropped because ownership could not be confirmed. A device_cleanup_in_progress hint includes the remaining hold plus the bounded teardown time, resetting when the hold settles.