From 832a3f29d03847128bf5d3edb2fe747d6d667389 Mon Sep 17 00:00:00 2001 From: arzafran Date: Fri, 14 Aug 2026 10:57:03 -0300 Subject: [PATCH 1/2] perf: cut portal churn residue that leaks into typing latency Workspace churn (the lag-gate scenario) closes panes and cycles workspaces, then types immediately. Two portal behaviors added latency residue inside that window: - removePaneChrome broadcast its reassert request app-wide (object: nil), so every anchor in every window republished on every pane close. The request is now scoped to the closing registry's window; nil stays a broadcast for compatibility. The dismantle-race survivor it exists for is always in the same window. - updateClusters re-added the control clusters on every coalesced geometry pass; re-adding an existing subview removes and re-inserts it, dirtying layout every runloop turn during animation storms. Clusters now reorder only when a pane bar actually sits above them. --- Sources/WindowPaneChromePortal.swift | 32 ++++++++++++++++--- .../Internal/Views/PaneContainerView.swift | 9 ++++-- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/Sources/WindowPaneChromePortal.swift b/Sources/WindowPaneChromePortal.swift index b401669d..efca0dec 100644 --- a/Sources/WindowPaneChromePortal.swift +++ b/Sources/WindowPaneChromePortal.swift @@ -122,7 +122,7 @@ final class WindowPaneChromePortalRegistry: NSObject, BonsplitPaneChromePortalBr newTabCluster.setActions([active.onNewTab, active.onNewBrowserTab]) newTabCluster.isHidden = false - hostView.addSubview(newTabCluster) // keep above pane bars + ensureAboveBars(newTabCluster) newTabCluster.frame = NSRect( x: hostView.bounds.maxX - newTabCluster.preferredWidth - 8, y: y, @@ -148,7 +148,7 @@ final class WindowPaneChromePortalRegistry: NSObject, BonsplitPaneChromePortalBr ) ) splitCluster.isHidden = false - hostView.addSubview(splitCluster) + ensureAboveBars(splitCluster) splitCluster.frame = NSRect( x: hostView.bounds.maxX - splitCluster.preferredWidth - 8, y: y, @@ -160,6 +160,26 @@ final class WindowPaneChromePortalRegistry: NSObject, BonsplitPaneChromePortalBr } } + /// Re-adding an already-parented subview removes and re-inserts it, which + /// dirties layout — and updateClusters runs on every coalesced geometry + /// pass during animation storms. Reorder only when a pane bar (added on + /// top by updatePaneChrome) actually sits above the cluster. + private func ensureAboveBars(_ cluster: NSView) { + guard cluster.superview === hostView else { + hostView.addSubview(cluster) + return + } + let subviews = hostView.subviews + guard let clusterIndex = subviews.firstIndex(of: cluster) else { return } + let topBarIndex = subviews.enumerated() + .filter { $0.element is NativePaneTabBarView } + .map(\.offset) + .max() + if let topBarIndex, clusterIndex < topBarIndex { + hostView.addSubview(cluster) + } + } + func removePaneChrome(for paneID: PaneID, anchorView: NSView) { let matches = descriptors[paneID]?.anchorView === anchorView #if DEBUG @@ -177,11 +197,13 @@ final class WindowPaneChromePortalRegistry: NSObject, BonsplitPaneChromePortalBr updateClusters() // Split-tree churn can register two anchor instances for one pane; the // dying instance publishes last and its dismantle lands here, deleting the - // survivor's registration. Ask live anchors to reassert on the next turn. - DispatchQueue.main.async { + // survivor's registration. Ask live anchors to reassert on the next turn — + // scoped to this window so workspace churn (a storm of pane closes) does + // not trigger app-wide republish storms. + DispatchQueue.main.async { [weak self] in NotificationCenter.default.post( name: BonsplitPaneChromeAnchorNotifications.reassertRequest, - object: nil + object: self?.window ) } } diff --git a/vendor/bonsplit/Sources/Bonsplit/Internal/Views/PaneContainerView.swift b/vendor/bonsplit/Sources/Bonsplit/Internal/Views/PaneContainerView.swift index c04717a5..ba824a28 100644 --- a/vendor/bonsplit/Sources/Bonsplit/Internal/Views/PaneContainerView.swift +++ b/vendor/bonsplit/Sources/Bonsplit/Internal/Views/PaneContainerView.swift @@ -155,8 +155,13 @@ private final class BonsplitPaneChromeAnchorView: NSView { forName: BonsplitPaneChromeAnchorNotifications.reassertRequest, object: nil, queue: .main - ) { [weak self] _ in - guard let self, self.window != nil else { return } + ) { [weak self] note in + guard let self, let window = self.window else { return } + // A request scoped to one window (object = NSWindow) must not fan + // out to every anchor in the app — workspace churn posts these per + // pane close, and app-wide republish storms cost typing latency. + // nil object stays a broadcast for compatibility. + if let target = note.object as? NSWindow, target !== window { return } self.onReassert?() } } From df23a8f9937c89a54dd9f56bcd4d50b9355bf384 Mon Sep 17 00:00:00 2001 From: arzafran Date: Fri, 14 Aug 2026 11:08:43 -0300 Subject: [PATCH 2/2] fix: never fall back to a broadcast reassert when the window is gone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Posting with a nil object is an app-wide broadcast — the storm the scoping exists to prevent — and a window that died before the post has no anchors left to reassert. Skip the post instead. --- Sources/WindowPaneChromePortal.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/WindowPaneChromePortal.swift b/Sources/WindowPaneChromePortal.swift index efca0dec..49c003ce 100644 --- a/Sources/WindowPaneChromePortal.swift +++ b/Sources/WindowPaneChromePortal.swift @@ -200,10 +200,14 @@ final class WindowPaneChromePortalRegistry: NSObject, BonsplitPaneChromePortalBr // survivor's registration. Ask live anchors to reassert on the next turn — // scoped to this window so workspace churn (a storm of pane closes) does // not trigger app-wide republish storms. - DispatchQueue.main.async { [weak self] in + // Capture the window itself: posting with a nil object would be an + // app-wide broadcast (the storm this scoping exists to prevent), and a + // window that died before the post has no anchors left to reassert. + DispatchQueue.main.async { [weak window = self.window] in + guard let window else { return } NotificationCenter.default.post( name: BonsplitPaneChromeAnchorNotifications.reassertRequest, - object: self?.window + object: window ) } }