Keep one split-tree observation armed per root
Keeps the split root at one live withObservationTracking registration.
updateTrackingNestedSplits armed a new registration on every updateNSView. These registrations are one-shot and can't be cancelled, so the previous one stayed registered until its properties changed. Over a long session, the controller and split-state registrars grew without bound. Each tracked change then re-rendered the whole tree once per leftover registration.
cmux field hangs show the main thread inside these registrations: the largest cmux macOS hang group, https://github.com/manaflow-ai/cmux/issues/22703. That group has about 31k events from about 4.5k devices on 0.65.1ā0.65.2.
How the fix works:
SplitTreeObservationGatearms a registration only when none is live for the current controller and state.- A registration for a superseded scope is retired by generation, so a late fire from it does nothing.
renderTreere-arms as soon as the live registration fires.
Test: SplitTreeObservationTests counts the tree renders that one observed change triggers after 25 SwiftUI updates.
- Without the fix, at test commit
83a0903: 26 renders for the first change and 53 for the second, where 1 and 2 are expected. https://github.com/manaflow-ai/bonsplit/actions/runs/39540663317 - With the fix, at
aed1a61: passes. https://github.com/manaflow-ai/bonsplit/actions/runs/39540846201
The test suspends so the root's queued main-queue re-render can run before the counts are read. A @MainActor test can't drain those blocks with a nested run loop.
Stacked on https://github.com/manaflow-ai/bonsplit/pull/287 (fix/tab-chrome-render-work). The cmux main submodule pointer already references that PR's head d67d1b1, which isn't on Bonsplit main yet. Merge https://github.com/manaflow-ai/bonsplit/pull/287 first, then this PR.
š¤ Generated with Claude Code