Frame owner S2 ON: drags lay out once per display frame; springs keep their curve after a late frame

Merged
LA
3d ago
+2-2

Turns on motion.frameOwner.timing (FRAME OWNER REFACTOR stage S2), which #33725 landed off. The diff is one default and its comment.

Summary

With this on, two things change in every window:

  • A divider, column-edge or row-edge drag lays the layout out once per display frame, in the window's frame tick, at the newest pointer. Before, every mouse event changed the model and laid the layout out in a separate turn. In film pp-drag-browser the display link ticked 45 times in 1205 ms during a drag beside two Chromium pages, while geometry committed from the 110 mouse events between ticks.
  • Springs keep their curve after a late frame. Every spring and motion client steps to the time its frame shows (the display link's target), so after a refresh-rate change or a late frame each frame shows the curve at its own time. One frame catches up at most 0.1 s; a longer hang pauses the motion instead of jumping.

How it works and its tests: #33725.

Testing

Films, OFF against ON (refactor owner, STATUS 9071): cmux18, kit 77a62dcfe69, 120 Hz 2x. OFF build facfb7f7 (af210bb800a), ON build e9408920 (9b7b1671f13: the same app source with the default on). Specs ab-s2-off-18 / ab-s2-on-18; gallery runs ab-s2-off-af210bb-20261103T124519Z-cmux18 and ab-s2-on-9b7b167-20261103T125041Z-cmux18.

  • Trace judge (judge_trace.py): 0 dropped refreshes in both arms (px-col-drag-2-s1r, px-row-drag-3r-s1r, x1 and Reduce Motion). Both arms show the same 2 lurches at the drag start (+8.5 pt, the 8 pt soft snap; the KEYS stage removes it).
  • Pixel judge (judge_pixel.py): no frame is worse with ON. Max content-edge deviation in px-col-drag-2-s1r: 50 px OFF, 25 px ON; the rest is equal. The remaining deviations belong to stages S3, S4 and S5.
  • Divider against pointer in the slow drag: p50 1.2-1.4 pt OFF, 1.2-1.5 pt ON (the same).
  • Not judged: px-col-drag-4-s1r (the kit's drag moved no divider in either arm) and OFF's rkd-col-drag-2 (its recorder did not start).

Typing under load (typing-load-l6 with the P0 runner flags, cmux18, specs tul-s2-off-18 / tul-s2-on-18), key-to-commit ms p50 / p95 / p99 / max:

p50p95p99max
OFF3.439.5215.5736.16
ON2.949.2613.9019.07

Hot-path bench, OFF against ON: GREEN (refactor owner's bench_ab.py). LayoutDragBench (5 rounds) + PaneMotionBench, one job at a time on cmux19-perf (mini scheduler), interleaved OFF / ON / OFF / ON: OFF = feat 8100c5157e7 (jobs 67696c90, f32eb17c), ON = this head 150b342ba53 (jobs 7e540e8a, f68ba191). All 4 passed.

Drag (the path S2 changes), msp50p95max
split, OFF (2 runs)0.222-0.2240.235-0.2400.87
split, ON (2 runs)0.215, 0.2140.231, 0.2291.11
column, OFF (2 runs)0.229-0.2310.239-0.2450.69
column, ON (2 runs)0.220, 0.2200.236, 0.2360.79

ON is at or below OFF in p50 and p95, and every drag frame is under 4 ms, so the main-thread p99 per 120 Hz frame is under 4 ms in both arms. In the 21 PaneMotionBench cases with no drag (the switch adds only an empty check and the spring time base): ON's p50 is lower in 15 of 21 (median ON/OFF 0.992), p95 median ON/OFF 0.999. The per-run p95 medians move with time, not with the arm (OFF 1.009, ON 1.020, OFF 0.909, ON 0.928), and the two OFF runs differ by a median 8.6% from each other, so there is no systematic cost. Frames over 4 ms: only unzoom, 30 panes, in both arms (OFF max 5.15 ms, ON 4.44 ms): a base finding in the zoom path, reported in STATUS.

Suites with the switch on, at this head: fleet 74d21190ed020440a9c09cf6 (the 26 suites of #33725: passed, PaneMotionWork skips itself) and 28091b381bbae8f013ca409f (33 more suites that drive divider drags or a frame clock: 32 passed). The one red, ParkedPageCatcherPanelsTests, leaks 7 catcher panels at feat with the switch off too (step ffa86d29173d644d7a47940c; comment below).

Plan: delete the switch

The lead's 13:30Z rule: the next PR after ON deletes the switch. It opens after the first FEAT DOGFOOD BUILD with ON passes its gate. It removes MotionTunables.frameOwnerTiming, FrameTick.motionElapsed (callers read frameElapsed), LayoutViewContext.dragsOnFrameTick / frameTimingOverride (drags always wait for the frame tick), the event-turn apply branches in DividerDrag.handle, and the switch setup in the S2 tests; FrameTick.elapsed stays for the probes and the film kit.

Observed / Not run

Observed: the films, the typing pair, the bench and the fleet steps named above. Excluded: my first two bench sets on aws-m4pro-4. In one set the OFF and ON steps ran at the same time on that host. In the other, one ON run overlapped a cold app build (job 254b7a49) and was 2.5x slower in every case, also in the cases where the switch adds only an empty check. The CI cmux-next swift test reds at this head are base findings (red at feat with the switch off: step f2c95c0091b7230e2109a047, or on other PRs); see the comments below.

Not run: a full package test run with the switch on outside CI.

Changelog

Changed: Animations keep their pace after a stutter instead of slowing down, and a dragged divider moves once per display frame, to the pointer's newest position.

Proof

No capture: the film A/B above (OFF against ON, same mini) is the visual evidence.