perf(next): Cmd-N shows a workspace view built before the key

Merged
LA
2h ago
+1310-96

Summary

Cmd-N no longer builds the workspace view tree in the key's turn (STATUS 15250: WindowController.show took 17.9 ms of main thread per press before the first frame; WorkspaceContentController.init 6.8 ms, the window entry 5.9 ms). As Chrome keeps a spare renderer, the local daemon reserves the next plain Cmd-N's workspace before the key, and the key main window builds its view at a quiet moment. The key only shows it.

  • Store (CmuxNextDaemon). DaemonStore.reserveWorkspace makes the next workspace's WorkspaceModel with its screen in ProvisionalWorkspaceModels.reserved. It is outside workspaces, the index and the sidebar, and outside the settled-create prune. ProvisionalWorkspaceOverlay.apply moves the reserved model and screen into the pending create's models when the intent's key matches. It sets the press's name and appends the screen only when the model does not hold it. Undo, confirm and the stand-in hand-over (#36215) do not change. ReservedWorkspaceUse (hit / miss / not plain) and ReservedWorkspaceRelease (pressure / disconnected / unshown) hold the decisions and the bench span names.
  • Page (AgentTabStore). The reservation gives its first tab its New Tab page: the prewarmed spare, else a page made then. At the key, the page gets the press's key-hold token, which keeps the same screen. It gets a new context (cmdN.reserved.recontext) only when the press's folder is a different one (a focused terminal's).
  • Window. WindowController.reservedContent (IdleSpare, background work after a frame commit) builds the view on the reserved model. The view is mounted hidden and parked in the content area. It is in its own slot: never in parked, mountedContents, the record saver or parked_workspaces. Only the key main window holds it. show takes it: no WorkspaceContentController.init and no addSubview in the key's turn.
  • Release. Warning or critical memory pressure releases the window views and the store reservation with its page, and nothing is reserved again until the pressure is normal. Other releases: the window closes or another window becomes key (the view only), the daemon is not connected, or a press took the key but did not show it.
  • Marks: cmdN.reserved.hit, cmdN.reserved.miss.none, cmdN.reserved.miss.daemon, cmdN.reserved.recontext, cmdN.reserved.released.<reason>.

Depends on #36939 (merged first). The branch contains #36939's commits, so the push range carries its -121 app_lines and this PR's +17 glue (tip net -104). No arch exception.

The decisions are in packages, each with a unit test that pins today's output: WorkspaceSpareSlot (CmuxNextAppCore: the refill in three idle tasks, reserve, build cmdN.reserved.build.init, mount cmdN.reserved.build.mount; it drops the other windows' views after the frame), DaemonStore reservation and ReservedWorkspaceUse / ReservedWorkspaceRelease (CmuxNextDaemon), WorkspaceSpawn.isPlainNewTabPage, startFolder, terminalEnvironment (CmuxNextAppCore), DaemonConnection.pinWorkspaceBeforeCreate, newFirstTab, NewConversationTabRequest.firstAgentTab, Response.tabKey (CmuxNextDaemon), DaemonService.settledCheck, reservesWorkspace (CmuxNextDaemonService), NSView.addHiddenSubview (CmuxNextDesign), and the parking eviction decision in ParkedWorkspaceBudget (lead rule: meet app_lines by moving logic to its package). The App keeps only the calls.

Moved request lines (R-INPUT's area): the first-tab request in AgentTabStore.showWorkspace's confirm closure (the NewConversationTabRequest(agentSession:workspace:origin:mutationID:) init, request.transaction = transaction, and the retryingLateReplies(NewConversationTabRequest.command, until: giveUp) send) is now connection.newFirstTab(.firstAgentTab(...), until: giveUp). The request bytes are unchanged (ReservedWorkspaceTests.theFirstTabRequestKeepsItsFields compares the JSON encodings).

Not changed

  • After the first frame, one main-thread burst runs 88-118 ms after the key: median 24.5 ms on-CPU over 27 ms wall (11-34 ms). It is the hand-over, when the daemon's create replies land. In 7/8 presses it comes after the first frame. No single item is the cause: the AppKit display cycle and CA commit take about 9 ms, RegistryControlBridge.refresh 1.9 ms, the observation applies and layout sync about 2 ms, PaneController.Spare.init 1.0 ms, ShellWindow.addChildWindow (page window) 1.0 ms, AgentPaneNewTabActions.step 0.8 ms, the sidebar same-turn rows 0.6 ms, the CEF pump 0.5 ms and the page focus grant 0.4 ms. This PR does not change the hand-over path. Stand-in view reuse (STATUS 14080) is the follow-up. (Time Profiler, base 70a6e34c, cmux22, 8 presses, INSTANT OWNER.)
  • Memory: each local daemon keeps one more live New Tab page (the reserved workspace's), beside the New Tab spare pool. It goes at warning or critical memory pressure, and none is reserved until the pressure is normal.
  • Two main windows, one frame: if window B becomes key and gets Cmd-N before the after-commit hop that drops window A's reserved view, A keeps a hidden view of the model B shows until A's next fill drops it (the same teardown path as a parked workspace that moved to another window).
  • Only a plain Cmd-N takes the reservation: the New Tab page into a window, with no chat, session, command, browser profile, folder or key of its own, on this Mac's daemon. Every other create takes the old path.

Testing

  • Red: fleet step da4ea27f5f0544bcf390ef36 at 94b27383f049 fails at CmdNPrebuiltWorkspaceViewTests.swift:36 "Cmd-N built its workspace view in the key's turn: it was not in the window before the key".
  • Green: step 81f962e402491e273f7a4426 at eba896e985: all 16 suites passed (CmdNPrebuiltWorkspaceViewTests, ReservedWorkspaceKeepsFocusTests, ReservedWorkspaceTests, WorkspaceSpareSlotTests, WorkspaceSpawnCreatePathTests, ParkedWorkspaceBudgetTests, InstantCreateIntentTests, NewWorkspaceDefaultSlotTests, CmdNThenNewTerminalTabTests, CmdNStandInTargetTests, CmdNSlowDaemonKeepsWorkspaceTests, CmdNAlwaysNewWorkspaceTests, CmdNRowSlotTests, PendingTabSelectTests, NewTabHoldFocusTests, NativeNewTabTypeAheadTests). CmdNThenNewTerminalTabTests runs 2 and 3 at the head: steps fefd8ba367b358c4dcc73dba and 6b7c59c3b058e28cd5a8341a, both passed.
  • Regression found on the fleet and fixed: the view built before the key mounted no pane (the reserved model is in no store index). CmdNThenNewTerminalTabTests is green on the base (step c59fa9993b0f486e3f8b4319) and was red on 798317eb28 (step ee9a3b2a3cf413a8999bc85e). The new focused-pane assertion in CmdNPrebuiltWorkspaceViewTests is red on 798317eb28 (+ the new test file, ee1b010af18c): step d8889423d0135399c3a07416, CmdNPrebuiltWorkspaceViewTests.swift:43 (shown.focusedPane -> nil) != nil.
  • A/B on cmux22 (cmdn-refill kit 64a243c64fc base/fix/base/fix, cmdn-latency, one Cmd-T bench round; with after_main_max_ms, after_missed, the reserved hit rate and the longest refill step): INSTANT OWNER posts the table as a PR comment when it ends. The merge does not wait for it.
  • Focus mutant: step b4b7253c82fce16c52b1fdb4 at 9e9f27bb59 (the reserved controller built with parked: false) is red: the refill took the keyboard to the reserved New Tab page ("the keyboard is not in the terminal: PageWKWebView"). A first mutant that called makeFirstResponder from the mount passed, because ShellWindow.makeFirstResponder already refuses a page's own focus outside an input event (AgentPaneView.refusesSelfFocus) and a plain view takes no first responder. The real risk is the topology of an unparked controller, so the mutant builds the reserved controller with parked: false.
  • Launch: fresh-home step 710e3811adc9e1c9d9101143 on build e286e3f1: up 30 s, no crash, no exception, the first-run history page over Home rendered at 12.9 s. The validator verdict "Home did not render in a visible window" is the same base finding as on the exact base build (step 5e86239260e2e4e35a7ba376; see the PR comment).
  • Builds: base df6b24b068f9445042f07e9e (61640bdb6a2: the red test + #36939, no fix commit), fix e286e3f17f6d9f67360caa1a (eba896e985)

Observed / Not run

Observed: the red test at the base and green at the head (steps above); the 16-suite green set at the head; CmdNThenNewTerminalTabTests in 3 separate runs; the focus mutant; a fleet Release build of the head; a fresh-home launch of the head and of the exact base (both show the first-run history page over Home and both get the same validator verdict, "Home did not render in a visible window": a base finding, see the PR comment); the A/B (PR comment).
Found and fixed on the fleet: a Cmd-N between the refill's reserve and build steps built a second view of the shown workspace (CmdNThenNewTerminalTabTests, 1 of 4 tests). Each refill step now runs only while its workspace is still the reservation; cmdNDuringTheRefillBuildsNoSecondViewOfItsWorkspace and WorkspaceSpareSlotTests pin it.
Follow-ups (agreed with R-INPUT, not in this PR): the pane fallback only while the model is reserved (daemon.store.reserves(workspace)), with an empty-view test; autoresizingMask = [.width, .height] at the hidden mount (a resize before the key now resizes the view in the key's turn); the focus test pauses the reservation during its setup, so the mutant fails at the post-refill check; R-INPUT's reserved-path hand-over test.
Not run: a STRESS round; two main windows by hand (the slot's other-window drop is covered by WorkspaceSpareSlotTests.aFillDropsTheOtherWindowsViews only); macOS 14/15.

No capture: the change keeps the same pixels on screen. Cmd-N shows the same New Tab page workspace, built earlier. The evidence is the latency table and the tests.

Changelog

  • Changed: Cmd-N shows its new workspace sooner. The workspace view is built before the key.

Review (R-INPUT): SHIP at eba896e9858acb7b485f50b1a7110e277108dae0