scripts/testing/bench-desktop-page.ts
Rama main · réplica de lectura del repositorio original.
// The browser half of scripts/bench-desktop.ts: a disposable headless Chrome driven over the DevTools protocol, and
// the probe each page loads first. The probe counts React commits and which components rendered in each (through
// the hook React calls on every commit), timer and animation-frame callbacks, server frames, long tasks and when the
// sidebar and the transcript first show.
import { spawn, type ChildProcess, execFileSync } from "node:child_process";
import { existsSync, mkdtempSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { removeTempDir, waitForExit } from "../../server/testing/cleanup.ts";
type Params = Record<string, unknown>;
const CDP_ANSWER_MS = 120_000;
class Cdp {
private nextId = 0;
private pending = new Map<number, { resolve: (value: Params) => void; reject: (error: Error) => void; method: string }>();
private listeners = new Map<string, Set<(params: Params, sessionId?: string) => void>>();
private socket: WebSocket;
constructor(socket: WebSocket) {
this.socket = socket;
socket.addEventListener("close", () => {
for (const waiter of this.pending.values()) waiter.reject(new Error(`${waiter.method}: Chrome closed the connection`));
this.pending.clear();
});
socket.addEventListener("message", (event) => {
const message = JSON.parse(String(event.data)) as { id?: number; result?: Params; error?: { message: string }; method?: string; params?: Params; sessionId?: string };
if (message.id !== undefined) {
const waiter = this.pending.get(message.id);
this.pending.delete(message.id);
if (message.error) waiter?.reject(new Error(`${waiter.method}: ${message.error.message}`));
else waiter?.resolve(message.result ?? {});
return;
}
for (const listener of this.listeners.get(message.method ?? "") ?? []) listener(message.params ?? {}, message.sessionId);
});
}
send(method: string, params: Params = {}, sessionId?: string): Promise<Params> {
const id = (this.nextId += 1);
return new Promise((resolve, reject) => {
// A page stuck in a loop never answers: fail by name rather than wait forever.
const timer = setTimeout(() => {
this.pending.delete(id);
reject(new Error(`${method}: no answer in ${CDP_ANSWER_MS / 1000} s`));
}, CDP_ANSWER_MS);
this.pending.set(id, { resolve: (value) => { clearTimeout(timer); resolve(value); }, reject: (error) => { clearTimeout(timer); reject(error); }, method });
this.socket.send(JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) }));
});
}
on(method: string, listener: (params: Params, sessionId?: string) => void): void {
const set = this.listeners.get(method) ?? new Set();
set.add(listener);
this.listeners.set(method, set);
}
close(): void { this.socket.close(); }
}
export interface ProbeOptions {
storage: Record<string, string>;
/** A stand-in desktop bridge with only the rail's entry point, for loading the rail's page. */
railBridge?: boolean;
}
/** Runs in the page before any of its scripts. Kept free of TypeScript-only syntax: it is sent as source. */
function probe(options: ProbeOptions): void {
const bench = {
marks: {} as Record<string, number>, commits: 0, commitMs: 0, probeMs: 0, renderedTotal: 0, maxRendered: 0,
renders: {} as Record<string, number>, selfMs: {} as Record<string, number>, mounts: {} as Record<string, number>,
why: {} as Record<string, Record<string, number>>,
timerFires: {} as Record<string, number>, liveIntervals: new Map<number, string>(),
sse: { frames: 0, bytes: 0, kinds: {} as Record<string, { n: number; bytes: number }> },
longTasks: [] as Array<[number, number]>,
reset() {
bench.commits = 0; bench.commitMs = 0; bench.probeMs = 0; bench.renderedTotal = 0; bench.maxRendered = 0;
bench.renders = {}; bench.selfMs = {}; bench.mounts = {}; bench.why = {}; bench.timerFires = {}; bench.longTasks = [];
bench.sse = { frames: 0, bytes: 0, kinds: {} };
},
};
(window as unknown as { __bench: typeof bench }).__bench = bench;
for (const [key, value] of Object.entries(options.storage)) { try { localStorage.setItem(key, value); } catch { /* storage is optional */ } }
if (options.railBridge) (window as unknown as { ogb: unknown }).ogb = { rail: { expand() {}, onSkin() { return () => {}; } } };
try {
new PerformanceObserver((list) => { for (const entry of list.getEntries()) bench.longTasks.push([Math.round(entry.startTime), Math.round(entry.duration)]); })
.observe({ type: "longtask", buffered: true });
new PerformanceObserver((list) => { const entry = list.getEntries().at(-1); if (entry) bench.marks.lcp = entry.startTime; })
.observe({ type: "largest-contentful-paint", buffered: true });
} catch { /* observers are best effort */ }
const fire = (key: string) => { bench.timerFires[key] = (bench.timerFires[key] ?? 0) + 1; };
const bucket = (ms: number) => ms <= 0 ? "0" : ms < 50 ? "<50ms" : ms < 1000 ? `${Math.round(ms / 50) * 50}ms` : `${Math.round(ms / 1000)}s`;
const nativeTimeout = window.setTimeout.bind(window);
const nativeInterval = window.setInterval.bind(window);
const nativeClearInterval = window.clearInterval.bind(window);
const nativeFrame = window.requestAnimationFrame.bind(window);
(window as unknown as { setTimeout: unknown }).setTimeout = (callback: unknown, ms?: number, ...args: unknown[]) => {
if (typeof callback !== "function") return nativeTimeout(callback as never, ms, ...args);
const key = `timeout:${bucket(Number(ms) || 0)}`;
return nativeTimeout((...inner: unknown[]) => { fire(key); return (callback as (...a: unknown[]) => unknown)(...inner); }, ms, ...args);
};
(window as unknown as { setInterval: unknown }).setInterval = (callback: unknown, ms?: number, ...args: unknown[]) => {
const key = `interval:${bucket(Number(ms) || 0)}`;
const id = nativeInterval((...inner: unknown[]) => { fire(key); return (callback as (...a: unknown[]) => unknown)(...inner); }, ms, ...args);
bench.liveIntervals.set(id, key);
return id;
};
window.clearInterval = (id?: number) => { if (id !== undefined) bench.liveIntervals.delete(id); nativeClearInterval(id); };
window.requestAnimationFrame = (callback: FrameRequestCallback) => nativeFrame((time) => { fire("raf"); callback(time); });
const NativeSource = window.EventSource;
if (NativeSource) {
window.EventSource = class extends NativeSource {
constructor(url: string | URL, init?: EventSourceInit) {
super(url, init);
this.addEventListener("message", (event) => {
const started = performance.now();
const data = String((event as MessageEvent).data);
bench.sse.frames += 1;
bench.sse.bytes += data.length;
let kind = "?";
try { kind = String(JSON.parse(data).kind ?? "?"); } catch { /* counted as ? */ }
const entry = bench.sse.kinds[kind] ?? (bench.sse.kinds[kind] = { n: 0, bytes: 0 });
entry.n += 1;
entry.bytes += data.length;
bench.probeMs += performance.now() - started;
});
}
} as typeof EventSource;
}
interface Fiber { tag: number; type: { displayName?: string; name?: string; render?: { displayName?: string; name?: string } } | null; alternate: Fiber | null; child: Fiber | null; sibling: Fiber | null; flags: number; actualDuration?: number; memoizedProps?: Record<string, unknown> | null }
const COMPOSITE = new Set([0, 1, 11, 15]);
const nameOf = (fiber: Fiber) => {
const type = fiber.type;
if (!type) return "?";
if (fiber.tag === 11) return type.displayName ?? type.render?.displayName ?? type.render?.name ?? "ForwardRef";
return type.displayName || type.name || "Anonymous";
};
(window as unknown as { __REACT_DEVTOOLS_GLOBAL_HOOK__: unknown }).__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
renderers: new Map(), supportsFiber: true, isDisabled: false,
checkDCE() {}, onScheduleFiberRoot() {}, onCommitFiberUnmount() {}, onPostCommitFiberRoot() {}, setStrictMode() {},
inject(renderer: unknown) { const self = this as { renderers: Map<number, unknown> }; const id = self.renderers.size + 1; self.renderers.set(id, renderer); return id; },
onCommitFiberRoot(_id: number, root: { current: Fiber }) {
const started = performance.now();
bench.commits += 1;
bench.commitMs += root.current.actualDuration ?? 0;
let rendered = 0;
const stack: Fiber[] = [root.current];
while (stack.length) {
const fiber = stack.pop()!;
const previous = fiber.alternate;
if (COMPOSITE.has(fiber.tag)) {
const name = nameOf(fiber);
const mounted = !previous;
if (mounted || fiber.flags & 1) {
const counts = mounted ? bench.mounts : bench.renders;
counts[name] = (counts[name] ?? 0) + 1;
if (previous) {
// Why a component rendered again: the props that changed, a parent's new props object with equal values,
// or the same props (its own state, a context or a store it reads).
const before = previous.memoizedProps ?? {}, after = fiber.memoizedProps ?? {};
const changed = before === after ? ["self"] : Object.keys(after).filter((key) => after[key] !== before[key]).map((key) => `prop:${key}`);
const reasons = bench.why[name] ?? (bench.why[name] = {});
for (const reason of changed.length ? changed : [before === after ? "self" : "parent"]) reasons[reason] = (reasons[reason] ?? 0) + 1;
}
rendered += 1;
if (typeof fiber.actualDuration === "number") {
let self = fiber.actualDuration;
for (let child = fiber.child; child; child = child.sibling) self -= child.actualDuration ?? 0;
bench.selfMs[name] = (bench.selfMs[name] ?? 0) + Math.max(0, self);
}
}
}
if (previous && fiber.child === previous.child) continue;
for (let child = fiber.child; child; child = child.sibling) stack.push(child);
}
bench.renderedTotal += rendered;
bench.maxRendered = Math.max(bench.maxRendered, rendered);
bench.probeMs += performance.now() - started;
},
};
const milestones: Array<[string, string]> = [
["sidebar", "[data-sidebar-bot-row], [data-sidebar-group-row]"],
["transcript", "[data-mid]"],
];
const watcher = new MutationObserver(() => {
for (const [name, selector] of milestones) {
if (bench.marks[name] === undefined && document.querySelector(selector)) {
bench.marks[name] = performance.now();
nativeFrame(() => { bench.marks[`${name}Frame`] = performance.now(); });
}
}
if (milestones.every(([name]) => bench.marks[name] !== undefined)) watcher.disconnect();
});
watcher.observe(document, { childList: true, subtree: true });
}
export const probeSource = (options: ProbeOptions) => `(${probe.toString()})(${JSON.stringify(options)});`;
/** Finds the transcript's scrolling element, keeps it for later reads, and reports where it is and what it holds. */
export const TRANSCRIPT_BOX = `(() => {
let el = document.querySelector("[data-mid]");
while (el && !(el.scrollHeight > el.clientHeight + 10 && /auto|scroll/.test(getComputedStyle(el).overflowY))) el = el.parentElement;
if (!el) return null;
window.__benchScroller = el;
const box = el.getBoundingClientRect();
return { x: box.left + box.width / 2, y: box.top + box.height / 2, rows: document.querySelectorAll("[data-mid]").length,
nodes: document.getElementsByTagName("*").length, top: el.scrollTop, height: el.scrollHeight };
})()`;
/** Records the time between animation frames until FRAMES_STOP runs. */
export const FRAMES_START = `(() => {
const b = window.__bench;
b.frames = []; b.recording = true; b.longTasksAtStart = b.longTasks.length;
let last = performance.now();
const step = () => { const now = performance.now(); b.frames.push(now - last); last = now; if (b.recording) requestAnimationFrame(step); };
requestAnimationFrame(step);
})()`;
export const FRAMES_STOP = `(() => {
const b = window.__bench;
b.recording = false;
const el = window.__benchScroller;
return { frames: b.frames.slice(1), longTasks: b.longTasks.slice(b.longTasksAtStart), rows: document.querySelectorAll("[data-mid]").length,
nodes: document.getElementsByTagName("*").length, top: el ? el.scrollTop : null, height: el ? el.scrollHeight : null };
})()`;
/** Clicks a sidebar row and resolves, in the page, on the first frame after \`ready\` holds: the switch as a person sees it. */
export const switchExpression = (botId: string, ready: string) => `new Promise((resolve) => {
const row = document.querySelector('[data-sidebar-bot-row="${botId}"]');
if (!row) { resolve({ clicked: false }); return; }
const started = performance.now();
row.click();
const check = () => {
if (${ready}) requestAnimationFrame(() => resolve({ clicked: true, ms: Math.round(performance.now() - started) }));
else if (performance.now() - started > 30000) resolve({ clicked: true, timedOut: true });
else setTimeout(check, 0);
};
check();
})`;
export class Page {
private cdp: Cdp;
readonly sessionId: string;
readonly targetId: string;
constructor(cdp: Cdp, sessionId: string, targetId: string) {
this.cdp = cdp;
this.sessionId = sessionId;
this.targetId = targetId;
}
send(method: string, params: Params = {}): Promise<Params> { return this.cdp.send(method, params, this.sessionId); }
async evaluate<T = unknown>(expression: string, awaitPromise = true): Promise<T> {
const result = await this.send("Runtime.evaluate", { expression, returnByValue: true, awaitPromise }) as { result?: { value?: T }; exceptionDetails?: { exception?: { description?: string }; text?: string } };
if (result.exceptionDetails) throw new Error(`page: ${result.exceptionDetails.exception?.description ?? result.exceptionDetails.text}`);
return result.result?.value as T;
}
async metrics(): Promise<Record<string, number>> {
const { metrics } = await this.send("Performance.getMetrics") as { metrics: Array<{ name: string; value: number }> };
return Object.fromEntries(metrics.map((metric) => [metric.name, metric.value]));
}
async waitFor(expression: string, timeoutMs: number): Promise<boolean> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (await this.evaluate<boolean>(`Boolean(${expression})`, false).catch(() => false)) return true;
await new Promise((resolve) => setTimeout(resolve, 50));
}
return false;
}
async close(): Promise<void> { await this.cdp.send("Target.closeTarget", { targetId: this.targetId }).catch(() => {}); }
}
export interface Chrome {
pid: number;
newPage(options: ProbeOptions, width?: number, height?: number): Promise<Page>;
/** CPU seconds per Chrome process type (browser, renderer, GPU, utility), from Chrome itself. */
cpu(): Promise<Record<string, number>>;
/** Resident memory of each renderer process, in MB. */
rendererRssMb(): Promise<number[]>;
close(): Promise<void>;
}
const CHROME_FLAGS = [
"--headless=new", "--no-first-run", "--no-default-browser-check", "--disable-background-networking", "--disable-component-update",
"--disable-sync", "--disable-default-apps", "--mute-audio", "--use-mock-keychain", "--password-store=basic", "--disable-extensions",
"--enable-precise-memory-info",
];
export async function launchChrome(executable: string): Promise<Chrome> {
const profile = mkdtempSync(join(tmpdir(), "openmausbot-bench-chrome-"));
const child: ChildProcess = spawn(executable, [...CHROME_FLAGS, `--user-data-dir=${profile}`, "--remote-debugging-port=0", "about:blank"], { stdio: "ignore" });
const portFile = join(profile, "DevToolsActivePort");
const deadline = Date.now() + 30_000;
while (!existsSync(portFile) || readFileSync(portFile, "utf8").split("\n").length < 2) {
if (Date.now() > deadline || child.exitCode !== null) {
child.kill("SIGKILL");
await removeTempDir(profile);
throw new Error("Chrome did not open its DevTools port");
}
await new Promise((resolve) => setTimeout(resolve, 50));
}
const [port, path] = readFileSync(portFile, "utf8").trim().split("\n");
const socket = new WebSocket(`ws://127.0.0.1:${port}${path}`);
await new Promise((resolve, reject) => { socket.addEventListener("open", resolve); socket.addEventListener("error", () => reject(new Error("Chrome's DevTools socket did not open"))); });
const cdp = new Cdp(socket);
const processes = async () => ((await cdp.send("SystemInfo.getProcessInfo")) as { processInfo: Array<{ type: string; id: number; cpuTime: number }> }).processInfo;
return {
pid: child.pid!,
async newPage(options, width = 1440, height = 900) {
const { targetId } = await cdp.send("Target.createTarget", { url: "about:blank", width, height, newWindow: true }) as { targetId: string };
const { sessionId } = await cdp.send("Target.attachToTarget", { targetId, flatten: true }) as { sessionId: string };
const page = new Page(cdp, sessionId, targetId);
await page.send("Page.enable");
await page.send("Performance.enable", { timeDomain: "threadTicks" });
await page.send("Page.addScriptToEvaluateOnNewDocument", { source: probeSource(options) });
return page;
},
async cpu() {
const totals: Record<string, number> = {};
for (const entry of await processes()) totals[entry.type] = (totals[entry.type] ?? 0) + entry.cpuTime;
return totals;
},
async rendererRssMb() {
const ids = (await processes()).filter((entry) => entry.type === "renderer").map((entry) => entry.id);
if (!ids.length) return [];
const out = execFileSync("ps", ["-o", "rss=", "-p", ids.join(",")], { encoding: "utf8" });
return out.trim().split("\n").map((line) => Math.round(Number(line.trim()) / 1024));
},
async close() {
await cdp.send("Browser.close").catch(() => {});
cdp.close();
await waitForExit(child, { signal: "SIGTERM" });
await removeTempDir(profile);
},
};
}