feat: reuse parked iOS simulators across worktrees - #286
Merged
Conversation
janicduplessis
force-pushed
the
feat/simulator-pool
branch
from
September 3, 2026 06:24
959c7e1 to
81330e6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
A cache-hit iOS run in a fresh worktree still paid the simulator's first boot: 30-35 seconds locally, versus about 9 seconds after that simulator had booted once.
worktree removedeleted the owned simulator, so no later workspace could reuse that warmed state.This implements phase 1 of the simulator-pool design for owned iOS simulators. The blast radius is the local iOS lifecycle:
worktree remove, the nextiosrun,status, andgc. A parked simulator retains some system-level state (Safari data, pasteboard, photos, profiles, and Simulator settings); app data, privacy grants, keychain state, and apps from other workspaces are cleared. Projects that require a clean system image can set the pool bound to0.Solution
worktree removenow shuts down an owned simulator, clears the departing app's container, renames it, and atomically transfers its ownership record into a bounded pool. The default bound is three; redirectedSTIM_HOMEenvironments opt out unless they explicitly setSTIM_POOL_IOS_PARKED_MAX, keeping tests and temporary homes hermetic.An
iosrun adopts the oldest exact device-type/runtime match before creating a simulator. It revalidates the live simulator's Stim-owned name as well as its model and runtime before transferring ownership. Privacy and keychain resets overlap the boot, other user apps are removed before install proof, and the parked build key cheaply rules out an impossible unchanged-install result. Adoption remains retryable across crashes through a persisted pending marker. If app listing or removal cannot be proven complete, Stim retains that marker and refuses before install or launch. Parking likewise falls back to ownership-checked deletion if the departing app's data path, container directories, or metadata cannot be proven readable and cleared.gcreports and deletes pool entries. Every parked-simulator deletion now routes through the centralized teardown path and uses a persisted, PID-live deletion claim around the timeout-boundedsimctlwork. Adoption honors that claim, and only the matching deletion token can finalize or clear it, so neither the ordinary config-lock stale window nor a concurrent workspace can transfer and then lose the same simulator. Structurally invalidsimctlJSON, missing required availability fields, malformed device records, and malformed app records fail closed. A failed simulator listing or deletion retains the ownership record for a latergc, and failed overflow eviction can temporarily leave the pool above its bound rather than orphaning the simulator.This is stacked on #275, which is stacked on #272; the pool relies on their boot/fingerprint overlap. The pool implementation and review hardening are the final five commits on this branch.
Test plan
node test/e2e/native/run-pool-e2e.mjs --framework expoon an iPhone 17 / iOS 26.5 simulator. It created two distinct simulators, parked and evicted at a bound of one, adopted the survivor in 9.6 seconds, proved the retained app unchanged in 291 ms, then left no simulator, process, worktree, or registry entry aftergc --delete.simctlcalls on a bounded scratch simulator: create, boot, privacy reset, keychain reset, listapps/plutil parsing, shutdown rename, re-boot, shutdown, and delete. Fresh boot completed in 35 seconds; the warmed re-boot completed in 9 seconds.simctlrecords, timeout-bounded parked deletion, failed overflow eviction, missing or unreadable departing app data, incomplete cross-workspace app cleanup, malformed app-list JSON, ownership re-resolution on parking fallback, and platform-correct installed-app hints.pnpm run format:check,pnpm run lint,pnpm run build,pnpm run typecheck,pnpm test(3,390 tests),pnpm run test:e2e(20 tests),pnpm run test:runtime, andpnpm run knippass.knipretains its existingpodconfiguration hint.Fixes #273