perf(new tab): Cmd-T makes the next spare after its open settled; the bar's palette actions are read in background slices

Merged
LA
1d ago
+404-81

Summary

Fixes #32910.

Takes back #29760: only its "next main-actor turn" refill. The next spare page is now made 250 ms after a handout (one fixed timer; keys do not push it back), because making it on the next turn put a web view and a Chromium browser into the key's own pass and its setup and load into the open's frames (4-17 dropped refreshes per Cmd-T). The cost: a second Cmd-T less than 250 ms after the one before gets no spare and loads its page cold, as any Cmd-T with no spare did before #29760.

Each Cmd-T (New Tab page, idle, dark, animations off, 120 Hz) dropped 4-17 refreshes in the 50-150 ms after the key (PACING TRIAGE, STATUS 8128): the app's own frame trace missed 8-11 vsyncs per press and newTab.default committed at 29 ms p50, 50 ms max. Profiles of three presses show two jobs of the open on the main thread that do not show in that open:

  1. The next spare page was made in the key's own pass. Since #29760 the pool replaced a handed-out spare "on the next main-actor turn": a web view and a Chromium browser before the key's commit (4 ms), then that browser's setup calls, show, first load and CEF pump passes in the open's frames (+30 to +120 ms). The pool now makes it once input has been quiet for 250 ms after the handout (NewTabSparePool.openSettle, the launch warm's wait), never in the open's turn or frames; a key or a click pushes it back. Chrome makes its spare renderer the same way: best-effort work after the navigation it served, never in the click's task. The next Cmd-T still finds a page that is loaded or loading.
  2. The bar's palette actions were read in one task. NewTabPage.fillPaletteActions read every catalog action's state (RegistryPaletteProvider.makeItems, 7-10 ms on an E core) in one frame task, then JSON-encoded the whole bar on the main thread for each page. makeItems is now an ItemsPass that can read in pieces; PaletteController.commandListing() reads it a slice at a time, and AgentPaneNewTabActions (CmuxNextAgentPane) runs the start, each slice, the arrangement and the hand-over to the pages as separate budgeted background tasks (FrameBatchQueue class .background, 0.5 ms a slice, yielding to input and to the frame's budget), as Chrome's scheduler runs best-effort work in each frame's idle time. It starts from the first background task, so a page that joins after the request (Cmd-N's first page, a restored page, the [+] column page) gets the actions too. The actions' JSON is encoded once, off the main thread; a list that did not change is not encoded or sent again. The palette's own open (Shift-Cmd-P) still reads all rows at once, since the person waits for that list. Out of scope: the native New Tab page (tabs.nativeNewTabPage, off by default) still reads them in one task.

What the key turn itself still does (profiles, for the next step): the store intent and PaneContentView.install (2-4 ms: a divider-catcher child window, theme CSS, a focus-rule JSON round trip), a synchronous CefBrowserHost::SetFocus (2-6 ms), and the commit's AppKit layout of sidebar and titlebar buttons (5-10 ms).

Testing

Regression test NewTabOpenFramesTests (CmuxNextAppTests), three cases:

  • cmdTMakesTheNextSpareOnlyAfterItsOpenSettled: after Cmd-T adopts the parked spare, no new spare is made within 60 ms (the open's frames), and one is made within 3 s.
  • cmdTListsTheBarsPaletteActionsInSlices: the bar's palette actions are read in at least 4 main-thread tasks (newTab.paletteActions bench spans: start, two or more slices, arrangement).
  • cmdNsFirstPageGetsThePalettesActions: a page that joins newTabPages after the fill was asked for gets the actions (review finding on 4f98db5; it passes on the base too, where the fill ran two frames later).

Focused command (same on both commits): cmux-ci run --class light --script scripts/ci/package-test-lane.sh --arg suite --arg Packages/macOS/CmuxNext --arg NewTabOpenFramesTests --ref SHA

  • red at f3bf3021041 (branch test/ndf-32910-red-v2-never-merge: base f338fc81fd35 + this test file only), step 950729e61b0a41093dbc06be: cmdTMakesTheNextSpareOnlyAfterItsOpenSettled fails (made.count → 2) == (before → 1) (the next spare was made within 60 ms); cmdTListsTheBarsPaletteActionsInSlices fails (reads.count → 1) >= 4 (every action in one task); cmdNsFirstPageGetsThePalettesActions passes (expected on the base).
  • green at 4ad853256a9 (head), step 7fe4cc7a63630a95df6baaa2: 3 of 3 passed ("Test run with 3 tests in 1 suite passed after 31.333 seconds").

Before/after (bench builds, pacing-triage kit 0c43dd9dd50, sf-newtab-default-openbrowser-warm-idle-dark-anim-off, 120 Hz virtual display, real HID keys, 10 Cmd-T per film, compared within start tier):

Builds: base f338fc81fd35 (job ad0da90ae9ff261ab4051139), head a6a3bbad353 (job 750878ca497859fc81820af2; the later commits change test code and the order of background tasks only). Rounds in order base, head, head, base (cmux21, cmux23, cmux18, cmux21). Judge: the triage's (kit 0c43dd9dd50).

armstart tierpressesdrops per press p50/p95/max (sum)main missed vsyncs p50/p95/maxcommit_ms p50/p95/max
baseall204.0/6.6/17 (97)8.0/13.1/1428.2/47.2/54.6
headall201.0/13.1/14 (41)7.5/9.1/1024.6/33.9/34.9
baseEfficiency124.5/10.9/17 (64)9.0/13.4/1437.4/50.3/54.6
headEfficiency141.0/13.3/14 (40)8.0/9.3/1027.1/34.2/34.9
baseSuper84.0/5.0/5 (33)6.0/7.6/816.9/24.0/26.6
headSuper60.0/0.8/1 (1)5.5/7.8/818.1/22.9/23.8

With the kit's newer judge (e111df4, STATUS 8130 drop classes), the same films: base 2.0/3.1/4 (43), head 0.0/1.1/2 (6) drops per press. The head's E-tier p95 of 13-14 is the first press of each film (the app's first New Tab after launch). Films: https://cmux-orchestrator-1.tail137216.ts.net/resize-videos/ndf-nt-base-1-120x1/ (and ndf-nt-base-2-, ndf-nt-head-1-, ndf-nt-head-2-).

The commit is still over the 10 ms budget: the key turn's own work (store intent, PaneContentView.install, a synchronous CEF SetFocus, the commit's AppKit layout of sidebar and titlebar buttons) stays and is the next item.

Observed / Not run

Observed: the red and green steps above; 4 judged bench rounds (20 Cmd-T per arm); a SHIP review receipt at 4ad853256a9. Base-red checks on this PR's head that it does not cause: cmux-next checks "No fleet host names in gallery fixtures" and defaults-snapshot/defaults guard check-menu-actions in CmuxNextTabs/Playground/TabBarPlaygroundNewTabControl.swift (neither file is changed here; the same reds are on #32936).
Not run: a bench round of the final head 4ad853256a9 itself (its source differs from the measured a6a3bbad353 only in the order of the background tasks); a profile round of the head; rapid Cmd-T (< 250 ms apart) films.

P0 proof (real app)

Changelog

Changed: Cmd-T opens a New Tab with fewer dropped frames: the next prepared page and the bar's command list are made after the tab shows.

Proof

No capture: the change is timing only; nothing on screen changes. The films are in the measurement table above.