Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ struct DataPayload: Codable {
let nodes: [SnapshotNode]?
let truncated: Bool?
let snapshotQuality: SnapshotQuality?
var snapshotTiming: SnapshotTiming?
let gestureStartUptimeMs: Double?
let gestureEndUptimeMs: Double?
let x: Double?
Expand Down Expand Up @@ -245,6 +246,7 @@ struct DataPayload: Codable {
nodes: [SnapshotNode]? = nil,
truncated: Bool? = nil,
snapshotQuality: SnapshotQuality? = nil,
snapshotTiming: SnapshotTiming? = nil,
gestureStartUptimeMs: Double? = nil,
gestureEndUptimeMs: Double? = nil,
x: Double? = nil,
Expand Down Expand Up @@ -282,6 +284,7 @@ struct DataPayload: Codable {
self.nodes = nodes
self.truncated = truncated
self.snapshotQuality = snapshotQuality
self.snapshotTiming = snapshotTiming
self.gestureStartUptimeMs = gestureStartUptimeMs
self.gestureEndUptimeMs = gestureEndUptimeMs
self.x = x
Expand Down Expand Up @@ -314,6 +317,13 @@ struct DataPayload: Codable {
}
}

struct SnapshotTiming: Codable {
let totalMs: Double
let phases: [String: Double]?
let backends: [String: Double]?
let selectedBackend: String?
}

struct ErrorPayload: Codable {
let code: String?
let message: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,21 @@ extension RunnerTests {
private static let flatInteractiveFallbackBudget: TimeInterval = 1.0

func snapshotFast(app: XCUIApplication, options: SnapshotOptions) throws -> DataPayload {
let timing = SnapshotTimingRecorder()
if let blocking = blockingSystemAlertSnapshot() {
return blocking
var payload = blocking
payload.snapshotTiming = timing.payload()
return payload
}
return try runSnapshotCapturePlan(
var payload = try runSnapshotCapturePlan(
Self.regularVisiblePlan,
app: app,
options: options,
terminal: .sparseWithFatalOnAXFailure
terminal: .sparseWithFatalOnAXFailure,
timing: timing
)
payload.snapshotTiming = timing.payload()
return payload
}

func recursiveTreeSnapshotPayload(
Expand Down Expand Up @@ -246,15 +252,21 @@ extension RunnerTests {
}

func snapshotRaw(app: XCUIApplication, options: SnapshotOptions) throws -> DataPayload {
let timing = SnapshotTimingRecorder()
if let blocking = blockingSystemAlertSnapshot() {
return blocking
var payload = blocking
payload.snapshotTiming = timing.payload()
return payload
}
return try runSnapshotCapturePlan(
var payload = try runSnapshotCapturePlan(
Self.rawDiagnosticPlan,
app: app,
options: options,
terminal: .throwOnAXFailure
terminal: .throwOnAXFailure,
timing: timing
)
payload.snapshotTiming = timing.payload()
return payload
}

func rawTreeSnapshotPayload(
Expand Down Expand Up @@ -302,7 +314,11 @@ extension RunnerTests {
return DataPayload(nodes: nodes, truncated: false)
}

func snapshotFlatInteractive(app: XCUIApplication, options: SnapshotOptions) -> DataPayload {
func snapshotFlatInteractive(
app: XCUIApplication,
options: SnapshotOptions,
timing: SnapshotTimingRecorder? = nil
) -> DataPayload {
var nodes: [SnapshotNode] = [
interactiveRootNode(rect: .zero)
]
Expand All @@ -313,39 +329,45 @@ extension RunnerTests {
let deadline = options.interactiveOnly
? Date().addingTimeInterval(Self.flatInteractiveFallbackBudget)
: Date.distantFuture
let viewport = safeSnapshotViewport(app: app)
let viewport = measureSnapshotPhase(timing, "query_sweep_viewport") {
safeSnapshotViewport(app: app)
}
var seen = Set<String>()
var candidates: [SnapshotNode] = []
let flatElements = flatInteractiveElements(app: app, deadline: deadline)
var truncated = flatElements.truncated
for element in flatElements.elements {
if Date() >= deadline {
NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_FLAT_FALLBACK_DEADLINE")
truncated = true
break
}
guard let node = flatSnapshotNode(
element: element,
index: 0,
parentIndex: 0,
viewport: viewport,
options: options
) else {
continue
}
let key = "\(node.type)-\(node.label ?? "")-\(node.identifier ?? "")-\(node.value ?? "")-\(node.rect.x)-\(node.rect.y)-\(node.rect.width)-\(node.rect.height)"
if seen.contains(key) { continue }
seen.insert(key)
candidates.append(node)
let flatElements = measureSnapshotPhase(timing, "query_sweep_elements") {
flatInteractiveElements(app: app, deadline: deadline)
}
candidates.sort { left, right in
if left.rect.y != right.rect.y {
return left.rect.y < right.rect.y
var truncated = flatElements.truncated
measureSnapshotPhase(timing, "query_sweep_shape") {
for element in flatElements.elements {
if Date() >= deadline {
NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_FLAT_FALLBACK_DEADLINE")
truncated = true
break
}
guard let node = flatSnapshotNode(
element: element,
index: 0,
parentIndex: 0,
viewport: viewport,
options: options
) else {
continue
}
let key = "\(node.type)-\(node.label ?? "")-\(node.identifier ?? "")-\(node.value ?? "")-\(node.rect.x)-\(node.rect.y)-\(node.rect.width)-\(node.rect.height)"
if seen.contains(key) { continue }
seen.insert(key)
candidates.append(node)
}
if left.rect.x != right.rect.x {
return left.rect.x < right.rect.x
candidates.sort { left, right in
if left.rect.y != right.rect.y {
return left.rect.y < right.rect.y
}
if left.rect.x != right.rect.x {
return left.rect.x < right.rect.x
}
return left.type < right.type
}
return left.type < right.type
}

// The synthetic root doubles as the daemon's viewport (find.ts prefers on-screen matches
Expand Down Expand Up @@ -560,16 +582,26 @@ extension RunnerTests {

func makeSnapshotTraversalContext(
app: XCUIApplication,
options: SnapshotOptions
options: SnapshotOptions,
timing: SnapshotTimingRecorder? = nil
) throws -> SnapshotTraversalContext? {
let viewport = safeSnapshotViewport(app: app)
let queryRoot = options.scope.flatMap { findScopeElement(app: app, scope: $0) } ?? app
let viewport = measureSnapshotPhase(timing, "viewport") {
safeSnapshotViewport(app: app)
}
let queryRoot = measureSnapshotPhase(timing, "scope_resolve") {
options.scope.flatMap { findScopeElement(app: app, scope: $0) } ?? app
}

guard let rootSnapshot = try captureSnapshotRoot(queryRoot) else {
let maybeRootSnapshot = try measureSnapshotPhase(timing, "xctest_root_snapshot") {
try captureSnapshotRoot(queryRoot)
}
guard let rootSnapshot = maybeRootSnapshot else {
return nil
}

let (flatSnapshots, snapshotRanges) = flattenedSnapshots(rootSnapshot)
let (flatSnapshots, snapshotRanges) = measureSnapshotPhase(timing, "flatten_tree") {
flattenedSnapshots(rootSnapshot)
}
return SnapshotTraversalContext(
queryRoot: queryRoot,
rootSnapshot: rootSnapshot,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,69 @@ struct SnapshotBackendCapture {
let effectiveDepth: Int?
}

final class SnapshotTimingRecorder {
private let startedAt = Date()
private(set) var phases: [String: Double] = [:]
private(set) var backends: [String: Double] = [:]
private var selectedBackend: SnapshotBackendKind?

@discardableResult
func measure<T>(_ phase: String, _ work: () throws -> T) rethrows -> T {
let started = Date()
defer { phases[phase] = roundedElapsedMs(since: started) }
return try work()
}

@discardableResult
func measureBackend<T>(_ backend: SnapshotBackendKind, _ work: () throws -> T) rethrows -> T {
let started = Date()
defer { backends[backend.rawValue] = roundedElapsedMs(since: started) }
return try work()
}

func selectBackend(_ backend: SnapshotBackendKind) {
selectedBackend = backend
}

func payload() -> SnapshotTiming {
SnapshotTiming(
totalMs: roundedElapsedMs(since: startedAt),
phases: phases.isEmpty ? nil : phases,
backends: backends.isEmpty ? nil : backends,
selectedBackend: selectedBackend?.rawValue
)
}

private func roundedElapsedMs(since started: Date) -> Double {
let elapsed = Date().timeIntervalSince(started) * 1000
return (elapsed * 10).rounded() / 10
}
}

@discardableResult
func measureSnapshotPhase<T>(
_ timing: SnapshotTimingRecorder?,
_ phase: String,
_ work: () throws -> T
) rethrows -> T {
if let timing {
return try timing.measure(phase, work)
}
return try work()
}

@discardableResult
func measureSnapshotBackend<T>(
_ timing: SnapshotTimingRecorder?,
_ backend: SnapshotBackendKind,
_ work: () throws -> T
) rethrows -> T {
if let timing {
return try timing.measureBackend(backend, work)
}
return try work()
}

extension RunnerTests {
static let sparseRecoveryTruncatedNodeThreshold = 8
/// Umbrella wall-clock budget for one capture plan. Individual backends bound themselves,
Expand All @@ -69,7 +132,8 @@ extension RunnerTests {
_ plan: [SnapshotBackendKind],
app: XCUIApplication,
options: SnapshotOptions,
terminal: SnapshotCaptureTerminalPolicy
terminal: SnapshotCaptureTerminalPolicy,
timing: SnapshotTimingRecorder? = nil
) throws -> DataPayload {
var best: (kind: SnapshotBackendKind, capture: SnapshotBackendCapture)?
var firstFailure: (reason: String, code: String)?
Expand All @@ -86,7 +150,7 @@ extension RunnerTests {
}
let capture: SnapshotBackendCapture
do {
guard let result = try captureWithBackend(kind, app: app, options: options) else {
guard let result = try captureWithBackend(kind, app: app, options: options, timing: timing) else {
continue
}
capture = result
Expand Down Expand Up @@ -119,6 +183,7 @@ extension RunnerTests {
firstFailure?.reason ?? "sparse tree"
)
}
timing?.selectBackend(kind)
return stampedSnapshotPayload(
capture,
backend: kind,
Expand Down Expand Up @@ -147,7 +212,10 @@ extension RunnerTests {
}

let fallbackPayload =
best.map { stampedSnapshotPayload($0.capture, backend: $0.kind, state: "sparse", reason: firstFailure) }
best.map {
timing?.selectBackend($0.kind)
return stampedSnapshotPayload($0.capture, backend: $0.kind, state: "sparse", reason: firstFailure)
}
?? stampedSnapshotPayload(
SnapshotBackendCapture(payload: sparseTruncatedSnapshotPayload(), effectiveDepth: nil),
backend: plan.last ?? .recursiveTree,
Expand All @@ -160,24 +228,31 @@ extension RunnerTests {
private func captureWithBackend(
_ kind: SnapshotBackendKind,
app: XCUIApplication,
options: SnapshotOptions
options: SnapshotOptions,
timing: SnapshotTimingRecorder?
) throws -> SnapshotBackendCapture? {
switch kind {
case .recursiveTree:
guard let context = try makeSnapshotTraversalContext(app: app, options: options) else {
return nil
try measureSnapshotBackend(timing, kind) {
switch kind {
case .recursiveTree:
guard let context = try makeSnapshotTraversalContext(app: app, options: options, timing: timing) else {
return nil
}
let payload = options.raw
? try measureSnapshotPhase(timing, "raw_tree_shape") {
try rawTreeSnapshotPayload(context: context, options: options)
}
: measureSnapshotPhase(timing, "visible_tree_shape") {
recursiveTreeSnapshotPayload(context: context, options: options)
}
return SnapshotBackendCapture(payload: payload, effectiveDepth: nil)
case .querySweep:
return SnapshotBackendCapture(
payload: snapshotFlatInteractive(app: app, options: options, timing: timing),
effectiveDepth: nil
)
case .privateAX:
return privateAXSnapshotCapture(app: app, options: options)
}
let payload = options.raw
? try rawTreeSnapshotPayload(context: context, options: options)
: recursiveTreeSnapshotPayload(context: context, options: options)
return SnapshotBackendCapture(payload: payload, effectiveDepth: nil)
case .querySweep:
return SnapshotBackendCapture(
payload: snapshotFlatInteractive(app: app, options: options),
effectiveDepth: nil
)
case .privateAX:
return privateAXSnapshotCapture(app: app, options: options)
}
}

Expand Down
Loading
Loading