Add workspace stress profiling and reduce switch churn (#1218)

* Add workspace stress profiling and reduce switch churn

* Address workspace stress review feedback
This commit is contained in:
Lawrence Chen 2026-03-13 04:44:16 -07:00 committed by GitHub
parent d732a2c1b1
commit f95a32ea52
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 426 additions and 23 deletions

View file

@ -93,6 +93,7 @@
F7000000A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7000001A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift */; };
F8000000A1B2C3D4E5F60718 /* SocketControlPasswordStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8000001A1B2C3D4E5F60718 /* SocketControlPasswordStoreTests.swift */; };
F9000000A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9000001A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift */; };
FA000000A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA000001A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift */; };
A5008381 /* BrowserFindJavaScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5008380 /* BrowserFindJavaScriptTests.swift */; };
A5008383 /* CommandPaletteSearchEngineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5008382 /* CommandPaletteSearchEngineTests.swift */; };
DA7A10CA710E000000000003 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = DA7A10CA710E000000000001 /* Localizable.xcstrings */; };
@ -238,6 +239,7 @@
F7000001A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceContentViewVisibilityTests.swift; sourceTree = "<group>"; };
F8000001A1B2C3D4E5F60718 /* SocketControlPasswordStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketControlPasswordStoreTests.swift; sourceTree = "<group>"; };
F9000001A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyEnsureFocusWindowActivationTests.swift; sourceTree = "<group>"; };
FA000001A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceStressProfileTests.swift; sourceTree = "<group>"; };
A5008380 /* BrowserFindJavaScriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserFindJavaScriptTests.swift; sourceTree = "<group>"; };
A5008382 /* CommandPaletteSearchEngineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteSearchEngineTests.swift; sourceTree = "<group>"; };
DA7A10CA710E000000000001 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
@ -472,6 +474,7 @@
F7000001A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift */,
F8000001A1B2C3D4E5F60718 /* SocketControlPasswordStoreTests.swift */,
F9000001A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift */,
FA000001A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift */,
A5008380 /* BrowserFindJavaScriptTests.swift */,
A5008382 /* CommandPaletteSearchEngineTests.swift */,
);
@ -711,6 +714,7 @@
F7000000A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift in Sources */,
F8000000A1B2C3D4E5F60718 /* SocketControlPasswordStoreTests.swift in Sources */,
F9000000A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift in Sources */,
FA000000A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift in Sources */,
A5008381 /* BrowserFindJavaScriptTests.swift in Sources */,
A5008383 /* CommandPaletteSearchEngineTests.swift in Sources */,
);

View file

