Skip to content

Commit 3aabddd

Browse files
committed
perf(runner): tag the settle window with its origin instead of inferring it
1 parent fb1e51a commit 3aabddd

3 files changed

Lines changed: 101 additions & 111 deletions

File tree

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

Lines changed: 36 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,6 @@ extension RunnerTests {
196196
mainOwned.bundleId = nil
197197
mainOwned.processIdentifier = nil
198198
resetTargetBoundState()
199-
firstInteractionReadyUptime = nil
200199
}
201200

202201
/// State that belongs to the currently bound target and must not outlive it: the text-entry tap
@@ -218,20 +217,20 @@ extension RunnerTests {
218217
mainOwned.bundleId = nil
219218
mainOwned.processIdentifier = nil
220219
resetTargetBoundState()
221-
// Only the announced relaunch below leaves a deadline behind a dropped binding, which is what
222-
// lets a skipped activation keep it. Any other invalidation starts the next activation fresh.
223-
firstInteractionReadyUptime = nil
224220
}
225221

226222
@MainActor
227223
func resetTargetAfterExternalRelaunch() -> Response {
224+
// The wire carries no bundle, so the window is attributed to the binding being dropped; with
225+
// none bound it matches whichever bundle activates next.
226+
let announcedBundleId = mainOwned.bundleId
228227
invalidateCachedTarget(reason: "external_app_relaunch")
229228
// The app process is replaced, but the retained runner survives. Clear
230229
// process-bound capture state explicitly because invalidation drops the
231230
// old PID before refreshCachedTargetIfProcessChanged can observe it.
232231
clearSnapshotXCTestChannelPenalty(reason: "external_app_relaunch")
233232
clearPrivateAXAcceptedDepth(reason: "external_app_relaunch")
234-
beginFirstInteractionStabilization()
233+
beginFirstInteractionStabilization(origin: .announcedRelaunch(bundleId: announcedBundleId))
235234
return Response(ok: true, data: DataPayload(message: "target reset"))
236235
}
237236

@@ -370,8 +369,6 @@ extension RunnerTests {
370369
initialState.rawValue,
371370
reason
372371
)
373-
let previousBundleId = mainOwned.bundleId
374-
let previousProcessIdentifier = mainOwned.processIdentifier
375372
var performedActivation = false
376373
// activate avoids terminating and relaunching the target app
377374
if initialState == .runningForeground {
@@ -400,18 +397,14 @@ extension RunnerTests {
400397
}
401398
mainOwned.app = target
402399
mainOwned.bundleId = bundleId
403-
let processIdentifier = Self.processIdentifier(of: target)
404-
mainOwned.processIdentifier = processIdentifier
400+
mainOwned.processIdentifier = Self.processIdentifier(of: target)
405401
resetTargetBoundState()
406-
let reacquiredBoundTarget =
402+
let announcedRelaunchCoversBinding =
407403
!performedActivation
408-
&& Self.reacquiresBoundTarget(
409-
previousBundleId: previousBundleId,
410-
previousProcessIdentifier: previousProcessIdentifier,
411-
bundleId: bundleId,
412-
processIdentifier: processIdentifier
413-
)
414-
beginFirstInteractionStabilization(reacquiredBoundTarget ? .keepPendingWindow : .freshWindow)
404+
&& firstInteractionWindow?.origin.announcesRelaunch(of: bundleId) == true
405+
if !announcedRelaunchCoversBinding {
406+
beginFirstInteractionStabilization(origin: .activation)
407+
}
415408
return target
416409
}
417410

@@ -506,39 +499,39 @@ extension RunnerTests {
506499
sleepFor(postSnapshotInteractionDelay)
507500
mainOwned.needsPostSnapshotInteractionDelay = false
508501
}
509-
if let readyUptime = firstInteractionReadyUptime {
510-
sleepFor(readyUptime - ProcessInfo.processInfo.systemUptime)
511-
firstInteractionReadyUptime = nil
502+
if let window = firstInteractionWindow {
503+
sleepFor(window.readyUptime - ProcessInfo.processInfo.systemUptime)
504+
firstInteractionWindow = nil
512505
}
513506
}
514507

