Skip to content

Prevent idle AppKit sidebar layout livelock - #8532

Merged
austinywang merged 5 commits into
mainfrom
issue-8525-idle-high-cpu-crash
Jul 21, 2026
Merged

austinywang merged 5 commits into
mainfrom
issue-8525-idle-high-cpu-crash

Conversation

@austinywang

@austinywang austinywang commented Jul 20, 2026 •

Copy link
Copy Markdown
Contributor

Closes #8525

Root cause

v0.64.20 shipped the native AppKit workspace table from #8270 (b7bd90103c) and made it the fallback/default in #8433 (e35b74407a). That exposed the new bridge to users who had remained on the SwiftUI sidebar in v0.64.19.

SidebarWorkspaceTableView.updateNSView synchronously called the controller's apply. That path can reload/move rows and reconcile row heights. Those table mutations emit bounds/layout callbacks while the originating SwiftUI representable update is still resolving; the callbacks can synchronously perform more viewport and row-height work, producing a SwiftUI/AppKit layout transaction that does not converge while the app is otherwise idle.

Fix

  • Add one controller-owned SidebarWorkspaceTableMutationScheduler.
  • Stage immutable table snapshots and viewport signals instead of mutating NSTableView inside representable/layout callbacks.
  • Coalesce table applies to the latest snapshot and repeated viewport notifications to one signal.
  • Flush on the next common main-run-loop turn, after the originating callback returns.
  • Keep existing row diffing, height-cache, scrolling, hover, and drag behavior behind the shared mutation boundary.

This removes the reentrant mutation edge for the whole table controller instead of throttling one symptom.

Regression proof

The test and fix are separate commits:

  1. Test-only edd08429af fails because the table already has two rows before the callback boundary: https://github.com/manaflow-ai/cmux/actions/runs/29779690222
  2. Fix 6400ffaf51 introduces the deferred/coalesced mutation boundary.
  3. Final HEAD 10c6df9b48 has both scoped tests passing in a 12-test execution: https://github.com/manaflow-ai/cmux/actions/runs/29781424661

The final suite's two other failures are stale assertions already contradicted by current implementation: .fullWidth versus .plain, and an old-width cache value expected to be removed. Neither covers the changed scheduling path.

The exact tagged app build succeeded: https://github.com/manaflow-ai/cmux/actions/runs/29782552764. Its reload-build commit has final PR HEAD as its sole parent and only adds a provisioning log; the app debug dylib SHA-256 is c99674c11e6cbd7fe5b880adc68e167ccb50b233cdd77fccc140fa3c5f075fa4.

Runtime verification

Normal idle on macOS 26.5.1, 10 continuous minutes after settle:

  • CPU median 0.6%, p95 7.4%, max 18.9%; no sample reached 50%.
  • First/last two-minute CPU medians both 0.5%.
  • RSS first/last 60-second medians 267.750/265.719 MiB; slope -0.086 MiB/min.
  • Same PID/socket remained alive and the tagged CLI returned PONG.
  • Main thread was waiting 99.958% at minute 5 and 98.878% at minute 10; scheduler flush was 0.0721% of profiled cycles, with no reloadData or row-height samples.

Normal idle on macOS 15.7.4, 10 continuous minutes after settle:

  • CPU median 0.6%, p95 8.1%, max 28%; all >=5% spikes lasted one 2-second interval.
  • First/last two-minute CPU medians both 0.6%.
  • RSS median delta +14.078 MiB; slope +1.602 MiB/min.
  • Native table DEBUG event, process/socket liveness, and tagged CLI response were confirmed.

