An add-on's storage.get read and parsed its whole store file on every call,
and storage.set read, parsed and rewrote it — synchronously, on the main
thread. Traced on a real profile (installed 0.3.70): with a 7.5 MB Aegis
store, 30 of the first 35 s of main-thread time went to storage.get, the
window sat in "Not Responding" from 3 s to 19 s, and the first page showed at
19 s. One get cost ~73 ms; Aegis does dozens per state update.
lib/addon-store.cjs keeps one in-memory copy per store, shared by the
add-on's api.storage (addons-host.js) and its pages (addon-storage-* IPC in
main.js). After a one-time load a get costs microseconds; values are copied
in and out (structuredClone), so callers keep the old semantics. Writes are
coalesced (100 ms) and land as temp-file + rename, and are flushed on quit;
a store that doesn't parse is moved aside instead of being replaced by {}.
Measured on copies of the same profile, dev build:
first page 16.9-17.6 s -> 1.5-1.7 s; main thread blocked 24.7-25.8 s of
30 -> 1.0-1.1 s; longest freeze 13.7-15.0 s -> 0.6 s.
The Aegis side (capping its unbounded txCache) ships separately through
Aegis's own update channel. The boot tracer gains total/longest block columns.
80 lines
4.8 KiB
JavaScript
80 lines
4.8 KiB
JavaScript
// Measure Theseus launches. Runs N traced starts of the dev tree against a
|
|
// throwaway profile and prints one row per run, so a change can be checked
|
|
// against the previous numbers.
|
|
//
|
|
// node scripts/boot-trace/run.mjs [--runs 3] [--seconds 25] [--fresh] [--profile <dir>]
|
|
//
|
|
// --fresh start from an empty profile (the first run then also measures
|
|
// first-launch work: seeding the bundled add-ons, filter builds)
|
|
// --profile profile dir to use (default: <tmp>/theseus-boot-trace)
|
|
//
|
|
// The profile never touches the real one (THESEUS_USER_DATA), and update
|
|
// checks are off (THESEUS_NO_UPDATE_CHECK) so a dev tree that is older than
|
|
// the live release can't offer — let alone install — an update.
|
|
import { spawn } from "node:child_process";
|
|
import { mkdirSync, rmSync, readFileSync, openSync, closeSync, existsSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
const APP = path.resolve(here, "..", "..");
|
|
const args = process.argv.slice(2);
|
|
const opt = (k, d) => { const i = args.indexOf(k); return i >= 0 && args[i + 1] ? args[i + 1] : d; };
|
|
const RUNS = Number(opt("--runs", 3));
|
|
const SECONDS = Number(opt("--seconds", 25));
|
|
const PROFILE = path.resolve(opt("--profile", path.join(tmpdir(), "theseus-boot-trace")));
|
|
const ELECTRON = path.join(APP, "node_modules", "electron", "dist", process.platform === "win32" ? "electron.exe" : "electron");
|
|
if (!existsSync(ELECTRON)) { console.error("electron binary not found:", ELECTRON); process.exit(1); }
|
|
if (args.includes("--fresh")) rmSync(PROFILE, { recursive: true, force: true });
|
|
mkdirSync(PROFILE, { recursive: true });
|
|
const traceDir = path.join(PROFILE, "..", path.basename(PROFILE) + "-traces");
|
|
mkdirSync(traceDir, { recursive: true });
|
|
|
|
function runOnce(i) {
|
|
const out = path.join(traceDir, `run${i}.json`);
|
|
const log = openSync(path.join(traceDir, `run${i}.log`), "w");
|
|
return new Promise((resolve) => {
|
|
// Output to a file, not inherited: an Electron child holding the parent's
|
|
// pipes keeps a shell pipeline open after it exits.
|
|
const p = spawn(ELECTRON, [path.join(here, "trace-main.js")], {
|
|
cwd: APP, stdio: ["ignore", log, log], windowsHide: false,
|
|
env: { ...process.env, THESEUS_USER_DATA: PROFILE, THESEUS_NO_UPDATE_CHECK: "1", BOOT_TRACE_OUT: out, BOOT_TRACE_MS: String(SECONDS * 1000) },
|
|
});
|
|
const kill = setTimeout(() => { try { p.kill(); } catch {} }, SECONDS * 1000 + 30000);
|
|
p.on("exit", () => { clearTimeout(kill); closeSync(log); resolve(out); });
|
|
});
|
|
}
|
|
|
|
function summarise(file) {
|
|
const ev = JSON.parse(readFileSync(file, "utf8"));
|
|
const at = (re) => (ev.find((e) => re.test(e.label)) || {}).t ?? null;
|
|
const metric = (label) => ev.find((e) => e.kind === "metrics" && e.label === label) || {};
|
|
return {
|
|
ready: at(/^ready$/),
|
|
toolbar: at(/loaded #\d+ file:chrome\.html/),
|
|
firstPage: at(/loaded #\d+ file:home\.html/),
|
|
blocks6s: ev.filter((e) => e.kind === "block" && e.endedAt < 6000).reduce((a, e) => a + e.ms, 0),
|
|
blockedAll: ev.filter((e) => e.kind === "block").reduce((a, e) => a + e.ms, 0),
|
|
longestBlock: ev.filter((e) => e.kind === "block").reduce((a, e) => Math.max(a, e.ms), 0),
|
|
addons: ev.filter((e) => e.kind === "addon").reduce((a, e) => a + e.ms, 0),
|
|
overlaysLoaded: ev.filter((e) => e.kind === "wc" && /loaded #\d+ file:(popover|engine-picker|downloads|address-picker|pw-fill|link-status|approval|js-dialog)\.html/.test(e.label)).length,
|
|
fetches6s: ev.filter((e) => e.kind === "fetch" && e.start < 6000).length,
|
|
proc15: metric("15s").processes ?? null,
|
|
mem15: metric("15s").privateMB ?? null,
|
|
failed: ev.filter((e) => e.kind === "fail").map((e) => e.label),
|
|
};
|
|
}
|
|
|
|
const rows = [];
|
|
for (let i = 1; i <= RUNS; i++) {
|
|
process.stdout.write(`run ${i}/${RUNS}… `);
|
|
const file = await runOnce(i);
|
|
if (!existsSync(file)) { console.log("no trace written (see " + file.replace(/json$/, "log") + ")"); continue; }
|
|
const r = summarise(file); rows.push(r);
|
|
console.log(r.failed.length ? `INVALID — ${r.failed.length} page load(s) failed: ${r.failed[0]}` : "done");
|
|
}
|
|
const cols = [["ready", "ready"], ["toolbar", "toolbar"], ["firstPage", "1st page"], ["blocks6s", "blocked<6s"], ["blockedAll", "blocked all"], ["longestBlock", "longest"], ["addons", "addons"], ["overlaysLoaded", "overlays"], ["fetches6s", "fetch<6s"], ["proc15", "procs@15s"], ["mem15", "MB@15s"]];
|
|
console.log("\n" + ["run", ...cols.map((c) => c[1])].map((h) => h.padStart(11)).join(""));
|
|
rows.forEach((r, i) => console.log((r.failed.length ? "!" : " ") + [String(i + 1), ...cols.map(([k]) => String(r[k] ?? "—"))].map((v) => v.padStart(11)).join("")));
|
|
console.log(`\ntimes in ms since process start; MB = private memory of all Theseus processes. Traces: ${traceDir}`);
|