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)
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 navigationpendingRestore, an agent's loadloadAfterAttach) 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, whichload()sets early. A later commit in the same load paints again. CEFTab.onReadyForKeysreports readiness once per browser and writesready-for-keysto the lifecycle trace.TabContentCacheforwards it toonBrowserReady, andCreationInputCoordinator.pageArrivedflushes 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.pageExpiredstops 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.
- Reds (each fails before its fix):
- 7079f16: step ee930d3280a8ba71026cdae6,
AwaitedContentTests. - 21881f5: 921e626290d0284f228a78f3.
- d1aab43: 0b48071fd1da53cbcc6f5878.
- 2a4f696: fc583d59686d0e69e6d6bd51.
- 43eb924: 3ff6571e4175b1529f00ab82.
- 6313093: 0a3c44dc8d62c2d272dcd41a.
- 983e424: 13cdb8241296a622d1fc04ee.
- 7e14930: 6aacbf00711ead25e442db13.
- 7079f16: step ee930d3280a8ba71026cdae6,
- Green ce9d115: step 9a92bc5c3dba30be936d95fa, 19 test runs passed:
AwaitedContentTests,CEFDeferredFocusTestsandCreationInputHoldTests5x each, plusColdFocusedTerminalTypeAheadTests,ColdBrowserFocusKeysTests,PageFirstKeysTestsandCEFFirstPaintBaseTests. Green cf2c412 (this head: the trace lines and the click-flush removal): step d4925535d564751686a741aa, 19 test runs passed. - Journeys (REOPEN VIDEO, real keys on minis), on build 1c6390004fb94fd0fdb813af (ce9d115 + #32537):
- Cycle B passed 3/3 in each of two sets.
- In one round of the six, the hold reached its 2 s bound (see "Not in this PR").
- browser-click-then-type passed 3/3.
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.pageReadyandpageCreatedlive inCEFTab+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.