| 1 | import { execFileSync } from "node:child_process" |
| 2 | import { mkdirSync, readFileSync, writeFileSync } from "node:fs" |
| 3 | import os from "node:os" |
| 4 | import { dirname, join, resolve } from "node:path" |
| 5 | import { createRequire } from "node:module" |
| 6 | import { fileURLToPath } from "node:url" |
| 7 | |
| 8 | const implementations = [ |
| 9 | "effect-interpreted", |
| 10 | "valibot", |
| 11 | "zod-jitless", |
| 12 | "effect-jit", |
| 13 | "effect-aot", |
| 14 | "zod-compiled" |
| 15 | ] as const |
| 16 | |
| 17 | const cases = [ |
| 18 | "struct-decode", |
| 19 | "struct-invalid", |
| 20 | "struct-is", |
| 21 | "array-decode", |
| 22 | "union-decode", |
| 23 | "transform-decode", |
| 24 | "default-make" |
| 25 | ] as const |
| 26 | |
| 27 | const [outputArgument = "tmp/schema-compiler-resources.json", roundsArgument = "5", countsArgument = "100,500"] = |
| 28 | process.argv.slice(2) |
| 29 | const rounds = Number(roundsArgument) |
| 30 | const counts = countsArgument.split(",").map(Number) |
| 31 | if (!Number.isSafeInteger(rounds) || rounds <= 0) throw new Error("rounds must be a positive integer") |
| 32 | if (counts.some((count) => !Number.isSafeInteger(count) || count <= 0)) { |
| 33 | throw new Error("counts must be comma-separated positive integers") |
| 34 | } |
| 35 | |
| 36 | const root = process.cwd() |
| 37 | const output = resolve(outputArgument) |
| 38 | const worker = fileURLToPath(new URL("./resources.mts", import.meta.url)) |
| 39 | const require = createRequire(import.meta.url) |
| 40 | const packageVersion = (name: string) => { |
| 41 | let directory = dirname(require.resolve(name)) |
| 42 | while (true) { |
| 43 | try { |
| 44 | const metadata = JSON.parse(readFileSync(join(directory, "package.json"), "utf8")) |
| 45 | if (metadata.name === name) return metadata.version as string |
| 46 | } catch { |
| 47 | // Continue at the parent directory. |
| 48 | } |
| 49 | const parent = dirname(directory) |
| 50 | if (parent === directory) throw new Error(`package.json not found for ${name}`) |
| 51 | directory = parent |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | const results: Array<unknown> = [] |
| 56 | for (let round = 0; round < rounds; round++) { |
| 57 | const orderedImplementations = round % 2 === 0 ? implementations : implementations.toReversed() |
| 58 | const orderedCounts = round % 2 === 0 ? counts : counts.toReversed() |
| 59 | for (const count of orderedCounts) { |
| 60 | for (const caseName of cases) { |
| 61 | for (const implementation of orderedImplementations) { |
| 62 | const sample = JSON.parse(execFileSync( |
| 63 | process.execPath, |
| 64 | ["--expose-gc", worker, "measure", root, implementation, caseName, String(count)], |
| 65 | { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 } |
| 66 | )) |
| 67 | results.push({ round, ...sample }) |
| 68 | } |
| 69 | } |
| 70 | process.stderr.write(`resource round ${round + 1}/${rounds}, count ${count} complete\n`) |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | mkdirSync(dirname(output), { recursive: true }) |
| 75 | writeFileSync(output, JSON.stringify({ |
| 76 | environment: { |
| 77 | arch: process.arch, |
| 78 | cpu: os.cpus()[0]?.model ?? "unknown", |
| 79 | node: process.version, |
| 80 | platform: process.platform, |
| 81 | valibot: packageVersion("valibot"), |
| 82 | v8: process.versions.v8, |
| 83 | zod: packageVersion("zod") |
| 84 | }, |
| 85 | measurement: { |
| 86 | cases, |
| 87 | counts, |
| 88 | implementations, |
| 89 | rounds |
| 90 | }, |
| 91 | results |
| 92 | }, null, 2)) |
| 93 | process.stdout.write(`${output}\n`) |