Skip to content

Add workspace stress profiling and reduce switch churn - #1218

Merged
lawrencecchen merged 2 commits into
mainfrom
task-workspace-stress-profile-switching-lag
Mar 13, 2026
Merged

lawrencecchen merged 2 commits into
mainfrom
task-workspace-stress-profile-switching-lag

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Mar 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • add a workspace stress profile unit test that creates many workspaces, fills each with terminal surfaces, and reports create/populate/switch timings
  • instrument direct workspace selection so socket-driven select-workspace runs show real switch IDs in debug logs
  • reduce sidebar row invalidation on workspace count changes and tighten workspace handoff cleanup checks during rapid switching

Testing

  • ssh cmux-macmini 'cd /Users/cmux/fun/cmuxterm-hq/worktrees/task-workspace-stress-profile-switching-lag && xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -destination "platform=macOS" -derivedDataPath /tmp/cmux-workspace-stress-profile-fix6 test -only-testing:cmuxTests/WorkspaceStressProfileTests/testWorkspaceCreationAndSwitchingStressProfile -quiet'
  • ssh cmux-macmini runtime benchmark with tagged debug app and CMUX_DISABLE_SESSION_RESTORE=1: 20 workspaces, 10 surfaces each
  • runtime create benchmark after the fix: createMs stayed around 190-210ms, explicit selectMs around 240-360ms, and populateMs rose from about 1.4s to about 2.1s
  • runtime switch profile from /tmp/cmux-debug-wsrtfix7.log: ws.view.selectedChange avg 11.63ms, p95 13.48ms; ws.handoff.complete avg 94.95ms, p95 102.52ms, max 122.76ms

Task

  • Task: write a stress test that creates many workspaces, profiles creation, fills each with many bonsplit tabs, switches them rapidly, and re-profile the hotspots on cmux-macmini

Summary by cubic

Adds a workspace stress profiling test and speeds up workspace switching with a fast-ready handoff and less sidebar churn. Improves debug logs for consistent switch timing and IDs.

  • New Features

    • Added WorkspaceStressProfileTests to create many workspaces, populate terminal surfaces, and time create/populate/switch (dispatch/drain1/unfocus/drain2); prints a report and attaches it.
    • Configurable via env vars for counts and optional p95 budgets; asserts on budgets when set.
  • Refactors

    • Faster handoff: added a ready-check loop that completes as soon as the target workspace has a loaded terminal or focused browser; cancels fallback/ready tasks on changes; logs ws.handoff.fastReady.
    • Reduced sidebar invalidation by precomputing and passing workspaceShortcutDigit, canCloseWorkspace, and accessibilityWorkspaceCount into TabItemView with tighter Equatable keys; close button and accessibility title now use these values.
    • Unified switch instrumentation in TabManager: prime triggers for “create”, “select”, “focus”, “select_index”, and prepare logs for “next/prev”; selectedTabId willSet ensures ws.switch.begin has consistent IDs, triggers, and start times across all paths.

Written for commit 4ef62db. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Workspace handoff ready-check to improve transition reliability
  • Improvements

    • Enhanced tab controls with per-workspace shortcut digits and close-button logic
    • Accessibility updates for workspace counts and tab announcements
    • Structured debug tracing for workspace switching (DEBUG builds)
  • Tests

    • Added end-to-end workspace stress profiling tests with timing reports

@vercel

vercel Bot commented Mar 12, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Mar 13, 2026 11:44am

@coderabbitai

coderabbitai Bot commented Mar 12, 2026 •

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 40173d80-d6f3-4ea4-b740-5323cf2e48df

📥 Commits

Reviewing files that changed from the base of the PR and between 976fcee and 4ef62db.

📒 Files selected for processing (1)
  • GhosttyTabs.xcodeproj/project.pbxproj

📝 Walkthrough

Walkthrough

Adds a workspace handoff ready-check task and refactors tab UI payloads in ContentView; introduces DEBUG-only staged workspace-switch tracing in TabManager; adds a new end-to-end workspace stress test and wires it into the Xcode project.

Changes

