1import { pathToFileURL } from "node:url"
2import { Bench } from "tinybench"
3import { median } from "./stats.mts"
4
5let sink
6
7const args = process.argv.slice(2)
8const readOption = (name) => {
9 const index = args.indexOf(name)
10 if (index === -1) return undefined
11 const value = args[index + 1]
12 if (value === undefined || value.startsWith("--")) {
13 throw new Error(`Missing value for ${name}`)
14 }
15 return value
16}
17
18const readPositiveNumber = (name, fallback) => {
19 const raw = readOption(name)
20 if (raw === undefined) return fallback
21 const value = Number(raw)
22 if (!Number.isFinite(value) || value <= 0) {
23 throw new Error(`${name} must be a positive number`)
24 }
25 return value
26}
27
28const isPromiseLike = (value) =>
29 value !== null &&
30 (typeof value === "object" || typeof value === "function") &&
31 typeof value.then === "function"
32
33const loadCase = async (fixturePath, exportName) => {
34 const fixture = await import(`${pathToFileURL(fixturePath).href}?runtimeperf=${process.pid}`)
35 const factory = fixture[exportName]
36 if (typeof factory !== "function") {
37 throw new Error(`Fixture export ${exportName} is not a function`)
38 }
39 const runtimeCase = factory()
40 if (
41 runtimeCase === null ||
42 typeof runtimeCase !== "object" ||
43 typeof runtimeCase.run !== "function" ||
44 typeof runtimeCase.validate !== "function"
45 ) {
46 throw new Error(`Fixture export ${exportName} did not return a RuntimePerfCase`)
47 }
48 const validationResult = runtimeCase.run()
49 if (isPromiseLike(validationResult)) {
50 throw new Error(`Fixture export ${exportName} returned a Promise`)
51 }
52 runtimeCase.validate(validationResult)
53 sink = validationResult
54 return runtimeCase
55}
56
57const runBatch = (run, batchSize) => {
58 let value
59 for (let index = 0; index < batchSize; index++) {
60 value = run()
61 }
62 if (isPromiseLike(value)) {
63 throw new Error("Synchronous runtimeperf task returned a Promise")
64 }
65 sink = value
66}
67
68const calibrate = (runtimeCase, targetBatchTimeNs, maxBatchSize) => {
69 const warnings = []
70 let batchSize = 1
71 while (true) {
72 const samples = new Array(5)
73 for (let attempt = 0; attempt < samples.length; attempt++) {
74 const start = process.hrtime.bigint()
75 runBatch(runtimeCase.run, batchSize)
76 samples[attempt] = Number(process.hrtime.bigint() - start)
77 }
78 if (median(samples) >= targetBatchTimeNs || batchSize >= maxBatchSize) {
79 if (batchSize >= maxBatchSize && median(samples) < targetBatchTimeNs) {
80 warnings.push(`Maximum batch size ${maxBatchSize} did not reach ${targetBatchTimeNs} ns`)
81 }
82 return { batchSize, warnings }
83 }
84 batchSize = Math.min(batchSize * 2, maxBatchSize)
85 }
86}
87
88const measure = (runtimeCase, batchSize, timeMs, warmupTimeMs) => {
89 const bench = new Bench({
90 iterations: 1,
91 time: timeMs,
92 warmup: true,
93 warmupIterations: 1,
94 warmupTime: warmupTimeMs,
95 timestampProvider: "hrtimeNow"
96 })
97 bench.add("runtimeperf", () => runBatch(runtimeCase.run, batchSize), { async: false })
98 bench.runSync()
99 const task = bench.tasks[0]
100 const result = task.result
101 if (result?.state !== "completed") {
102 throw new Error(`Tinybench task did not complete: ${result?.state ?? "missing result"}`)
103 }
104 const latencyToNs = (value) => value * 1_000_000 / batchSize
105 return {
106 nsPerOp: result.totalTime * 1_000_000 / (task.runs * batchSize),
107 runs: task.runs,
108 batchSize,
109 totalTimeMs: result.totalTime,
110 latency: {
111 p50Ns: latencyToNs(result.latency.p50),
112 p99Ns: latencyToNs(result.latency.p99),
113 minNs: latencyToNs(result.latency.min),
114 maxNs: latencyToNs(result.latency.max),
115 rme: result.latency.rme,
116 samplesCount: result.latency.samplesCount
117 },
118 runtime: result.runtime,
119 runtimeVersion: result.runtimeVersion,
120 timestampProviderName: result.timestampProviderName
121 }
122}
123
124const main = async () => {
125 const mode = readOption("--mode")
126 const fixturePath = readOption("--fixture")
127 const exportName = readOption("--export")
128 if (mode !== "calibrate" && mode !== "measure") {
129 throw new Error("--mode must be calibrate or measure")
130 }
131 if (fixturePath === undefined || exportName === undefined) {
132 throw new Error("--fixture and --export are required")
133 }
134 const runtimeCase = await loadCase(fixturePath, exportName)
135 const output = mode === "calibrate"
136 ? calibrate(
137 runtimeCase,
138 readPositiveNumber("--target-batch-time-ns", 100_000),
139 readPositiveNumber("--max-batch-size", 1_048_576)
140 )
141 : measure(
142 runtimeCase,
143 readPositiveNumber("--batch-size", 1),
144 readPositiveNumber("--time-ms", 500),
145 readPositiveNumber("--warmup-time-ms", 150)
146 )
147 runtimeCase.validate(sink)
148 process.stdout.write(`${JSON.stringify({ ok: true, mode, ...output })}\n`)
149}
150
151main().catch((error) => {
152 process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`)
153 process.exitCode = 1
154})