@ -1331,6 +1331,7 @@ struct ContentView: View {
@State private var workspaceHandoffGeneration: UInt64 = 0
@State private var workspaceHandoffFallbackTask: Task<Void, Never>?
@State private var didApplyUITestSidebarSelection = false
@State private var workspaceHandoffReadyCheckTask: Task<Void, Never>?
@State private var titlebarThemeGeneration: UInt64 = 0
@State private var sidebarDraggedTabId: UUID?
@State private var titlebarTextUpdateCoalescer = NotificationBurstCoalescer(delay: 1.0 / 30.0)
@ -2881,6 +2882,8 @@ struct ContentView: View {
retiringWorkspaceId = nil
workspaceHandoffFallbackTask?.cancel()
workspaceHandoffFallbackTask = nil
workspaceHandoffReadyCheckTask?.cancel()
workspaceHandoffReadyCheckTask = nil
return
}
@ -2888,6 +2891,7 @@ struct ContentView: View {
let generation = workspaceHandoffGeneration
retiringWorkspaceId = oldSelectedId
workspaceHandoffFallbackTask?.cancel()
workspaceHandoffReadyCheckTask?.cancel()
#if DEBUG
if let snapshot = tabManager.debugCurrentWorkspaceSwitchSnapshot() {
@ -2903,6 +2907,36 @@ struct ContentView: View {
}
#endif
workspaceHandoffReadyCheckTask = Task { [generation, newSelectedId] in
for delay in [0, 20_000_000, 40_000_000, 60_000_000] {
if delay > 0 {
do {
try await Task.sleep(nanoseconds: UInt64(delay))
} catch {
return
}
}
let completed = await MainActor.run { () -> Bool in
guard workspaceHandoffGeneration == generation else { return false }
guard retiringWorkspaceId != nil else { return false }
guard canCompleteWorkspaceHandoffImmediately(for: newSelectedId) else { return false }
#if DEBUG
if let snapshot = tabManager.debugCurrentWorkspaceSwitchSnapshot() {
let dtMs = (CACurrentMediaTime() - snapshot.startedAt) * 1000
dlog(
"ws.handoff.fastReady id=\(snapshot.id) dt=\(debugMsText(dtMs)) selected=\(debugShortWorkspaceId(newSelectedId))"
)
} else {
dlog("ws.handoff.fastReady id=none selected=\(debugShortWorkspaceId(newSelectedId))")
}
#endif
completeWorkspaceHandoff(reason: "ready")
return true
}
if completed { return }
}
}
workspaceHandoffFallbackTask = Task { [generation] in
do {
try await Task.sleep(nanoseconds: 150_000_000)
@ -2922,9 +2956,20 @@ struct ContentView: View {
completeWorkspaceHandoff(reason: reason)
}
private func canCompleteWorkspaceHandoffImmediately(for workspaceId: UUID) -> Bool {
guard let workspace = tabManager.tabs.first(where: { $0.id == workspaceId }) else { return true }
if let focusedPanelId = workspace.focusedPanelId,
workspace.browserPanel(for: focusedPanelId) != nil {
return true
}
return workspace.hasLoadedTerminalSurface()
}
private func completeWorkspaceHandoff(reason: String) {
workspaceHandoffFallbackTask?.cancel()
workspaceHandoffFallbackTask = nil
workspaceHandoffReadyCheckTask?.cancel()
workspaceHandoffReadyCheckTask = nil
let retiring = retiringWorkspaceId
// Hide portal-hosted views for the retiring workspace BEFORE clearing
@ -7221,6 +7266,9 @@ struct VerticalTabsSidebar: View {
}
var body: some View {
let workspaceCount = tabManager.tabs.count
let canCloseWorkspace = workspaceCount > 1
VStack(spacing: 0) {
GeometryReader { proxy in
ScrollView {
@ -7237,7 +7285,12 @@ struct VerticalTabsSidebar: View {
tab: tab,
index: index,
isActive: tabManager.selectedTabId == tab.id,
tabCount: tabManager.tabs.count,
workspaceShortcutDigit: WorkspaceShortcutMapper.commandDigitForWorkspace(
at: index,
workspaceCount: workspaceCount
),
canCloseWorkspace: canCloseWorkspace,
accessibilityWorkspaceCount: workspaceCount,
unreadCount: notificationStore.unreadCount(forTabId: tab.id),
latestNotificationText: {
guard showsSidebarNotificationMessage,
@ -9506,7 +9559,9 @@ private struct TabItemView: View, Equatable {
lhs.tab === rhs.tab &&
lhs.index == rhs.index &&
lhs.isActive == rhs.isActive &&
lhs.tabCount == rhs.tabCount &&
lhs.workspaceShortcutDigit == rhs.workspaceShortcutDigit &&
lhs.canCloseWorkspace == rhs.canCloseWorkspace &&
lhs.accessibilityWorkspaceCount == rhs.accessibilityWorkspaceCount &&
lhs.unreadCount == rhs.unreadCount &&
lhs.latestNotificationText == rhs.latestNotificationText &&
lhs.rowSpacing == rhs.rowSpacing &&
@ -9522,7 +9577,9 @@ private struct TabItemView: View, Equatable {
@ObservedObject var tab: Tab
let index: Int
let isActive: Bool
let tabCount: Int
let workspaceShortcutDigit: Int?
let canCloseWorkspace: Bool
let accessibilityWorkspaceCount: Int
let unreadCount: Int
let latestNotificationText: String?
let rowSpacing: CGFloat
@ -9625,12 +9682,8 @@ private struct TabItemView: View, Equatable {
usesInvertedActiveForeground ? 1.0 : 0.9
}
private var workspaceShortcutDigit: Int? {
WorkspaceShortcutMapper.commandDigitForWorkspace(at: index, workspaceCount: tabCount)
}
private var showCloseButton: Bool {
isHovering && tabCount > 1 && !(showsModifierShortcutHints || alwaysShowShortcutHints)
isHovering && canCloseWorkspace && !(showsModifierShortcutHints || alwaysShowShortcutHints)
}
private var workspaceShortcutLabel: String? {
@ -10267,7 +10320,7 @@ private struct TabItemView: View, Equatable {
}
private var accessibilityTitle: String {
String(localized: "accessibility.workspacePosition", defaultValue: "\(tab.title), workspace \(index + 1) of \(tabCount)")
String(localized: "accessibility.workspacePosition", defaultValue: "\(tab.title), workspace \(index + 1) of \(accessibilityWorkspaceCount)")
}
private func moveBy(_ delta: Int) {

View file

@ -655,6 +655,33 @@ class TabManager: ObservableObject {
private static var nextPortOrdinal: Int = 0
private static let initialWorkspaceGitProbeDelays: [TimeInterval] = [0, 0.5, 1.5, 3.0, 6.0, 10.0]
@Published var selectedTabId: UUID? {
willSet {
#if DEBUG
guard newValue != selectedTabId else {
debugPendingWorkspaceSwitchTrigger = nil
debugPendingWorkspaceSwitchTarget = nil
debugPreparedWorkspaceSwitchTarget = nil
return
}
if debugPreparedWorkspaceSwitchTarget == newValue {
debugPreparedWorkspaceSwitchTarget = nil
debugPendingWorkspaceSwitchTrigger = nil
debugPendingWorkspaceSwitchTarget = nil
} else {
let trigger = (debugPendingWorkspaceSwitchTarget == newValue
? debugPendingWorkspaceSwitchTrigger
: nil) ?? "direct"
debugPendingWorkspaceSwitchTrigger = nil
debugPendingWorkspaceSwitchTarget = nil
debugBeginWorkspaceSwitch(
trigger: trigger,
from: selectedTabId,
to: newValue
)
}
#endif
}
didSet {
guard selectedTabId != oldValue else { return }
sentryBreadcrumb("workspace.switch", data: [
@ -731,6 +758,9 @@ class TabManager: ObservableObject {
private var debugWorkspaceSwitchCounter: UInt64 = 0
private var debugWorkspaceSwitchId: UInt64 = 0
private var debugWorkspaceSwitchStartTime: CFTimeInterval = 0
private var debugPendingWorkspaceSwitchTrigger: String?
private var debugPendingWorkspaceSwitchTarget: UUID?
private var debugPreparedWorkspaceSwitchTarget: UUID?
#endif
#if DEBUG
@ -930,6 +960,9 @@ class TabManager: ObservableObject {
newWorkspace.requestBackgroundTerminalSurfaceStartIfNeeded()
}
if select {
#if DEBUG
debugPrimeWorkspaceSwitchTrigger("create", to: newWorkspace.id)
#endif
selectedTabId = newWorkspace.id
NotificationCenter.default.post(
name: .ghosttyDidFocusTab,
@ -1507,6 +1540,9 @@ class TabManager: ObservableObject {
}
func selectWorkspace(_ workspace: Workspace) {
#if DEBUG
debugPrimeWorkspaceSwitchTrigger("select", to: workspace.id)
#endif
selectedTabId = workspace.id
}
@ -2093,6 +2129,9 @@ class TabManager: ObservableObject {
// Keep selected-surface intent stable across selectedTabId didSet async restore.
lastFocusedPanelByTab[tabId] = surfaceId
}
#if DEBUG
debugPrimeWorkspaceSwitchTrigger("focus", to: tabId)
#endif
selectedTabId = tabId
NotificationCenter.default.post(
name: .ghosttyDidFocusTab,
@ -2160,13 +2199,7 @@ class TabManager: ObservableObject {
let nextIndex = (currentIndex + 1) % tabs.count
#if DEBUG
let nextId = tabs[nextIndex].id
debugWorkspaceSwitchCounter &+= 1
debugWorkspaceSwitchId = debugWorkspaceSwitchCounter
debugWorkspaceSwitchStartTime = CACurrentMediaTime()
dlog(
"ws.switch.begin id=\(debugWorkspaceSwitchId) dir=next from=\(Self.debugShortWorkspaceId(currentId)) " +
"to=\(Self.debugShortWorkspaceId(nextId)) hot=\(isWorkspaceCycleHot ? 1 : 0) tabs=\(tabs.count)"
)
debugPrepareWorkspaceSwitch("next", from: currentId, to: nextId)
#endif
activateWorkspaceCycleHotWindow()
selectedTabId = tabs[nextIndex].id
@ -2178,13 +2211,7 @@ class TabManager: ObservableObject {
let prevIndex = (currentIndex - 1 + tabs.count) % tabs.count
#if DEBUG
let prevId = tabs[prevIndex].id
debugWorkspaceSwitchCounter &+= 1
debugWorkspaceSwitchId = debugWorkspaceSwitchCounter
debugWorkspaceSwitchStartTime = CACurrentMediaTime()
dlog(
"ws.switch.begin id=\(debugWorkspaceSwitchId) dir=prev from=\(Self.debugShortWorkspaceId(currentId)) " +
"to=\(Self.debugShortWorkspaceId(prevId)) hot=\(isWorkspaceCycleHot ? 1 : 0) tabs=\(tabs.count)"
)
debugPrepareWorkspaceSwitch("prev", from: currentId, to: prevId)
#endif
activateWorkspaceCycleHotWindow()
selectedTabId = tabs[prevIndex].id
@ -2257,6 +2284,40 @@ class TabManager: ObservableObject {
return (debugWorkspaceSwitchId, debugWorkspaceSwitchStartTime)
}
private func debugPrimeWorkspaceSwitchTrigger(_ trigger: String, to target: UUID?) {
guard selectedTabId != target else {
debugPendingWorkspaceSwitchTrigger = nil
debugPendingWorkspaceSwitchTarget = nil
return
}
debugPendingWorkspaceSwitchTrigger = trigger
debugPendingWorkspaceSwitchTarget = target
}
private func debugPrepareWorkspaceSwitch(_ trigger: String, from: UUID?, to: UUID?) {
guard from != to else {
debugPendingWorkspaceSwitchTrigger = nil
debugPendingWorkspaceSwitchTarget = nil
debugPreparedWorkspaceSwitchTarget = nil
return
}
debugPendingWorkspaceSwitchTrigger = nil
debugPendingWorkspaceSwitchTarget = nil
debugBeginWorkspaceSwitch(trigger: trigger, from: from, to: to)
debugPreparedWorkspaceSwitchTarget = to
}
private func debugBeginWorkspaceSwitch(trigger: String, from: UUID?, to: UUID?) {
debugWorkspaceSwitchCounter &+= 1
debugWorkspaceSwitchId = debugWorkspaceSwitchCounter
debugWorkspaceSwitchStartTime = CACurrentMediaTime()
dlog(
"ws.switch.begin id=\(debugWorkspaceSwitchId) trigger=\(trigger) " +
"from=\(Self.debugShortWorkspaceId(from)) to=\(Self.debugShortWorkspaceId(to)) " +
"hot=\(isWorkspaceCycleHot ? 1 : 0) tabs=\(tabs.count)"
)
}
private static func debugShortWorkspaceId(_ id: UUID?) -> String {
guard let id else { return "nil" }
return String(id.uuidString.prefix(5))
@ -2269,6 +2330,9 @@ class TabManager: ObservableObject {
func selectTab(at index: Int) {
guard index >= 0 && index < tabs.count else { return }
#if DEBUG
debugPrimeWorkspaceSwitchTrigger("select_index", to: tabs[index].id)
#endif
selectedTabId = tabs[index].id
}

View file

@ -0,0 +1,282 @@
import XCTest
#if canImport(cmux_DEV)
@testable import cmux_DEV
#elseif canImport(cmux)
@testable import cmux
#endif
@MainActor
final class WorkspaceStressProfileTests: XCTestCase {
private struct StressConfig {
let workspaceCount: Int
let tabsPerWorkspace: Int
let switchPasses: Int
let createP95BudgetMs: Double?
let switchP95BudgetMs: Double?
static func current(environment: [String: String] = ProcessInfo.processInfo.environment) -> StressConfig {
StressConfig(
workspaceCount: parseInt(environment["CMUX_WORKSPACE_STRESS_WORKSPACES"], default: 48, minimum: 2),
tabsPerWorkspace: parseInt(environment["CMUX_WORKSPACE_STRESS_TABS_PER_WORKSPACE"], default: 10, minimum: 1),
switchPasses: parseInt(environment["CMUX_WORKSPACE_STRESS_SWITCH_PASSES"], default: 6, minimum: 1),
createP95BudgetMs: parseDouble(environment["CMUX_WORKSPACE_STRESS_CREATE_P95_BUDGET_MS"]),
switchP95BudgetMs: parseDouble(environment["CMUX_WORKSPACE_STRESS_SWITCH_P95_BUDGET_MS"])
)
}
private static func parseInt(_ value: String?, default defaultValue: Int, minimum: Int) -> Int {
guard let value, let parsed = Int(value) else { return defaultValue }
return max(minimum, parsed)
}
private static func parseDouble(_ value: String?) -> Double? {
guard let value, let parsed = Double(value) else { return nil }
return parsed
}
}
private struct TimedSample {
let label: String
let elapsedMs: Double
}
private struct TimingSummary {
let count: Int
let averageMs: Double
let medianMs: Double
let p95Ms: Double
let maxMs: Double
let totalMs: Double
init(samples: [TimedSample]) {
let sorted = samples.map(\.elapsedMs).sorted()
count = sorted.count
totalMs = sorted.reduce(0, +)
averageMs = count > 0 ? totalMs / Double(count) : 0
medianMs = Self.percentile(0.50, in: sorted)
p95Ms = Self.percentile(0.95, in: sorted)
maxMs = sorted.last ?? 0
}
private static func percentile(_ percentile: Double, in sortedValues: [Double]) -> Double {
guard !sortedValues.isEmpty else { return 0 }
let clamped = min(max(percentile, 0), 1)
let index = Int((Double(sortedValues.count - 1) * clamped).rounded(.up))
return sortedValues[min(sortedValues.count - 1, max(0, index))]
}
}
func testWorkspaceCreationAndSwitchingStressProfile() {
let config = StressConfig.current()
let welcomeWasShown = UserDefaults.standard.object(forKey: WelcomeSettings.shownKey)
UserDefaults.standard.set(true, forKey: WelcomeSettings.shownKey)
defer {
if let welcomeWasShown {
UserDefaults.standard.set(welcomeWasShown, forKey: WelcomeSettings.shownKey)
} else {
UserDefaults.standard.removeObject(forKey: WelcomeSettings.shownKey)
}
}
var creationSamples: [TimedSample] = []
var populationSamples: [TimedSample] = []
var switchSamples: [TimedSample] = []
var switchDispatchSamples: [TimedSample] = []
var switchFirstDrainSamples: [TimedSample] = []
var switchUnfocusSamples: [TimedSample] = []
var switchSecondDrainSamples: [TimedSample] = []
let manager = timed("workspace-000-create", collectInto: &creationSamples) {
TabManager()
}
guard let bootstrapWorkspace = manager.selectedWorkspace else {
XCTFail("Expected bootstrap workspace")
return
}
timed("workspace-000-populate", collectInto: &populationSamples) {
populate(workspace: bootstrapWorkspace, tabsPerWorkspace: config.tabsPerWorkspace)
}
settleWorkspaceSelection(manager)
for workspaceIndex in 1..<config.workspaceCount {
let workspace = timed("workspace-\(label(for: workspaceIndex))-create", collectInto: &creationSamples) {
manager.addWorkspace(
select: true,
eagerLoadTerminal: false,
autoWelcomeIfNeeded: false
)
}
settleWorkspaceSelection(manager)
timed("workspace-\(label(for: workspaceIndex))-populate", collectInto: &populationSamples) {
populate(workspace: workspace, tabsPerWorkspace: config.tabsPerWorkspace)
}
settleWorkspaceSelection(manager)
}
XCTAssertEqual(manager.tabs.count, config.workspaceCount)
XCTAssertTrue(manager.tabs.allSatisfy { $0.panels.count == config.tabsPerWorkspace })
for pass in 0..<config.switchPasses {
for switchIndex in 0..<manager.tabs.count {
timed("pass-\(label(for: pass))-next-\(label(for: switchIndex))", collectInto: &switchSamples) {
timed("pass-\(label(for: pass))-next-dispatch-\(label(for: switchIndex))", collectInto: &switchDispatchSamples) {
manager.selectNextTab()
}
timed("pass-\(label(for: pass))-next-drain1-\(label(for: switchIndex))", collectInto: &switchFirstDrainSamples) {
drainMainQueue()
}
timed("pass-\(label(for: pass))-next-unfocus-\(label(for: switchIndex))", collectInto: &switchUnfocusSamples) {
manager.completePendingWorkspaceUnfocus(reason: "workspace_stress_profile")
}
timed("pass-\(label(for: pass))-next-drain2-\(label(for: switchIndex))", collectInto: &switchSecondDrainSamples) {
drainMainQueue()
}
}
}
for switchIndex in 0..<manager.tabs.count {
timed("pass-\(label(for: pass))-prev-\(label(for: switchIndex))", collectInto: &switchSamples) {
timed("pass-\(label(for: pass))-prev-dispatch-\(label(for: switchIndex))", collectInto: &switchDispatchSamples) {
manager.selectPreviousTab()
}
timed("pass-\(label(for: pass))-prev-drain1-\(label(for: switchIndex))", collectInto: &switchFirstDrainSamples) {
drainMainQueue()
}
timed("pass-\(label(for: pass))-prev-unfocus-\(label(for: switchIndex))", collectInto: &switchUnfocusSamples) {
manager.completePendingWorkspaceUnfocus(reason: "workspace_stress_profile")
}
timed("pass-\(label(for: pass))-prev-drain2-\(label(for: switchIndex))", collectInto: &switchSecondDrainSamples) {
drainMainQueue()
}
}
}
}
XCTAssertNotNil(manager.selectedWorkspace)
let creationSummary = TimingSummary(samples: creationSamples)
let populationSummary = TimingSummary(samples: populationSamples)
let switchSummary = TimingSummary(samples: switchSamples)
let switchDispatchSummary = TimingSummary(samples: switchDispatchSamples)
let switchFirstDrainSummary = TimingSummary(samples: switchFirstDrainSamples)
let switchUnfocusSummary = TimingSummary(samples: switchUnfocusSamples)
let switchSecondDrainSummary = TimingSummary(samples: switchSecondDrainSamples)
let report = [
"Workspace stress config workspaces=\(config.workspaceCount) tabsPerWorkspace=\(config.tabsPerWorkspace) switchPasses=\(config.switchPasses)",
reportLine(title: "create", summary: creationSummary, slowest: slowest(creationSamples)),
reportLine(title: "populate", summary: populationSummary, slowest: slowest(populationSamples)),
reportLine(title: "switch", summary: switchSummary, slowest: slowest(switchSamples)),
reportLine(title: "switch.dispatch", summary: switchDispatchSummary, slowest: slowest(switchDispatchSamples)),
reportLine(title: "switch.drain1", summary: switchFirstDrainSummary, slowest: slowest(switchFirstDrainSamples)),
reportLine(title: "switch.unfocus", summary: switchUnfocusSummary, slowest: slowest(switchUnfocusSamples)),
reportLine(title: "switch.drain2", summary: switchSecondDrainSummary, slowest: slowest(switchSecondDrainSamples))
].joined(separator: "\n")
print(report)
let attachment = XCTAttachment(string: report)
attachment.name = "workspace-stress-profile"
attachment.lifetime = .keepAlways
add(attachment)
if let createP95BudgetMs = config.createP95BudgetMs {
XCTAssertLessThanOrEqual(
creationSummary.p95Ms,
createP95BudgetMs,
"Workspace creation p95 exceeded budget"
)
}
if let switchP95BudgetMs = config.switchP95BudgetMs {
XCTAssertLessThanOrEqual(
switchSummary.p95Ms,
switchP95BudgetMs,
"Workspace switch p95 exceeded budget"
)
}
}
private func populate(workspace: Workspace, tabsPerWorkspace: Int) {
guard tabsPerWorkspace > 0 else { return }
while workspace.panels.count < tabsPerWorkspace {
let created = workspace.newTerminalSurfaceInFocusedPane(focus: false)
guard created != nil else {
XCTFail("Expected terminal tab creation to succeed")
return
}
}
}
private func settleWorkspaceSelection(_ manager: TabManager) {
drainMainQueue()
manager.completePendingWorkspaceUnfocus(reason: "workspace_stress_profile")
drainMainQueue()
}
private func drainMainQueue() {
let deadline = Date(timeIntervalSinceNow: 1.0)
var drained = false
DispatchQueue.main.async {
drained = true
}
while !drained {
if Date() >= deadline {
XCTFail("Timed out draining main queue")
return
}
let sliceDeadline = min(deadline, Date(timeIntervalSinceNow: 0.001))
_ = RunLoop.main.run(mode: .default, before: sliceDeadline)
}
}
@discardableResult
private func timed<T>(
_ label: String,
collectInto samples: inout [TimedSample],
operation: () -> T
) -> T {
let startedAt = ProcessInfo.processInfo.systemUptime
let value = operation()
let elapsedMs = (ProcessInfo.processInfo.systemUptime - startedAt) * 1000.0
samples.append(TimedSample(label: label, elapsedMs: elapsedMs))
return value
}
private func slowest(_ samples: [TimedSample], count: Int = 5) -> String {
samples
.sorted { lhs, rhs in
if lhs.elapsedMs == rhs.elapsedMs {
return lhs.label < rhs.label
}
return lhs.elapsedMs > rhs.elapsedMs
}
.prefix(count)
.map { "\($0.label)=\(formatMs($0.elapsedMs))" }
.joined(separator: ", ")
}
private func reportLine(title: String, summary: TimingSummary, slowest: String) -> String {
[
"\(title):",
"count=\(summary.count)",
"avg=\(formatMs(summary.averageMs))",
"median=\(formatMs(summary.medianMs))",
"p95=\(formatMs(summary.p95Ms))",
"max=\(formatMs(summary.maxMs))",
"total=\(formatMs(summary.totalMs))",
"slowest=[\(slowest)]"
].joined(separator: " ")
}
private func formatMs(_ value: Double) -> String {
String(format: "%.2fms", value)
}
private func label(for index: Int) -> String {
String(format: "%03d", index)
}
}