perf(new tab): two spares, made after an open settled, so a rapid second Cmd-T does not load cold
Summary
Stacked on #32979 (base branch for review: its head 4ad853256a9; this PR merges feat-cmux-next in once #32979 lands). Lead decision on #32979 (STATUS 8392): a rapid second Cmd-T must not load cold.
On #32979's head the New Tab pool keeps one spare and makes its replacement 250 ms after the handout, so the second of two Cmd-T 150 ms apart loads its page cold (a web view and a Chromium browser made in the key's own turn). As Chrome keeps a spare renderer ready and makes the next one as best-effort work after the navigation it served:
- Two spares.
AgentPaneSpareSlotnow holds up to a capacity (default 1); the New Tab pool keeps two. A rapid pair adopts two loaded pages. Every parked spare draws the prepared context; the next one handed out gives the cover;dropAll(memory pressure, the last window closing) drops both. - The refill waits for the open to end.
AgentPaneSpareRefiller(CmuxNextAgentPane) makes a handout's replacement after the key's frame committed (AfterFrameCommit), the open's tab-strip motion ended (newTabStripView.whenStill) and the adopted page drew its first frame in its pane (redraw.afterNextFrame), then 250 ms of idle time that input does not push back, as one background-class task on the app's scheduler (FrameBatchQueue .background: it yields to the person's input and the frame's budget). The second spare is made only after a spare served an open (a launch loads one page, as base does), on every make path (onewantsPagecondition: a retarget, the return to normal pressure, the capacity setter and the WebKit probe also make one page only). A later open cancels an earlier open's pending refill, and no parked page is prepared, captured or made until the latest open settled. - Per-page prepare and covers. Each ready parked page is prepared on its own (an unpainted one is waited on once); covers are captured per page when parked, fitted or prepared, never in the key's turn (
AgentPaneSpareCovers). - Memory. Spares are made only at normal memory pressure; after a pressure drop the pool waits for normal; one spare on a Mac with less than 16 GB.
debug.new_tab capacity Nsets the count (A/B).
Cost: one more parked page (a WKWebView or Chromium renderer) while the app is not under memory pressure.
CmuxNextApp app_lines -2. Owner note: TabStripView+Layout.swift gets whenStill(_:) (PL-instant, OWNERS.md row "Tab strip").
Testing
Regression tests NewTabRapidPairTests (CmuxNextAppTests): aRapidSecondCmdTAdoptsASpareToo (two Cmd-T 150 ms apart both adopt a spare; nothing made in the pair's frames; dropAll leaves no spare), aSecondCmdTRestartsTheFirstOnesRefill (the first open's pending refill makes nothing in the 500 ms after a second open whose page has not framed), anUnpaintedSecondSpareDoesNotHoldTheFirstsContext (the context changes after both spares parked; the second never paints; the first still draws the new context).
Command: cmux-ci run --class light --script scripts/ci/package-test-lane.sh --arg suite --arg Packages/macOS/CmuxNext --arg NewTabRapidPairTests --ref SHA
- red 1 at 8a56a7918f2 (#32979 head + the first test), step 6e4d2ba9: the pair's second Cmd-T loaded cold.
- red 2 at 329f8681876 (eba59673d60 + the review tests, branch
test/ndf-refill-red-v3-never-merge), step 2384a13b88d298257d128e8b:aSecondCmdTRestartsTheFirstOnesRefillfails(made.count → 3) == 2,anUnpaintedSecondSpareDoesNotHoldTheFirstsContextfails (the first spare never took the new context); the pair test passes. - red 3 at f77069887c3 (8dd130304b5 +
aLaunchMakesOneSpareUntilOneServedAnOpen, branchtest/ndf-refill-red-v4-never-merge), step 1224f623ce0d37010eee9e65: fails(rig.made.count → 2) == 1(the launch made two pages before any open); the other 3 pass. - red 4 at 5287e5dc7e3 (e65dd84c5a9 + the retarget step in
aLaunchMakesOneSpareUntilOneServedAnOpen, branchtest/ndf-refill-red-v5-never-merge), step cb0bf882be7af93703047691: fails(rig.made.count → 2) == 1(a retarget made page 2 before any open). - green at 6f12bb74b14 (head, after the feat merges; feat has #33013), step d89630e078b0f58e24e58b23: NewTabRapidPairTests 4 of 4, NewTabSpareContextTests 1 of 1, NewTabSpareVisibilityTests 1 of 1 passed; the lane exits 0.
- earlier green at 9289a89552b, step 4d06f5aca7e4b5ccb7e9f07c: NewTabRapidPairTests 4 of 4 and NewTabOpenFramesTests 3 of 3 pass; the lane then stops on the known XCTest signal-6 fleet fault, so NewTabSpareContextTests and NewTabSpareVisibilityTests gave no result in that step ("Test run with 3 tests in 1 suite passed"). The lane exits 1 after the Swift Testing phase only for the known XCTest signal-6 / CoreData 134020 fleet fault.
Measured (re-review rounds, head e65dd84c5a9)
Builds: base = #32979 a6a3bbad353 (job 750878ca); head e65dd84c5a9 (job dfbe3399). Kit cp-scratch/ndf-refill-kit-never-merge 8d1af43bbdb, judge 0c43dd9dd50, 120 Hz, real HID keys, cmux22/23/24 (no cmux21). One base round (cmux23) failed its set-up at step 3 and is not counted; a replacement ran.
First Cmd-T of the sf-newtab film when it is the first journey after launch (drops):
- head: [14, 12, 1, 1, 0, 1, 2] (7 rounds: head-1, -2, -4 .. -8)
- base: [0, 1, 2, 0, 1] (5 rounds); earlier base rounds: [1, 0, 14, 0] (the 14 on a film that was the second journey)
Single Cmd-T film, all presses of those rounds:
| arm | presses | drops/press p50/p95/max (sum) | main missed vsyncs p50/p95/max | commit_ms p50/p95/max |
|---|---|---|---|---|
| base | 50 | 1/2/3 (50) | 7.5/10/12 | 24.9/42.4/55.5 |
| head | 70 | 1/2.5/14 (84) | 4/7.5/10 | 24.7/42.3/50.0 |
Rapid pairs (head 4 rounds, base 2 rounds; 2nd press from pair 1):
| press | arm | drops/press p50/p95/max (sum) | main missed vsyncs | commit_ms p50/p95/max |
|---|---|---|---|---|
| 2nd | base | 34/35.1/37 (599) | 9/11/11 | 21.4/24.1/25.8 |
| 2nd | head | 2/4/4 (83) | 4/7/7 | 26.7/33.0/35.1 |
| 1st | base | 17/19/19 (334) | 12/12.1/13 | 28.2/34.6/35.8 |
| 1st | head | 17/18/19 (660) | 8.5/11/12 | 32.5/34.6/36.0 |
Openings: base 20 spare / 20 cold per film; head 30 of 30 spare in all 4 films.
Capacity 1 vs 2 on one build (head job dfbe3399, debug.new_tab capacity; 2 rounds each, cmux22/23/24)
| case | spares | drops/press p50/p95/max | main missed vsyncs | commit_ms p50/p95/max |
|---|---|---|---|---|
| single Cmd-T | 2 | 1/3.5/13 | 4/6.1/8 | 24.7/36.9/42.6 |
| single Cmd-T | 1 | 1/2.5/12 | 4/6.1/8 | 24.6/33.9/34.3 |
| pair, 2nd | 2 | 2/4/4 | 4/5.1/6 | 27.8/29.3/29.8 |
| pair, 2nd | 1 | 30.5/36/37 | 8.5/10.1/11 | 17.1/23.3/26.1 |
| pair, 1st | 2 | 17/17/17 | 9/10/10 | 31.5/34.8/35.3 |
| pair, 1st | 1 | 17/19.1/20 | 10/11.1/12 | 32.9/34.2/34.4 |
Single Cmd-T commit is the same with one or two spares (24.7 vs 24.6 ms p50). The pair's 2nd-press commit rise (17.1 -> 27.8 ms p50) comes with the spare: with one spare that press loads cold, its turn adds the tab and the rest of the page loads later; with two it adopts a loaded page and shows it in its turn (the showNow path of a spare), so the turn does more and the frames after it far less (drops 30.5 -> 2).
P0 proof (real app, head 6f12bb74b14)
- Display step c5876d38cc416cc1881c8ed8 at 6f12bb74b144896b4d10a40a192446bbb0d1ccdf (cmux14, gui-session-v1, CMUX_TEST_REQUIRE_GUI=1), exit 0: NewTabRapidPairTests 4 of 4, NewTabSpareContextTests 1 of 1, NewTabSpareVisibilityTests 1 of 1, NewTabOpenFramesTests 3 of 3 passed on screen.
Observed / Not run
Observed: the red and green steps above; 6 judged review rounds and 1 profile round (plus 6 earlier rounds on 03b6144e0b0 and eba59673d60).
Not run or not known: a memory figure for the second page (debug.new_tab reports the footprint of WebKit pages only; these spares are Chromium pages, so footprint_mb is null); the fix for the first press after launch (the second spare's load overlapping it); last_retarget_ms with two spares; a Release compile on its own (the bench builds compiled the head).
Changelog
Changed: Two Cmd-T in quick succession both open a New Tab page that is already loaded.
Proof
No capture: the change is timing only; the page drawn is the same. Films are in the measurement above.