// Performance probe for what a wayBetter app injects into its site's tabs. Loads the app's // document-start scripts (the tab bridge, its observer, its page runtime) as init scripts, like the // native tab webview does, into headless Chromium and measures: script evaluation time, fetch/XHR // wrapper layers and their synchronous overhead, Response clones and large JSON.parse calls made by // the scripts, MutationObserver callbacks / rAF / timer work attributed to them, and main-thread // long tasks. The app supplies the scripts and its scenarios (fixtures that intercept every request // locally, or a live page); see wayBetter for X's scripts/perf-probe.mjs. // // import { chromium } from 'playwright'; // import { runPerfProbe } from '/dev-tools/perf-probe.mjs'; // const results = await runPerfProbe({ chromium, scripts: [{ name: 'runtime', code }], seconds: 8, runs: 3, // nativeReplies: { some_command: [] }, // scenarios: [{ name: 'fixture /home', url: 'https://site.test/home', route: async context => { … } }] }); // // Numbers are headless Chromium, not WebView2; compare runs on the same machine only. PROFILE=1 // adds the scripts' self time per function. // ------------------------------------------------------------------------------------------ probe function prelude(names, replies) { // Work is "ours" when one of the probed scripts is on the stack (they are evaluated as bt-.js). const pattern = new RegExp('bt-(?:' + names.join('|') + ')\\.js'); const ours = () => pattern.test(new Error().stack || ''); const P = window.__probe = { evals: [], fetchSets: 0, xhrOpenSets: 0, xhrSendSets: 0, fetchCalls: 0, fetchOverheadMs: 0, xhrCalls: 0, clones: 0, cloneReads: 0, bigParses: 0, bigParseMs: 0, bigParseBytes: 0, mo: { ours: 0, oursMs: 0, oursObservers: 0, page: 0 }, raf: { ours: 0, oursMs: 0 }, timers: { ours: 0, oursMs: 0, intervals: 0 }, longtasks: [], outerStart: 0, }; const nativeFetch = window.fetch; let current = function probeInner(...a) { if (P.outerStart) { P.fetchOverheadMs += performance.now() - P.outerStart; P.outerStart = 0; } return Reflect.apply(nativeFetch, this, a); }; Object.defineProperty(window, 'fetch', { configurable: true, get: () => current, set: value => { if (ours()) P.fetchSets++; current = value; }, }); const xproto = XMLHttpRequest.prototype; for (const [key, counter] of [['open', 'xhrOpenSets'], ['send', 'xhrSendSets']]) { let fn = xproto[key]; Object.defineProperty(xproto, key, { configurable: true, get: () => fn, set: v => { if (ours()) P[counter]++; fn = v; } }); } const clone = Response.prototype.clone; const cloned = new WeakSet(); Response.prototype.clone = function () { P.clones++; const c = clone.call(this); cloned.add(c); return c; }; for (const reader of ['text', 'json', 'arrayBuffer']) { const orig = Response.prototype[reader]; Response.prototype[reader] = function () { if (cloned.has(this)) P.cloneReads++; return orig.call(this); }; } const parse = JSON.parse; JSON.parse = function (text, reviver) { if (typeof text === 'string' && text.length > 20000 && ours()) { const t = performance.now(); try { return parse.call(this, text, reviver); } finally { P.bigParses++; P.bigParseMs += performance.now() - t; P.bigParseBytes += text.length; } } return parse.call(this, text, reviver); }; const MO = window.MutationObserver; window.MutationObserver = class extends MO { constructor(cb) { const mine = ours(); super((records, obs) => { if (!mine) { P.mo.page++; return cb(records, obs); } const t = performance.now(); try { return cb(records, obs); } finally { P.mo.ours++; P.mo.oursMs += performance.now() - t; } }); if (mine) P.mo.oursObservers++; } }; const raf = window.requestAnimationFrame.bind(window); window.requestAnimationFrame = cb => { if (!ours()) return raf(cb); return raf(ts => { const t = performance.now(); try { cb(ts); } finally { P.raf.ours++; P.raf.oursMs += performance.now() - t; } }); }; for (const name of ['setTimeout', 'setInterval']) { const orig = window[name].bind(window); window[name] = (cb, ms, ...rest) => { if (typeof cb !== 'function' || !ours()) return orig(cb, ms, ...rest); if (name === 'setInterval') P.timers.intervals++; return orig((...a) => { const t = performance.now(); try { cb(...a); } finally { P.timers.ours++; P.timers.oursMs += performance.now() - t; } }, ms, ...rest); }; } try { new PerformanceObserver(list => { for (const e of list.getEntries()) P.longtasks.push(e.duration); }) .observe({ type: 'longtask', buffered: true }); } catch { /* unsupported */ } // What the native app provides to site tabs: every command answers null unless `replies` names it. const invoke = (cmd) => Promise.resolve(cmd in replies ? replies[cmd] : null); window.__TAURI_INTERNALS__ = { invoke }; window.__TAURI__ = { core: { invoke } }; } function postlude() { const P = window.__probe; const inner = window.fetch; // The outermost layer: the site's own calls start here. const outer = function probeOuter(...a) { P.fetchCalls++; P.outerStart = performance.now(); return Reflect.apply(inner, this, a); }; const set = Object.getOwnPropertyDescriptor(window, 'fetch').set; Object.defineProperty(window, 'fetch', { configurable: true, get: () => outer, set }); const open = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (...a) { P.xhrCalls++; return open.apply(this, a); }; } const wrap = ({ name, code }) => `window.__probe.evals.push(['${name}', performance.now()]);\n${code}\n;window.__probe.evals.push(['${name}:end', performance.now()]);\n//# sourceURL=bt-${name}.js`; async function run(browser, options, { name, url, route, scroll }) { const { scripts, seconds, nativeReplies = {} } = options; const userAgent = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${browser.version().split('.')[0]}.0.0.0 Safari/537.36`; const context = await browser.newContext({ viewport: { width: 1280, height: 900 }, userAgent }); if (route) await route(context); await context.addInitScript({ content: `(${prelude})(${JSON.stringify(scripts.map(script => script.name))}, ${JSON.stringify(nativeReplies)});\n//# sourceURL=bt-probe-prelude.js` }); for (const script of scripts) await context.addInitScript({ content: wrap(script) }); await context.addInitScript({ content: `(${postlude})();\n//# sourceURL=bt-probe-postlude.js` }); const page = await context.newPage(); const cdp = await context.newCDPSession(page); await cdp.send('Performance.enable'); if (process.env.PROFILE) { await cdp.send('Profiler.enable'); await cdp.send('Profiler.setSamplingInterval', { interval: 50 }); await cdp.send('Profiler.start'); } const started = Date.now(); await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60_000 }); const deadline = started + seconds * 1000; while (Date.now() < deadline) { if (scroll) await page.mouse.wheel(0, 700).catch(() => {}); await page.waitForTimeout(500); } const result = await page.evaluate(() => { const P = window.__probe; const nav = performance.getEntriesByType('navigation')[0]; const paint = Object.fromEntries(performance.getEntriesByType('paint').map(e => [e.name, e.startTime])); const evals = {}; for (let i = 0; i < P.evals.length; i += 2) evals[P.evals[i][0]] = +(P.evals[i + 1][1] - P.evals[i][1]).toFixed(2); return { evalMs: evals, fetchWrapperLayers: P.fetchSets, xhrOpenLayers: P.xhrOpenSets, xhrSendLayers: P.xhrSendSets, fetchCalls: P.fetchCalls, fetchOverheadMsTotal: +P.fetchOverheadMs.toFixed(2), xhrCalls: P.xhrCalls, responseClones: P.clones, cloneReads: P.cloneReads, bigJsonParses: P.bigParses, bigJsonParseMs: +P.bigParseMs.toFixed(1), bigJsonParseMB: +(P.bigParseBytes / 1e6).toFixed(1), observers: P.mo.oursObservers, observerCallbacks: P.mo.ours, observerMs: +P.mo.oursMs.toFixed(1), pageObserverCallbacks: P.mo.page, rafCallbacks: P.raf.ours, rafMs: +P.raf.oursMs.toFixed(1), timerCallbacks: P.timers.ours, timerMs: +P.timers.oursMs.toFixed(1), intervals: P.timers.intervals, longTasks: P.longtasks.length, longTaskMs: Math.round(P.longtasks.reduce((a, b) => a + b, 0)), domContentLoadedMs: nav ? Math.round(nav.domContentLoadedEventEnd) : null, fcpMs: paint['first-contentful-paint'] != null ? Math.round(paint['first-contentful-paint']) : null, }; }); const secs = (Date.now() - started) / 1000; if (process.env.PROFILE) { const { profile } = await cdp.send('Profiler.stop'); const dt = new Map(); const byId = new Map(profile.nodes.map(n => [n.id, n])); const counts = new Map(); profile.samples.forEach((id, i) => counts.set(id, (counts.get(id) ?? 0) + (profile.timeDeltas[i] ?? 0))); for (const [id, us] of counts) { const n = byId.get(id); const f = n.callFrame; if (!/bt-/.test(f.url)) continue; const k = f.url.replace(/.*bt-/, '') + ':' + (f.functionName || '(anon)') + ':' + f.lineNumber; dt.set(k, (dt.get(k) ?? 0) + us / 1000); } result.selfMs = Object.fromEntries([...dt].sort((a, b) => b[1] - a[1]).slice(0, 25).map(([k, v]) => [k, +v.toFixed(1)])); } // Whole-page main-thread totals (the site or fixture + ours), from Chromium's own counters. const metrics = Object.fromEntries((await cdp.send('Performance.getMetrics')).metrics.map(m => [m.name, m.value])); result.pageScriptMs = Math.round(metrics.ScriptDuration * 1000); result.pageTaskMs = Math.round(metrics.TaskDuration * 1000); result.pageLayoutMs = Math.round(metrics.LayoutDuration * 1000); result.pageStyleMs = Math.round(metrics.RecalcStyleDuration * 1000); result.layoutCount = metrics.LayoutCount; result.styleRecalcCount = metrics.RecalcStyleCount; result.observerCallbacksPerSec = +(result.observerCallbacks / secs).toFixed(1); result.ourMainThreadMsPerSec = +((result.observerMs + result.rafMs + result.timerMs + result.fetchOverheadMsTotal + result.bigJsonParseMs) / secs).toFixed(2); await context.close(); return { scenario: name, ...result }; } /** * Runs every scenario `runs` times and returns the median of each numeric field per scenario. * `scripts`: `{ name, code }` in injection order (none: the page alone, for a baseline). * `scenarios`: `{ name, url, route?(context), scroll? }`. */ export async function runPerfProbe(options) { const { chromium, scenarios, runs = 3 } = options; const settings = { scripts: options.scripts ?? [], seconds: options.seconds ?? 8, nativeReplies: options.nativeReplies ?? {} }; const browser = await chromium.launch({ headless: true }); const results = []; try { for (const scenario of scenarios) { const all = []; for (let i = 0; i < runs; i++) all.push(await run(browser, settings, scenario)); const out = { ...all[0] }; for (const [key, value] of Object.entries(out)) { if (typeof value !== 'number') continue; const sorted = all.map(result => result[key]).sort((a, b) => a - b); out[key] = sorted[Math.floor(sorted.length / 2)]; } results.push(out); } } finally { await browser.close(); } return results; }