import http from "node:http"; import path from "node:path"; import { createRequire } from "node:module"; import { fileURLToPath } from "node:url"; import { readFileSync } from "node:fs"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const require = createRequire(import.meta.url); const express = require( path.join(root, "packages/scramjet/packages/runway/node_modules/express") ); const { chromium } = require( path.join(root, "packages/scramjet/packages/runway/node_modules/playwright") ); const { startHarness } = await import("../packages/scramjet/packages/runway/src/harness/scramjet/index.ts"); const baseline = process.env.PRE_IDL_ROOT ?? "/tmp/browserjs-preidl-bench"; const extraScript = readFileSync( path.join(root, "tools/benchmark-element-app.js"), "utf8" ); const appScript = ` const root = document.getElementById("bench-root"); const url = "https://example.com/products/sku-"; function dashboard() { root.innerHTML = ""; const rows = []; const frag = document.createDocumentFragment(); for (let i = 0; i < 180; i++) { const row = document.createElement("div"); row.className = "row"; row.dataset.id = String(i); row.innerHTML = 'Item'; frag.append(row); rows.push(row); } root.append(frag); const start = performance.now(); let digest = 0; for (let pass = 0; pass < 8; pass++) { for (let i = 0; i < rows.length; i++) { const row = rows[i]; const link = row.querySelector("a.title"); link.href = url + i + "?view=" + pass; link.textContent = "Product " + i; row.classList.toggle("selected", (i + pass) % 3 === 0); row.setAttribute("aria-label", "Product " + i); row.dataset.status = pass % 2 ? "ready" : "pending"; row.querySelector(".state").textContent = row.dataset.status; digest += row.matches(".selected") ? 1 : 0; digest += link.getAttribute("href").length; } } return { ms: performance.now() - start, digest }; } function feed() { root.innerHTML = ""; let digest = 0; const start = performance.now(); for (let page = 0; page < 24; page++) { let html = ""; for (let i = 0; i < 24; i++) { const id = page * 24 + i; html += '
Product ' + id + '

Details

'; } root.insertAdjacentHTML("beforeend", html); const cards = root.querySelectorAll("article.card"); for (let i = cards.length - 24; i < cards.length; i++) { const card = cards[i]; digest += Number(card.dataset.id); digest += card.querySelector("a").getAttribute("href").length; digest += card.querySelector("img").getAttribute("src").length; } } return { ms: performance.now() - start, digest }; } function form() { root.innerHTML = ""; const inputs = []; const frag = document.createDocumentFragment(); for (let i = 0; i < 120; i++) { const label = document.createElement("label"); const input = document.createElement("input"); input.name = "field-" + i; label.append("Field " + i, input); frag.append(label); inputs.push(input); } root.append(frag); const start = performance.now(); let digest = 0; for (let pass = 0; pass < 12; pass++) { for (let i = 0; i < inputs.length; i++) { const input = inputs[i]; input.value = "value-" + pass + "-" + i; input.setAttribute("data-dirty", pass % 2 ? "yes" : "no"); input.toggleAttribute("required", i % 4 === 0); input.setAttribute("aria-invalid", i % 7 === 0 ? "true" : "false"); digest += input.getAttribute("data-dirty").length; digest += input.hasAttribute("required") ? 1 : 0; digest += input.attributes.length; } } return { ms: performance.now() - start, digest }; } window.runBench = { dashboard, feed, form }; `; const target = express(); target.get("/", (_req, res) => res .type("html") .send( '
' ) ); target.get("/app.js", (_req, res) => res.type("js").send(appScript + "\n" + extraScript) ); const targetServer = http.createServer(target); await new Promise((resolve) => targetServer.listen(4600, resolve)); await startHarness(); const baseApp = express(); baseApp.use( "/scramjet", express.static(path.join(baseline, "packages/scramjet/packages/core/dist")) ); baseApp.use( "/controller", express.static( path.join(baseline, "packages/scramjet/packages/controller/dist") ) ); baseApp.use( "/libcurl", express.static( path.join( root, "packages/scramjet/packages/runway/node_modules/@mercuryworkshop/libcurl-transport/dist" ) ) ); baseApp.use( express.static( path.join( root, "packages/scramjet/packages/runway/src/harness/scramjet/public" ) ) ); const baseServer = http.createServer(baseApp); await new Promise((resolve) => baseServer.listen(4510, resolve)); const browser = await chromium.launch({ headless: true }); const results = {}; let scenarios; const variants = [ ["pre-idl", "http://localhost:4510/"], ["rewrite", "http://localhost:4500/"], ["chromium", "http://localhost:4600/"], ]; if (process.env.BENCH_REVERSE === "1") variants.reverse(); let runError; try { for (const [name, url] of variants) { const context = await browser.newContext(); const page = await context.newPage(); page.on("pageerror", (error) => console.error(name, "page error:", error.message) ); await page.goto(url); let frame = page.mainFrame(); if (name !== "chromium") { await page.waitForFunction( () => typeof window.__runwayNavigate === "function", { timeout: 30000 } ); await page.evaluate(() => window.__runwayNavigate("http://localhost:4600/") ); await page .frameLocator("#testframe") .locator("#bench-root") .waitFor({ state: "attached", timeout: 30000 }); frame = page .frames() .find((f) => f !== page.mainFrame() && f.url().includes("4600")); if (!frame) throw new Error( "Target frame missing: " + page .frames() .map((f) => f.url()) .join(", ") ); } await frame.waitForFunction(() => window.runBench); const names = ( await frame.evaluate(() => Object.keys(window.runBench)) ).filter( (name) => !process.env.BENCH_FILTER || name.includes(process.env.BENCH_FILTER) ); if (scenarios && names.join() !== scenarios.join()) throw new Error("Scenario lists differ"); scenarios = names; results[name] = {}; for (const scenario of scenarios) { try { for (let i = 0; i < 2; i++) await frame.evaluate((key) => window.runBench[key](), scenario); const samples = []; let digest; for (let i = 0; i < 8; i++) { const result = await frame.evaluate( (key) => window.runBench[key](), scenario ); samples.push(result.ms); digest = result.digest; } samples.sort((a, b) => a - b); results[name][scenario] = { medianMs: (samples[3] + samples[4]) / 2, minMs: samples[0], maxMs: samples.at(-1), digest, samples, }; } catch (error) { results[name][scenario] = { error: String(error).split("\n")[0] }; } console.log(name, scenario, JSON.stringify(results[name][scenario])); } await context.close(); } const mismatches = []; for (const scenario of scenarios) { if (variants.some(([name]) => results[name][scenario].error)) { mismatches.push({ scenario, errors: Object.fromEntries( variants.map(([name]) => [ name, results[name][scenario].error ?? null, ]) ), }); continue; } const digests = variants.map(([name]) => results[name][scenario].digest); if (!digests.every((value) => value === digests[0])) { mismatches.push({ scenario, digests: Object.fromEntries( variants.map(([name], index) => [name, digests[index]]) ), }); } } console.log("MISMATCHES", JSON.stringify(mismatches)); console.log("RESULT_JSON", JSON.stringify(results)); } catch (error) { runError = error; console.error(error); } finally { await browser.close(); await new Promise((resolve) => baseServer.close(resolve)); await new Promise((resolve) => targetServer.close(resolve)); process.exit(runError ? 1 : 0); }