Restored scale on macOS 26.5.1:

  • Created 128 workspaces / 250 panes / 250 terminal surfaces, quit cleanly, and relaunched to a new PID; all 128/250/250 items restored within four seconds with one selected row and authoritative order preserved.
  • After a fixed 120-second settle, the restored PID idled 15 continuous minutes.
  • CPU median 1.0%, p95 10.4%; no sustained >=75% episode. First/second-half medians were 0.2%/0.3% using the independent process sampler.
  • RSS grew 36.469 MiB, within the 128 MiB gate; the final five minutes were flat (+0.156 MiB, 0.0133 MiB/min).
  • Minute-5 and minute-10 samples found the main thread in the normal AppKit wait 99.623%/99.381% of samples, with no reloadData, row-height, or layout-subtree frames.
  • Same restored PID/socket survived, CLI returned PONG, topology remained 128/250/250, and no crash/hang/DiagnosticReport or reentrant/layout-loop warning appeared.

Artifacts and raw metrics are under artifacts/issue-8525-idle-high-cpu-crash/ in the verification worktree.

Verification limitations

  • The macOS 15.7.4 restored-scale extension could not start on the separately leased slot: that account had no Aqua launchd domain (OSLaunchdErrorDomain 125), so the tagged app could not create a PID/socket, and its VNC endpoint only offered unsupported Apple-auth security modes. The successful macOS 15.7.4 normal 10-minute run remains valid; no scaled result is claimed for that OS.
  • Full-display capture on the macOS 26 host was blocked by Screen Recording denial, unavailable Accessibility/Computer Use, and a refused raw-VNC endpoint. A fresh macOS 26 cloud runner also failed to expose its Tailscale/VNC host within the bounded 900-second provisioning window: https://github.com/manaflow-ai/cmux-loader/actions/runs/29787107347. No TCC state was altered and no synthetic capture is claimed.
  • Runtime create/close/select/reorder behavior was exercised, and unit coverage checks deferred viewport/drag behavior. GUI scroll/resize/drop-indicator exercise was blocked with the same unavailable Accessibility/VNC paths and is not claimed.
  • The restored macOS 26 PID has minute-5/minute-10 samples; the 120-second Time Profiler trace was captured in the same 128/250/250 state before the clean restart, on the prior tagged PID. No same-restored-PID 120-second trace is claimed.
  • The reporter has not yet supplied their crash report or macOS version, so runtime verification covers macOS 15.7.4 and 26.5.1 but cannot yet match their exact environment.
  • sentry-cli is present locally but has no authenticated session.

Static and localization checks

  • Tagged Debug build passed with no warnings in the changed files.
  • pbxproj normalization, test wiring, workspace package grouping, Package.resolved policy, and git diff --check passed.
  • The Swift file-length budget guard was removed from this repository by Remove Swift file length budget #8125; the new Swift files are 11 and 63 lines, no tracked budget TSV exists, and no budget file changed.
  • No user-facing strings or localization keys changed; the localization audit found no new UI, Settings, menu, shortcut, schema, docs, alert, or tooltip copy.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The sidebar workspace table now defers and coalesces apply and viewport mutations through a main-actor scheduler. Tests verify deferred updates and that the newest apply snapshot is used.

Changes

Sidebar workspace table mutation scheduling

Layer / File(s) Summary
Scheduler contract and deferred flush
Sources/Sidebar/AppKitList/SidebarWorkspaceTableApplyInput.swift, Sources/Sidebar/AppKitList/SidebarWorkspaceTableMutationScheduler.swift
Adds immutable apply inputs and main-run-loop scheduling that coalesces apply and viewport mutations before flushing.
Controller apply and viewport integration
Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift
Stages table updates and moves reconciliation into deferred flushApply and flushViewportChange methods.
Build integration and coalescing validation
cmux.xcodeproj/project.pbxproj, cmuxTests/SidebarWorkspaceTableTests.swift
Adds the new files to the build and tests deferred application of the newest table snapshot and viewport-related behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Controller as SidebarWorkspaceTableController
  participant Scheduler as SidebarWorkspaceTableMutationScheduler
  participant RunLoop as RunLoop.main
  participant Table as NSTableView

  Controller->>Scheduler: stageApply(...)
  Scheduler->>RunLoop: schedule deferred flush
  Controller->>Scheduler: stageViewportChange()
  RunLoop->>Scheduler: flushPendingMutations()
  Scheduler->>Controller: flushApply(...)
  Controller->>Table: reconcile rows and table state
  Scheduler->>Controller: flushViewportChange()
  Controller->>Table: update viewport-dependent state
Loading

Possibly related issues

  • manaflow-ai/cmux#8224: Directly concerns the coalesced apply chokepoint and deferred AppKit table mutation strategy.
  • manaflow-ai/cmux#8263: Concerns deferred and coalesced sidebar table mutations related to recurring UI update work.
  • manaflow-ai/cmux#2487: Relates to sidebar CPU activity, although its described expensive row recomputation is not changed here.

Possibly related PRs

Suggested reviewers: azooz2003-bit

🚥 Pre-merge checks | ✅ 24 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (24 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement the idle high-CPU/crash fix for #8525 by deferring and coalescing sidebar AppKit mutations.
Out of Scope Changes check ✅ Passed All changes appear directly related to the sidebar livelock fix, with no obvious unrelated scope creep.
Cmux Swift Actor Isolation ✅ Passed PASS: New scheduler/controller are explicitly @MainActor, the value model stays on the main actor, and no new background-access or Sendable-sharing pattern appears.
Cmux Swift Blocking Runtime ✅ Passed The diff only adds allowed run-loop deferral for AppKit safety; no new blocking waits, sleeps, syncs, or locks appear in production code, and the flush helper is test-only.
Cmux Browser Automation Off-Main ✅ Passed PR only changes sidebar/AppKit table files, project wiring, and tests; no browser automation routing or WebKit wait code was touched.
Cmux Expensive Synchronous Load ✅ Passed Changed Swift code only stages/flushes AppKit table mutations via RunLoop.main; no agent-history load, JSON/transcript, or other expensive sync I/O was added.
Cmux Cache Substitution Correctness ✅ Passed The PR only adds a run-loop scheduler for deferred UI table mutations; no fresh authoritative read was replaced by a cache in any persistence/history/undo/snapshot path.
Cmux No Hacky Sleeps ✅ Passed PASS: The PR only changes Swift/test/Xcodeproj files; no TS/JS/shell/runtime-script sleeps or fixed delays were added. The only wait-like code is deterministic test RunLoop flushing.
Cmux Algorithmic Complexity ✅ Passed Fix only adds O(1) staging/coalescing; existing O(n) row diffing/height work is unchanged, with no new nested scans or repeated sorts.
Cmux Swift Concurrency ✅ Passed No new disallowed legacy async patterns were introduced; the deferred RunLoop callback is a UI boundary, and the new continuation is test-only.
Cmux Swift @Concurrent ✅ Passed No new @concurrent or nonisolated async violations; the added async test helper and scheduler stay MainActor/UI-bound with explicit main-run-loop hops.
Cmux Swift Package Boundaries ✅ Passed The new scheduler/input stay in the app’s AppKit bridge (NSViewRepresentable/NSTableView) and are UI glue, not reusable domain logic needing a SwiftPM package.
Cmux Swiftpm Lockfiles ✅ Passed HEAD only changes a test file; no .gitignore, Package.swift, project.pbxproj, or Package.resolved diffs appear, so the lockfile rule isn’t implicated.
Cmux Swift Logging ✅ Passed The only file changed vs parent is a DEBUG test; no added print/debugPrint/dump/NSLog/Logger calls or sensitive logging appear in the diff.
Cmux User-Facing Error Privacy ✅ Passed PASS: The PR only adds internal scheduler/input code and DEBUG tests; no user-facing errors, alerts, recovery copy, or upstream/vendor details were introduced.
Cmux Full Internationalization ✅ Passed The diff only adds AppKit scheduling logic, project wiring, and DEBUG tests; no new user-facing text or localization assets were introduced.
Cmux Swiftui State Layout ✅ Passed Diff only adds an AppKit bridge scheduler and value snapshot; no ObservableObject/@published, GeometryReader, lazy-list store refs, or render-time state writes are introduced.
Cmux Architecture Rethink ✅ Passed Controller stays the single MainActor owner; the new scheduler only stages immutable snapshots and defers AppKit mutations to the next run-loop turn for bridge safety.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Only cmuxTests/SidebarWorkspaceTableTests.swift changed, and its NSWindow is a test-only fixture; no cmux-owned window/controller or identifier wiring changed.
Cmux Source Artifacts ✅ Passed Only changed path is a handwritten Swift test file; no logs, caches, DerivedData, screenshots, or other artifact paths were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed Parent→HEAD diff only changes cmuxTests; no production Sources diff adds a test/debug seam.
Cmux No Ambient Global State ✅ Passed The new scheduler/input are instance-scoped types owned by SidebarWorkspaceTableController; no new file-scope funcs, mutable vars, or singletons were added.
Title check ✅ Passed The title is concise and accurately summarizes the main change: preventing idle AppKit sidebar layout livelock.
Description check ✅ Passed It explains the fix, root cause, testing, and verification in detail, with only the template’s demo video, review trigger, and checklist sections omitted.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-8525-idle-high-cpu-crash

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.

@greptile-apps

greptile-apps Bot commented Jul 20, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes an idle CPU livelock caused by SidebarWorkspaceTableView.updateNSView synchronously calling apply on the controller, which mutated NSTableView (triggering bounds notifications) while the originating SwiftUI/AppKit layout transaction was still on the call stack, producing unbounded reentrant relayout. The fix introduces SidebarWorkspaceTableMutationScheduler, which stages both apply inputs and viewport signals, coalesces repeated calls to the latest snapshot, and flushes them on the next main run-loop turn after the originating callback has returned.

  • SidebarWorkspaceTableMutationScheduler (@MainActor final class, 63 lines): holds one pendingApply and one viewport-change flag; uses RunLoop.main.perform(inModes: [.common]) with MainActor.assumeIsolated for deferred delivery. State is cleared before invoking callbacks so any mutation re-staged during a flush correctly schedules a new cycle.
  • SidebarWorkspaceTableApplyInput: immutable @MainActor struct capturing all five apply parameters for safe staging without aliasing.
  • Tests: new tableApplyCoalescesAndMutatesOnlyAfterTheCurrentCallbackReturns asserts zero row count before flush and the latest (second) snapshot after; dropTargetGeometryIsIdleDuringScrollAndTracksDragLifecycle updated to async with flush checkpoints at the correct lifecycle boundaries.

Confidence Score: 5/5

Safe to merge — the fix surgically removes the reentrant mutation boundary without changing any observable behavior for row diffing, height reconciliation, scrolling, hover, or drag.

The scheduler is small and well-bounded: it holds at most one staged apply and one viewport flag, clears state before calling callbacks so re-staged mutations during a flush correctly enqueue a new cycle, and uses RunLoop.main.perform(inModes: [.common]) — the AppKit-native deferred scheduling mechanism — rather than a sleep or timer. Actor isolation is correct throughout. Existing tests were correctly updated to await the new deferred boundary, and the new coalescing test covers both the zero-row-before-flush and latest-snapshot-after-flush invariants. No new debug seams were added to production source.

No files require special attention.

Important Files Changed

Filename Overview
Sources/Sidebar/AppKitList/SidebarWorkspaceTableMutationScheduler.swift New scheduler that coalesces table applies and viewport signals, flushing on the next main run-loop turn via RunLoop.main.perform(inModes: [.common]). Actor isolation is correct throughout; state is cleared before callbacks fire so re-staged mutations during a flush correctly schedule a new cycle.
Sources/Sidebar/AppKitList/SidebarWorkspaceTableApplyInput.swift Immutable @mainactor value type capturing all five apply parameters so they can be staged safely without reference aliasing.
Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift apply() now stages to mutationScheduler instead of mutating NSTableView synchronously; flushApply and flushViewportChange are private helpers called only by the scheduler. No new debug/test seams introduced; all existing #if DEBUG probes are pre-existing and unchanged.
cmuxTests/SidebarWorkspaceTableTests.swift Adds a new #if DEBUG coalescing test and a shared flushStagedTableMutations helper (test-target only); updates the drag/scroll test to be async and insert flush checkpoints at the right lifecycle moments. flushStagedTableMutations correctly enqueues behind the scheduler's pending block.
cmux.xcodeproj/project.pbxproj Adds both new Swift files to the app target's Sources build phase and the AppKitList file group. No package-reference or dependency changes; Package.resolved is unaffected.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant SwiftUI as SwiftUI updateNSView
    participant Controller as SidebarWorkspaceTableController
    participant Scheduler as SidebarWorkspaceTableMutationScheduler
    participant RunLoop as RunLoop.main
    participant Table as NSTableView

    SwiftUI->>Controller: apply(rows:actions:...)
    Controller->>Scheduler: stageApply(input)
    Scheduler->>Scheduler: "pendingApply = input"
    Scheduler->>RunLoop: perform inModes common
    SwiftUI->>Controller: apply(rows:actions:...) coalesced
    Controller->>Scheduler: stageApply(input2)
    Scheduler->>Scheduler: "pendingApply = input2"
    Note over SwiftUI,Scheduler: originating callback returns
    RunLoop->>Scheduler: flushPendingMutations
    Scheduler->>Scheduler: capture and clear state
    Scheduler->>Controller: flushApply(input2)
    Controller->>Table: reloadData or moveRow
    Table-->>Controller: boundsDidChange
    Controller->>Scheduler: stageViewportChange
    Scheduler->>RunLoop: new perform cycle
    Scheduler->>Controller: flushViewportChange
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant SwiftUI as SwiftUI updateNSView
    participant Controller as SidebarWorkspaceTableController
    participant Scheduler as SidebarWorkspaceTableMutationScheduler
    participant RunLoop as RunLoop.main
    participant Table as NSTableView

    SwiftUI->>Controller: apply(rows:actions:...)
    Controller->>Scheduler: stageApply(input)
    Scheduler->>Scheduler: "pendingApply = input"
    Scheduler->>RunLoop: perform inModes common
    SwiftUI->>Controller: apply(rows:actions:...) coalesced
    Controller->>Scheduler: stageApply(input2)
    Scheduler->>Scheduler: "pendingApply = input2"
    Note over SwiftUI,Scheduler: originating callback returns
    RunLoop->>Scheduler: flushPendingMutations
    Scheduler->>Scheduler: capture and clear state
    Scheduler->>Controller: flushApply(input2)
    Controller->>Table: reloadData or moveRow
    Table-->>Controller: boundsDidChange
    Controller->>Scheduler: stageViewportChange
    Scheduler->>RunLoop: new perform cycle
    Scheduler->>Controller: flushViewportChange
Loading

Reviews (2): Last reviewed commit: "test: await sidebar mutation callback bo..." | Re-trigger Greptile

Comment thread cmuxTests/SidebarWorkspaceTableTests.swift Outdated

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/SidebarWorkspaceTableTests.swift`:
- Around line 341-344: Replace the fixed 0.02-second deadline in
flushStagedTableMutations with a deterministic completion signal or explicitly
controlled test scheduler. Ensure the method drains staged mutations only after
the deferred callback has completed, without relying on real wall-clock timing
or scheduler load.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: efab10e4-94dd-49b4-b09b-846f8e3ef4ec

📥 Commits

Reviewing files that changed from the base of the PR and between 6400ffa and bafa190.

📒 Files selected for processing (4)
  • Sources/Sidebar/AppKitList/SidebarWorkspaceTableApplyInput.swift
  • Sources/Sidebar/AppKitList/SidebarWorkspaceTableMutationScheduler.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/SidebarWorkspaceTableTests.swift
💤 Files with no reviewable changes (1)
  • Sources/Sidebar/AppKitList/SidebarWorkspaceTableMutationScheduler.swift

Comment thread cmuxTests/SidebarWorkspaceTableTests.swift
@austinywang
austinywang merged commit 95e334c into main Jul 21, 2026
12 checks passed
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.

High CPU while idle then crash within 1-5 minutes after updating to 0.64.20 (also on NIGHTLY)

1 participant