Skip to content

Commit fe3c292

Browse files
committed
fix(ios): bound the query sweep by its tier slice deadline
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
1 parent 8226713 commit fe3c292

3 files changed

Lines changed: 39 additions & 19 deletions

File tree

‎apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift‎

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,20 @@ extension RunnerTests {
132132
]
133133

134134
static let flatInteractiveFallbackBudget: TimeInterval = 1.0
135+
/// The least slice time a sweep query may start with. XCTest cannot cancel a query, so one that
136+
/// starts later outlives the slice its caller waits for and holds the main thread (#2783).
137+
static let flatInteractiveQueryBudget: TimeInterval = 0.1
138+
139+
/// The deadline the query-sweep tier's caller waits for: one slice, clamped to the plan deadline.
140+
/// Interactive and non-interactive requests share it, since the caller discards a later result.
141+
static func querySweepSliceDeadline(startedAt: Date, planDeadline: Date) -> Date {
142+
min(startedAt.addingTimeInterval(flatInteractiveFallbackBudget), planDeadline)
143+
}
144+
145+
/// Whether a sweep query started at `now` can still finish before the slice `deadline`.
146+
static func querySweepCanStartQuery(deadline: Date, now: Date) -> Bool {
147+
deadline.timeIntervalSince(now) >= flatInteractiveQueryBudget
148+
}
135149

136150
/// What one capture may spend reading the keyboard band before it gives up on the fact and lets the
137151
/// tap guard fall back to the tree rule. The scroll path pays this query per gesture and stays well
@@ -424,7 +438,7 @@ extension RunnerTests {
424438
func querySweepSnapshotAcquisition(
425439
app: XCUIApplication,
426440
hint: CaptureHint,
427-
planDeadline: Date = .distantFuture
441+
sliceDeadline deadline: Date
428442
) -> SnapshotAcquisition {
429443
var nodes: [RawAXNode] = [
430444
interactiveRootNode(rect: .zero)
@@ -440,19 +454,13 @@ extension RunnerTests {
440454
)
441455
}
442456

443-
// Bounded by both its own sweep budget and the umbrella capture-plan deadline, so a
444-
// chained recovery tier can never push the plan past the main-thread watchdog (#1105).
445-
let sweepDeadline = hint.interactiveOnly
446-
? Date().addingTimeInterval(Self.flatInteractiveFallbackBudget)
447-
: Date.distantFuture
448-
let deadline = min(sweepDeadline, planDeadline)
449457
let viewport = safeSnapshotViewport(app: app)
450458
var seen = Set<String>()
451459
var candidates: [RawAXNode] = []
452460
let flatElements = flatInteractiveElements(app: app, deadline: deadline)
453461
var truncated = flatElements.truncated
454462
for element in flatElements.elements {
455-
if Date() >= deadline {
463+
if !Self.querySweepCanStartQuery(deadline: deadline, now: Date()) {
456464
NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_FLAT_FALLBACK_DEADLINE")
457465
truncated = true
458466
break

‎apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotAcquisition.swift‎

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -487,23 +487,34 @@ extension RunnerTests {
487487
app.images
488488
]
489489

490-
var elements: [XCUIElement] = []
491-
var truncated = false
490+
return Self.runFlatInteractiveQueries(queries, deadline: deadline) { query in
491+
self.snapshotElementsQuery {
492+
query.allElementsBoundByIndex
493+
}
494+
}
495+
}
496+
497+
/// Runs sweep queries in order until one reports AX unavailable, or until the next one could not
498+
/// finish before `deadline` (`querySweepCanStartQuery`).
499+
static func runFlatInteractiveQueries<Query, Element>(
500+
_ queries: [Query],
501+
deadline: Date,
502+
now: () -> Date = { Date() },
503+
run: (Query) -> (elements: [Element], axUnavailable: Bool)
504+
) -> (elements: [Element], truncated: Bool) {
505+
var elements: [Element] = []
492506
for query in queries {
493-
if Date() >= deadline {
507+
if !querySweepCanStartQuery(deadline: deadline, now: now()) {
494508
NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_FLAT_FALLBACK_DEADLINE")
495-
truncated = true
496-
break
497-
}
498-
let result = snapshotElementsQuery {
499-
query.allElementsBoundByIndex
509+
return (elements, true)
500510
}
511+
let result = run(query)
501512
elements.append(contentsOf: result.elements)
502513
if result.axUnavailable {
503514
break
504515
}
505516
}
506-
return (elements, truncated)
517+
return (elements, false)
507518
}
508519

509520
func snapshotElementsQuery(

‎apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -450,15 +450,16 @@ extension RunnerTests {
450450
: try self.recursiveTreeSnapshotAcquisition(context: context, hint: hint)
451451
}
452452
case .querySweep:
453+
let sliceDeadline = Self.querySweepSliceDeadline(startedAt: Date(), planDeadline: deadline)
453454
return try self.runMainThreadWork(
454455
"query_sweep",
455-
timeout: min(Self.flatInteractiveFallbackBudget, max(0.1, deadline.timeIntervalSinceNow)),
456+
timeout: max(0.1, sliceDeadline.timeIntervalSinceNow),
456457
timeoutError: self.snapshotMainThreadTimeoutError("running query-sweep snapshot")
457458
) {
458459
self.querySweepSnapshotAcquisition(
459460
app: app,
460461
hint: hint,
461-
planDeadline: deadline
462+
sliceDeadline: sliceDeadline
462463
)
463464
}
464465
case .privateAX:

0 commit comments

Comments
 (0)