perf(layout): pane content takes its pointer from the screen's one tracking area; a moving pane costs AppKit no cursor-rect walk

Merged
LA
1d ago
+478-23

Summary

RESIZE MAIN BUDGET (cmux-ad m16 item 5 / m18 SMOOTH), pointer areas PR 1. Each pane-content view that owned an NSTrackingArea or cursor rects (the terminal surface, the tab strip, the hover-reveal probe, the pane toolbar's corner) made AppKit invalidate cursor rects when its pane moved, walk the window's views and rebuild the window's structural regions in that display cycle. A spring or a drag moves panes in every frame, so this ran in every motion frame: 480 to 510 cursor-rect invalidations and 136 re-added tracking areas in one 27-frame key resize of a 2x2 terminal layout.

The fix follows Chrome's model (one native pointer area per window that does not move; the pointer is hit-tested against the current frames). It extends the divider path that already worked this way (ScreenDividerPointer), with no second path:

  • CmuxNextDesign/PanePointer.swift: PanePointerFollowing (a view the area hit-tests: its cursor, enter, move, exit), PanePointerRegion (a region by geometry: a probe that takes no clicks), PanePointerArea.
  • ScreenDividerPointer: after the divider hover, the content under the pointer hears it come in, move and leave, and shows its cursor unless a divider shows its resize cursor. Leaving content that set a cursor gives it back (the arrow, then the cursor rects around, once). The area is .activeAlways, as the terminal's was.
  • Inside a screen: TerminalSurfaceView has no tracking area and no cursor rect (its I-beam or Ghostty's shape comes from the area; a shape change sets it at once). TabStripView has no tracking area. HoverRevealProbe and the pane toolbar's corner are regions. Outside a screen each view keeps its own area, as before.

Owners told: PL-ghostty (terminal), PL-instant (tabs), PL-motion-core (layout).

Tests

Focused command: scripts/ci/package-test-lane.sh suite Packages/macOS/CmuxNext PaneMotionPointerAreasTests (display worker).

  • Red at a7a7b05542 (step a0747e0948fa1ef27f6f04d7): invalidations=480 [TerminalSurfaceView:480] areas_added=136 [TerminalSurfaceView:68,TabStripView:68] (own frames off), 510 and 136 (own frames on).
  • Green at 2280755ac0f (step cb4fefa2285c92d270f07ce4): passes, with DividerCursorNoRegionRebuildTests, DividerCursorUnderDockTests, PointerHoverRefreshCostTests, TabStripFollowsPaneTickTests, KeyboardResizeContentHoldTests and PaneGroundCoversPaneTests (7 suites passed). The test also checks the cursor over a terminal (I-beam) and over its strip (arrow) before and after a key resize.

Bench (same mini cmux19-perf, 120 Hz virtual display, motion.frameOwner.terminals ON)

Main-thread ms per motion frame, rounds with background daemons over 1 s CPU dropped.

Rowbefore p50 / p95 / p99 / maxmissedafter p50 / p95 / p99 / maxmissed
keys2.33 / 6.58 / 11.37 / 13.2961.79 / 5.59 / 10.61 / 11.564
keys-held2.26 / 6.92 / 9.91 / 12.72162.19 / 6.21 / 9.18 / 13.1511
light (drag)3.39 / 6.04 / 7.22 / 10.8213.07 / 6.30 / 7.21 / 7.770
stress (drag)12.04 / 17.48 / 19.60 / 21.962575.67 / 9.52 / 11.57 / 13.8549
keys-stress10.69 / 14.48 / 15.83 / 16.91654.54 / 8.03 / 10.26 / 12.098
keys-stress-held12.33 / 15.87 / 16.76 / 32.092805.10 / 9.59 / 11.15 / 14.6237

Jobs: keys, keys-held, light, stress from ba53f9ae (before) / 4e9aca87 (after); keys-stress rows from eec91eb1 (before) / b0a5f31c (after), because mediaanalysisd used 2.6-4.7 s CPU in each keys-stress round of 4e9aca87.

Not yet under 8.33 ms p99: the rest is browser chrome cursor rects (PR 2, stacked on this one), the tab strip layout (SMOOTH-STRIP) and the per-pane layout cost (next PR).

Changelog

none

Observed / Not run

  • Observed: the red and green display runs and the bench table above.
  • Not run: the full CmuxNextAppTests suite; launch smoke on macOS 14/15 (RESIZE PR WORKER runs it).

Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Moves pane content's pointer tracking to each layout screen's single, stationary tracking area, so a moving pane costs AppKit no cursor-rect invalidation or window-region rebuild.

Pane content that owned its own tracking area or cursor rects — the terminal surface, the tab strip, the hover-reveal probe, and the pane toolbar's corner — made AppKit invalidate cursor rects, walk the window's views, and rebuild structural regions in every motion frame. Now the screen's one tracking area (ScreenDividerPointer, already the divider hover path) hit-tests the current frames and delivers enter/move/exit and the cursor through the new PanePointerFollowing and PanePointerRegion protocols; the area stays .activeAlways as the terminal's was. Views outside a layout screen keep their own tracking areas and cursor rects as before, and the terminal's cursor still updates immediately on shape change.

Benchmarks

  • Main-thread milliseconds per motion frame (120 Hz, motion.frameOwner.terminals on): stress drag p50 12.04→5.67, keys-stress p50 10.69→4.54; missed frames drop 257→49 (stress) and 65→8 (keys-stress).
  • PaneMotionPointerAreasTests counts zero cursor-rect invalidations, tracking-area additions, and drag-margin rebuilds across a key-resize spring; on the base commit those counts were 480/510 and 136.