perf(new tab): Cmd-T makes the next spare after its open settled; the bar's palette actions are read in background slices
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:
- 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. - The bar's palette actions were read in one task.
NewTabPage.fillPaletteActionsread 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.makeItemsis now anItemsPassthat can read in pieces;PaletteController.commandListing()reads it a slice at a time, andAgentPaneNewTabActions(CmuxNextAgentPane) runs the start, each slice, the arrangement and the hand-over to the pages as separate budgeted background tasks (FrameBatchQueueclass.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.paletteActionsbench spans: start, two or more slices, arrangement).cmdNsFirstPageGetsThePalettesActions: a page that joinsnewTabPagesafter 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:cmdTMakesTheNextSpareOnlyAfterItsOpenSettledfails(made.count → 2) == (before → 1)(the next spare was made within 60 ms);cmdTListsTheBarsPaletteActionsInSlicesfails(reads.count → 1) >= 4(every action in one task);cmdNsFirstPageGetsThePalettesActionspasses (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).
| arm | start tier | presses | drops per press p50/p95/max (sum) | main missed vsyncs p50/p95/max | commit_ms p50/p95/max |
|---|---|---|---|---|---|
| base | all | 20 | 4.0/6.6/17 (97) | 8.0/13.1/14 | 28.2/47.2/54.6 |
| head | all | 20 | 1.0/13.1/14 (41) | 7.5/9.1/10 | 24.6/33.9/34.9 |
| base | Efficiency | 12 | 4.5/10.9/17 (64) | 9.0/13.4/14 | 37.4/50.3/54.6 |
| head | Efficiency | 14 | 1.0/13.3/14 (40) | 8.0/9.3/10 | 27.1/34.2/34.9 |
| base | Super | 8 | 4.0/5.0/5 (33) | 6.0/7.6/8 | 16.9/24.0/26.6 |
| head | Super | 6 | 0.0/0.8/1 (1) | 5.5/7.8/8 | 18.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)
-
Display step 21cb39c15c88ad2c9d30cf6d at 35f7385b5a32331cb25c839d0b655d798e696686 (this branch after the lead-ordered merge of feat-cmux-next, with #33013; later commits are cmux-bot base merges only): cmux-austin-mini-0, gui-session-v1, CMUX_TEST_REQUIRE_GUI=1, exit 0: NewTabOpenFramesTests 3 of 3 and NewTabSpareContextTests 1 of 1 passed on screen. Review receipt at that head: https://github.com/manaflow-ai/cmux/pull/32979#issuecomment-6600037016
-
Earlier, at 4ad853256a9: display step 64d227fd0ad7a4baadedd98c (same tests passed; lane red only for the XCTest signal-6 base red that #33013 fixed). The smoke-gate round (build b06c72d2, cmux24) is not proof: it failed at step 43, the New Column key gap, and its journey has no Cmd-T step.
-
Judged Cmd-T film on cmux24 (mini scheduler round ndf-nt-p0, GO 08:37Z), bench build 7d3b94c903698528334eb0c8 of this head: 10 Cmd-T, drops per press p50/p95/max 1.0/6.9/11, main missed vsyncs 8.0/10.1/11, commit_ms 26.2/41.7/45.1 (E 8, S 2); film https://cmux-orchestrator-1.tail137216.ts.net/resize-videos/ndf-nt-p0-120x1/.
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.