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 ----
// 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 {}