Follow-up from PR #1107 (merged). The bounded tree capture path runs element.snapshot() on DispatchQueue.global, but XCTest UI/accessibility work is main-run-loop–bound. Under load — especially when abandoned captures are still draining — this risks flakes, main-thread assertion failures, or undefined behavior.
Problem
captureSnapshotRootBounded in RunnerTests+Snapshot.swift dispatches captureSnapshotRoot (which calls element.snapshot()) to a worker queue, then the caller synchronously semaphore.waits up to sliceSeconds (up to 8s). Two related risks:
- Off-main-thread XCTest snapshot —
DispatchQueue.global(qos: .userInitiated).async { … element.snapshot() … } violates XCTest threading expectations.
- Main thread still pinned for the full slice — even with the worker dispatch, the caller blocks the main queue for the entire slice on every tree attempt, so
RUNNER_BUSY / watchdog abandonment can still cascade on heavy screens.
Related gaps from the same PR (same issue family, fix together or stack):
executeDispatched skips currentMainThreadBusyState() when Thread.isMainThread, allowing re-entrant XCTest work behind abandoned captures the busy gate was meant to reject (RunnerTests+CommandExecution.swift).
- No automated coverage for transport command coalescing,
RUNNER_BUSY while abandoned work drains, or tree-capture slice timeout → private-AX recovery without wedging the runner.
Suggested direction
- Keep snapshot work on the main queue but enforce the slice budget asynchronously (main-queue work item + watchdog that marks the capture abandoned and returns without blocking the transport thread), or use
XCTWaiter with a bounded expectation on the main actor.
- Apply the same busy/wedge check before the
Thread.isMainThread fast path (or remove the bypass if production never hits it).
- Add Swift unit tests for transport coalescing and busy/wedge state; add a TS unit test that
RUNNER_BUSY surfaces as retriable COMMAND_FAILED while RUNNER_WEDGED triggers session invalidation.
Acceptance
- Tree capture no longer calls XCTest snapshot APIs from a background queue.
- Simulator integration or replay test forces a slow tree capture and asserts the runner stays responsive (no wedging, recovery tier advances).
- New tests cover at least: duplicate
commandId coalescing, RUNNER_BUSY during abandoned-work drain, slice timeout → private-AX fallback.
Context
Follow-up from PR #1107 (merged). The bounded tree capture path runs
element.snapshot()onDispatchQueue.global, but XCTest UI/accessibility work is main-run-loop–bound. Under load — especially when abandoned captures are still draining — this risks flakes, main-thread assertion failures, or undefined behavior.Problem
captureSnapshotRootBoundedinRunnerTests+Snapshot.swiftdispatchescaptureSnapshotRoot(which callselement.snapshot()) to a worker queue, then the caller synchronouslysemaphore.waits up tosliceSeconds(up to 8s). Two related risks:DispatchQueue.global(qos: .userInitiated).async { … element.snapshot() … }violates XCTest threading expectations.RUNNER_BUSY/ watchdog abandonment can still cascade on heavy screens.Related gaps from the same PR (same issue family, fix together or stack):
executeDispatchedskipscurrentMainThreadBusyState()whenThread.isMainThread, allowing re-entrant XCTest work behind abandoned captures the busy gate was meant to reject (RunnerTests+CommandExecution.swift).RUNNER_BUSYwhile abandoned work drains, or tree-capture slice timeout → private-AX recovery without wedging the runner.Suggested direction
XCTWaiterwith a bounded expectation on the main actor.Thread.isMainThreadfast path (or remove the bypass if production never hits it).RUNNER_BUSYsurfaces as retriableCOMMAND_FAILEDwhileRUNNER_WEDGEDtriggers session invalidation.Acceptance
commandIdcoalescing,RUNNER_BUSYduring abandoned-work drain, slice timeout → private-AX fallback.Context