Skip to content

feat: reuse parked iOS simulators across worktrees - #286

Merged
janicduplessis merged 7 commits into
feat/fingerprint-during-bootfrom
feat/simulator-pool
Sep 3, 2026
Merged

feat: reuse parked iOS simulators across worktrees#286
janicduplessis merged 7 commits into
feat/fingerprint-during-bootfrom
feat/simulator-pool

Conversation

@janicduplessis

@janicduplessis janicduplessis commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

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 remove deleted 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 next ios run, status, and gc. 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 to 0.

Solution

worktree remove now 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; redirected STIM_HOME environments opt out unless they explicitly set STIM_POOL_IOS_PARKED_MAX, keeping tests and temporary homes hermetic.

An ios run 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.

gc reports 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-bounded simctl work. 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 invalid simctl JSON, 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 later gc, 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

  • Ran node test/e2e/native/run-pool-e2e.mjs --framework expo on 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 after gc --delete.
  • Exercised the changed real simctl calls 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.
  • Added regressions for GC/adoption races, a cross-process deletion exceeding the ordinary 10-second lock stale window, renamed-away ownership before adoption, failed simulator enumeration, structurally invalid and incomplete simctl records, 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, and pnpm run knip pass. knip retains its existing pod configuration hint.

Fixes #273

@janicduplessis
janicduplessis marked this pull request as ready for review September 3, 2026 06:44
@janicduplessis
janicduplessis merged commit 9136251 into main Sep 3, 2026
7 of 8 checks passed
@janicduplessis
janicduplessis deleted the feat/simulator-pool branch September 3, 2026 06:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Simulator pool: park owned simulators on worktree remove instead of deleting them, adopt on create

1 participant