pi-mono/packages/schema/test/bench/memory.ts
Mario Zechner fab2c5353e feat(schema): add pi-schema, JSON Schema builders and validation without eval
@earendil-works/pi-schema compiles JSON Schema (drafts 2020-12 and 7) once into
plain closures: no eval or new Function, so it runs under a strict CSP. It
passes 2,222 of the 2,223 core tests of the official JSON-Schema-Test-Suite,
checks Chord-style argument schemas within 1.2x of TypeBox's JIT, and keeps
3,200 compiled schemas in 7.9 MB (TypeBox: 54.7 MB).

- compile(schema, options) returns a Validator: check() is the boolean check
  (a type guard for builder schemas), errors() reports each failure with a
  JSON Pointer path, missing and unknown fields at the field.
- Builders (str, int, obj, opt, union, ...) return plain JSON Schema typed with
  the values it accepts (Static<typeof schema>).
- Formats are an optional entry point (./formats), adapted from TypeBox (MIT).
- Tests run the vendored suite (drafts 2020-12 and 7, with optional and format
  tests); npm run bench compares conformance, speed, setup cost, memory and
  bundle size with TypeBox, @cfworker/json-schema and omptype.
2026-10-31 21:39:34 +00:00

73 lines
3.3 KiB
TypeScript

// Retained heap of 3,200 distinct compiled schemas per candidate. Each candidate runs in its own process with
// --expose-gc: the schemas and values are built before the baseline, every library's returned validator is kept,
// and the heap is read after forced collections, once after compiling and once after checking each value 3 times
// (when libraries that compile lazily, like omptype, have compiled).
// Run: node test/bench/memory.ts [--json <file>]
import { execFileSync } from "node:child_process";
import { writeFileSync } from "node:fs";
import { byName, candidates } from "./candidates.ts";
import { corpus } from "./corpus.ts";
const COUNT = Number(process.env.COUNT ?? 3200);
/** MB retained by the schemas themselves, and by each candidate after compiling and after checking. */
export interface MemoryResults {
readonly count: number;
readonly schemasMB: number;
readonly candidates: Record<string, { readonly compiledMB: number; readonly checkedMB: number }>;
}
const child = process.argv.indexOf("--child");
if (child >= 0) measure(process.argv[child + 1]);
else run();
function run(): void {
const results: MemoryResults = { count: COUNT, schemasMB: 0, candidates: {} };
const names = [...candidates.map((c) => c.name), "typebox interpreter"];
for (const name of names) {
const output = execFileSync(process.execPath, ["--expose-gc", import.meta.filename, "--child", name], {
encoding: "utf8",
env: { ...process.env, COUNT: String(COUNT) },
});
const result = JSON.parse(output);
Object.assign(results, { schemasMB: result.schemasMB });
results.candidates[name] = { compiledMB: result.compiledMB, checkedMB: result.checkedMB };
}
console.log(`${COUNT} distinct schemas take ${results.schemasMB.toFixed(1)} MB themselves (not counted below).\n`);
console.log("| candidate | after compiling | after checking |");
console.log("|---|---:|---:|");
for (const [name, r] of Object.entries(results.candidates)) {
console.log(`| ${name} | ${r.compiledMB.toFixed(1)} MB | ${r.checkedMB.toFixed(1)} MB |`);
}
const out = process.argv.indexOf("--json");
if (out >= 0) writeFileSync(process.argv[out + 1], JSON.stringify(results, null, "\t"));
}
function measure(name: string): void {
const candidate = byName.get(name);
if (candidate === undefined) throw new Error(`Unknown candidate ${name}`);
const gc = (globalThis as { gc?: () => void }).gc;
if (gc === undefined) throw new Error("Run with --expose-gc");
const heap = () => {
for (let i = 0; i < 4; i++) gc();
return process.memoryUsage().heapUsed;
};
// Warm the library's own code on another corpus, so its lazily compiled functions are not counted.
for (const { schema, value } of corpus(100, 7)) {
const { check } = candidate.prepare(schema, { draft: "2020-12" });
for (let i = 0; i < 4; i++) check(value);
}
const empty = heap();
const items = corpus(COUNT, 42);
const base = heap();
const prepared = items.map(({ schema }) => candidate.prepare(schema, { draft: "2020-12" }));
const compiled = heap();
for (let i = 0; i < items.length; i++) for (let k = 0; k < 3; k++) prepared[i].check(items[i].value);
const checked = heap();
(globalThis as { keep?: unknown }).keep = [items, prepared];
const mb = (bytes: number) => bytes / 1048576;
console.log(
JSON.stringify({ schemasMB: mb(base - empty), compiledMB: mb(compiled - base), checkedMB: mb(checked - base) }),
);
}