Repository navigation
fix(next): keep New Tab prerendered composer warm - #29760
Conversation
Leo asked for every New Tab and new workspace to show a fully rendered composer on the first frame. Keep the parked renderer visible to its offscreen parking view and immediately begin a replacement warmup after each handout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-790564c45249a0c4cd53
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-790564c45249a0c4cd53
Bugbot needs on-demand usage enabledBugbot uses usage-based billing for this team and requires on-demand usage to be enabled. A team admin can enable on-demand usage in the Cursor dashboard. |
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Not verified: macOS jobs did not run: compile and app tests were skipped on CI passes on Written by Shipped in:
|
Do not create a replacement web view for a cold handout that found no spare; preserve the idle path when no parked page was available. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-790564c45249a0c4cd53
|
Review (codex gpt-6-astra high, router-review): SHIP at fc66f4b No findings. Visual review was not applicable to this native lifecycle/performance fix; the code verdict is SHIP at the exact head. |
|
Review (codex, router-review): SHIP at fc66f4b |
|
Review (codex, router-review): SHIP at fc66f4b |
… the bar's palette actions are read in background slices PACING TRIAGE #32910: each Cmd-T (idle, dark, animations off, 120 Hz) dropped 4-17 refreshes 50-150 ms after the key; the app's own frame trace missed 8-11 vsyncs and the action committed at 29 ms p50. Two jobs of the open held the main thread and neither shows in that open: - The spare pool made the next spare page on the next main-actor turn after the handout (#29760): a web view and a Chromium browser in the key's own pass before its commit (4 ms), then its Chromium setup calls, show and first load in the open's frames. It is now made once input has been quiet for 250 ms after the handout (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 New Tab bar's palette actions were read in one task the frame after the page showed (RegistryPaletteProvider.makeItems over every catalog action, 7-10 ms on an E core), and the whole bar was JSON-encoded on the main thread for each page. RegistryPaletteProvider.makeItems is now an ItemsPass that can read in pieces; PaletteController.commandListing() reads it a slice at a time, and AgentPaneNewTabActions runs the slices as budgeted background work (FrameBatchQueue .background: 0.5 ms a task, yielding to input and to the frame's budget), as Chrome's scheduler runs best-effort work in the idle time of each frame. 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: the person waits for that list.
…round task; the refill keeps its time - AgentPaneNewTabActions.list(wanted:_:) only queues: the palette's context and catalog are read in the first background task, which checks then whether a page lacks the actions (Cmd-N's page joins newTabPages after the call; the check in the caller's turn left it without actions). - The pages take the list as background work too, and a newer request drops an older list still being encoded (generation fence on every request). - PaletteCommandListing arranges the rows in the task after the last read, so no task holds more than one slice. - NewTabSparePool: the handout's replacement comes openSettle (250 ms) after the handout and input does not push it back, so rapid Cmd-T still finds a page loaded or loading (Takes back the next-turn refill of #29760 only for the open's frames).
… bar's palette actions are read in background slices (#32979) * test(new tab): Cmd-T makes the next spare and reads the bar's palette actions outside its open's frames (red) PACING TRIAGE #32910 (120 Hz films of Cmd-T on feat 5851715): each Cmd-T dropped 4-17 refreshes 50-150 ms after the key while the main thread made the next spare page (a web view and a Chromium browser) in the key's own pass, and read every palette action for the bar in one 7-10 ms task. These fail on the base: the next spare is made within 60 ms of the open, and the first and the last palette action are read in the same main-thread task. * perf(new tab): Cmd-T makes the next spare after its open settled, and the bar's palette actions are read in background slices PACING TRIAGE #32910: each Cmd-T (idle, dark, animations off, 120 Hz) dropped 4-17 refreshes 50-150 ms after the key; the app's own frame trace missed 8-11 vsyncs and the action committed at 29 ms p50. Two jobs of the open held the main thread and neither shows in that open: - The spare pool made the next spare page on the next main-actor turn after the handout (#29760): a web view and a Chromium browser in the key's own pass before its commit (4 ms), then its Chromium setup calls, show and first load in the open's frames. It is now made once input has been quiet for 250 ms after the handout (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 New Tab bar's palette actions were read in one task the frame after the page showed (RegistryPaletteProvider.makeItems over every catalog action, 7-10 ms on an E core), and the whole bar was JSON-encoded on the main thread for each page. RegistryPaletteProvider.makeItems is now an ItemsPass that can read in pieces; PaletteController.commandListing() reads it a slice at a time, and AgentPaneNewTabActions runs the slices as budgeted background work (FrameBatchQueue .background: 0.5 ms a task, yielding to input and to the frame's budget), as Chrome's scheduler runs best-effort work in the idle time of each frame. 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: the person waits for that list. * test(new tab): follow the page's key to the store's; the Cmd-N first page gets the palette's actions The red run at f28cced timed out waiting for the bar's actions: the test read them under the provisional key Cmd-T makes, which the store's key replaces. The slice check now asks for a first-action read in a turn with no last-action read. New case: a page that joins newTabPages after the fill was asked for (Cmd-N's first page) gets the actions (review of #32979). * fix(new tab): review of #32979: the listing starts in its first background task; the refill keeps its time - AgentPaneNewTabActions.list(wanted:_:) only queues: the palette's context and catalog are read in the first background task, which checks then whether a page lacks the actions (Cmd-N's page joins newTabPages after the call; the check in the caller's turn left it without actions). - The pages take the list as background work too, and a newer request drops an older list still being encoded (generation fence on every request). - PaletteCommandListing arranges the rows in the task after the last read, so no task holds more than one slice. - NewTabSparePool: the handout's replacement comes openSettle (250 ms) after the handout and input does not push it back, so rapid Cmd-T still finds a page loaded or loading (Takes back the next-turn refill of #29760 only for the open's frames). * test(new tab): count the bar's palette reads by their bench spans The turn probe on two actions' palette titles read nothing for the first action on the fleet (step 1d232f52); the read count now comes from the newTab.paletteActions bench spans, one per main-thread task that reads palette actions. * fix(new tab): review of #32979: the listing's start is its own timed task; an unchanged list is handed over as background work The slice test asks for at least four palette read spans (start, two or more slices, arrange), so one task reading every action fails it. --------- Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>
…palette's actions as budgeted background work (#33076) * test(new tab): Cmd-T makes the next spare and reads the bar's palette actions outside its open's frames (red) PACING TRIAGE #32910 (120 Hz films of Cmd-T on feat 5851715): each Cmd-T dropped 4-17 refreshes 50-150 ms after the key while the main thread made the next spare page (a web view and a Chromium browser) in the key's own pass, and read every palette action for the bar in one 7-10 ms task. These fail on the base: the next spare is made within 60 ms of the open, and the first and the last palette action are read in the same main-thread task. * perf(new tab): Cmd-T makes the next spare after its open settled, and the bar's palette actions are read in background slices PACING TRIAGE #32910: each Cmd-T (idle, dark, animations off, 120 Hz) dropped 4-17 refreshes 50-150 ms after the key; the app's own frame trace missed 8-11 vsyncs and the action committed at 29 ms p50. Two jobs of the open held the main thread and neither shows in that open: - The spare pool made the next spare page on the next main-actor turn after the handout (#29760): a web view and a Chromium browser in the key's own pass before its commit (4 ms), then its Chromium setup calls, show and first load in the open's frames. It is now made once input has been quiet for 250 ms after the handout (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 New Tab bar's palette actions were read in one task the frame after the page showed (RegistryPaletteProvider.makeItems over every catalog action, 7-10 ms on an E core), and the whole bar was JSON-encoded on the main thread for each page. RegistryPaletteProvider.makeItems is now an ItemsPass that can read in pieces; PaletteController.commandListing() reads it a slice at a time, and AgentPaneNewTabActions runs the slices as budgeted background work (FrameBatchQueue .background: 0.5 ms a task, yielding to input and to the frame's budget), as Chrome's scheduler runs best-effort work in the idle time of each frame. 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: the person waits for that list. * test(new tab): follow the page's key to the store's; the Cmd-N first page gets the palette's actions The red run at f28cced timed out waiting for the bar's actions: the test read them under the provisional key Cmd-T makes, which the store's key replaces. The slice check now asks for a first-action read in a turn with no last-action read. New case: a page that joins newTabPages after the fill was asked for (Cmd-N's first page) gets the actions (review of #32979). * fix(new tab): review of #32979: the listing starts in its first background task; the refill keeps its time - AgentPaneNewTabActions.list(wanted:_:) only queues: the palette's context and catalog are read in the first background task, which checks then whether a page lacks the actions (Cmd-N's page joins newTabPages after the call; the check in the caller's turn left it without actions). - The pages take the list as background work too, and a newer request drops an older list still being encoded (generation fence on every request). - PaletteCommandListing arranges the rows in the task after the last read, so no task holds more than one slice. - NewTabSparePool: the handout's replacement comes openSettle (250 ms) after the handout and input does not push it back, so rapid Cmd-T still finds a page loaded or loading (Takes back the next-turn refill of #29760 only for the open's frames). * test(new tab): count the bar's palette reads by their bench spans The turn probe on two actions' palette titles read nothing for the first action on the fleet (step 1d232f52); the read count now comes from the newTab.paletteActions bench spans, one per main-thread task that reads palette actions. * fix(new tab): review of #32979: the listing's start is its own timed task; an unchanged list is handed over as background work The slice test asks for at least four palette read spans (start, two or more slices, arrange), so one task reading every action fails it. * test(new tab): the native New Tab page reads the palette's actions in budgeted slices (red) Follow-up of #32979 (lead: no partial fixes): the native page (tabs.nativeNewTabPage) still read every palette action in one main-thread task, on the frame after it showed or at the first keystroke. Fails on 4ad8532: one newTab.paletteActions span per page. * perf(new tab): the native New Tab page and the History page read the palette's actions as budgeted background work Follow-up of #32979 (lead: no partial fixes). The native New Tab page (tabs.nativeNewTabPage) read every palette action in one main-thread task on the frame after it showed, or in its first keystroke's turn; the History page read them in its open's turn. Both now go through AgentPaneNewTabActions: - NativeNewTabModel asks for the actions (PaletteActionsRequest) and takes them when they come; a keystroke's turn reads nothing (the rows show the actions once listed, as Chrome's omnibox shows a provider's matches when they arrive). An adoption drops the earlier page's late answer. - AgentPaneNewTabActions keeps every waiter of a listing (the web pages' fill and each native page), not only the last one. - The History page opens without the actions and fills them as the New Tab pages do. * fix(new tab): review of #33076: the test reads the new tab's own page; a closed native page starts no listing - NativeNewTabSpareTests: the cold pass found the first, parked page's model; it now takes the page of the tab it opened (pages.existingView), checks it is a new model, and counts the reads before and after the keystroke. - PaletteActionsRequest carries the page's wanted check: a native page that is gone (or adopted again) by the listing's first task starts no read.
…ond Cmd-T does not load cold (#33136) * test(new tab): Cmd-T makes the next spare and reads the bar's palette actions outside its open's frames (red) PACING TRIAGE #32910 (120 Hz films of Cmd-T on feat 5851715): each Cmd-T dropped 4-17 refreshes 50-150 ms after the key while the main thread made the next spare page (a web view and a Chromium browser) in the key's own pass, and read every palette action for the bar in one 7-10 ms task. These fail on the base: the next spare is made within 60 ms of the open, and the first and the last palette action are read in the same main-thread task. * perf(new tab): Cmd-T makes the next spare after its open settled, and the bar's palette actions are read in background slices PACING TRIAGE #32910: each Cmd-T (idle, dark, animations off, 120 Hz) dropped 4-17 refreshes 50-150 ms after the key; the app's own frame trace missed 8-11 vsyncs and the action committed at 29 ms p50. Two jobs of the open held the main thread and neither shows in that open: - The spare pool made the next spare page on the next main-actor turn after the handout (#29760): a web view and a Chromium browser in the key's own pass before its commit (4 ms), then its Chromium setup calls, show and first load in the open's frames. It is now made once input has been quiet for 250 ms after the handout (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 New Tab bar's palette actions were read in one task the frame after the page showed (RegistryPaletteProvider.makeItems over every catalog action, 7-10 ms on an E core), and the whole bar was JSON-encoded on the main thread for each page. RegistryPaletteProvider.makeItems is now an ItemsPass that can read in pieces; PaletteController.commandListing() reads it a slice at a time, and AgentPaneNewTabActions runs the slices as budgeted background work (FrameBatchQueue .background: 0.5 ms a task, yielding to input and to the frame's budget), as Chrome's scheduler runs best-effort work in the idle time of each frame. 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: the person waits for that list. * test(new tab): follow the page's key to the store's; the Cmd-N first page gets the palette's actions The red run at f28cced timed out waiting for the bar's actions: the test read them under the provisional key Cmd-T makes, which the store's key replaces. The slice check now asks for a first-action read in a turn with no last-action read. New case: a page that joins newTabPages after the fill was asked for (Cmd-N's first page) gets the actions (review of #32979). * fix(new tab): review of #32979: the listing starts in its first background task; the refill keeps its time - AgentPaneNewTabActions.list(wanted:_:) only queues: the palette's context and catalog are read in the first background task, which checks then whether a page lacks the actions (Cmd-N's page joins newTabPages after the call; the check in the caller's turn left it without actions). - The pages take the list as background work too, and a newer request drops an older list still being encoded (generation fence on every request). - PaletteCommandListing arranges the rows in the task after the last read, so no task holds more than one slice. - NewTabSparePool: the handout's replacement comes openSettle (250 ms) after the handout and input does not push it back, so rapid Cmd-T still finds a page loaded or loading (Takes back the next-turn refill of #29760 only for the open's frames). * test(new tab): count the bar's palette reads by their bench spans The turn probe on two actions' palette titles read nothing for the first action on the fleet (step 1d232f52); the read count now comes from the newTab.paletteActions bench spans, one per main-thread task that reads palette actions. * fix(new tab): review of #32979: the listing's start is its own timed task; an unchanged list is handed over as background work The slice test asks for at least four palette read spans (start, two or more slices, arrange), so one task reading every action fails it. * test(new tab): a rapid second Cmd-T adopts a spare too (red) Lead decision on #32979: a Cmd-T 150 ms after the one before must not load cold. On #32979's head the 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. The test also asks that no spare is made in the pair's frames and that dropAll leaves no spare parked. * perf(new tab): two spares, made after an open settled as background work A rapid second Cmd-T (150 ms after the first) loaded its New Tab page cold: the pool kept one spare and made the next 250 ms after the handout. As Chrome keeps a spare renderer ready and makes the next one as best-effort work after the navigation it served: - AgentPaneSpareSlot holds up to a capacity; 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. - AgentPaneSpareRefiller (CmuxNextAgentPane) decides when a handout's replacement is made: after the key's frame committed, the open's tab-strip motion ended (TabStripView.whenStill) and the adopted page drew its first frame in its pane, then 250 ms of idle time (input does not push it back), as one background-class task on the app's scheduler (it yields to the person's input and the frame's budget). The second spare is made only after the first one drew, so two makes never share frames. The quiet-input warm stays armed as the backstop. CmuxNextApp app_lines -2. * test(new tab): the rapid-pair test asks that every opening of the pair is a spare, and waits for the fill without an issue The red run (step 600126c2) failed at a count of 3 openings: a cold open also records the store tab's opening. * fix(new tab): the second spare is made once input is quiet, not when the first one painted Films of the head (6c09d472): the first Cmd-T of each film dropped 12-13 refreshes 130-200 ms after the key. A parked Chromium page reports its first paint only once it shows, so the second spare's make (waiting on that paint) ran in the first open's frames. It now waits for the pool's quiet-input warm (750 ms, pushed back by each key or click). * test(new tab): review of #33136: a 250 ms pair and an unpainted second spare; waits fail when they time out - aSecondCmdT250msLaterRestartsTheRefill: the first open's refill wait must not land in a second open 250 ms later. - anUnpaintedSecondSpareDoesNotHoldTheFirstsContext: the next spare handed out draws the pane's context while the other spare has not painted. - The fill waits with until (fails on timeout); the dropAll check covers every spare made after the pair. * fix(new tab): review of #33136: per-page prepare and covers, no capture in the key's turn, a later open restarts the refill, spares only at normal memory pressure - prepareParked prepares each parked page that finished its handshake and first paint, and waits (once per page) only on the others; preparedFor follows the next spare handed out. It does nothing while an open settles. - AgentPaneSpareCovers (CmuxNextAgentPane): one capture generation per page, taken when the page is parked, fitted or prepared; take() captures nothing (it took a WebKit snapshot of the next spare during Cmd-T's motion). - AgentPaneSpareRefiller: each handout cancels the pending wait and starts its own (a generation fence), and no parked page changes until the latest open settled; the pool's quiet-input warm ends a wait whose page never framed. - Memory: spares are made only at normal pressure; after a pressure drop the pool waits for normal; one spare on a Mac with less than 16 GB. - debug.new_tab lists every parked spare (pid, footprint) and takes capacity N (an A/B of one against two spares on one build). - warmNow's recycled-page path arms the second spare; whenStill's doc comment sits on whenStill. * test(new tab): the refill and context tests fail without the review fixes The red run at cfaa252 passed all three on the old code. Now: - aSecondCmdTRestartsTheFirstOnesRefill: the second open's page never reports its frame, so the first open's pending wait must not make a spare in the 500 ms after the second key. - anUnpaintedSecondSpareDoesNotHoldTheFirstsContext: the context changes after both spares are parked (the second never paints). * test(new tab): a launch makes one spare until one served an open; the rig sets capacity 2 and hands out the launch spare Re-review of #33136 (B2): the second spare's load met the session's first Cmd-T (12-13 dropped refreshes on that press). * fix(new tab): the second spare only after a spare served an open; review lows - NewTabSparePool makes its second spare only after this session used a spare (the first handout's refill makes it otherwise): a launch loads one page as before, and the second page's load never meets the session's first open, as Chrome makes its spare renderer after the navigation it served. - dropAll clears the paint waits; the quiet-input backstop that ends a pending open prepares the parked pages; no native spare under memory pressure; debug.new_tab reports each spare's own window. * test(new tab): a retarget to another window before any open still makes one spare (red) Re-review of #33136: the one-page-at-launch gate was only on warmNow's trailing calls, so a retarget (another window clicked first) made page 2 after the quiet-input warm. * fix(new tab): one page until a spare served an open, on every make path One condition (wantsPage: fewer than capacity spares, or than one before a spare served an open) in warmNow's make guard and scheduleWarm, instead of gates on two trailing calls: a retarget, the return to normal memory pressure, the capacity setter and the WebKit probe no longer make page 2 at launch. --------- Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>
New Tab and new workspace handouts now keep a fully rendered composer warm through the first visible frame. The parked page stays attached to its offscreen parking view so its WebKit or Chromium renderer remains active, and each handout schedules a fresh spare on the next main-actor turn instead of waiting for the idle-input debounce.
Leo's request (2026-10-31): "EVERY NEW fing WORKSPACE" and "EVERY CLICK INTO A NEW TAB PAGE" "fing renders the composer from fing 0" — "where is the cache", "where is the fing prerendering", and "where is the f***ing reuse".
The exact regression was #26975's parked-cover completion setting the spare
AgentPaneViewhidden.PageWebViewthen reported the parked page as not visible and suspended its renderer; adopting it necessarily paid a cold render. The offscreen parking view already keeps the page out of sight while preserving renderer visibility, so the spare now remains warm. Rewarming after handout is scheduled at zero delay on the main actor and retains the existing one-shot timer cleanup; no new repeating timer or observer was added.Changelog
Changed: parked New Tab renderers remain warm, and the next warm renderer starts immediately after each New Tab or new workspace handout.
Validation
NewTabSpareVisibilityTestsand the app compile run on the fleet at the final head.NewTabPage.openandAgentTabStore.showWorkspaceboth use the same warmed spare handout path.Performance
NewTabSpareVisibilityTestson final headAgentTabStore.showWorkspacehandout path + final-head compileNewTabSparePooltimer pathAgent session: agent session
🤖 Generated with Claude Code