fix(ios-runner): keep snapshot-plan target state on main and bound the query sweep by its slice - #2836
Conversation
Size Report
Startup median (7 runs, lower is better):
|
|
Reviewed at 8f69c76. A non-interactive snapshot where the tree tier fails fast and the query sweep runs long now returns a shallow partial result instead of arming the timeout penalty.
Not blocking: the 0.1 s admission margin in On size: the diff adds about 490 net lines, with four new seams. Could No run shows the query-sweep tier inside the full Checks were still queued at review time. This diff overlaps the iOS Smoke route and the Repo Guards XCTest list, so failures there need a look. The known The next step is the two fixes above, each with a plan-level test. |
8f69c76 to
9719773
Compare
|
Reviewed at 9719773. Both blocking findings from the earlier review (8f69c76, #2836 (comment)) are still open; this is a rebase, and commits 1, 2, and 4 are byte-identical in range-diff, so neither fix landed yet. In a non-interactive plan the tree tier fails fast and the query sweep runs long.
No simulator run yet shows the query-sweep tier inside a full Smoke Tests and Repo Guards were still running with no failure excerpt captured; Smoke covers the runner snapshot capture plan and the ios.yml workflow change, and Repo Guards covers the XCTest file and test list touched by the new UnitTests files, so a failure in either should be treated as possibly related to this diff. Before this is ready to merge, please push the slice-deadline sweep fix so it arms the penalty and falls through to privateAX, move the target-identity reads to main, add a plan-level test for each, and then run the live simulator validation described above. |
9719773 to
5385c2a
Compare
|
Both re-opened findings are addressed at Finding 1 — a deadline-exhausted query sweep was not treated as a tier timeout. The tier stopped starting queries at its slice deadline but the plan classified only what it collected, and Finding 2 — the coordinate tap's routing probe keyed a penalty with target identity read off main. One limit, stated plainly: the live triple you asked for (tree tier fails fast + slow sweep, then |
|
Reviewed at 5385c2a. Both findings from the earlier pass (#2836 (comment)) are fixed: snapshot-plan target state stays on main, and the query sweep is bounded by its slice. This is ready to merge. All 18 checks are green. The iOS runner-regressions job runs the two new tests, which exercise the changed capture-plan and tap-routing code, so its result covers this diff. I did not run the on-simulator tests or read the CI logs, so the "red before the fix" claims rest on tracing the pre-fix code rather than a reproduced failure. The plan-level test depends on Not blocking: on the |
…ites The capture plan runs on the command queue but read currentBundleId, currentAppProcessIdentifier and the penalty warm-up exemption, and wrote runnerAccessibilityHealth and invalidated the cached target directly. Snapshot preparation now takes a SnapshotCaptureTarget on main (app, bundle id, pid, consumed warm-up exemption) and the plan, the modal probe penalty, the XCTest penalty recorder and the private AX tier read only that copy. Health writes and the fail-closed invalidation go through applyMainOwnedSnapshotState, the abandoned-work-guarded main hop that invalidateCachedTargetAfterSnapshotFailure now also uses. Fixes #2781
A non-interactive query sweep used Date.distantFuture as its own deadline, so only the up-to-20 s plan deadline bounded it while the caller abandoned the tier after its 1 s slice. XCTest queries cannot be cancelled, so the sweep kept the main thread busy and later commands got RUNNER_BUSY. The caller now derives one slice deadline (querySweepSliceDeadline) for both its wait and the sweep, for interactive and non-interactive requests alike. Neither the element queries nor the per-element reads start with less than flatInteractiveQueryBudget left before that deadline (querySweepCanStartQuery). Fixes #2783
…that runs Taking the snapshot target on main consumed the exemption during command preparation, so a snapshot answered by the blocking system-modal probe, or an abandoned preparation that ran late, spent it without running a plan. The exemption now lives in a lock-owned type that lifecycle code arms on main and runSnapshotCapturePlan consumes at its start.
…ugh the plan Swizzles XCUIElementQuery.allElementsBoundByIndex with a slow query and runs a non-interactive [.querySweep] plan with a 20 s plan deadline. The sweep must not be abandoned, and no query may start after the plan answers, so passing the plan deadline to the sweep in either loop turns it red.
…ity to main Two regressions #2781 still leaves open, both red on this head: A query sweep that stops at its own slice deadline is a tier timeout, but the plan classified its payload by node count, so a partial sweep that beat the sparse threshold armed no XCTest-channel penalty and no later capture of the same screen was deferred. The coordinate tap's system-modal routing probe armed its penalty with currentBundleId read on the command queue, while main owned that state and could still be clearing or rebinding it.
…er timeout The sweep tier stopped starting queries at its slice deadline, but the plan classified only what it collected. sparsePayloadReason accepts anything above the sparse node threshold, so a partial flat sweep became a `recovered` capture: no XCTest-channel penalty was armed, private AX never ran, and every later capture of that screen paid for the full sweep again. `runFlatInteractiveQueries` now returns a typed `SnapshotTierOutcome`, the sweep tier carries it into the attempt, and the plan rejects a `.deadlineExhausted` tier through `snapshotTierRejectionReason` — keeping its payload only as the fallback, arming the penalty through the existing tier timeout reason, and letting the next backend answer.
The coordinate tap resolved its system-modal routing on the command queue and armed an abandoned probe's penalty with `currentBundleId` read there, while `applyMainOwnedSnapshotState` and the lifecycle code write that identity through `DispatchQueue.main.async`. A tap could penalize a target main was still clearing or rebinding. The probe now takes a `SnapshotProbePenaltyTarget`: a capture hands over the identity it already took on main, and the tap route hands over nothing and lets the probe's own main-side block capture the identity main holds once its work starts. No off-main path reads target identity any more.
main (#2854, #2855) moved the runner's inline unit tests into UnitTests/ and split RunnerTests+CommandExecution.swift along command families. Rebase conflicts dropped this PR's edits from the inline blocks the rebase kept on main; relocate them into the file main now owns for each family: - RunnerTests+SnapshotTests.swift: the query-sweep slice-deadline and fail-closed-invalidation tests, and the bounded-modal-probe helper now takes a SnapshotCaptureTarget. - RunnerTests+SnapshotCapturePlanTests.swift: the tier-timeout rejection pair and the private-AX depth helper now builds a SnapshotCaptureTarget. - RunnerTests+SnapshotTimingTests.swift: the deadline-exhausted tier penalty test.
5385c2a to
7414149
Compare
|
Reviewed at 7414149. The rebase keeps the reviewed behavior from 5385c2a. The snapshot-plan target state stays on main, and the query sweep stops at its slice deadline. The rebased patch matches the reviewed one, apart from one added type annotation. No code on main is reverted. The new commit moves this PR's tests into main's split UnitTests files, and no stale callers remain. I did not build or run the runner XCTest bundle. CI's iOS runner job for 7414149 confirms that the moved tests compile and pass. Smoke Tests is still running and has not failed. This diff touches the iOS snapshot and tap-routing paths, so please check that job before merge. |
|
Summary
Two related iOS runner snapshot-plan bugs:
snapshotAccessibilityUnavailablewrote bundleidentity and accessibility health directly instead of keeping snapshot-plan target state on
main, risking state for the wrong target. Fix: take target identity on main and hop plan-state
writes behind abandoned main-thread work. The coordinate tap's system-modal routing probe now
reads
currentBundleIdonly on main too.slice deadline, letting a slow query starve later tiers. A sweep that ends on its slice deadline
is now a typed tier timeout (
SnapshotTierOutcome.deadlineExhausted), so it arms the XCTest-channelpenalty and falls through to private AX — the plan branches on the tier outcome, not node count.
Closes #2781, Closes #2783.
Validation
Rebased onto
origin/main(fd8277e391); head7414149edb. main had moved the runner's inline unittests into
UnitTests/(#2854) and splitRunnerTests+CommandExecution.swiftalong command families(#2855), so the conflicts were file-location/file-split, not semantic. Adopted main's layout: the
snapshot-command production changes live in
RunnerTests+SnapshotExecution.swift, the routing probe inRunnerTests+CommandDispatch.swift, and this PR's tests in theUnitTests/…Tests.swiftfile main nowowns for each family (final commit
test(ios): place this PR's runner tests in main's split UnitTests files). NoRunnerTests+CommandExecution.swiftcontent was resurrected and no test main carries wasduplicated.
Both re-opened findings were re-proven on the rebased sources on an iPhone 17 / iOS 26.2 simulator
(created for the run, deleted after); the runner
TEST BUILD SUCCEEDEDwithAGENT_DEVICE_RUNNER_UNIT_TESTSand both pin tests passed, so main's refactor broke neither — notest-first red-fix was required:
testQuerySweepThatEndsOnItsSliceDeadlinePenalizesChannelAndReachesPrivateAX— passed (4.7s). Asserts the armed
queries_backend_timeoutpenalty, the next plan deferring itsXCTest tiers, and
private-axanswering.testCoordinateTapRoutingPenalizesTheIdentityMainSettledOnWhileTheWriteWasPending— passed (7.9s). Asserts the abandoned routing probe penalizes the identity main settled on, not the
one whose write was still pending.
The 9 relocated unit tests over the moved code also pass (query-sweep slice helpers, the tier-rejection
pair, the fail-closed invalidation-on-main hop, the
snapshotFast/snapshotRawmodal-probe route, theviewport bundle-id test):
Executed 9 tests, with 0 failures.Gates at
7414149edb:pnpm formatclean,pnpm lintandpnpm typecheckpass,pnpm check:xctest-selectionclean (295 declaredAgentDeviceRunnerUITestsmethods, 0 reachable by nolane),
pnpm check:affected --runall runnable checks passed.The requested live triple (tree tier fails fast while the sweep is slow, then
reason=queries_backend_timeout, thenprivate-ax) is not reachable through the CLI on a stocksurface: the tree tier answers first and the runner has no fault-injection seam for a real
kAXErrorIllegalArgumentrefusal or 50 000 presented nodes. The on-simulator XCTest above is theequivalent proof — it plants the fast tree failure and a gated slow sweep and asserts the full
penalize → defer →
private-axsequence.