Files
yaak-mountain-loop/packages/plugin-sandbox/bench/import.mjs
T
Gregory Schier 45adc2d023 Cut comments back to the non-obvious
Rationale that explains a decision rather than the code below it belongs in
the sandbox README or the PR, not in a header paragraph on every file.
2026-08-17 22:10:12 -07:00

130 lines
4.7 KiB
JavaScript

/**
* How much slower is an importer inside the sandbox? Yaak's OpenAPI importer is
* first-party JavaScript, so a large spec is parsed by whatever engine the
* runtime uses. Numbers are in the README.
*
* node packages/plugin-sandbox/bench/import.mjs <spec.json> [iterations]
*/
import { build } from "esbuild";
import { mkdirSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { pathToFileURL } from "node:url";
import { bundlePlugin } from "../../../scripts/bundle-sandbox-plugins.mjs";
const root = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
const PLUGIN = "importer-openapi";
const specPath = process.argv[2];
const iterations = Number(process.argv[3] ?? 3);
if (specPath == null) {
console.error("usage: node bench/import.mjs <spec.json> [iterations]");
process.exit(1);
}
const spec = readFileSync(specPath, "utf8");
console.log(`Spec: ${specPath} (${(spec.length / 1024 / 1024).toFixed(1)} MB)`);
console.log(`Iterations: ${iterations}\n`);
async function loadHost() {
const outDir = join(root, "node_modules", ".cache", "yaak-plugin-sandbox");
mkdirSync(outDir, { recursive: true });
const outfile = join(outDir, "host.mjs");
await build({
entryPoints: [join(root, "packages/plugin-sandbox/src/host/sandbox.ts")],
bundle: true,
format: "esm",
platform: "node",
target: "node22",
outfile,
// Resolved from the repo at run time, so the wasm variant is the real one.
external: ["@jitl/*", "quickjs-emscripten-core"],
});
return import(pathToFileURL(outfile).href);
}
const ctxStub = { id: "bench", label: null, workspaceId: "wk_bench" };
function stats(times) {
const sorted = [...times].sort((a, b) => a - b);
const mean = times.reduce((a, b) => a + b, 0) / times.length;
return { min: sorted[0], median: sorted[Math.floor(sorted.length / 2)], mean };
}
function report(label, times, resourceCount) {
const { min, median } = stats(times);
console.log(
`${label.padEnd(20)} first ${times[0].toFixed(0).padStart(5)} ms ` +
`best ${min.toFixed(0).padStart(5)} ms ` +
`median ${median.toFixed(0).padStart(5)} ms (${resourceCount} requests)`,
);
// The spread is the point: V8 compiles this across the first few passes and
// QuickJS does not compile at all.
console.log(`${" ".repeat(20)} runs: ${times.map((t) => t.toFixed(0)).join(", ")} ms`);
return { first: times[0], best: min };
}
/* --------------------------------- Node ---------------------------------- */
const nodeTimes = [];
let nodeCount = 0;
{
const { createRequire } = await import("node:module");
const require = createRequire(join(root, "package.json"));
const mod = require(join(root, "plugins", PLUGIN, "build", "index.js"));
const plugin = mod.plugin ?? mod.default;
for (let i = 0; i < iterations; i++) {
const started = performance.now();
const result = await plugin.importer.onImport(ctxStub, { text: spec });
nodeTimes.push(performance.now() - started);
nodeCount = result?.resources?.httpRequests?.length ?? 0;
}
}
const node = report("Node (V8)", nodeTimes, nodeCount);
/* -------------------------------- QuickJS -------------------------------- */
const quickTimes = [];
let quickCount = 0;
{
const { PluginSandboxHost } = await loadHost();
const source = await bundlePlugin(PLUGIN);
const host = new PluginSandboxHost(
async () => JSON.stringify({ type: "empty_response" }),
(log) => console.error(`[${log.level}] ${log.message}`),
);
const loadStarted = performance.now();
await host.load(PLUGIN, source);
console.log(`(sandbox boot + load: ${(performance.now() - loadStarted).toFixed(0)} ms)\n`);
for (let i = 0; i < iterations; i++) {
const started = performance.now();
const reply = JSON.parse(
await host.dispatch(
PLUGIN,
JSON.stringify({ context: ctxStub, payload: { type: "import_request", content: spec } }),
),
);
quickTimes.push(performance.now() - started);
if (reply.type === "error_response") throw new Error(reply.error);
quickCount = reply.resources?.httpRequests?.length ?? 0;
}
host.dispose();
}
const quick = report("QuickJS (sandbox)", quickTimes, quickCount);
console.log(
`\nFirst run (what a user waits for): ${(quick.first / 1000).toFixed(1)}s in the sandbox ` +
`vs ${(node.first / 1000).toFixed(1)}s in Node — ${(quick.first / node.first).toFixed(1)}x.`,
);
console.log(
`Best run (both warm): ${(quick.best / node.best).toFixed(1)}x, which is the ceiling once V8 has compiled.`,
);
if (nodeCount !== quickCount) {
console.log(`WARNING: request counts differ (${nodeCount} vs ${quickCount}) — not the same work.`);
}