feat: fingerprint the tree while the simulator boots - #275
Merged
janicduplessis merged 1 commit intoSep 3, 2026
Merged
Conversation
janicduplessis
force-pushed
the
feat/fingerprint-during-boot
branch
from
September 3, 2026 01:02
f61d949 to
7a4a214
Compare
janicduplessis
marked this pull request as ready for review
September 3, 2026 06:18
janicduplessis
force-pushed
the
feat/fingerprint-during-boot
branch
from
September 3, 2026 06:24
7a4a214 to
9067d8d
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
Stacked on #272 (
fix/skip-redundant-sim-list); this PR's base is that branch, and its diff is only the second commit. Read #272 first.A cache-hit run in a fresh worktree spends ~30 s in the created simulator's first
simctl bootstatus -band then another 3-11 s walking the tree for the fingerprint, one after the other. The fingerprint never touches the simulator and the boot never touches the tree. Worse, the two fight when they do overlap by accident: the same walk measures 1.4-1.6 s on a quiet machine and 5.6-12.6 s while a sim boots (investigations/2026-09-02-cache-hit-run-time.md), which is where the "17-20 s first fingerprint" in the gate transcripts comes from -- it is contention, not a cold tree (@expo/fingerprint0.20.10 keeps no on-disk cache).Two things kept them serial, and both had to go:
commands/ios.tsawaitedbootPromisebefore the fingerprint on thedevice.createdpath only (the reuse path already skipped that await).bootIosSimwaited onbootstatusthrough the synchronousexec.run, so even with the await deleted the boot would have blocked the event loop and the fingerprint could not have progressed. The reuse path's "overlap" was therefore never real either.Solution
bootIosSimis now async and waits onbootstatusthrough the executor'sspawn(the same wrapper the collector and SimSlim use) instead ofexecSync. The argument list is byte-for-byte what it was --xcrun simctl bootstatus <udid> -b-- only the invocation is a child process now, so the run's own thread is free while the simulator boots. The per-attempt bound that #128 added is preserved: a hung attempt is killed and retried against the deadline, with the sameETIMEDOUT-> re-list -> retry loop. The bound is now anattemptMsoption so tests can exercise the retry in milliseconds instead of four minutes.ensureOwnedIosDeviceno longer waits the boot out. It creates (or resolves) the sim, issuessimctl boot, and hands backbooting: { udid, done }-- the promise that covers thebootstatuswait and the SimSlim reconcile that has to follow it, since a profile change can reboot the sim.ensureBootedjoins that promise where #272 trusted its result, so the fingerprint and the tiered cache resolve run against a booting simulator and the run joins before it installs. A sim that was already booted is untouched: no promise, no deferral,configureOwnedIosSimstill runs and still throws inline.Invariant 10 is unaffected -- nothing about the keys moved. The initial lookup and the single-flight lock still use the pre-mutation key,
prebuild/pod installstill re-fingerprint, and the artifact is still stored only under the post-mutation key. Invariant 11 is unaffected: the launch happens after the join, andlaunchedis untouched.What a boot failure now costs. A dead simulator on the created path used to fail inside
ensureOwnedDevice, before the build. It now fails where the reuse path has always failed it: at the join infinishIosRun, before the install but after a cache miss would have compiled. That is deliberate --ios-command.test.tshas pinned "a device that will not boot is refused at install, after the build has been stored" since612a447, and the artifact is worth storing even when the device is gone. I tried joining the boot beforebuildIosinstead and dropped it: it contradicts that contract for the sake of a failure mode that only bites on a miss. On a hit -- the case this PR is about -- nothing is installed or launched either way.Blast radius: every iOS run, plus every caller of
bootIosSim. Both callers are inengine/device.tsand both now await it. Nothing else in the codebase boots a simulator.Test plan
Real tool (invariant 9, the
bootstatusinvocation changed): built this branch's CLI, then fromtrailhead-benchworktree create qa-269 --carry-ignored,start --jsonin the new worktree, and threeios --jsonruns. Cleaned up after:stop,worktree remove --force, nostim-*simulator of mine left (stim-issue-245-measureis another session's and was left alone),/back to 16 GB free. No--device, no physical device.1. Fresh worktree, created sim (the baseline case).
durationMs: 61914.device ... createdis 1.3 s where the investigation's three baselines measured 24.8 / 28.3 / 54.2 s: the create andsimctl bootare all that is left in it. The 11.1 s fingerprint ran inside the 34.2 s boot wait rather than after it. 61.9 s against the 72.6 s baseline mean (range 61.2-83.8 s) on a machine with load average 5.5 and aninstallthat came in at 12.5 s, itself twice the 4.9-7.1 s the baselines measured -- so the phase this PR targets improved while the run's noise floor was worse than the baseline's.2. Same worktree, warm (sim booted, app installed).
durationMs: 8245.8.2 s against the investigation's 7.97 s warm reference -- unchanged, as expected: an already-booted sim takes neither the deferral nor fix: skip the redundant simulator list after a boot this run performed #272's skip.
3. After
stop, reuse of a shut-down sim (the other path that now defers).durationMs: 29035,fingerprint hit (15.4s),device ... booted (7.2s): the boot finished 7.2 s in while the fingerprint was still walking, and the run joined it before installing. Comparable to the investigation's parked-shut-down runs (20.3 s / 32.5 s).sim-ios.test.ts: thebootstatusargv is asserted exactly (xcrun simctl bootstatus UDID-A -b), plus the retry after a hung attempt, the timed-out-but-Booted case, the deadline message, the vanished sim, the failing device list, and a non-timeout failure surfaced from the child's exit code and stderr.engine-device.test.ts:ensureBootedjoins a pendingbooting.donewithout issuing any command, and turns its rejection into the refusal (no listing).ensureOwnedDeviceregisters the created sim and hands the boot back before it is awaited (spawnofbootstatusonly happens once the caller awaitsdone), keeps the SimSlim reconcile on that promise, and hands back a boot on the reuse-after-shutdown path but not on the already-booted one.ios-command.test.ts: fake clock -- the boot and the fingerprint start at the same instant, interleave in the orderboot start, fingerprint start, fingerprint end, boot end, and each phase line reports its own elapsed time. A created sim that fails to boot refuses withSTIM_NO_DEVICEand never installs or launches; an uncomputable fingerprint still refuses withSTIM_NO_FINGERPRINTbefore any of it. Ablation: restoring thedevice.createdawait fails the overlap test.guide lifecyclenow says the iOS device step hands the boot back and that thedevice ... bootedandfingerprintlines overlap, so their durations cannot be summed.pnpm run format:check,lint,build,typecheck,pnpm test(83 files, 3341 tests),pnpm run knipall pass.Fixes #269