Skip to content

Commit 8899013

Browse files
okwasniewskithymikee
authored andcommitted
refactor(move): own main-thread work in RunnerTests+MainThreadWork.swift
Pure move out of the 2.9k-line RunnerTests+CommandExecution.swift: runMainThreadWork, its work state, the busy/wedged state model, mainThreadExecutionTimeoutError, and their two unit tests. The suppression classifier test moves from RunnerTests+SnapshotCapturePlan.swift to UnitTests/RunnerTests+RecordedIssueSuppressionTests.swift. No behavior change. The one edit is mainThreadExecutionTimeoutError losing `private`, because its callers stay behind in the command execution file.
1 parent 465af75 commit 8899013

4 files changed

Lines changed: 234 additions & 220 deletions

File tree

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

Lines changed: 0 additions & 184 deletions
Original file line numberDiff line numberDiff line change
@@ -825,87 +825,6 @@ extension RunnerTests {
825825
XCTAssertTrue(response.error?.hint?.contains("runner session will be restarted") == true)
826826
}
827827

828-
func testRunMainThreadWorkExecutesOffMainCallerOnMainThread() {
829-
final class ResultBox {
830-
var observedMainThread: Bool?
831-
var error: Error?
832-
}
833-
let box = ResultBox()
834-
let finished = expectation(description: "off-main caller finished")
835-
836-
DispatchQueue(label: "agent-device.runner.tests.off-main").async {
837-
do {
838-
box.observedMainThread = try self.runMainThreadWork(
839-
timeout: 1,
840-
timeoutError: self.mainThreadExecutionTimeoutError
841-
) {
842-
Thread.isMainThread
843-
}
844-
} catch {
845-
box.error = error
846-
}
847-
finished.fulfill()
848-
}
849-
850-
wait(for: [finished], timeout: 2)
851-
XCTAssertNil(box.error)
852-
XCTAssertEqual(box.observedMainThread, true)
853-
}
854-
855-
func testRunMainThreadWorkTimeoutMarksAbandonedUntilDrained() {
856-
final class ResultBox {
857-
var error: Error?
858-
var abandonedCount: Int?
859-
var abandonedSinceSet: Bool?
860-
var drainedCount: Int?
861-
var drainedSinceCleared: Bool?
862-
}
863-
let box = ResultBox()
864-
let releaseWork = DispatchSemaphore(value: 0)
865-
let observedAbandoned = DispatchSemaphore(value: 0)
866-
let finished = expectation(description: "off-main caller timed out")
867-
let drained = expectation(description: "abandoned main work drained")
868-
869-
DispatchQueue(label: "agent-device.runner.tests.timeout").async {
870-
do {
871-
_ = try self.runMainThreadWork(
872-
timeout: 0,
873-
timeoutError: self.mainThreadExecutionTimeoutError,
874-
onAbandoned: {
875-
box.abandonedCount = self.abandonedMainThreadWorkCount
876-
box.abandonedSinceSet = self.abandonedMainThreadWorkSince != nil
877-
observedAbandoned.signal()
878-
},
879-
onDrained: {
880-
self.mainThreadWorkLock.lock()
881-
box.drainedCount = self.abandonedMainThreadWorkCount
882-
box.drainedSinceCleared = self.abandonedMainThreadWorkSince == nil
883-
self.mainThreadWorkLock.unlock()
884-
drained.fulfill()
885-
}
886-
) {
887-
_ = releaseWork.wait(timeout: .now() + 1)
888-
return true
889-
}
890-
} catch {
891-
box.error = error
892-
}
893-
finished.fulfill()
894-
}
895-
896-
DispatchQueue(label: "agent-device.runner.tests.release-timeout").async {
897-
_ = observedAbandoned.wait(timeout: .now() + 1)
898-
releaseWork.signal()
899-
}
900-
901-
wait(for: [finished, drained], timeout: 2)
902-
XCTAssertEqual((box.error as NSError?)?.code, RunnerErrorCode.mainThreadExecutionTimedOut)
903-
XCTAssertEqual(box.abandonedCount, 1)
904-
XCTAssertEqual(box.abandonedSinceSet, true)
905-
XCTAssertEqual(box.drainedCount, 0)
906-
XCTAssertEqual(box.drainedSinceCleared, true)
907-
}
908-
909828
func testPostSnapshotDelayMarkDoesNotQueueBehindAbandonedTreeCapture() {
910829
abandonedTreeCaptureCount = 1
911830
defer {
@@ -977,13 +896,6 @@ extension RunnerTests {
977896
)
978897
}
979898

980-
/// Tracks one main-queue dispatch so the watchdog and the dispatched block can agree —
981-
/// under `mainThreadWorkLock` — on exactly one of: finished in time, or abandoned.
982-
private final class MainThreadWorkState {
983-
var finished = false
984-
var abandoned = false
985-
}
986-
987899
struct ActiveCommandContext {
988900
let app: XCUIApplication
989901
/// Set when `app` is a system surface served in place over the still-bound session app (#2438).
@@ -995,31 +907,6 @@ extension RunnerTests {
995907
case context(ActiveCommandContext)
996908
}
997909

998-
enum MainThreadBusyState {
999-
case idle
1000-
case busy(abandonedForSeconds: TimeInterval)
1001-
case wedged(abandonedForSeconds: TimeInterval)
1002-
1003-
/// Whether the main thread is occupied by watchdog-abandoned work, for the occupancy stamp that
1004-
/// every successful response carries. Wedged is still occupied: it only differs in that a
1005-
/// restart, not waiting, is the cure.
1006-
var reportsMainThreadBusy: Bool {
1007-
if case .idle = self { return false }
1008-
return true
1009-
}
1010-
}
1011-
1012-
func currentMainThreadBusyState() -> MainThreadBusyState {
1013-
mainThreadWorkLock.lock()
1014-
defer { mainThreadWorkLock.unlock() }
1015-
guard abandonedMainThreadWorkCount > 0 else { return .idle }
1016-
let abandonedFor = abandonedMainThreadWorkSince.map { Date().timeIntervalSince($0) } ?? 0
1017-
if abandonedFor > mainThreadWedgeThreshold {
1018-
return .wedged(abandonedForSeconds: abandonedFor)
1019-
}
1020-
return .busy(abandonedForSeconds: abandonedFor)
1021-
}
1022-
1023910
private func runnerBusyResponse(command: Command, abandonedForSeconds: TimeInterval) -> Response {
1024911
NSLog(
1025912
"AGENT_DEVICE_RUNNER_BUSY command=%@ commandId=%@ abandonedForSeconds=%.1f",
@@ -1118,77 +1005,6 @@ extension RunnerTests {
11181005
}
11191006
}
11201007

1121-
func runMainThreadWork<T>(
1122-
timeout: TimeInterval,
1123-
timeoutError: @escaping () -> Error,
1124-
onAbandoned: (() -> Void)? = nil,
1125-
onDrained: (() -> Void)? = nil,
1126-
_ work: @escaping () throws -> T
1127-
) throws -> T {
1128-
if Thread.isMainThread {
1129-
return try work()
1130-
}
1131-
var result: Result<T, Error>?
1132-
let semaphore = DispatchSemaphore(value: 0)
1133-
let workState = MainThreadWorkState()
1134-
DispatchQueue.main.async {
1135-
do {
1136-
result = .success(try work())
1137-
} catch {
1138-
result = .failure(error)
1139-
}
1140-
self.mainThreadWorkLock.lock()
1141-
if workState.abandoned {
1142-
self.abandonedMainThreadWorkCount -= 1
1143-
if self.abandonedMainThreadWorkCount == 0 {
1144-
self.abandonedMainThreadWorkSince = nil
1145-
NSLog("AGENT_DEVICE_RUNNER_ABANDONED_WORK_DRAINED")
1146-
}
1147-
self.mainThreadWorkLock.unlock()
1148-
onDrained?()
1149-
} else {
1150-
workState.finished = true
1151-
self.mainThreadWorkLock.unlock()
1152-
}
1153-
semaphore.signal()
1154-
}
1155-
let waitResult = semaphore.wait(timeout: .now() + timeout)
1156-
if waitResult == .timedOut {
1157-
mainThreadWorkLock.lock()
1158-
let stillRunning = !workState.finished
1159-
if stillRunning {
1160-
workState.abandoned = true
1161-
abandonedMainThreadWorkCount += 1
1162-
if abandonedMainThreadWorkSince == nil {
1163-
abandonedMainThreadWorkSince = Date()
1164-
}
1165-
onAbandoned?()
1166-
}
1167-
mainThreadWorkLock.unlock()
1168-
throw timeoutError()
1169-
}
1170-
switch result {
1171-
case .success(let value):
1172-
return value
1173-
case .failure(let error):
1174-
throw error
1175-
case .none:
1176-
throw NSError(
1177-
domain: RunnerErrorDomain.general,
1178-
code: RunnerErrorCode.noResponseFromMainThread,
1179-
userInfo: [NSLocalizedDescriptionKey: "no response from main thread"]
1180-
)
1181-
}
1182-
}
1183-
1184-
private func mainThreadExecutionTimeoutError() -> Error {
1185-
NSError(
1186-
domain: RunnerErrorDomain.general,
1187-
code: RunnerErrorCode.mainThreadExecutionTimedOut,
1188-
userInfo: [NSLocalizedDescriptionKey: "main thread execution timed out"]
1189-
)
1190-
}
1191-
11921008
// MARK: - Command Handling
11931009

11941010
private func executeOnMainSafely(

0 commit comments

Comments
 (0)