Skip to content

Sidebar causes 65-101% CPU with many workspaces/panes; hiding sidebar drops to 4-12% #2487

Description

@mohamorui

cmux version

0.63.1

macOS version

macOS 26.4 (Tahoe)

Mac chip

Apple Silicon (M1/M2/M3/M4)

Installation method

Direct download (DMG)

Can you reproduce this on cmux NIGHTLY?

I could not test NIGHTLY

Bug description

cmux consumes 65–101% CPU continuously when the sidebar is visible in a session with many workspaces and panes. Hiding the sidebar immediately drops CPU to 4–12%. The high CPU usage causes the machine to heat up significantly and WindowServer also stays elevated at 40–60%.

The issue appears to be caused by sidebar row recomputation: every metadata/status/branch/PR/directory update triggers a full recomputation of all workspace rows, including expensive panel ordering and branch-directory aggregation, even when the update only affects a narrow section of the sidebar.

Expected behavior

With the sidebar visible, cmux CPU usage should remain low during normal operation. Metadata and status updates from shell integration should not cause continuous high CPU.

Steps to reproduce

  1. Open cmux with a session containing many workspaces and panes (my session: 1 window, 10 workspaces, 35 panes, 8 status entries, 8 Yazi panes, several dynamic titles from Claude Code)
  2. Keep the sidebar visible with branch-directory and PR display enabled (default settings)
  3. Observe CPU usage via Activity Monitor — cmux stays at 65–101%, WindowServer at 40–60%
  4. Hide the sidebar (toggle sidebar visibility)
  5. Observe CPU immediately drops to 4–12%
  6. Show sidebar again — CPU spikes back up

Shell and environment

zsh. Multiple panes running Yazi (file manager) and Claude Code (which emits frequent set_status, report_pr, report_pwd shell integration updates).

Relevant logs or crash reports

**Pre-hide `sample` output** (5s, sidebar visible):
Hot leaf frames are dominated by sidebar-specific symbols:
- `Workspace.sidebarBranchDirectoryEntriesInDisplayOrder(...)`
- `Workspace.sidebarOrderedPanelIds()`
- `Workspace.sidebarHomeDirectoryForCanonicalization(...)`

**Post-hide `sample` output** (5s, sidebar hidden):
These sidebar symbols no longer appear in the hot path.

**Yazi is not the direct cause**: sampled Yazi processes show 0.0% CPU, mostly sleeping in `pthread_cond_wait` and `kevent`. Yazi increases sidebar complexity (more panes/branches to aggregate) but is not actively consuming CPU.

Screenshots or screen recordings

No response

Additional context

Root-cause analysis

I investigated the source code and identified the following chain:

  1. Shell integration emits updates (set_status, report_pr, report_pwd, report_shell_state)
  2. These mutate Workspace @Published fields (statusEntries, panelPullRequests, panelDirectories, etc.)
  3. Workspace.sidebarObservationPublisher (Workspace.swift:5604) merges ~19 signals into a single invalidation stream
  4. TabItemView subscribes to this merged publisher and bumps workspaceObservationGeneration on each coalesced event (ContentView.swift:11793)
  5. TabItemView.body reads workspaceObservationGeneration at the top (ContentView.swift:11445), forcing full body re-evaluation
  6. The body then recomputes sidebarOrderedPanelIds() (bonsplit tree traversal), branch-directory aggregation, and PR display for every workspace row
  7. The existing 40ms debounce (ContentView.swift:11166) is not sufficient because each debounced update still triggers the full expensive recomputation

Suggested fix direction

The cheapest structural fix would be to split the sidebar row into two view groups:

  • Parent TabItemView: keeps lightweight sections (title, pin, color, remote, metadata, log, progress, ports) — subscribes to a narrowed publisher with $title, $isPinned, $customColor, $remoteConfiguration, $remoteConnectionState, $remoteConnectionDetail, $activeRemoteTerminalSessionCount, $statusEntries, $metadataBlocks, $logEntries, $progress, $listeningPorts
  • Child view: branch-directory + PR sections — subscribes to a separate narrowed publisher with $panels, $panelDirectories, $currentDirectory, $panelGitBranches, $gitBranch, $panelPullRequests, $remoteConfiguration

This way, a metadata/status update only re-evaluates the parent (cheap), without triggering the expensive panel ordering and branch-directory aggregation in the child view.

Key implementation notes:

  • Publisher split alone is not enough — the view must also be split, because the current single workspaceObservationGeneration counter forces full body recomputation regardless of which signal fired
  • $panels only needs to route to the child view (no panel count display exists in the basic row)
  • $pullRequest (singular) can be excluded from both publishers — fallbackPullRequest is hardcoded to nil in sidebarPullRequestsInDisplayOrder
  • PR display also depends on $panelGitBranches for branch-matching filter
  • Branch-directory canonicalization depends on $remoteConfiguration via isRemoteWorkspace

Hardware note: The chip dropdown only goes up to M4; this machine is an Apple M5.

I have a detailed investigation write-up with a full dependency matrix and verification plan available if it would be helpful to the maintainers.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions