Frame owner S4: terminals follow their pane in every frame of a key resize (switch-on path; default stays off)
Summary
FRAME OWNER REFACTOR stage S4 for terminals, the key-resize step (cmux-ad m6). #33956 landed the frame owner for terminals behind motion.frameOwner.terminals, off by default (feat e5ccc759105). This PR makes the switch-on path follow the pane in every frame of a keyboard resize, fixes the independent review's findings, changes the key-resize tests to the m6 contract, and keeps the default path's checks with the switch off. The switch stays off by default (lead's order): with it on, a held key resize with 7 or more moving terminals drops more refreshes than with it off (below). The flip goes in its own PR after the Ghostty fork change that cuts the per-frame cost, and only when that A/B is no worse with ON.
The m6 contract (lead). In every frame of a key resize, each terminal shows pixels at its pane's size in that frame, or the previous frame's pixels clipped and anchored top-left. The pixels are never stretched and never leave a background gap. The program's grid (the PTY size) changes at most once per press.
The defect. Lawrence on dogfood 363e39f2: "tabbar moves smoothly on ctrl shift hjkl but browsers/terminals do not anymore". In a key resize, the terminal host follows its pane in every frame (FRAME OWNER S3). But the surface kept one size per press: restAt held it at the press's final size, or S3 held it at its start size, on the hold's still edges. So the content froze, then jumped. Film on 437f5a22 (px-keys-single-3k-s1r): the terminal right of the divider had its left ring on its pane's left edge in only 14 of 50 motion frames. It was off by -8 to +21 px in 20 frames and cut off in 16.
With the switch on:
TerminalHostView.followedSurfaceFramegives the surface the area it shows in, in every frame.- The frame owner (S4, ready-or-keep) shows each size's frame together with its bounds. Until a size is drawn, it keeps the last frame, anchored top-left. One size is in flight at a time, and the newest size goes next.
- Nothing draws or waits on the main thread. The main thread gives Ghostty the new size, and Ghostty draws it on its renderer thread.
restAtstill reports the final grid at the press: that is the PTY's one resize. It no longer holds the surface at the final size.- The kept frame's anchor is the top-left again. An earlier commit in this branch placed the kept frame on the S3 hold's trailing or bottom edge (
b0a6f600e45); it is reverted. Its test keeps its scenario and expects the top-left anchor.
With the switch off, the behavior is the same as before this PR (S3's producer hold on the still edges).
Fixes from the independent review at 665b1874e6e (BLOCK, comment 6635411077), red first:
- F1: when a terminal's frame for a size was not drawn at the landing (a busy program, or one that holds its frames with synchronized output, mode 2026), the landing's release reported the grid of that stale size, and then the newest size reported the final grid again. One press sent three grids to the PTY. Now a size in flight that is no longer the wanted size reports no grid (
TerminalSurfaceView.updateSurfaceSize,30eccfc5903). TestaTerminalWhoseFrameWaitsAtTheLandingGetsOneGridForThePress(display): red at1578c8aaed7(=712df02da95+ the test fixec9992f5a2a), stepcd3f57162b30f380684570f4: p1 got 61x33, 55x33, 61x33 for one press. Green atec9992f5a2a, stepe62615850f8eb172be13f40e. - F2: a pane that Ctrl-Shift-J grows down, with a prompt in its lower half, now keeps S3's bottom hold: the surface keeps its final size on the moving bottom edge, so the prompt stays in view. A split and a close already keep this rule. A surface at the pane's size showed the final grid's top rows, so the prompt was out of view for the whole spring (
restAtandfollowedSurfaceFrame,30eccfc5903). TestaPromptOnTheLastRowRidesTheBottomOfAPaneAKeyGrowsDown: red at712df02da95, step88c13d12f2e07855a8ca5037("the terminal kept its final size on 0 frames"); green at30eccfc5903, step59488e61824f720f9e84bf19. - F3 (a parked tab shown mid-spring):
5d62688a264is reverted bycc2c26ec9a3. Its red test passed without it (stepaef1ba3d3e128b7e7a5b8b01): the tab is unhidden in the same layout pass in which its area hands it the hold, so no frame shows the stale hold. The test stays as a guard. - T1: the key-resize display test also requires each terminal to take a new size in more than 3 spring frames (a terminal that never moved passed the size check).
- G1 (the review at
447f784c3cf, BLOCK, comment 6639299648): with the switch on,restAtreturned early for a followed host with a.topanchor and left an older surface hold until the landing. A J press with a low prompt then a K press kept the bottom hold through the reverse spring; a terminal that joined mid-hold then got a second press sent grid1, grid2, grid1, grid2.restAtnow releases the hold first (5aab4968d19). Tests:aReversePressAfterALowPromptGrowsDownFollowsItsPanered at7abac06b51d, step1b0702f8dbf54ae7091c8dad("24 frames showed a surface off its pane's size after the reverse press");aTerminalThatJoinsAKeyHoldGetsOneGridPerPressThroughASecondPresspassed there in its first form (a tab added new mid-hold hears the follow indidAddSubviewbefore its surface joins, so it never takes a hold), soc4d6c936c92moves it to the parked-tab path the review named: red at4e8f53fc46b(=7abac06b51d+ that test), stepf257aa893b08d66204c893d1("the landing asked the PTY for [80x44, 88x44] after the presses' grids"; the reverse-press test fails there too); green atc4d6c936c92, step4e80341dde6de693eb6a5a67(PaneContentPolicyTests passes). - G2 (P3, not proven): after F1 holds a stale grid at the landing, Ghostty's reflow preview can show that stale frame rewrapped for one refresh. Not changed here; noted for the flip PR's films.
- T5 (the review at
c4d6c936c92, SHIP, comment 6640562380; P2, to fix beforeready-to-land): the default path (switch off) lost its "one surface size per press" checks when the m6 tests turned the switch on.c0c7f9b0100adds the base's five checks back as their own tests, with the switch pinned off after eachawait:PaneContentPolicyTests.aKeyPressSizesEachSurfaceOnceWithTheSwitchOff(4 directions),aHeldKeySizesEachSurfaceOnlyInAPressTurnWithTheSwitchOff,aPressAndItsReverseRestsAtTheStartSizeWithTheSwitchOff,KeyboardResizeContentHoldTests.theProducerResizesOnceWithTheSwitchOffandKeyboardResizeReflowAfterLandingTests.theProducerLaysOutOnceWithTheSwitchOff(left, right). They are the base's tests atbb23623917fwith new names. They guard behavior that this PR does not change, so they pass on the base and at the head: restored coverage, not a fix, so there is no red step.162ea37166bcorrects the comments the review named (P3): the switch's help text, thePaneContentPolicyTestsheader,TabStripFollowsPaneTickTests.Content,TerminalHostView.releaseSurfaceHoldand theTerminalLaunchFrameKeeperTestswaits.6522020512a(the lead: run the tests with the switch off and with it on) gives the switch as an argument,[false, true], to each other test this PR changed that has a meaning in both states: in PaneContentPolicyTests the F3 guard, F2, and G1 (a) and (b); the TabStrip strip-and-terminal test; both KeyResizeFirstTickPinTests tests; the key-press and held-key ground tests of PaneGroundCoversPaneTests; and the two resize saves of TerminalLaunchFrameKeeperTests. With the switch off, each checks the base's rule: at most one new surface size per press, none at the landing, and one grid per press. The m6 contract tests stay switch-on; their switch-off counterparts are the five restored tests. TerminalKeyResizeFrameTests and TerminalSurfaceCommitSizeTests test the frame owner itself, which runs only with the switch on. - H1-H5 (the same review; P3, switch on only, none worse than the switch-off path): recorded for the flip PR, which fixes or decides each one with a red test first. H1: the low-prompt bottom hold keeps the final width and the hold's first edges (a press on the other axis, or two grow presses and then a shrink press, in one spring). H2: after the G1 release, a request in flight from the older hold still lands for one or two refreshes. H3: see Frames. H4: a bottom hold whose frame waits shows a band at the top for one or two refreshes. H5 (contrived): a grid that the F1 rule holds can wait with no follow-up.
- Other items:
TerminalFramePresenter.debugState(of:)is debug-only;place,requestWantedFrameandlandRequestedFramehaveperf-hotmarkers; the contract comments say what each switch state does.
Files on the per-frame and per-key paths since the films and typing were measured (8c981e32979): TerminalSurfaceView.swift (F1: the grid report decision; no present change), TerminalSurfaceView+PaneMotion.swift (F2: the press start; G1, 5aab4968d19: with the switch on, restAt releases an older surface hold at a press) and TerminalHostView+PaneMotion.swift (F2: the surface frame of a low prompt in a pane that grows). pixel-perfect refilmed the key journeys at ded0cb09a56 (the code of 447f784c3cf with the default on; see Observed). G1 changes no frame of those journeys: it acts only when a press comes while an older hold is on the surface (a low prompt's bottom hold, or a terminal that joined mid-hold), and the films have no such press; the two G1 tests check its frames. TerminalFramePresenter.swift and TerminalSurfaceView+FrameOwner.swift change only by a debug guard and comments; TerminalHostView.swift and TerminalDragTunables.swift change only by comments and the switch's help text (162ea37166b).
Why the default stays off. In a held key resize with 7 or more moving terminals, ON misses more refreshes: keys-stress held, cmux24 job c33bd85dbb064376b348d0c2 (ref 6a266873a17, the app code of 665b1874e6e), 6 rounds per arm: 44 missed with OFF and 55 with ON, ON higher in 5 of 6 rounds; the mean main time of a motion frame grows by 0.27 ms with 7 moving terminals. Single presses, the 4-terminal key resize and drags are equal or better, and ON adds no layout per frame (16.9 view layouts per frame in both arms).
- Cause: each moving terminal gets a new IOSurface each frame (Ghostty allocates a render target per size), and each frame drawn at a new size wakes the main thread through a main-queue block that the main thread then drops.
- Fix: a Ghostty fork change: one coalesced frame-ready wake for the embedder, and render-target slack, so that one press reuses one IOSurface. Owner: frame-owner-s4, with PL-ghostty for the fork. Dates: fork PR by 2026-11-05 18:00Z, then its release and the pin move.
- ON date: the flip PR by 2026-11-06 12:00Z. Its gate: after the fork fix, the held keys-stress A/B on cmux24 (
--rounds=6 --key-repeat=6) is no worse with ON (pixel-perfect's rule: a finding if ON misses more in at least 5 of 6 rounds). The switch deletion follows the flip.
Frames (what the person sees in each frame with the switch on, before and after; REVIEW.md section 12). With the default off, nothing changes for a person in this PR:
- Frames: terminal, key resize (Ctrl-Shift-H/J/K/L). Before: the tab strip moves with the pane, but the terminal text stands at one size through the spring and jumps to its final layout at the end. After: in every frame the terminal shows its text at its pane's size. One size is in flight at a time and the next one is asked for when it lands, so in most spring frames the terminal shows the previous frame's size, anchored top-left: clipped at the pane's right or bottom edge when the pane shrinks, and beside a strip of the terminal's own ground on the pane's right or bottom edge when it grows (the moving edge for a pane that grows right or down; the edge that does not move for a pane that grows left or up); never stretched. The program gets its new size once per press, at the start. Two exceptions. (1) A pane that grows taller (down or up) with its prompt in the lower half keeps its terminal at the final size on the pane's bottom edge (as a split and a close do), so the prompt stays in view and the rows above it come into view as the pane grows. (2) H3 (decided in the flip PR): a pane that gets shorter with its prompt on the last row shows the old grid cut at the bottom until the terminal's owner announces the press's grid (one or two refreshes with the local daemon, more for a remote terminal), because Ghostty's reflow preview draws only at the final size. With the switch off the surface has the final size, so the prompt stays in view.
- Frames: terminal, split, close and zoom springs. Before: the terminal's bounds changed at once, and Ghostty's frame of that size came 1-4 refreshes later, so the old frame stretched or left a gap. After: each new size shows together with its frame drawn at that size; until then, the previous frame stays at its own size.
- Frames: terminal, divider drag: no change (
terminalDrag.holdSurfacekeeps the text through the drag and reflows once at the release). - Frames: tab strip, page, agent chat: no change.
- No content stops following its pane.
The default stays off (447f784c3cf). This branch had the default flip (b3ab63bded6, then 1e0df52dcc6); the lead reversed it: no new red, and a stage turns ON only when no layer is worse than OFF. TerminalDragTunables.framesOwnedByCommit defaults to false. The key-resize contract tests turn the switch on for their frames (PaneGroundCoversPaneTests.ownFrames(); the store is process-wide, so an async test turns it on again after each await), and the default path's guards pin it off (ownFrames(false)). The What's New highlight moves to the flip PR.
Tests changed to the m6 contract (cmux-ad m6 (c), for terminals). These test windows draw nothing, so each size shows at once:
- PaneContentPolicyTests:
aKeyPressShowsEachSurfaceAtItsPanesSizeAndSendsOneGrid,aHeldKeyShowsEachSurfaceAtItsPanesSizeAndSendsAtMostOneGridPerPress,aPressAndItsReverseInOneHoldSendEachGridInItsTurn. KeyboardResizeContentHoldTests.theTerminalFollowsItsPaneInEveryFrameAndSendsOneGridPerTerminal.KeyboardResizeReflowAfterLandingTests.theTerminalFollowsItsPaneInEveryFrameAndItsGridGoesAtThePress.- In every frame, each surface has the size its pane shows it at. The grid checks (one grid per press) do not change.
- The default path keeps the base's checks (T5): the base's versions of the first five tests above, renamed
...WithTheSwitchOff, with the switch pinned off: each surface takes one size per press, none in a spring frame and none at the landing. The other key-resize tests this PR changed run in both switch states (6522020512a). - TabStripFollowsPaneTickTests (b), the lead's m6 (c) item:
aKeyResizeKeepsEachStripAndTerminalOnItsPaneInEveryFrameputs a real terminal in each pane: in every frame each strip is on its pane and each terminal at its pane's size, one grid per press. Red on featbb23623917f(step705edba00632aa949c1144e5: the surfaces held at the press's final size), green with this PR (step03f179c8c4a3ed7ec729e21a). - TerminalLaunchFrameKeeperTests: its two resize cases wait (2 s at most) until the surface shows its new size. With the frame owner, the surface takes that size in the commit that shows its frame, a refresh or two after the layout, and the keeper saves it then.
Debug only (DEBUG and CMUX_BENCH_HOOKS). The debug.motion trace records, per frame and per terminal: the layer rect, the shown frame size, Ghostty's present counters, the presenter state and the host frame state (TerminalSurfaceView+MotionSample.swift, TerminalFramePresenter.debugState(of:)). The film judge uses it to tell which present path drew each frame.
Merge of feat (bb23623917f). #35161 deleted motion.frameOwner.contentPolicy and PaneMotionHold.containersFollow, so containers always follow. The merge commit removes the key-resize test's contentPolicyOverride pin, and c642350625a removes the containersFollow: arguments from the kept-frame test.
Not caused by the switch, and not held here (pixel-perfect's alternating rounds, STATUS 11574). A whole-window +1 refresh in drags comes and goes between films, in both modes. With build 5eb5406 it followed ON three times in round 1, then showed in two OFF films and in no ON film in round 2. Its owner is not found yet. A drag-cadence test written to find it (f863559be0c) never reproduced it and is removed here.
Testing
Display run at this head: cmux-ci display step d33333ef60fbc8bb4250b0f0 (--display --label gui-session-v1, ref 6522020512a03fd915d9a1f9e23c357e7633da20, done on cmux9s-Mac-mini, exit 0): TerminalKeyResizeFrameTests 2 tests, TerminalFrameOwnerMainThreadTests 1 test, TerminalSurfaceCommitSizeTests 6 tests; all passed. The 42 terminal suites at this head: step 167bfcdf2e06625c1688bc1d, all passed.
TerminalKeyResizeFrameTests (display workers) uses the app's PaneContentView (tab strip over content area), with a terminal in each of p1 | p2, in a window on screen. It presses Ctrl-Shift-L once on p2, so p2's left edge moves (the film's case). The test steps the spring itself. After each frame's commit, it checks each terminal:
- its top-left is within 1 device px of its area's top-left;
- its size is one that its pane had in this press;
- the frame is never stretched;
- after the landing, it shows its final size;
- its PTY gets at most one grid per press.
It also prints each frame's main-thread time (the step plus the layout).
Red, then green (fleet steps):
TerminalKeyResizeFrameTests: red atb367fadbdae, step4cea036c2b42cb8eab55aab0: misplaced 15, unsized 28 (p2 kept the final size on the still trailing edge; p1 showed its final size before its pane had it). Green atc642350625a, step6082bec2c6cef89d9a1f48d2: misplaced 0, unsized 0, stretched 0, PTY grids per terminal [1, 1]; main ms per frame p50 0.30, p95 0.44, max 0.79 (0.27 / 0.41 / 0.55 at the red). At1e0df52dcc6(the default on): step989a83316987d65da365e75egreen: misplaced 0, unsized 0, stretched 0, PTY grids per terminal [1, 1]; main ms per frame p50 0.34, p95 0.63, max 0.96.aKeptFrameStaysOnTheEdgesItsHoldNames(an S3 hold with a size change): red at0ffa721f8d4, stepcaf779c086289d39ce7fda9d, with "the narrower size did not wait for its frame". Green atc642350625ain stepdac29204614b3d8bbb99c5a7(TerminalSurfaceCommitSizeTests passed).- The m6 (c) contract tests: red at
2da280e5c61(switch off), step95093a235a1f69730aa52111: PaneContentPolicyTests, KeyboardResizeContentHoldTests and KeyboardResizeReflowAfterLandingTests fail, each with a surface held at the press's final size while its area moves (for example "frame 1: p1's surface (534.0, 772.0) in a (591.0, 772.0) area"); no grid check fails; TerminalLaunchFrameKeeperTests passes. Green at1e0df52dcc6in the step below.
All test suites that drive a terminal surface (every CmuxNextAppTests suite that uses TerminalSession or TerminalSurfaceView): at 1e0df52dcc6, step da4c624b9f266aaf509def51: 41 suites pass, and TerminalFrameOwnerMainThreadTests needs a display worker (it runs in the display step). At 447f784c3cf (default off): step 28e2a76129822912cbeb9023: all 42 suites pass; display suites, step 538dccf3f6002506fb3b7549: all 3 pass. At 5aab4968d19 (the G1 fix): step 62747f12ead93b25cd1c67f3: all 42 suites pass; display step 840f73703081fc6a206fd781: all 3 pass. c4d6c936c92 changes one test only. At 162ea37166b (T5's restored tests and the comments): step c8543eac4261795bf6b12ce6: all 42 suites pass. At the final head 6522020512a (the tests in both switch states): step 167bfcdf2e06625c1688bc1d: all 42 suites pass, and each both-state test ran its two cases (for example aKeyResizeKeepsEachStripAndTerminalOnItsPaneInEveryFrame(direction:switchOn:) with 8 test cases, aReversePressAfterALowPromptGrowsDownFollowsItsPane(switchOn:) with 2); the five ...WithTheSwitchOff tests pass. Display step d33333ef60fbc8bb4250b0f0: all 3 suites pass; the key resize shows misplaced 0, unsized 0, stretched 0, PTY grids per terminal [1, 1]; main ms per frame p50 0.32, p95 1.29, max 13.21 (one frame; the same app code gave max 0.89 at 5aab4968d19 on another worker; this test does not judge main time, the A/B does). Receipt: SHIP at 6522020512a (comment 6642054231, the same reviewer as the SHIP at c4d6c936c92). At c642350625a, before the test changes, 38 passed and the 7 old-contract tests above failed (step e9e6a30c3d9effb058d77e64).
A/B in one launch (#35407's bench; ref fix/frame-owner-s4-abbench-nomerge = 6a266873a17, whose app sources are those of 665b1874e6e: before F1, F2 and G1. The decision to keep the default off does not depend on them; the flip PR runs the A/B at its own head). Command: split-spring.sh --suite=terminal-resize --config=bench --vdisplay=120 --layouts=keys,light,stress,keys-stress --ab=motion.frameOwner.terminals. The keys layout is 4 terminals in a 2x2 grid with Ctrl-Shift-L, then Ctrl-Shift-H. Each round runs both arms, and the arm order alternates per round.
- First run (cmux24, job
b5834b1d8ebc36c55386d7cd, 3 rounds per arm; the light and stress drag layouts did not run because of a bench counter defect, fixed in #35407):- keys, OFF: 534 frames, 5 missed; main ms p50 0.11, p95 3.53, p99 7.32, max 11.63; frames that laid out a view p50 2.49, p99 11.05.
- keys, ON: 534 frames, 8 missed; main ms p50 0.12, p95 3.93, p99 8.90, max 12.32; frames that laid out a view p50 2.99, p99 12.03.
- keys-stress (7 terminals resize per press), OFF: 4 missed, main p99 8.52. ON: 6 missed, main p99 8.18.
- One grid per press in both arms and both layouts (max_grids_per_press 1).
- 6 rounds per arm (cmux24, job
597e886a2188c237394e92ac, ref6a266873a17, all probes passed: the light drag moved -40 pt and 2 terminals took a new grid; STRESS 7):- keys (4 terminals): missed refreshes OFF 15 / ON 15. Frames that laid out a view: OFF n 213, p50 3.33, p95 7.54, p99 11.53, max 12.05 ms; ON n 249, p50 3.04, p95 8.40, p99 13.17, max 14.11 ms. Per round, p99 OFF 7.54, 12.05, 11.53, 10.04, 10.88, 11.58 (median 11.2) / ON 3.75, 14.11, 12.64, 13.97, 12.37, 12.65 (median 12.65).
- keys-stress (14 terminals, 7 take a new grid per press): missed 18 / 18; p50 3.93 / 4.46, p95 9.88 / 10.27, p99 11.28 / 11.85 ms; per-round p99 medians 11.18 / 11.76.
- light drag (2 terminals): missed 3 / 1; p99 7.01 / 7.07 ms. STRESS drag (14 terminals): missed 22 / 8; p99 9.19 / 8.55 ms.
- One grid per press in every key layout and both arms (max_grids_per_press 1).
- Reading: for single presses, ON drops no more refreshes than OFF in any layout (fewer in drags). Held keys with many terminals: see below. Its one cost is about +1.4 ms at the p99 of a 4-terminal key resize's motion frames (p50 is lower): each terminal now gets a new size and a present in each frame instead of once per press, which the m6 contract asks for. Both arms are above the 8.33 ms frame budget at the motion p99 (11-13 ms) in key resizes. That is the base's main-thread work in a spring frame, not this change; it stays an open item for the motion owner.
- A held-key 6-round run (
--key-repeat=8, step0ac1a7fbbc0903d1d6413981, cmux-austin-mini-1, a saturated host: the STRESS drag misses about 400 refreshes per arm there): keys 69 / 70, keys-stress 380 / 455 (ON higher in 5 of 6 rounds), light drag 76 / 41, STRESS drag 419 / 406 (OFF / ON); one grid per press in both arms. On cmux24 (jobc33bd85dbb064376b348d0c2,--key-repeat=6) keys-stress held is 44 / 55, ON higher in 5 of 6: the reason the default stays off (below).
Films (pixel-perfect, round ab-s4on-sw: build of 8c981e32979, the same app code; cmux24, 120 Hz at 2x; one launch per journey with the switch set and read back in every film; judge 3e.1). m6 failing film frames, OFF / ON:
- px-keys-single-3k: terminal x1 1 / 0, Reduce Motion 0 / 0. Page 104 / 103 and 68 / 69: the same in both arms, all the page's own shell gap (the kit turns the page backdrop off; pages are S5). Press to first divider move: x1 2 1 1 1 / 2 2 1 1 refreshes; Reduce Motion 2 2 2 2 / 2 2 2 2.
- px-keys-held-2: terminal x1 2 (E4) / 0, Reduce Motion 0 / 0. Page 130 / 132 and 81 / 77.
- px-close-column-c3: terminal 63 / 64 and 60 / 61, all E3 at rest after the landing in both arms: a judge artifact (two terminals' grounds of the same color read as one run; R-HARNESS item 3e.3). Not S4.
- So in ON, no terminal frame of a key resize is off its pane. OFF has 1-2 such frames per film.
Whole-window "late" frames (E7) in held keys. The first held-2 film had 155 late frames with ON and 6 with OFF. A repeat (ab-s4rep: 3 OFF/ON pairs of held keys in one launch) gave OFF 5, 147, 4 / ON 109, 160, 162. E7 follows each film's fitted clock: in the late films, the tab strips and the terminal content both sit one trace sample behind the fixed clock, for the whole film, and this happens in OFF films too (off2). The person-side measure is the refreshes from each key-down (the app's input journal) to the first divider move in the film, with no trace involved. First press of each film (the only one that starts from rest): OFF 2, 2, 3 / ON 1, 2, 2; single-3k OFF 2 1 1 1 / ON 2 2 1 1; held-2 first press OFF 1 / ON 1. No press is slower in ON as a whole, so the E7 difference is a trace-clock effect, not latency a person sees. A second repeat with per-film debug.frames (ab-s4rep2: 3 more pairs) confirms it: the fitted clock was +1 in all 6 films, and E7 was OFF 73, 160, 158 / ON 158, 158, 162. Over all 7 held-key films per arm, OFF was late as a whole in 4 and ON in 7: the late state follows the run's conditions in both arms. First press, refreshes to the first divider move, over 6 films per arm: OFF mean 2.17, ON mean 1.67.
Typing (pixel-perfect, typing-load-l6 in-build ABBA on cmux22, build of the same app code, the switch set after each relaunch; key to committed frame, ms): ON p50 3.33 and 3.07, p95 8.87 and 8.75, p99 11.40 and 10.65, max 29.6 and 16.3; OFF p50 3.04 and 3.04, p95 8.98 and 8.78, p99 11.42 and 11.97, max 20.6 and 27.8. Mean p99 ON 11.03 vs OFF 11.70 ms; dispatch-to-commit p99 ON 3.74-3.94 vs OFF 5.09-5.64 ms. Typing is not worse with ON: PASS.
Main thread in moving frames (ab-s4rep2, held keys, frames with a view or window layout; debug.frames raw per film), OFF / ON per film:
- p50: OFF 3.01, 2.63, 2.65 / ON 2.73, 2.82, 2.78 ms.
- p95: OFF 7.51, 6.08, 6.02 / ON 6.82, 6.21, 4.44 ms.
- max inside the key motion: OFF 13.50, 10.09, 13.87 / ON 9.12, 14.25, 6.22 ms (the 14.25 ms ON frame and OFF's 12.90 ms are each the first frame of the first press). The film's longest frame (OFF 17.17, ON 25.98 and 24.12 ms) is one frame about 1.8 s after the motion, in both arms: the recorder stopping at the film's end, not a key press.
- frames over 8.33 ms inside the key motion: OFF 6, 4, 2 / ON 1, 5, 0. Missed refreshes (whole film): OFF 6, 2, 3 / ON 4, 5, 0.
- Ghostty display passes on the main thread (debug.terminal_display): OFF 21 per film (p95 2.6-2.7 ms, about 24 ms in all); ON 0 (the presenter shows each frame in the commit instead).
followedSurfaceFrame is // perf-hot: 4ms motion-frame (owner PL-motion-core). With the switch on it returns at once, with no hold arithmetic. The keys A/B above is its before (off) and after (on). The benches that perf-hot-benches.json names for that path, base bb23623917f against 665b1874e6e (the switch on by default there), on aws-m4pro-9 (the windows were not key, -inactive):
split-spring.sh --suite=pane-perf(split, close, New Column, zoom and drag with terminals), 3 rounds, base first (steps2ebdfd09d4e2f2955eab6e18,7953a9ae8bf86998b219d294): missed refreshes 1x2 1 / 7, 4x2 6 / 5, STRESS 24 / 24. Main-thread layout per frame p95 within 0.3 ms in every row.- The same, 6 rounds, head first (steps
6524b0b7715245a0f244440a,9bac0780bb89bdf00476ea0e): 1x2 4 / 4, 4x2 6 / 8. So the 1x2 difference of the first run was order or noise. --suite=anim-perf(tabs, splits, columns, workspace switch, sidebars, Home, chats, palette), 3 rounds (steps894bfe132a38aa27953d00e0,b85d56b55bb8a5c8595a6024): dropped frames 53 / 51 over 27 animations; no row moves by more than 2. Both steps exit 1 on the same 4 rows (tab moves that the bench's layout refuses: "the tab is already at the edge"), in both arms.
Observed / Not run
- Observed: the fleet steps above, the first A/B run, the films and their repeat, and
prepush-gate.pypasses. - Films of
ded0cb09a56(the same code as447f784c3cfwith the default on; pixel-perfect, cmux24, judge 3e.1). OFF arm (F1 is its only change): not worse. held-2: 3 terminal frames (E4) vs 2-3 in the earlier OFF films; close-column-c3: 59 and 60 vs 63 and 60; single-3k: 3 frames at the J press vs 1 earlier, inside the old spread (5 frames in the OFF films ofc4d64b5and30df24f), with the tab strip and the page late by the same amount, so the cause is the commit, not F1; whole-window late 42 vs 51. ON arm: terminal 0 in single-3k, 1 (E4) in held-2, 63 and 60 in close-column-c3. - F2 (new journey px-keys-lowprompt-3kp): before F2 (
8c981e32979, switch on) the prompt row was hidden at the start of each grow (4 frames at x1, 17 at x0.25). With F2 (ded0cb09a56, switch on) and on the switch-off path, the prompt row stays in view in every frame of the grow-down spring (0 of 23 frames hidden at x1, 0 of 70 at x0.25), and no shell gap opens. pixel-perfect keeps F2 in S4 (STATUS 12689); the lead can reject it until 2026-11-05 12:00Z. When the final grid's frame arrives, the prompt row moves up once (7 px OFF, 11 px ON) because the bottom padding grows, then it rides the bottom edge. - Reviews: BLOCK at
665b1874e6e(F1-F3), BLOCK at447f784c3cf(G1), SHIP atc4d6c936c92(comment 6640562380) with T5 to fix beforeready-to-land.c0c7f9b0100,162ea37166band6522020512afix T5 and the comments; the same reviewer's receipt at the final head is the one forready-to-land. - Not run at the final head: films and the A/B. Since
c4d6c936c92the head adds and changes tests, and changes comments and the switch's help text only, so the app's behavior is that of5aab4968d19. - Stage ON rule (pixel-perfect): on films of the head, no layer is worse with ON than OFF, and typing is not worse. Films: met (0 terminal frames off their pane with ON). Typing ABBA: PASS. A/B: single presses, 4 terminals and drags equal or better; held key resize with 7+ moving terminals worse (above), so the default stays off. The refactor's final acceptance (main-thread p99 < 4 ms per 120 Hz frame, PLAN.md) is not met in either arm in key resizes (11.53 ms OFF / 13.17 ms ON): open for the later stages (S5, S7, the main-thread work).
- Owner note (PL-ghostty, session cmux-b6): with the switch on, this changes the terminal surface's frame in key resizes; the default stays off. That is seam (b) in
plans/cmux-next/ghostty-integration-DESIGN.mdsection 3: the CA commit that carries a pane's new frame carries the terminal's frame at the new size, or the previous frame unchanged. The owner is told in STATUS.
No capture: with the default off nothing a person sees changes. The switch-on evidence is pixel-perfect's films above (OFF and ON in one launch) with the per-frame judge.
Changelog
none