Keep one split-tree observation armed per root

AU
18d ago
+157-1

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:

  • SplitTreeObservationGate arms 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.
  • renderTree re-arms as soon as the live registration fires.

Test: SplitTreeObservationTests counts the tree renders that one observed change triggers after 25 SwiftUI updates.

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