Cohort / File(s) Summary
Xcode project wiring
GhosttyTabs.xcodeproj/project.pbxproj
Added PBX entries to include cmuxTests/WorkspaceStressProfileTests.swift in project and test target build phases.
UI / Handoff
Sources/ContentView.swift
Added workspaceHandoffReadyCheckTask, canCompleteWorkspaceHandoffImmediately(for:), integrated ready-check task into workspace handoff flow; refactored TabItemView to replace tabCount with workspaceShortcutDigit, canCloseWorkspace, and accessibilityWorkspaceCount; updated VerticalTabsSidebar wiring and equality logic.
Debug tracing
Sources/TabManager.swift
Added DEBUG-only staged tracing state and helpers (debugPrimeWorkspaceSwitchTrigger, debugPrepareWorkspaceSwitch, debugBeginWorkspaceSwitch) and inserted priming calls in workspace selection/focus flows under #if DEBUG.
Stress tests
cmuxTests/WorkspaceStressProfileTests.swift
New end-to-end stress profiling test: WorkspaceStressProfileTests, StressConfig, timing helpers (TimedSample, TimingSummary), workload population, multi-pass tab switching, p95 budget checks, and test artifact attachment.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 I hopped through the handoff, ears on alert,

digits and close-flags now snug in their shirt,
DEBUG traces lined up in a neat little row,
stress tests keep timing the hop, to and fro —
carrot charts attached, and the report’s set to go. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main objectives: adding a workspace stress profiling test and reducing workspace switching churn through optimization.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering all key template sections: summary of changes, testing methodology with specific benchmarks, and a detailed checklist.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch task-workspace-stress-profile-switching-lag
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 4 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="cmuxTests/WorkspaceStressProfileTests.swift">

<violation number="1" location="cmuxTests/WorkspaceStressProfileTests.swift:206">
P1: A failed surface creation can cause an infinite loop in `populate`, because the loop condition never changes after `XCTAssertNotNil` fails.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread cmuxTests/WorkspaceStressProfileTests.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/ContentView.swift (1)

9492-9501: ⚠️ Potential issue | 🟡 Minor

Include accessibilityWorkspaceCount in TabItemView.==.

Line 9515 now feeds Line 10241, but Lines 9492-9501 never compare it. With .equatable() on each row, adding/removing a workspace can leave VoiceOver announcing a stale “workspace x of y” count when the other compared fields stay unchanged.

Proposed fix
     nonisolated static func == (lhs: TabItemView, rhs: TabItemView) -> Bool {
         lhs.tab === rhs.tab &&
         lhs.index == rhs.index &&
         lhs.isActive == rhs.isActive &&
         lhs.workspaceShortcutDigit == rhs.workspaceShortcutDigit &&
         lhs.canCloseWorkspace == rhs.canCloseWorkspace &&
+        lhs.accessibilityWorkspaceCount == rhs.accessibilityWorkspaceCount &&
         lhs.unreadCount == rhs.unreadCount &&
         lhs.latestNotificationText == rhs.latestNotificationText &&
         lhs.rowSpacing == rhs.rowSpacing &&
         lhs.showsModifierShortcutHints == rhs.showsModifierShortcutHints
     }
Based on learnings: In `ContentView.swift` TabItemView, do not add `EnvironmentObject`, `ObservedObject` (besides `tab`), or `Binding` properties without updating the `==` function and keeping `.equatable()` on ForEach.

Also applies to: 9513-9515, 10240-10242

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ContentView.swift` around lines 9492 - 9501, The equality operator
for TabItemView is missing a comparison for accessibilityWorkspaceCount which
causes VoiceOver to keep stale "workspace x of y" announcements when rows are
Equatable; update nonisolated static func == (lhs: TabItemView, rhs:
TabItemView) to include lhs.accessibilityWorkspaceCount ==
rhs.accessibilityWorkspaceCount alongside the other field comparisons, and
mirror the same addition wherever TabItemView equality is implemented/used
(referenced ranges around the existing == implementation and the other noted
locations corresponding to lines 9513-9515 and 10240-10242) so .equatable() on
the ForEach correctly detects workspace count changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/TabManager.swift`:
- Around line 2071-2076: Avoid emitting a fake "prepare" switch when the cycle
is a no-op: check whether the target id equals the current id before calling
debugPrepareWorkspaceSwitch (e.g., in the blocks around
activateWorkspaceCycleHotWindow and where selectedTabId is set). If
nextId/prevId == currentId, skip calling debugPrepareWorkspaceSwitch (and any
related begin switch logging) so that selectedTabId remains unchanged and no
stale debug switch timing is recorded. Apply the same guard to the other similar
blocks referenced (around lines with prevId/nextId and the range 2168-2184).