515-
enum FirstInteractionWindow {
516-
/// The target was activated, replaced, or switched: the window restarts, discarding any
517-
/// deadline an earlier activation left behind.
518-
case freshWindow
519-
/// The same already-foreground target was only re-bound: an existing deadline, pending or
520-
/// elapsed but not yet consumed, still describes the last activation and is kept.
521-
case keepPendingWindow
508+
struct FirstInteractionWindow {
509+
/// Where the deadline came from, so a later binding can tell whether it still describes the
510+
/// activation that binding relies on.
511+
enum Origin: Equatable {
512+
/// Stamped by an activation or binding this runner performed.
513+
case activation
514+
/// Stamped when the daemon announced it had already relaunched and activated the app.
515+
/// `bundleId` is the binding that was dropped, or nil when none was bound.
516+
case announcedRelaunch(bundleId: String?)
517+
518+
func announcesRelaunch(of bundleId: String) -> Bool {
519+
guard case .announcedRelaunch(let announcedBundleId) = self else { return false }
520+
return announcedBundleId == nil || announcedBundleId == bundleId
521+
}
522+
}
523+
524+
let readyUptime: TimeInterval
525+
let origin: Origin
522526
}
523527

524528
/// Start the post-activation settling window. Measured from now, so the time the caller spends
525529
/// getting back to us counts towards it instead of being charged twice.
526-
func beginFirstInteractionStabilization(_ window: FirstInteractionWindow = .freshWindow) {
527-
if window == .keepPendingWindow, firstInteractionReadyUptime != nil { return }
528-
firstInteractionReadyUptime =
529-
ProcessInfo.processInfo.systemUptime + firstInteractionAfterActivateDelay
530-
}
531-
532-
/// Whether binding an already-foreground target continues the binding that came before it.
533-
/// A cache that was dropped (`nil` bundle) carries no identity to contradict.
534-
static func reacquiresBoundTarget(
535-
previousBundleId: String?,
536-
previousProcessIdentifier: Int?,
537-
bundleId: String,
538-
processIdentifier: Int?
539-
) -> Bool {
540-
guard let previousBundleId else { return true }
541-
return previousBundleId == bundleId && previousProcessIdentifier == processIdentifier
530+
func beginFirstInteractionStabilization(origin: FirstInteractionWindow.Origin = .activation) {
531+
firstInteractionWindow = FirstInteractionWindow(
532+
readyUptime: ProcessInfo.processInfo.systemUptime + firstInteractionAfterActivateDelay,
533+
origin: origin
534+
)
542535
}
543536

544537
func sleepFor(_ delay: TimeInterval) {

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,11 @@ final class RunnerTests: XCTestCase {
8787
var lastLoggedFastAppGuardLine: String?
8888
var lastLoggedGesturePolicyLines: [SynthesizedGesturePolicyKind: String] = [:]
8989
var runnerMarkerWriter: @MainActor (String) -> Void = { NSLog("%@", $0) }
90-
/// When the first interaction after an activation may run, on the monotonic uptime clock.
91-
/// The guarantee is a minimum gap *since the activation*, not a pause at the interaction:
92-
/// a caller that already spent that gap elsewhere (an agent's round trip is 190-260 ms)
93-
/// has satisfied it and waits for nothing. `nil` = no activation is pending stabilization.
94-
var firstInteractionReadyUptime: TimeInterval?
90+
/// When the first interaction after an activation may run. The guarantee is a minimum gap
91+
/// *since the activation*, not a pause at the interaction: a caller that already spent that gap
92+
/// elsewhere (an agent's round trip is 190-260 ms) has satisfied it and waits for nothing.
93+
/// `nil` = no activation is pending stabilization.
94+
var firstInteractionWindow: FirstInteractionWindow?
9595
var activeRecording: ScreenRecorder?
9696
let commandJournal = RunnerCommandJournal()
9797
// Coalesces duplicate transport sends of the same commandId onto the single in-flight

‎apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+LifecycleCacheTests.swift‎

Lines changed: 60 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -85,52 +85,69 @@ extension RunnerTests {
8585
}
8686

8787
@MainActor
88-
func testSkippedActivationAfterTargetResetKeepsAnElapsedWindow() {
88+
func testSkippedActivationAfterAnnouncedRelaunchKeepsAnElapsedWindow() {
8989
withStubbedTargetActivation {
9090
mainOwned.needsPostSnapshotInteractionDelay = false
9191
RunnerTargetActivationSpy.state = .runningForeground
92+
mainOwned.app = app
93+
mainOwned.bundleId = "com.example.app"
9294
_ = resetTargetAfterExternalRelaunch()
93-
firstInteractionReadyUptime = ProcessInfo.processInfo.systemUptime - 1
95+
stampElapsedWindow(origin: .announcedRelaunch(bundleId: "com.example.app"))
9496

9597
_ = activateTarget(bundleId: "com.example.app", reason: "bundle_changed")
9698

9799
XCTAssertEqual(RunnerTargetActivationSpy.activationCount, 0)
98100
XCTAssertLessThan(measureStabilizationDuration(), firstInteractionAfterActivateDelay / 2)
99-
XCTAssertNil(firstInteractionReadyUptime)
101+
XCTAssertNil(firstInteractionWindow)
100102
}
101103
}
102104

103105
@MainActor
104-
func testSkippedActivationAfterTargetResetWaitsOnlyForTheRemainingWindow() {
106+
func testSkippedActivationAfterAnnouncedRelaunchWaitsOnlyForTheRemainingWindow() {
105107
withStubbedTargetActivation {
106108
mainOwned.needsPostSnapshotInteractionDelay = false
107109
RunnerTargetActivationSpy.state = .runningForeground
110+
mainOwned.app = app
111+
mainOwned.bundleId = "com.example.app"
108112
_ = resetTargetAfterExternalRelaunch()
109-
let resetDeadline = firstInteractionReadyUptime
113+
let resetWindow = firstInteractionWindow
110114
sleepFor(firstInteractionAfterActivateDelay / 2)
111115

112116
_ = activateTarget(bundleId: "com.example.app", reason: "bundle_changed")
113117

114-
XCTAssertEqual(firstInteractionReadyUptime, resetDeadline)
115-
let waited = measureStabilizationDuration()
116-
XCTAssertLessThan(waited, firstInteractionAfterActivateDelay * 0.75)
117-
XCTAssertNil(firstInteractionReadyUptime)
118+
XCTAssertEqual(firstInteractionWindow?.readyUptime, resetWindow?.readyUptime)
119+
XCTAssertEqual(firstInteractionWindow?.origin, .announcedRelaunch(bundleId: "com.example.app"))
120+
XCTAssertLessThan(measureStabilizationDuration(), firstInteractionAfterActivateDelay * 0.75)
121+
XCTAssertNil(firstInteractionWindow)
118122
}
119123
}
120124

121125
@MainActor
122-
func testInvalidationForErrorDiscardsTheWindowSoTheNextSkippedActivationStampsFresh() {
126+
func testAnnouncedRelaunchWithNoBoundTargetCoversWhicheverBundleActivatesNext() {
123127
withStubbedTargetActivation {
124128
mainOwned.needsPostSnapshotInteractionDelay = false
125129
RunnerTargetActivationSpy.state = .runningForeground
126-
mainOwned.app = app
127-
mainOwned.bundleId = "com.example.app"
128-
firstInteractionReadyUptime = ProcessInfo.processInfo.systemUptime - 1
130+
invalidateCachedTarget(reason: "unit_test_setup")
131+
_ = resetTargetAfterExternalRelaunch()
132+
XCTAssertEqual(firstInteractionWindow?.origin, .announcedRelaunch(bundleId: nil))
133+
stampElapsedWindow(origin: .announcedRelaunch(bundleId: nil))
129134

130-
invalidateCachedTarget(reason: "target_terminated")
131135
_ = activateTarget(bundleId: "com.example.app", reason: "bundle_changed")
132136

133-
XCTAssertEqual(RunnerTargetActivationSpy.activationCount, 0)
137+
XCTAssertLessThan(measureStabilizationDuration(), firstInteractionAfterActivateDelay / 2)
138+
}
139+
}
140+
141+
@MainActor
142+
func testSkippedActivationOfAnotherBundleThanTheAnnouncedRelaunchStampsFresh() {
143+
withStubbedTargetActivation {
144+
mainOwned.needsPostSnapshotInteractionDelay = false
145+
RunnerTargetActivationSpy.state = .runningForeground
146+
stampElapsedWindow(origin: .announcedRelaunch(bundleId: "com.example.a"))
147+
148+
_ = activateTarget(bundleId: "com.example.b", reason: "bundle_changed")
149+
150+
XCTAssertEqual(firstInteractionWindow?.origin, .activation)
134151
XCTAssertGreaterThanOrEqual(
135152
measureStabilizationDuration(),
136153
firstInteractionAfterActivateDelay * 0.8
@@ -139,14 +156,18 @@ extension RunnerTests {
139156
}
140157

141158
@MainActor
142-
func testSkippedActivationWithoutADeadlineStampsTheWindow() {
159+
func testStaleActivationWindowSurvivingTerminateInvalidationStampsFreshOnSkippedActivation() {
143160
withStubbedTargetActivation {
144161
mainOwned.needsPostSnapshotInteractionDelay = false
145162
RunnerTargetActivationSpy.state = .runningForeground
146-
firstInteractionReadyUptime = nil
163+
mainOwned.app = app
164+
mainOwned.bundleId = "com.example.app"
165+
stampElapsedWindow(origin: .activation)
147166

167+
invalidateCachedTarget(reason: "target_terminated")
148168
_ = activateTarget(bundleId: "com.example.app", reason: "bundle_changed")
149169

170+
XCTAssertEqual(RunnerTargetActivationSpy.activationCount, 0)
150171
XCTAssertGreaterThanOrEqual(
151172
measureStabilizationDuration(),
152173
firstInteractionAfterActivateDelay * 0.8
@@ -155,15 +176,15 @@ extension RunnerTests {
155176
}
156177

157178
@MainActor
158-
func testPerformedActivationReplacesAnElapsedWindow() {
179+
func testSkippedActivationWithoutAWindowStampsOne() {
159180
withStubbedTargetActivation {
160181
mainOwned.needsPostSnapshotInteractionDelay = false
161-
RunnerTargetActivationSpy.state = .runningBackground
162-
firstInteractionReadyUptime = ProcessInfo.processInfo.systemUptime - 1
182+
RunnerTargetActivationSpy.state = .runningForeground
183+
firstInteractionWindow = nil
163184

164185
_ = activateTarget(bundleId: "com.example.app", reason: "bundle_changed")
165186

166-
XCTAssertEqual(RunnerTargetActivationSpy.activationCount, 1)
187+
XCTAssertEqual(firstInteractionWindow?.origin, .activation)
167188
XCTAssertGreaterThanOrEqual(
168189
measureStabilizationDuration(),
169190
firstInteractionAfterActivateDelay * 0.8
@@ -172,55 +193,28 @@ extension RunnerTests {
172193
}
173194

174195
@MainActor
175-
func testSkippedActivationOfAnotherBundleReplacesAnElapsedWindow() {
196+
func testPerformedActivationReplacesAnElapsedAnnouncedWindow() {
176197
withStubbedTargetActivation {
177198
mainOwned.needsPostSnapshotInteractionDelay = false
178-
RunnerTargetActivationSpy.state = .runningForeground
179-
mainOwned.app = app
180-
mainOwned.bundleId = "com.example.previous"
181-
firstInteractionReadyUptime = ProcessInfo.processInfo.systemUptime - 1
199+
RunnerTargetActivationSpy.state = .runningBackground
200+
stampElapsedWindow(origin: .announcedRelaunch(bundleId: "com.example.app"))
182201

183202
_ = activateTarget(bundleId: "com.example.app", reason: "bundle_changed")
184203

204+
XCTAssertEqual(RunnerTargetActivationSpy.activationCount, 1)
205+
XCTAssertEqual(firstInteractionWindow?.origin, .activation)
185206
XCTAssertGreaterThanOrEqual(
186207
measureStabilizationDuration(),
187208
firstInteractionAfterActivateDelay * 0.8
188209
)
189210
}
190211
}
191212

192-
func testForegroundReacquisitionContinuesOnlyTheSameBoundProcess() {
193-
XCTAssertTrue(
194-
Self.reacquiresBoundTarget(
195-
previousBundleId: nil,
196-
previousProcessIdentifier: nil,
197-
bundleId: "com.example.app",
198-
processIdentifier: 42
199-
)
200-
)
201-
XCTAssertTrue(
202-
Self.reacquiresBoundTarget(
203-
previousBundleId: "com.example.app",
204-
previousProcessIdentifier: 42,
205-
bundleId: "com.example.app",
206-
processIdentifier: 42
207-
)
208-
)
209-
XCTAssertFalse(
210-
Self.reacquiresBoundTarget(
211-
previousBundleId: "com.example.app",
212-
previousProcessIdentifier: 41,
213-
bundleId: "com.example.app",
214-
processIdentifier: 42
215-
)
216-
)
217-
XCTAssertFalse(
218-
Self.reacquiresBoundTarget(
219-
previousBundleId: "com.example.other",
220-
previousProcessIdentifier: 42,
221-
bundleId: "com.example.app",
222-
processIdentifier: 42
223-
)
213+
@MainActor
214+
private func stampElapsedWindow(origin: FirstInteractionWindow.Origin) {
215+
firstInteractionWindow = FirstInteractionWindow(
216+
readyUptime: ProcessInfo.processInfo.systemUptime - 1,
217+
origin: origin
224218
)
225219
}
226220
#endif
@@ -296,7 +290,7 @@ extension RunnerTests {
296290
mainOwned.bundleId = "com.example.app"
297291
mainOwned.processIdentifier = 42
298292
snapshotXCTestPenaltyWarmupExemption.isPending = true
299-
firstInteractionReadyUptime = nil
293+
firstInteractionWindow = nil
300294
penalizeSnapshotXCTestChannel(bundleId: "com.example.app", reason: "test")
301295
XCTAssertTrue(isSnapshotXCTestChannelPenalized(bundleId: "com.example.app"))
302296

@@ -308,7 +302,7 @@ extension RunnerTests {
308302
XCTAssertNil(mainOwned.processIdentifier)
309303
XCTAssertFalse(snapshotXCTestPenaltyWarmupExemption.isPending)
310304
XCTAssertFalse(isSnapshotXCTestChannelPenalized(bundleId: "com.example.app"))
311-
XCTAssertNotNil(firstInteractionReadyUptime)
305+
XCTAssertEqual(firstInteractionWindow?.origin, .announcedRelaunch(bundleId: "com.example.app"))
312306
}
313307

314308
/// The settling window is a deadline measured from the activation, not a pause charged at the
@@ -319,10 +313,13 @@ extension RunnerTests {
319313
mainOwned.needsPostSnapshotInteractionDelay = false
320314

321315
// An activation whose window has already elapsed: the caller spent it getting back to us.
322-
firstInteractionReadyUptime = ProcessInfo.processInfo.systemUptime - 1
316+
firstInteractionWindow = FirstInteractionWindow(
317+
readyUptime: ProcessInfo.processInfo.systemUptime - 1,
318+
origin: .activation
319+
)
323320
let elapsedAfterSatisfiedWindow = measureStabilizationDuration()
324321
XCTAssertLessThan(elapsedAfterSatisfiedWindow, firstInteractionAfterActivateDelay / 2)
325-
XCTAssertNil(firstInteractionReadyUptime)
322+
XCTAssertNil(firstInteractionWindow)
326323

327324
// A fresh activation still gets the whole guard.
328325
beginFirstInteractionStabilization()
@@ -331,7 +328,7 @@ extension RunnerTests {
331328
elapsedAfterFreshActivation,
332329
firstInteractionAfterActivateDelay * 0.8
333330
)
334-
XCTAssertNil(firstInteractionReadyUptime)
331+
XCTAssertNil(firstInteractionWindow)
335332
}
336333

337334
@MainActor

0 commit comments

Comments
 (0)