Skip to content

fix(ios-runner): keep XCTest tree capture on the main thread #1122

Description

@cursor

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:

  1. Off-main-thread XCTest snapshotDispatchQueue.global(qos: .userInitiated).async { … element.snapshot() … } violates XCTest threading expectations.
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions