fix: deep split chains no longer freeze the main thread on first mount

Merged
TE
5d ago
+47-43

Summary

A deep split chain, such as an agent team's column of teammates, froze the main thread the first time it was shown or restored: about 28 s at depth 16 and about 2 minutes at depth 18 (#10311, #16393).

The cause is in Bonsplit: every split level nested another NSHostingView. SwiftUI's key-view-loop rebuild walks the focusable items of every nested host, and that walk roughly doubled with each level. manaflow-ai/bonsplit#284 hosts each pane once and builds nested splits as plain AppKit NSSplitViews, so pane hosts are siblings and the walk stays linear. The NSSplitView tree that cmux's portals inspect is unchanged.

This PR:

  • Bumps vendor/bonsplit from bd94ed1 to bc0d6dd, the bonsplit#284 commit only; #282 stays with #19593.
  • Fixes tests_v2/test_deep_split_chain_stays_responsive.py. The old version passed 5/5 while the main thread was still pinned for about 25 s, because it checked with ping, which is answered off the main actor. It now times pane.list, a main-actor request, after showing the 16-deep chain, after switching away and after switching back, and fails unless each answers within 1 s.

Credit to @Apptah: their diagnosis in manaflow-ai/cmux#10315 traced the hang to SwiftUI's key-view-loop focus walk over nested hosts, which is what this removes.

#19606's acceptsFirstResponder cache only trimmed about 10%, and bonsplit#284 removes it.

Testing

All runs were on capture minis, from fleet builds of this branch.

Regression test, recorded with the test commit before the bump:

BuildResultMain-actor stall after showing the chain
Main build 30d1f5aaRed, RESULT 3/427.92 s
Main-equivalent build e71060cc, in the suite runRed, RESULT 3/428.33 s
This head (1cb93c77)Green, RESULT 4/40.00 s

Depth scan, main-actor stall after the first show:

DepthBeforeThis head
121.1 s0.001 s
146.2 s0.001 s
1628–29 s0.001–0.007 s
18119 s0.009 s
24—0.02 s
32—0.02 s
40—0.26 s

Session restore: launching onto a saved session with an 18-deep and four 16-deep chains answers immediately, and each restored workspace shows without a stall. Main left the app unanswering at launch on the same session.

Existing split e2e tests, each in a fresh workspace, give identical results on the main-equivalent build and this head:

  • Pass on both:
    • focus_pane_by_number
    • nested_split_no_arranged_subview_underflow
    • nested_split_no_detach_during_update
    • nested_split_panel_routing
    • nested_split_preserves_existing_split
  • Fail the same way on both:
    • nested_split_does_not_disappear (no_space)
    • pane_break_swap_preserve_focus (rate_limited)
    • split_cmd_d_ctrl_d_geometry_fuzz (process_exited)
    • split_cmd_d_ctrl_d_two_pane_frame_guard (process_exited)
    • split_flash_and_layout (invalid_state)

Local checks: scripts/verify-local.py passes except swift-syntax on scripts/generate-computer-use-helper-icon.swift, which this PR doesn't touch. No review subagent was run.

Changelog

  • Fixed: Workspaces with deeply nested splits no longer freeze cmux when first shown or restored.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Tests
    • Expanded regression coverage for responsiveness in deeply nested workspaces. Checks now verify that pane lists respond promptly after the workspace is shown and when switching away and back, while confirming the expected pane count. This improves detection of delays during common workspace interactions.