Theseus: load each overlay page on first use, prewarm only the common ones

All nine overlay pages (site info, engine picker, downloads, address
suggestions, password fill, link pill, approvals, page dialogs) loaded 250 ms
after the toolbar, whether or not the session would ever open them. Each now
loads on its first use; the three used in nearly every session (address
suggestions, link pill, site info) are prewarmed one at a time after the
first page. Measured: 8 -> 3 overlay pages loaded at startup, ~30 MB less
private memory at 15 s (they share one renderer, so the process count is
unchanged); launch timing unchanged within noise.

The show functions send their data right after showing, which a page still
loading drops, so a first show waits for its page and then runs; a hide in
the meantime cancels it.

Also: the indexer's restart notice is logged when the restart happens,
not when the child exits — on app.exit() the timer never fires, so a
forced exit no longer prints a restart that does not happen.
This commit is contained in:
Local Dev 2026-10-03 13:26:43 +02:00
parent 1f1bde6e60
commit c5b2383df8

106
main.js
View file

@ -1330,26 +1330,55 @@ function restoreTabs() {
} }
// ---- deferred overlay loads ---- // ---- deferred overlay loads ----
// The floating overlays (site-info popover, engine picker, downloads, // The floating overlays (site-info popover, engine picker, downloads,
// address suggestions, password fill, link-status pill, add-on approval) // address suggestions, password fill, link-status pill, add-on approval,
// are WebContentsViews created with the window, but their HTML used to be // page dialogs) are WebContentsViews created with the window. A view whose
// loaded in the same tick as chrome.html — seven extra renderers racing the // page was never loaded has no renderer process; loading its page starts
// toolbar for the first paint. They now load shortly after chrome reports // one (~20–30 MB each). They used to load all together shortly after the
// did-finish-load, or on first use, whichever comes first. // toolbar — nine renderers most sessions never use. Now each one loads on
const overlayLoads = []; // [{ view, file }] waiting for loadOverlays() // its first use, except the few used in nearly every session (address
let overlaysLoaded = false; // 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 deferOverlayLoad(view, file) { overlayLoads.push({ view, file }); }
function loadOverlays() { function loadOverlay(view) {
if (overlaysLoaded) return; const i = overlayLoads.findIndex((o) => o.view === view);
overlaysLoaded = true; if (i < 0) return;
for (const { view, file } of overlayLoads.splice(0)) { const { file } = overlayLoads.splice(i, 1)[0];
try { view.webContents.loadFile(file); } catch (e) { console.warn(`overlay load failed (${file}):`, e?.message); } 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 // Resolves once `view` has finished loading its page, loading it first if
// first if that hasn't happened yet. Bounded so a wedged renderer can't // that hasn't happened yet. Bounded so a wedged renderer can't hang the
// hang the caller forever. // caller forever.
function overlayReady(view, timeoutMs = 4000) { function overlayReady(view, timeoutMs = 4000) {
loadOverlays(); loadOverlay(view);
const wc = view.webContents; const wc = view.webContents;
try { if (wc.getURL() && !wc.isLoading()) return Promise.resolve(); } catch {} try { if (wc.getURL() && !wc.isLoading()) return Promise.resolve(); } catch {}
return new Promise((resolve) => { return new Promise((resolve) => {
@ -1379,13 +1408,12 @@ function onChromeReady() {
// the first page is up — or after 4 s, whichever comes first. // the first page is up — or after 4 s, whichever comes first.
{ {
let went = false; let went = false;
const go = () => { if (!went) { went = true; startBnsPolling(); } }; const go = () => { if (!went) { went = true; startBnsPolling(); setTimeout(prewarmOverlays, 1000); } };
setTimeout(go, 4000); setTimeout(go, 4000);
try { activeTab()?.view.webContents.once("did-stop-loading", () => setTimeout(go, 500)); } catch {} try { activeTab()?.view.webContents.once("did-stop-loading", () => setTimeout(go, 500)); } catch {}
} }
// Re-emit any pending update notice — harmless if nothing is pending. // Re-emit any pending update notice — harmless if nothing is pending.
emitUpdateAvailable(); emitUpdateAvailable();
setTimeout(loadOverlays, 250);
scheduleStartupUpdateCheck(); scheduleStartupUpdateCheck();
refreshRemoteHomeCards().catch(() => {}); refreshRemoteHomeCards().catch(() => {});
} }
@ -1671,10 +1699,15 @@ function startIndexer() {
for (const settle of indexerPending.values()) settle(false); for (const settle of indexerPending.values()) settle(false);
indexerPending.clear(); indexerPending.clear();
if (indexerStopping) return; 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)); const wait = Math.min(30_000, 1000 * 2 ** Math.min(indexerStarts, 5));
console.warn(`[bns] indexer exited (${code}); restarting in ${wait / 1000}s`); setTimeout(() => {
setTimeout(startIndexer, wait); 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 }); 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" }); if (indexerGo) indexer.postMessage({ type: "go" });
@ -2831,14 +2864,14 @@ function positionPopover() {
function showPopover(show) { function showPopover(show) {
if (!popover) return; if (!popover) return;
if (show) { if (show) {
loadOverlays(); if (deferUntilOverlayLoaded(popover, () => showPopover(true))) return;
positionPopover(); positionPopover();
// Re-add to the top of the z-order (tabs added later would otherwise cover it). // Re-add to the top of the z-order (tabs added later would otherwise cover it).
win.contentView.removeChildView(popover); win.contentView.removeChildView(popover);
win.contentView.addChildView(popover); win.contentView.addChildView(popover);
popover.setVisible(true); popVisible = true; popover.setVisible(true); popVisible = true;
popover.webContents.send("site-info", decorate(activeTab()?.prov) || { kind: "home" }); 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() { function positionEnginePicker() {
if (!enginePicker) return; if (!enginePicker) return;
@ -2849,13 +2882,13 @@ function positionEnginePicker() {
function showEnginePicker(show) { function showEnginePicker(show) {
if (!enginePicker) return; if (!enginePicker) return;
if (show) { if (show) {
loadOverlays(); if (deferUntilOverlayLoaded(enginePicker, () => showEnginePicker(true))) return;
positionEnginePicker(); positionEnginePicker();
win.contentView.removeChildView(enginePicker); win.contentView.removeChildView(enginePicker);
win.contentView.addChildView(enginePicker); win.contentView.addChildView(enginePicker);
enginePicker.setVisible(true); epVisible = true; enginePicker.setVisible(true); epVisible = true;
enginePicker.webContents.send("engines", { engines: enabledEnginesList(), current: settings.searchEngine, detected: activeTab()?.detected || null }); 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() { function positionDownloads() {
if (!downloadsPop) return; if (!downloadsPop) return;
@ -2866,13 +2899,13 @@ function positionDownloads() {
function showDownloads(show) { function showDownloads(show) {
if (!downloadsPop) return; if (!downloadsPop) return;
if (show) { if (show) {
loadOverlays(); if (deferUntilOverlayLoaded(downloadsPop, () => showDownloads(true))) return;
positionDownloads(); positionDownloads();
win.contentView.removeChildView(downloadsPop); win.contentView.removeChildView(downloadsPop);
win.contentView.addChildView(downloadsPop); win.contentView.addChildView(downloadsPop);
downloadsPop.setVisible(true); dlVisible = true; downloadsPop.setVisible(true); dlVisible = true;
downloadsPop.webContents.send("downloads", downloadsPublic()); downloadsPop.webContents.send("downloads", downloadsPublic());
} else { downloadsPop.setVisible(false); dlVisible = false; } } else { cancelOverlayShow(downloadsPop); downloadsPop.setVisible(false); dlVisible = false; }
} }
function positionAddressPicker() { function positionAddressPicker() {
if (!addressPicker || !winAlive()) return; if (!addressPicker || !winAlive()) return;
@ -2897,13 +2930,18 @@ function positionLinkStatus() {
const h = Math.max(20, linkStatusH); const h = Math.max(20, linkStatusH);
linkStatus.setBounds({ x: 0, y: Math.max(0, height - h), width: w, height: h }); linkStatus.setBounds({ x: 0, y: Math.max(0, height - h), width: w, height: h });
} }
let linkStatusPendingUrl = "";
function showLinkStatus(url) { function showLinkStatus(url) {
if (!linkStatus || !winAlive()) return; if (!linkStatus || !winAlive()) return;
const s = String(url || ""); const s = String(url || "");
if (!s) { if (!s) {
cancelOverlayShow(linkStatus); linkStatusPendingUrl = "";
if (linkStatusVisible) { linkStatus.setVisible(false); linkStatusVisible = false; } if (linkStatusVisible) { linkStatus.setVisible(false); linkStatusVisible = false; }
return; return;
} }
// First hover before the prewarm got to it: show the latest URL once loaded.
linkStatusPendingUrl = s;
if (deferUntilOverlayLoaded(linkStatus, () => showLinkStatus(linkStatusPendingUrl))) return;
positionLinkStatus(); positionLinkStatus();
// Raise the pill above any tab view that was added after it. // Raise the pill above any tab view that was added after it.
try { win.contentView.removeChildView(linkStatus); win.contentView.addChildView(linkStatus); } catch {} try { win.contentView.removeChildView(linkStatus); win.contentView.addChildView(linkStatus); } catch {}
@ -2990,13 +3028,13 @@ function setSidebar(show, panelId) {
function showPwFill(show, matches) { function showPwFill(show, matches) {
if (!pwFillPop) return; if (!pwFillPop) return;
if (show) { if (show) {
loadOverlays(); if (deferUntilOverlayLoaded(pwFillPop, () => showPwFill(true, matches))) return;
positionPwFill(); positionPwFill();
win.contentView.removeChildView(pwFillPop); win.contentView.removeChildView(pwFillPop);
win.contentView.addChildView(pwFillPop); win.contentView.addChildView(pwFillPop);
pwFillPop.setVisible(true); pwfVisible = true; pwFillPop.setVisible(true); pwfVisible = true;
pwFillPop.webContents.send("pw-matches", { matches: matches || [] }); 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; // 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). // 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 (!addressPicker) return;
if (show) { if (show) {
if (!suggestions || !suggestions.length) return showAddressPicker(false); if (!suggestions || !suggestions.length) return showAddressPicker(false);
loadOverlays(); if (deferUntilOverlayLoaded(addressPicker, () => showAddressPicker(true, suggestions))) return;
positionAddressPicker(); positionAddressPicker();
win.contentView.removeChildView(addressPicker); win.contentView.removeChildView(addressPicker);
win.contentView.addChildView(addressPicker); win.contentView.addChildView(addressPicker);
addressPicker.setVisible(true); apVisible = true; addressPicker.setVisible(true); apVisible = true;
addressPicker.webContents.send("address-suggest", { suggestions }); 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. // Public view of a download — no DownloadItem refs leak to the renderer.
const downloadsPublic = () => downloads.map((d) => ({ ...d })); const downloadsPublic = () => downloads.map((d) => ({ ...d }));
@ -3816,7 +3854,9 @@ function closeTab(id) {
function createWindow() { function createWindow() {
chromeReadyDone = false; chromeReadyDone = false;
overlaysLoaded = false; overlaysPrewarmed = false;
overlayLoads.length = 0; // views of a previous window, if any
overlayWant.clear();
sessionSavedAtClose = false; sessionSavedAtClose = false;
if (tabs.length) { // leftovers from a window that closed while app windows kept the process alive 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 {} } for (const t of tabs.splice(0)) { try { t.view.webContents.destroy?.(); } catch {} }
@ -3855,7 +3895,7 @@ function createWindow() {
styleScrollbars(chrome.webContents); styleScrollbars(chrome.webContents);
chrome.webContents.loadFile("chrome.html"); chrome.webContents.loadFile("chrome.html");
// The overlays below are created now (cheap) but their pages load via // 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). // Floating site-info overlay (hidden until the address-bar badge is clicked).
popover = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "popover-preload.js") } }); popover = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "popover-preload.js") } });
try { popover.setBackgroundColor("#00000000"); } catch {} try { popover.setBackgroundColor("#00000000"); } catch {}