---

Outside diff comments:
In `@Sources/ContentView.swift`:
- Around line 9492-9501: The equality operator for TabItemView is missing a
comparison for accessibilityWorkspaceCount which causes VoiceOver to keep stale
"workspace x of y" announcements when rows are Equatable; update nonisolated
static func == (lhs: TabItemView, rhs: TabItemView) to include
lhs.accessibilityWorkspaceCount == rhs.accessibilityWorkspaceCount alongside the
other field comparisons, and mirror the same addition wherever TabItemView
equality is implemented/used (referenced ranges around the existing ==
implementation and the other noted locations corresponding to lines 9513-9515
and 10240-10242) so .equatable() on the ForEach correctly detects workspace
count changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1708cf2d-86ca-424c-9b7e-710f54c9a28a

📥 Commits

Reviewing files that changed from the base of the PR and between 6272f50 and 976fcee.

📒 Files selected for processing (4)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/ContentView.swift
  • Sources/TabManager.swift
  • cmuxTests/WorkspaceStressProfileTests.swift

Comment thread Sources/TabManager.swift
@greptile-apps

greptile-apps Bot commented Mar 12, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a workspace stress-profiling unit test, instruments debug workspace-switch logging with real trigger IDs for socket-driven select-workspace calls, and reduces TabItemView re-render churn by pre-computing workspaceShortcutDigit/canCloseWorkspace in the parent instead of invalidating all sidebar rows on every workspace count change. It also introduces a workspaceHandoffReadyCheckTask that eagerly completes the workspace handoff at 0/20/40/60 ms checkpoints before the existing 150 ms fallback fires.

  • accessibilityWorkspaceCount missing from TabItemView's Equatable == — the VoiceOver label ("workspace N of X") will be stale after workspace additions/removals because the equality check no longer reflects the total count (the old tabCount field covered this). This is a regression for VoiceOver users.
  • workspaceHandoffReadyCheckTask loop doesn't break after completing the handoff — relies on task cancellation to exit later iterations. Functionally correct (cancellation throws in Task.sleep), but an explicit break/return would be clearer.
  • The stress test defaults (48 workspaces × 10 terminals × 6 switch passes) are intentionally heavy; the test is run with an explicit -only-testing: filter and won't appear in standard CI runs.
  • TabManager debug instrumentation is cleanly #if DEBUG-gated and consolidates duplicated counter/timer boilerplate into debugBeginWorkspaceSwitch.

Confidence Score: 3/5

  • Safe to merge with minor risk — one accessibility regression should be fixed before shipping to VoiceOver users.
  • The handoff task logic, debug instrumentation, and stress test are all sound. The only correctness issue is that accessibilityWorkspaceCount was intentionally removed from the Equatable comparison as part of the invalidation-reduction refactor, but this causes the VoiceOver positional label to go stale after workspace count changes — a direct regression from the previous tabCount-based equality.
  • Sources/ContentView.swift — specifically the TabItemView == operator (line 9492) and the workspaceHandoffReadyCheckTask loop (line 2894).

Important Files Changed

Filename Overview
Sources/ContentView.swift Adds workspaceHandoffReadyCheckTask for eager handoff completion (up to 60ms before the 150ms fallback). Refactors TabItemView to accept pre-computed workspaceShortcutDigit/canCloseWorkspace instead of tabCount to reduce row invalidation churn. Bug: accessibilityWorkspaceCount is excluded from the Equatable == operator, causing stale VoiceOver labels when workspace count changes.
Sources/TabManager.swift DEBUG-only instrumentation to capture real switch trigger IDs (direct/select/create/focus/next/prev) via a willSet observer and priming helpers. Consolidates duplicated counter/timer inline code into debugBeginWorkspaceSwitch. No production logic changes.
cmuxTests/WorkspaceStressProfileTests.swift New profiling test that creates configurable workspaces (default 48) filled with terminal surfaces (default 10 each) and measures create/populate/switch timings. Well-structured with percentile stats, XCTAttachment reporting, and optional budget assertions via env vars. drainMainQueue spin loop is sound for a synchronous @mainactor test.
GhosttyTabs.xcodeproj/project.pbxproj Registers WorkspaceStressProfileTests.swift in the test target with a new PBX file reference and build file entry. Mechanical Xcode project change.

