diff --git a/main.js b/main.js index f211b695..517e6648 100644 --- a/main.js +++ b/main.js @@ -1330,26 +1330,55 @@ function restoreTabs() { } // ---- deferred overlay loads ---- // The floating overlays (site-info popover, engine picker, downloads, -// address suggestions, password fill, link-status pill, add-on approval) -// are WebContentsViews created with the window, but their HTML used to be -// loaded in the same tick as chrome.html — seven extra renderers racing the -// toolbar for the first paint. They now load shortly after chrome reports -// did-finish-load, or on first use, whichever comes first. -const overlayLoads = []; // [{ view, file }] waiting for loadOverlays() -let overlaysLoaded = false; +// address suggestions, password fill, link-status pill, add-on approval, +// page dialogs) are WebContentsViews created with the window. A view whose +// page was never loaded has no renderer process; loading its page starts +// one (~20–30 MB each). They used to load all together shortly after the +// toolbar — nine renderers most sessions never use. Now each one loads on +// its first use, except the few used in nearly every session (address +// suggestions, the link-status pill, site info), which are prewarmed one at a +// time once the first page is up (prewarmOverlays). +const overlayLoads = []; // [{ view, file }] not loaded yet function deferOverlayLoad(view, file) { overlayLoads.push({ view, file }); } -function loadOverlays() { - if (overlaysLoaded) return; - overlaysLoaded = true; - for (const { view, file } of overlayLoads.splice(0)) { - try { view.webContents.loadFile(file); } catch (e) { console.warn(`overlay load failed (${file}):`, e?.message); } +function loadOverlay(view) { + const i = overlayLoads.findIndex((o) => o.view === view); + if (i < 0) return; + const { file } = overlayLoads.splice(i, 1)[0]; + try { view.webContents.loadFile(file); } catch (e) { console.warn(`overlay load failed (${file}):`, e?.message); } +} +function overlayLoaded(view) { + try { return !!view.webContents.getURL() && !view.webContents.isLoading(); } catch { return false; } +} +// The show functions send their data right after showing, which a page still +// loading would drop. On first use: load the page, then run `again` (the +// same show call) once it is ready — unless the overlay was closed, or shown +// again, in the meantime (the hide paths call cancelOverlayShow). +const overlayWant = new Map(); // view -> token of the latest deferred show +function deferUntilOverlayLoaded(view, again) { + if (overlayLoaded(view)) return false; + const token = {}; + overlayWant.set(view, token); + overlayReady(view).then(() => { if (overlayWant.get(view) === token) { overlayWant.delete(view); again(); } }); + return true; +} +function cancelOverlayShow(view) { overlayWant.delete(view); } +// Idle prewarm, one at a time, of the overlays used in nearly every session. +let overlaysPrewarmed = false; +async function prewarmOverlays() { + if (overlaysPrewarmed) return; + overlaysPrewarmed = true; + for (const view of [addressPicker, linkStatus, popover]) { + if (!view || !winAlive()) return; + if (overlayLoaded(view)) continue; + await overlayReady(view); + await new Promise((r) => setTimeout(r, 150)); } } -// Resolves once `view` has finished loading its page, loading the overlays -// first if that hasn't happened yet. Bounded so a wedged renderer can't -// hang the caller forever. +// Resolves once `view` has finished loading its page, loading it first if +// that hasn't happened yet. Bounded so a wedged renderer can't hang the +// caller forever. function overlayReady(view, timeoutMs = 4000) { - loadOverlays(); + loadOverlay(view); const wc = view.webContents; try { if (wc.getURL() && !wc.isLoading()) return Promise.resolve(); } catch {} return new Promise((resolve) => { @@ -1379,13 +1408,12 @@ function onChromeReady() { // the first page is up — or after 4 s, whichever comes first. { let went = false; - const go = () => { if (!went) { went = true; startBnsPolling(); } }; + const go = () => { if (!went) { went = true; startBnsPolling(); setTimeout(prewarmOverlays, 1000); } }; setTimeout(go, 4000); try { activeTab()?.view.webContents.once("did-stop-loading", () => setTimeout(go, 500)); } catch {} } // Re-emit any pending update notice — harmless if nothing is pending. emitUpdateAvailable(); - setTimeout(loadOverlays, 250); scheduleStartupUpdateCheck(); refreshRemoteHomeCards().catch(() => {}); } @@ -1671,10 +1699,15 @@ function startIndexer() { for (const settle of indexerPending.values()) settle(false); indexerPending.clear(); if (indexerStopping) return; - // The mirror stays usable meanwhile; only freshness is lost. + // The mirror stays usable meanwhile; only freshness is lost. Logged when + // the restart happens, not here: on app.exit() the child goes down with + // the app and this timer never fires — nothing to report then. const wait = Math.min(30_000, 1000 * 2 ** Math.min(indexerStarts, 5)); - console.warn(`[bns] indexer exited (${code}); restarting in ${wait / 1000}s`); - setTimeout(startIndexer, wait); + setTimeout(() => { + if (indexerStopping || indexer) return; + console.warn(`[bns] indexer exited (${code}); restarting after ${wait / 1000}s`); + startIndexer(); + }, wait); }); indexer.postMessage({ type: "init", resolverPath: RESOLVER, userData: app.getPath("userData"), bundledSnapshot: SNAPSHOT_BUNDLED, torPort: torState === "on" ? TOR_PORT : null }); if (indexerGo) indexer.postMessage({ type: "go" }); @@ -2831,14 +2864,14 @@ function positionPopover() { function showPopover(show) { if (!popover) return; if (show) { - loadOverlays(); + if (deferUntilOverlayLoaded(popover, () => showPopover(true))) return; positionPopover(); // Re-add to the top of the z-order (tabs added later would otherwise cover it). win.contentView.removeChildView(popover); win.contentView.addChildView(popover); popover.setVisible(true); popVisible = true; popover.webContents.send("site-info", decorate(activeTab()?.prov) || { kind: "home" }); - } else { popover.setVisible(false); popVisible = false; } + } else { cancelOverlayShow(popover); popover.setVisible(false); popVisible = false; } } function positionEnginePicker() { if (!enginePicker) return; @@ -2849,13 +2882,13 @@ function positionEnginePicker() { function showEnginePicker(show) { if (!enginePicker) return; if (show) { - loadOverlays(); + if (deferUntilOverlayLoaded(enginePicker, () => showEnginePicker(true))) return; positionEnginePicker(); win.contentView.removeChildView(enginePicker); win.contentView.addChildView(enginePicker); enginePicker.setVisible(true); epVisible = true; enginePicker.webContents.send("engines", { engines: enabledEnginesList(), current: settings.searchEngine, detected: activeTab()?.detected || null }); - } else { enginePicker.setVisible(false); epVisible = false; } + } else { cancelOverlayShow(enginePicker); enginePicker.setVisible(false); epVisible = false; } } function positionDownloads() { if (!downloadsPop) return; @@ -2866,13 +2899,13 @@ function positionDownloads() { function showDownloads(show) { if (!downloadsPop) return; if (show) { - loadOverlays(); + if (deferUntilOverlayLoaded(downloadsPop, () => showDownloads(true))) return; positionDownloads(); win.contentView.removeChildView(downloadsPop); win.contentView.addChildView(downloadsPop); downloadsPop.setVisible(true); dlVisible = true; downloadsPop.webContents.send("downloads", downloadsPublic()); - } else { downloadsPop.setVisible(false); dlVisible = false; } + } else { cancelOverlayShow(downloadsPop); downloadsPop.setVisible(false); dlVisible = false; } } function positionAddressPicker() { if (!addressPicker || !winAlive()) return; @@ -2897,13 +2930,18 @@ function positionLinkStatus() { const h = Math.max(20, linkStatusH); linkStatus.setBounds({ x: 0, y: Math.max(0, height - h), width: w, height: h }); } +let linkStatusPendingUrl = ""; function showLinkStatus(url) { if (!linkStatus || !winAlive()) return; const s = String(url || ""); if (!s) { + cancelOverlayShow(linkStatus); linkStatusPendingUrl = ""; if (linkStatusVisible) { linkStatus.setVisible(false); linkStatusVisible = false; } return; } + // First hover before the prewarm got to it: show the latest URL once loaded. + linkStatusPendingUrl = s; + if (deferUntilOverlayLoaded(linkStatus, () => showLinkStatus(linkStatusPendingUrl))) return; positionLinkStatus(); // Raise the pill above any tab view that was added after it. try { win.contentView.removeChildView(linkStatus); win.contentView.addChildView(linkStatus); } catch {} @@ -2990,13 +3028,13 @@ function setSidebar(show, panelId) { function showPwFill(show, matches) { if (!pwFillPop) return; if (show) { - loadOverlays(); + if (deferUntilOverlayLoaded(pwFillPop, () => showPwFill(true, matches))) return; positionPwFill(); win.contentView.removeChildView(pwFillPop); win.contentView.addChildView(pwFillPop); pwFillPop.setVisible(true); pwfVisible = true; pwFillPop.webContents.send("pw-matches", { matches: matches || [] }); - } else { pwFillPop.setVisible(false); pwfVisible = false; } + } else { cancelOverlayShow(pwFillPop); pwFillPop.setVisible(false); pwfVisible = false; } } // Compute credential matches for a host. Exact hostname match in phase-1; // eTLD+1 upgrade queued for A.2.5 (needs the public-suffix-list snapshot). @@ -3066,13 +3104,13 @@ function showAddressPicker(show, suggestions) { if (!addressPicker) return; if (show) { if (!suggestions || !suggestions.length) return showAddressPicker(false); - loadOverlays(); + if (deferUntilOverlayLoaded(addressPicker, () => showAddressPicker(true, suggestions))) return; positionAddressPicker(); win.contentView.removeChildView(addressPicker); win.contentView.addChildView(addressPicker); addressPicker.setVisible(true); apVisible = true; addressPicker.webContents.send("address-suggest", { suggestions }); - } else { addressPicker.setVisible(false); apVisible = false; } + } else { cancelOverlayShow(addressPicker); addressPicker.setVisible(false); apVisible = false; } } // Public view of a download — no DownloadItem refs leak to the renderer. const downloadsPublic = () => downloads.map((d) => ({ ...d })); @@ -3816,7 +3854,9 @@ function closeTab(id) { function createWindow() { chromeReadyDone = false; - overlaysLoaded = false; + overlaysPrewarmed = false; + overlayLoads.length = 0; // views of a previous window, if any + overlayWant.clear(); sessionSavedAtClose = false; if (tabs.length) { // leftovers from a window that closed while app windows kept the process alive for (const t of tabs.splice(0)) { try { t.view.webContents.destroy?.(); } catch {} } @@ -3855,7 +3895,7 @@ function createWindow() { styleScrollbars(chrome.webContents); chrome.webContents.loadFile("chrome.html"); // The overlays below are created now (cheap) but their pages load via - // deferOverlayLoad → loadOverlays() after chrome has painted. + // deferOverlayLoad: each page loads on first use, or in the idle prewarm. // Floating site-info overlay (hidden until the address-bar badge is clicked). popover = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "popover-preload.js") } }); try { popover.setBackgroundColor("#00000000"); } catch {}