Skip to content

fix(ios): stop charging every capture for a slow Simulator app discovery - #2331

Open
thymikee wants to merge 2 commits into
mainfrom
claude/ios-bridge-target-probe-offpath
Open

fix(ios): stop charging every capture for a slow Simulator app discovery#2331
thymikee wants to merge 2 commits into
mainfrom
claude/ios-bridge-target-probe-offpath

Conversation

@thymikee

@thymikee thymikee commented Sep 6, 2026

Copy link
Copy Markdown
Member

Summary

Removes repeated concurrent app discovery on the iOS Simulator AX-bridge route and bounds how long each capture waits for it. Resolver and route regression tests pass; loaded-host smoke validation is what this PR's own CI run will show, and it was still pending when this was written.

The failure it targets

Since #2279 landed (09-05 21:06), the iOS Smoke lane has failed four times with one signature, the first wait text "Agent Device Tester" after a cold open --relaunch reporting wait_capture_stalled with readableCaptures: 0:

The request log of the third run (artifact ios-artifacts, sessions/ios-e2e-smoke-14ax/requests/45ad58af8fd6970d.ndjson) accounts for the 10s budget:

phase cost
runner findText on the just-launched app 5.8s
AX-bridge target discovery: xcrun simctl spawn … launchctl list, xcrun timed out after 3000ms 3.4s
fallback XCTest snapshot cancelled at the deadline

The failed-step screenshot and snapshot the harness took right after (#2315) show the home screen rendered with a healthy tree. That is consistent with the timeline, not proof the app was readable throughout the wait.

createSimulatorSnapshotTargetResolver (snapshot-target.ts) ran that spawn with a fixed 3s timeout on every cache miss, on the capture's critical path, and kept nothing on timeout, so the next capture ran its own probe again while xcrun was slow. xcrun takes about 1s on an idle Mac with simulators booted; on these CI hosts it exceeded 3s in both captures of that run, and the same hosts time out simctl terminate at 2s in other artifacts.

Change

  • Discovery is single-flight per target and detached from the capture that starts it. A capture waits at most 1.5s for the in-flight discovery, then takes the XCTest fallback with reason simulator-target-discovery-pending. Captures that keep arriving while the probe runs each wait up to that slice; the probe is shared, not repeated.
  • A discovery has one 15s deadline shared by both simctl probes and the ps identity read.
  • A cancelled caller no longer aborts the probe. A failed discovery is forgotten, so the next capture starts a new one.
  • The resolver error names its reason, so the ios_snapshot_route_fallback diagnostic says why the fallback ran.

A responsive host still reaches the bridge on the first capture (local probe ≈ 1s < 1.5s).

What is verified

  • Resolver tests (deferred spawn mock, fake timers): slow discovery yields to the fallback and finishes in the background; concurrent captures join one discovery; a cancelled caller leaves it running; one deadline is shared by the probes and the identity read. The first three fail on the old code (hang, second spawn, second spawn).
  • Route test with the production resolver over a simctl that answers only when released: the first capture falls back within the slice with the target-resolution-failed warning, the released discovery then serves the bridge without a second spawn.
  • Existing resolver and route tests pass (20 total); package typecheck, oxlint, oxfmt, fallow clean.
  • Live fixture E2E on a private iOS 26.2 simulator with the rebuilt dist: cold launch, wait for Agent Device Tester 269ms, 23 captures served by the bridge, smoke:automation-input green. This proves the healthy route, not the loaded-host case. (A later keyboard dismiss failure in that run is my local Simulator.app hardware-keyboard environment.)

Not verified here

  • A loaded-host wait completing through the fallback: no local way to make xcrun slow; the lane's own runs are the evidence.
  • The production poll path end to end under slow discovery; the route test covers one capture, not the wait loop.

Relation to open work

#2329 changes snapshot-route.ts (launch grace on typed bridge failures) and makes wait observe runner-free; it does not touch the probe, and its design needs the probe to be cheap on the critical path. #2325 covers open startup budgets. This PR does not change the wait contract.

Since #2279 every eligible Simulator capture resolves its AX-bridge target
first, and a cache miss spawns `simctl launchctl list` through xcrun with
a 3s timeout on the capture's own critical path. On a loaded CI host that
spawn takes longer than 3s, the timeout is not remembered, and the next
capture pays it again. A `wait` issued right after `open` lost its whole
10s budget that way (runner findText on the fresh app, then the probe
timeout, then a fallback capture cancelled at the deadline) and reported
`wait_capture_stalled` with the app already on screen: four iOS Smoke
failures on main and PRs since 2026-09-05 21:00, none before.

Discovery is now single-flight and detached from the capture that starts
it: a capture waits a bounded 1.5s for it, then takes the XCTest fallback
while the probe keeps running with its own 15s budget; later captures
join the in-flight probe or reuse its result. A responsive host still
reaches the bridge on the first capture. The resolver's error names its
reason so the route diagnostic says why the fallback ran.
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
Installed (including dependencies) 4.49 MB 4.49 MB +455 B
Package (unpacked) 4.49 MB 4.49 MB +455 B
Package (download) 1.33 MB 1.33 MB +181 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 27.7 ms 27.8 ms +0.1 ms
CLI --help 79.6 ms 81.0 ms +1.4 ms

…ssion

Review follow-up. The 15s budget was per subprocess (two simctl probes,
then a 3s ps read); discovery now shares one deadline across all three.
The comments and changelog no longer claim "once per app generation":
single-flight removes duplicate discoveries, each capture still waits up
to its 1.5s slice for the in-flight one, and a failed discovery is
forgotten so the next capture starts another. A route-level test runs
the production resolver over a simctl that answers only when released:
the first capture falls back within the slice, the released discovery
then serves the bridge without a second spawn.
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.

1 participant