Sequence Diagram

sequenceDiagram
    participant Caller as Caller (socket / UI)
    participant TM as TabManager
    participant CV as ContentView (MainActor)
    participant RCT as workspaceHandoffReadyCheckTask
    participant FBT as workspaceHandoffFallbackTask

    Caller->>TM: selectWorkspace(_:) / selectNextTab() / selectPreviousTab()
    TM->>TM: debugPrimeWorkspaceSwitchTrigger / debugPrepareWorkspaceSwitch
    TM->>TM: selectedTabId = newId  (willSet → debugBeginWorkspaceSwitch)
    TM-->>CV: @Published selectedTabId change

    CV->>CV: startWorkspaceHandoffIfNeeded(newSelectedId:)
    CV->>CV: workspaceHandoffGeneration &+= 1
    CV->>CV: cancel previous RCT & FBT

    CV->>RCT: Task { for delay in [0, 20ms, 40ms, 60ms] }
    CV->>FBT: Task { sleep(150ms) }

    loop Each delay checkpoint
        RCT->>CV: await MainActor.run { canCompleteWorkspaceHandoffImmediately? }
        alt Surface loaded / browser panel focused
            CV->>CV: completeWorkspaceHandoff(reason: "ready")
            CV->>RCT: cancel (task exits via CancellationError on next sleep)
            CV->>FBT: cancel
        else Not ready yet
            RCT->>RCT: sleep next delay
        end
    end

    alt Fallback fires (150ms, surface still not ready)
        FBT->>CV: await MainActor.run { completeWorkspaceHandoff(reason: "timeout") }
    end
Loading

Comments Outside Diff (1)

  1. Sources/ContentView.swift, line 9492-9502 (link)

    accessibilityWorkspaceCount omitted from Equatable conformance

    accessibilityWorkspaceCount is passed as a property but is not included in the custom == operator. Because SwiftUI uses this equality check to skip body re-evaluation (see the // PERF: comment above), adding or removing a workspace will not trigger a re-render for existing rows — meaning the VoiceOver label "\(tab.title), workspace \(index + 1) of \(accessibilityWorkspaceCount)" will be stale until some other property causes a redraw.

    In the old implementation tabCount was in ==, so VoiceOver was always correct. The refactor intentionally avoids re-rendering all rows on every count change, but accessibilityWorkspaceCount should still be included so VoiceOver users get accurate positional information.

    Note: accessibilityWorkspaceCount only changes when workspaceCount crosses the 9-workspace shortcut boundary, so this will still be far less invalidation than the old tabCount comparison, while keeping VoiceOver correct.

Last reviewed commit: 976fcee

Comment thread Sources/ContentView.swift

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 976fcee32b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cmuxTests/WorkspaceStressProfileTests.swift Outdated
Comment thread Sources/ContentView.swift
@lawrencecchen
lawrencecchen force-pushed the task-workspace-stress-profile-switching-lag branch from 02d6644 to 4ef62db Compare March 13, 2026 11:43
@lawrencecchen
lawrencecchen merged commit f95a32e into main Mar 13, 2026
10 of 11 checks passed
@lawrencecchen
lawrencecchen deleted the task-workspace-stress-profile-switching-lag branch March 13, 2026 11:44

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4ef62dbe31

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/ContentView.swift
dlog("ws.handoff.fastReady id=none selected=\(debugShortWorkspaceId(newSelectedId))")
}
#endif
completeWorkspaceHandoff(reason: "ready")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid completing handoff before pending unfocus is set

This ready-path completion can fire on the first poll when switching to an already-loaded workspace, but TabManager only creates pendingWorkspaceUnfocusTarget in the async selectedTabId.didSet side-effects (DispatchQueue.main.async → focusSelectedTabPanel). If this runs first, completePendingWorkspaceUnfocus no-ops, fallback is canceled, and later focus notifications skip completion because retiringWorkspaceId is already cleared, leaving the previous workspace unfocus deferred until a later switch. The race is most visible during rapid workspace switches where the target workspace is already warm.

Useful? React with 👍 / 👎.

This branch was successfully deployed

1 active deployment
Preview — 4ef62dbe Deployed Mar 13, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant