fix(focus): keys typed right after a reopen wait until the restored page has loaded, then reach it in order (reopen-browser-plain-keys cycle B)

Merged
LA
3d ago
+378-29

Summary

After Cmd-Q (keep) and reopen, plain keys typed at once into a restored web page were lost (P0 reopen-browser-plain-keys, cycle B; journey kit cp/reopen-video-kit@4ff1d5e5ce6). Lawrence's rule: a key typed before the restored content is ready is held, then delivered in order, never dropped.

Cause: the window key hold (CreationInputCoordinator, REOPEN-F4 #31684/#31707/#32030) waited for a restored Chromium tab only until its pane showed the tab. A restored tab shows before Chromium creates its browser, and before its page can take input. Keys sent on earlier went to the cmux window, to the empty document that the restore then replaced, or to a page that Chromium had not painted yet (Chromium drops input to a document before its first paint).

Fix: a shown Chromium tab is awaited until its page is ready for keys (AwaitedContent.Pane.shownPageReady, BrowserTab.pageReady):

  • A page that was asked to show something (an address pendingURL, a saved navigation pendingRestore, an agent's load loadAfterAttach) is ready once its main frame's document has loaded (a real page's load end, or a non-aborted error page) and has painted content (first contentful paint, CEFFirstPaintBridge), in either order. These are Chrome's main-frame signals. Subframes (ads, iframes) are not awaited.
  • A blank target (a New Tab page) or an error page needs no paint. A page with nothing to load is ready at once.
  • A first paint that reaches the app during Chromium's own load and before that load's first commit is kept for the committing document. The page channel can arrive before .loadStart. "Chromium's own load" means its .loadingState, not the app's state, which load() sets early. A later commit in the same load paints again.
  • CEFTab.onReadyForKeys reports readiness once per browser and writes ready-for-keys to the lifecycle trace. TabContentCache forwards it to onBrowserReady, and CreationInputCoordinator.pageArrived flushes the holds that await that tab, each key once, in order.
  • Cmd-L passes an empty page hold once Chromium has created the page (pageHoldKeeps(pageCreated:)).
  • Bound: holdLimit (2 s). After it, CreationHoldLog.pageExpired stops a new hold for that tab until its page is ready or a new page is installed for it.
  • While a page is not yet ready, the lifecycle trace records each readiness signal (ready-wait, cef-loading, cef-commit, cef-load-end, cef-load-error, cef-first-paint, cef-load-event-without-tab). This is used to find the open case below.

Cost: per key-down, one cast and a few flag reads when the focused content is a Chromium page. Per load, a few booleans and, before ready, a few trace lines.

Not in this PR:

  • Keys follow a click to another target (#33198). A click before the page is ready leaves the held keys held. A click into the page field works: the keys wait and then reach the page. A click into the address bar sends the held keys there when the hold ends. The root fix is a page lane. A first try flushed the hold before the click, which lost the keys (review 6597191272); it is removed (cf2c412).
  • One round in six waits the full 2 s. In that round the restored page sent no load event after its navigation, so the keys reached it in order but 2 s late. It is under investigation with the traces above, on measurement build d0dd2d4b.

Testing

Fleet, 5 runs each, proof-step.py. A step counts only if it has "Test run with N tests", N > 0.

Observed / Not run

  • Known limit: in about 1 of 43 restores no paint report comes and the keys wait the 2 s bound, then arrive in order. Follow-up: #33940 (ask the page once for a buffered paint).

  • Trace rounds on the measurement build d0dd2d4b (#32633 c9ba806 + #32537): 43 rounds, 0 keys lost, 1 round at the 2 s bound (rpk-trace2-cmux22/a3: loading stopped at 140 ms, no first paint report). Probe build 2a36b363 (cf2c412 + #32537 466e904): 12/12 pass, 0 at the bound.

  • Without this PR (feat 48a4f8aedf5e, build e543e68d, P0 JOURNEY RUNNER on cmux19): the hold ended when Chromium created the browser (362 ms), before the restore navigation, and the restored page got 0 of the 11 cycle B keys (STATUS 9185).

  • Observed: the rounds above. On trace build d0dd2d4b, the first 4 counted rounds were ready 1–2 ms after the first paint, with no round at the bound so far (STATUS 8478 and later).

  • Not run: a coordinator-level test of pageArrived → flush. The ready paths are tested at the tab level (CEFDeferredFocusTests) and the hold level (AwaitedContentTests, CreationInputHoldTests).

  • Known open (reviews): an error page counts as ready at .loadError, before it commits and paints. pageReady and pageCreated live in CEFTab+Visibility.swift.

Changelog

Fixed: Keys you type at once into a web page that a reopen restores reach the page, in order

Proof

No capture: key routing; the proof is the tests and the journeys.