benchforge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +432 -0
  2. package/bin/benchforge +3 -0
  3. package/dist/bin/benchforge.mjs +9 -0
  4. package/dist/bin/benchforge.mjs.map +1 -0
  5. package/dist/browser/index.js +914 -0
  6. package/dist/index.mjs +3 -0
  7. package/dist/src-CGuaC3Wo.mjs +3676 -0
  8. package/dist/src-CGuaC3Wo.mjs.map +1 -0
  9. package/package.json +49 -0
  10. package/src/BenchMatrix.ts +380 -0
  11. package/src/Benchmark.ts +33 -0
  12. package/src/BenchmarkReport.ts +156 -0
  13. package/src/GitUtils.ts +79 -0
  14. package/src/HtmlDataPrep.ts +148 -0
  15. package/src/MeasuredResults.ts +127 -0
  16. package/src/NodeGC.ts +48 -0
  17. package/src/PermutationTest.ts +115 -0
  18. package/src/StandardSections.ts +268 -0
  19. package/src/StatisticalUtils.ts +176 -0
  20. package/src/TypeUtil.ts +8 -0
  21. package/src/bin/benchforge.ts +4 -0
  22. package/src/browser/BrowserGcStats.ts +44 -0
  23. package/src/browser/BrowserHeapSampler.ts +248 -0
  24. package/src/cli/CliArgs.ts +64 -0
  25. package/src/cli/FilterBenchmarks.ts +68 -0
  26. package/src/cli/RunBenchCLI.ts +856 -0
  27. package/src/export/JsonExport.ts +103 -0
  28. package/src/export/JsonFormat.ts +91 -0
  29. package/src/export/PerfettoExport.ts +203 -0
  30. package/src/heap-sample/HeapSampleReport.ts +196 -0
  31. package/src/heap-sample/HeapSampler.ts +78 -0
  32. package/src/html/HtmlReport.ts +131 -0
  33. package/src/html/HtmlTemplate.ts +284 -0
  34. package/src/html/Types.ts +88 -0
  35. package/src/html/browser/CIPlot.ts +287 -0
  36. package/src/html/browser/HistogramKde.ts +118 -0
  37. package/src/html/browser/LegendUtils.ts +163 -0
  38. package/src/html/browser/RenderPlots.ts +263 -0
  39. package/src/html/browser/SampleTimeSeries.ts +389 -0
  40. package/src/html/browser/Types.ts +96 -0
  41. package/src/html/browser/index.ts +1 -0
  42. package/src/html/index.ts +17 -0
  43. package/src/index.ts +92 -0
  44. package/src/matrix/CaseLoader.ts +36 -0
  45. package/src/matrix/MatrixFilter.ts +103 -0
  46. package/src/matrix/MatrixReport.ts +290 -0
  47. package/src/matrix/VariantLoader.ts +46 -0
  48. package/src/runners/AdaptiveWrapper.ts +391 -0
  49. package/src/runners/BasicRunner.ts +368 -0
  50. package/src/runners/BenchRunner.ts +60 -0
  51. package/src/runners/CreateRunner.ts +11 -0
  52. package/src/runners/GcStats.ts +107 -0
  53. package/src/runners/RunnerOrchestrator.ts +374 -0
  54. package/src/runners/RunnerUtils.ts +2 -0
  55. package/src/runners/TimingUtils.ts +13 -0
  56. package/src/runners/WorkerScript.ts +256 -0
  57. package/src/table-util/ConvergenceFormatters.ts +19 -0
  58. package/src/table-util/Formatters.ts +152 -0
  59. package/src/table-util/README.md +70 -0
  60. package/src/table-util/TableReport.ts +293 -0
  61. package/src/table-util/test/TableReport.test.ts +105 -0
  62. package/src/table-util/test/TableValueExtractor.test.ts +41 -0
  63. package/src/table-util/test/TableValueExtractor.ts +100 -0
  64. package/src/test/AdaptiveRunner.test.ts +185 -0
  65. package/src/test/AdaptiveStatistics.integration.ts +119 -0
  66. package/src/test/BenchmarkReport.test.ts +82 -0
  67. package/src/test/BrowserBench.e2e.test.ts +44 -0
  68. package/src/test/BrowserBench.test.ts +79 -0
  69. package/src/test/GcStats.test.ts +94 -0
  70. package/src/test/PermutationTest.test.ts +121 -0
  71. package/src/test/RunBenchCLI.test.ts +166 -0
  72. package/src/test/RunnerOrchestrator.test.ts +102 -0
  73. package/src/test/StatisticalUtils.test.ts +112 -0
  74. package/src/test/TestUtils.ts +93 -0
  75. package/src/test/fixtures/test-bench-script.ts +30 -0
  76. package/src/tests/AdaptiveConvergence.test.ts +177 -0
  77. package/src/tests/AdaptiveSampling.test.ts +240 -0
  78. package/src/tests/BenchMatrix.test.ts +366 -0
  79. package/src/tests/MatrixFilter.test.ts +117 -0
  80. package/src/tests/MatrixReport.test.ts +139 -0
  81. package/src/tests/RealDataValidation.test.ts +177 -0
  82. package/src/tests/fixtures/baseline/impl.ts +4 -0
  83. package/src/tests/fixtures/bevy30-samples.ts +158 -0
  84. package/src/tests/fixtures/cases/asyncCases.ts +7 -0
  85. package/src/tests/fixtures/cases/cases.ts +8 -0
  86. package/src/tests/fixtures/cases/variants/product.ts +2 -0
  87. package/src/tests/fixtures/cases/variants/sum.ts +2 -0
  88. package/src/tests/fixtures/discover/fast.ts +1 -0
  89. package/src/tests/fixtures/discover/slow.ts +4 -0
  90. package/src/tests/fixtures/invalid/bad.ts +1 -0
  91. package/src/tests/fixtures/loader/fast.ts +1 -0
  92. package/src/tests/fixtures/loader/slow.ts +4 -0
  93. package/src/tests/fixtures/loader/stateful.ts +2 -0
  94. package/src/tests/fixtures/stateful/stateful.ts +2 -0
  95. package/src/tests/fixtures/variants/extra.ts +1 -0
  96. package/src/tests/fixtures/variants/impl.ts +1 -0
  97. package/src/tests/fixtures/worker/fast.ts +1 -0
  98. package/src/tests/fixtures/worker/slow.ts +4 -0
@@ -0,0 +1,374 @@
1
+ import { type ChildProcess, fork } from "node:child_process";
2
+ import path from "node:path";
3
+ import type { BenchmarkSpec } from "../Benchmark.ts";
4
+ import type { HeapProfile } from "../heap-sample/HeapSampler.ts";
5
+ import type { MeasuredResults } from "../MeasuredResults.ts";
6
+ import {
7
+ type AdaptiveOptions,
8
+ createAdaptiveWrapper,
9
+ } from "./AdaptiveWrapper.ts";
10
+ import type { RunnerOptions } from "./BenchRunner.ts";
11
+ import { createRunner, type KnownRunner } from "./CreateRunner.ts";
12
+ import { aggregateGcStats, type GcEvent, parseGcLine } from "./GcStats.ts";
13
+ import { debugWorkerTiming, getElapsed, getPerfNow } from "./TimingUtils.ts";
14
+ import type {
15
+ ErrorMessage,
16
+ ResultMessage,
17
+ RunMessage,
18
+ } from "./WorkerScript.ts";
19
+
20
+ const logTiming = debugWorkerTiming
21
+ ? (message: string) => console.log(`[RunnerOrchestrator] ${message}`)
22
+ : () => {};
23
+
24
+ type WorkerParams<T = unknown> = {
25
+ spec: BenchmarkSpec<T>;
26
+ runner: KnownRunner;
27
+ options: RunnerOptions;
28
+ params?: T;
29
+ };
30
+
31
+ type WorkerHandlers = {
32
+ resolve: (results: MeasuredResults[], heapProfile?: HeapProfile) => void;
33
+ reject: (error: Error) => void;
34
+ };
35
+
36
+ interface RunBenchmarkParams<T = unknown> {
37
+ spec: BenchmarkSpec<T>;
38
+ runner: KnownRunner;
39
+ options: RunnerOptions;
40
+ useWorker?: boolean;
41
+ params?: T;
42
+ }
43
+
44
+ /** Execute benchmarks directly or in worker process */
45
+ export async function runBenchmark<T = unknown>({
46
+ spec,
47
+ runner,
48
+ options,
49
+ useWorker = false,
50
+ params,
51
+ }: RunBenchmarkParams<T>): Promise<MeasuredResults[]> {
52
+ if (!useWorker) {
53
+ const resolvedSpec = spec.modulePath
54
+ ? await resolveModuleSpec(spec, params)
55
+ : { spec, params };
56
+
57
+ const base = await createRunner(runner);
58
+ const benchRunner = (options as any).adaptive
59
+ ? createAdaptiveWrapper(base, options as AdaptiveOptions)
60
+ : base;
61
+ return benchRunner.runBench(
62
+ resolvedSpec.spec,
63
+ options,
64
+ resolvedSpec.params,
65
+ );
66
+ }
67
+
68
+ return runInWorker({ spec, runner, options, params });
69
+ }
70
+
71
+ /** Resolve modulePath/exportName to a real function for non-worker mode */
72
+ async function resolveModuleSpec<T>(
73
+ spec: BenchmarkSpec<T>,
74
+ params: T | undefined,
75
+ ): Promise<{ spec: BenchmarkSpec<T>; params: T | undefined }> {
76
+ const module = await import(spec.modulePath!);
77
+
78
+ const fn = spec.exportName
79
+ ? module[spec.exportName]
80
+ : module.default || module;
81
+
82
+ if (typeof fn !== "function") {
83
+ const name = spec.exportName || "default";
84
+ throw new Error(
85
+ `Export '${name}' from ${spec.modulePath} is not a function`,
86
+ );
87
+ }
88
+
89
+ let resolvedParams = params;
90
+ if (spec.setupExportName) {
91
+ const setupFn = module[spec.setupExportName];
92
+ if (typeof setupFn !== "function") {
93
+ const msg = `Setup export '${spec.setupExportName}' from ${spec.modulePath} is not a function`;
94
+ throw new Error(msg);
95
+ }
96
+ resolvedParams = await setupFn(params);
97
+ }
98
+
99
+ return { spec: { ...spec, fn }, params: resolvedParams };
100
+ }
101
+
102
+ /** Run benchmark in isolated worker process */
103
+ async function runInWorker<T>(
104
+ workerParams: WorkerParams<T>,
105
+ ): Promise<MeasuredResults[]> {
106
+ const { spec, runner, options, params } = workerParams;
107
+ const msg = createRunMessage(spec, runner, options, params);
108
+ return runWorkerWithMessage(spec.name, options, msg);
109
+ }
110
+
111
+ /** Create worker process with timing logs */
112
+ function createWorkerWithTiming(gcStats: boolean) {
113
+ const workerStart = getPerfNow();
114
+ const gcEvents: GcEvent[] = [];
115
+ const worker = createWorkerProcess(gcStats);
116
+ const createTime = getPerfNow();
117
+ if (gcStats && worker.stdout) setupGcCapture(worker, gcEvents);
118
+ logTiming(
119
+ `Worker process created in ${getElapsed(workerStart, createTime).toFixed(1)}ms`,
120
+ );
121
+ return { worker, createTime, gcEvents };
122
+ }
123
+
124
+ /** Capture and parse GC lines from stdout (V8's --trace-gc-nvp outputs to stdout) */
125
+ function setupGcCapture(worker: ChildProcess, gcEvents: GcEvent[]): void {
126
+ let buffer = "";
127
+ worker.stdout!.on("data", (data: Buffer) => {
128
+ buffer += data.toString();
129
+ const lines = buffer.split("\n");
130
+ buffer = lines.pop() || ""; // Keep incomplete line in buffer
131
+ for (const line of lines) {
132
+ const event = parseGcLine(line);
133
+ if (event) {
134
+ gcEvents.push(event);
135
+ } else if (line.trim()) {
136
+ // Forward non-GC stdout to console (worker status messages)
137
+ process.stdout.write(line + "\n");
138
+ }
139
+ }
140
+ });
141
+ }
142
+
143
+ /** Spawn worker, wire handlers, send message, return results */
144
+ function runWorkerWithMessage(
145
+ name: string,
146
+ options: RunnerOptions,
147
+ message: RunMessage,
148
+ ): Promise<MeasuredResults[]> {
149
+ const startTime = getPerfNow();
150
+ const collectGcStats = options.gcStats ?? false;
151
+ logTiming(`Starting worker for ${name}`);
152
+
153
+ return new Promise((resolve, reject) => {
154
+ const { worker, createTime, gcEvents } =
155
+ createWorkerWithTiming(collectGcStats);
156
+ const handlers = createWorkerHandlers(
157
+ name,
158
+ startTime,
159
+ gcEvents,
160
+ resolve,
161
+ reject,
162
+ );
163
+ setupWorkerHandlers(worker, name, handlers);
164
+ sendWorkerMessage(worker, message, createTime);
165
+ });
166
+ }
167
+
168
+ /** Send message to worker with timing log */
169
+ function sendWorkerMessage(
170
+ worker: ReturnType<typeof createWorkerProcess>,
171
+ message: RunMessage,
172
+ createTime: number,
173
+ ): void {
174
+ const messageTime = getPerfNow();
175
+ worker.send(message);
176
+ logTiming(
177
+ `Message sent to worker in ${getElapsed(createTime, messageTime).toFixed(1)}ms`,
178
+ );
179
+ }
180
+
181
+ /** Setup worker event handlers with cleanup */
182
+ function setupWorkerHandlers(
183
+ worker: ReturnType<typeof createWorkerProcess>,
184
+ specName: string,
185
+ handlers: WorkerHandlers,
186
+ ) {
187
+ const { resolve, reject } = handlers;
188
+ const cleanup = createCleanup(worker, specName, reject);
189
+ worker.on(
190
+ "message",
191
+ createMessageHandler(specName, cleanup, resolve, reject),
192
+ );
193
+ worker.on("error", createErrorHandler(specName, cleanup, reject));
194
+ worker.on("exit", createExitHandler(specName, cleanup, reject));
195
+ }
196
+
197
+ /** Handle worker messages (results or errors) */
198
+ function createMessageHandler(
199
+ specName: string,
200
+ cleanup: () => void,
201
+ resolve: (results: MeasuredResults[], heapProfile?: HeapProfile) => void,
202
+ reject: (error: Error) => void,
203
+ ) {
204
+ return (msg: ResultMessage | ErrorMessage) => {
205
+ cleanup();
206
+ if (msg.type === "result") {
207
+ resolve(msg.results, msg.heapProfile);
208
+ } else if (msg.type === "error") {
209
+ const error = new Error(`Benchmark "${specName}" failed: ${msg.error}`);
210
+ if (msg.stack) error.stack = msg.stack;
211
+ reject(error);
212
+ }
213
+ };
214
+ }
215
+
216
+ /** Handle worker process errors */
217
+ function createErrorHandler(
218
+ specName: string,
219
+ cleanup: () => void,
220
+ reject: (error: Error) => void,
221
+ ) {
222
+ return (error: Error) => {
223
+ cleanup();
224
+ reject(
225
+ new Error(
226
+ `Worker process failed for benchmark "${specName}": ${error.message}`,
227
+ ),
228
+ );
229
+ };
230
+ }
231
+
232
+ /** Handle worker process exit */
233
+ function createExitHandler(
234
+ specName: string,
235
+ cleanup: () => void,
236
+ reject: (error: Error) => void,
237
+ ) {
238
+ return (code: number | null, _signal: NodeJS.Signals | null) => {
239
+ if (code !== 0 && code !== null) {
240
+ cleanup();
241
+ const msg = `Worker exited with code ${code} for benchmark "${specName}"`;
242
+ reject(new Error(msg));
243
+ }
244
+ };
245
+ }
246
+
247
+ /** Create cleanup for timeout and termination */
248
+ function createCleanup(
249
+ worker: ReturnType<typeof createWorkerProcess>,
250
+ specName: string,
251
+ reject: (error: Error) => void,
252
+ ) {
253
+ const timeoutId = setTimeout(() => {
254
+ cleanup();
255
+ reject(new Error(`Benchmark "${specName}" timed out after 60 seconds`));
256
+ }, 60000);
257
+ const cleanup = () => {
258
+ clearTimeout(timeoutId);
259
+ if (!worker.killed) worker.kill("SIGTERM");
260
+ };
261
+ return cleanup;
262
+ }
263
+
264
+ /** Create worker process with configuration */
265
+ function createWorkerProcess(gcStats: boolean) {
266
+ const workerPath = path.join(import.meta.dirname!, "WorkerScript.ts");
267
+ const execArgv = [
268
+ "--expose-gc",
269
+ "--allow-natives-syntax",
270
+ "--experimental-strip-types",
271
+ "--no-warnings=ExperimentalWarning",
272
+ ];
273
+ if (gcStats) execArgv.push("--trace-gc-nvp");
274
+
275
+ return fork(workerPath, [], {
276
+ execArgv,
277
+ silent: gcStats, // Capture stdout/stderr when collecting GC stats
278
+ env: {
279
+ ...process.env,
280
+ NODE_OPTIONS: "",
281
+ },
282
+ });
283
+ }
284
+
285
+ // Consider: --no-compilation-cache, --max-old-space-size=512, --no-lazy
286
+ // for consistency (less realistic)
287
+
288
+ /** @return handlers that attach GC stats and heap profile to results */
289
+ function createWorkerHandlers(
290
+ specName: string,
291
+ startTime: number,
292
+ gcEvents: GcEvent[] | undefined,
293
+ resolve: (results: MeasuredResults[]) => void,
294
+ reject: (error: Error) => void,
295
+ ): WorkerHandlers {
296
+ return {
297
+ resolve: (results: MeasuredResults[], heapProfile?: HeapProfile) => {
298
+ logTiming(
299
+ `Total worker time for ${specName}: ${getElapsed(startTime).toFixed(1)}ms`,
300
+ );
301
+ if (gcEvents?.length) {
302
+ const gcStats = aggregateGcStats(gcEvents);
303
+ for (const r of results) r.gcStats = gcStats;
304
+ }
305
+ if (heapProfile) for (const r of results) r.heapProfile = heapProfile;
306
+ resolve(results);
307
+ },
308
+ reject,
309
+ };
310
+ }
311
+
312
+ /** Create message for worker execution */
313
+ function createRunMessage<T>(
314
+ spec: BenchmarkSpec<T>,
315
+ runnerName: KnownRunner,
316
+ options: RunnerOptions,
317
+ params?: T,
318
+ ): RunMessage {
319
+ const { fn, ...rest } = spec;
320
+ const message: RunMessage = {
321
+ type: "run",
322
+ spec: rest as BenchmarkSpec,
323
+ runnerName,
324
+ options,
325
+ params,
326
+ };
327
+ if (spec.modulePath) {
328
+ message.modulePath = spec.modulePath;
329
+ message.exportName = spec.exportName;
330
+ if (spec.setupExportName) message.setupExportName = spec.setupExportName;
331
+ } else {
332
+ message.fnCode = fn.toString();
333
+ }
334
+ return message;
335
+ }
336
+
337
+ /** Parameters for running a matrix variant in worker */
338
+ export interface RunMatrixVariantParams {
339
+ variantDir: string;
340
+ variantId: string;
341
+ caseId: string;
342
+ caseData?: unknown;
343
+ casesModule?: string;
344
+ runner: KnownRunner;
345
+ options: RunnerOptions;
346
+ }
347
+
348
+ /** Run a matrix variant benchmark in isolated worker process */
349
+ export async function runMatrixVariant(
350
+ params: RunMatrixVariantParams,
351
+ ): Promise<MeasuredResults[]> {
352
+ const {
353
+ variantDir,
354
+ variantId,
355
+ caseId,
356
+ caseData,
357
+ casesModule,
358
+ runner,
359
+ options,
360
+ } = params;
361
+ const name = `${variantId}/${caseId}`;
362
+ const message: RunMessage = {
363
+ type: "run",
364
+ spec: { name, fn: () => {} },
365
+ runnerName: runner,
366
+ options,
367
+ variantDir,
368
+ variantId,
369
+ caseId,
370
+ caseData,
371
+ casesModule,
372
+ };
373
+ return runWorkerWithMessage(name, options, message);
374
+ }
@@ -0,0 +1,2 @@
1
+ export const msToNs = 1e6;
2
+ export const nsToMs = 1e-6;
@@ -0,0 +1,13 @@
1
+ export const debugWorkerTiming = false;
2
+
3
+ /** Get current time or 0 if debugging disabled */
4
+ export function getPerfNow(): number {
5
+ return debugWorkerTiming ? performance.now() : 0;
6
+ }
7
+
8
+ /** Calculate elapsed milliseconds between marks */
9
+ export function getElapsed(startMark: number, endMark?: number): number {
10
+ if (!debugWorkerTiming) return 0;
11
+ const end = endMark ?? performance.now();
12
+ return end - startMark;
13
+ }
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env node
2
+ import type { BenchmarkFunction, BenchmarkSpec } from "../Benchmark.ts";
3
+ import type { HeapProfile } from "../heap-sample/HeapSampler.ts";
4
+ import type { MeasuredResults } from "../MeasuredResults.ts";
5
+ import { variantModuleUrl } from "../matrix/VariantLoader.ts";
6
+ import {
7
+ type AdaptiveOptions,
8
+ createAdaptiveWrapper,
9
+ } from "./AdaptiveWrapper.ts";
10
+ import type { RunnerOptions } from "./BenchRunner.ts";
11
+ import { createRunner, type KnownRunner } from "./CreateRunner.ts";
12
+ import { debugWorkerTiming, getElapsed, getPerfNow } from "./TimingUtils.ts";
13
+
14
+ const workerStartTime = getPerfNow();
15
+ const maxLifetime = 5 * 60 * 1000; // 5 minutes
16
+
17
+ /** Message sent to worker process to start a benchmark run. */
18
+ export interface RunMessage {
19
+ type: "run";
20
+ spec: BenchmarkSpec;
21
+ runnerName: KnownRunner;
22
+ options: RunnerOptions;
23
+ fnCode?: string; // Made optional - either fnCode or modulePath is required
24
+ modulePath?: string; // Path to module for dynamic import
25
+ exportName?: string; // Export name from module
26
+ setupExportName?: string; // Setup function export name - called once, result passed to fn
27
+ params?: unknown;
28
+ // Variant directory mode (BenchMatrix)
29
+ variantDir?: string; // Directory URL containing variant .ts files
30
+ variantId?: string; // Variant filename (without .ts)
31
+ caseData?: unknown; // Data to pass to variant
32
+ caseId?: string; // Case identifier
33
+ casesModule?: string; // URL to cases module (exports cases[] and loadCase())
34
+ }
35
+
36
+ /** Message returned from worker process with benchmark results. */
37
+ export interface ResultMessage {
38
+ type: "result";
39
+ results: MeasuredResults[];
40
+ heapProfile?: HeapProfile;
41
+ }
42
+
43
+ /** Message returned from worker process when benchmark fails. */
44
+ export interface ErrorMessage {
45
+ type: "error";
46
+ error: string;
47
+ stack?: string;
48
+ }
49
+
50
+ export type WorkerMessage = RunMessage | ResultMessage | ErrorMessage;
51
+
52
+ /**
53
+ * Worker process for isolated benchmark execution.
54
+ * Uses eval() safely in isolated child process with trusted code.
55
+ */
56
+ process.on("message", async (message: RunMessage) => {
57
+ if (message.type !== "run") return;
58
+
59
+ logTiming(`Processing ${message.spec.name} with ${message.runnerName}`);
60
+
61
+ try {
62
+ const start = getPerfNow();
63
+ const baseRunner = await createRunner(message.runnerName);
64
+
65
+ const runner = (message.options as any).adaptive
66
+ ? createAdaptiveWrapper(baseRunner, message.options as AdaptiveOptions)
67
+ : baseRunner;
68
+
69
+ logTiming("Runner created in", getElapsed(start));
70
+
71
+ const benchStart = getPerfNow();
72
+
73
+ // Run with heap sampling if enabled (covers module import + execution)
74
+ if (message.options.heapSample) {
75
+ const { withHeapSampling } = await import(
76
+ "../heap-sample/HeapSampler.ts"
77
+ );
78
+ const heapOpts = {
79
+ samplingInterval: message.options.heapInterval,
80
+ stackDepth: message.options.heapDepth,
81
+ };
82
+ const { result: results, profile: heapProfile } = await withHeapSampling(
83
+ heapOpts,
84
+ async () => {
85
+ const { fn, params } = await resolveBenchmarkFn(message);
86
+ return runner.runBench(
87
+ { ...message.spec, fn },
88
+ message.options,
89
+ params,
90
+ );
91
+ },
92
+ );
93
+ logTiming("Benchmark execution took", getElapsed(benchStart));
94
+ sendAndExit({ type: "result", results, heapProfile }, 0);
95
+ } else {
96
+ const { fn, params } = await resolveBenchmarkFn(message);
97
+ const results = await runner.runBench(
98
+ { ...message.spec, fn },
99
+ message.options,
100
+ params,
101
+ );
102
+ logTiming("Benchmark execution took", getElapsed(benchStart));
103
+ sendAndExit({ type: "result", results }, 0);
104
+ }
105
+ } catch (error) {
106
+ sendAndExit(createErrorMessage(error), 1);
107
+ }
108
+ });
109
+
110
+ // Exit after 5 minutes to prevent zombie processes
111
+ setTimeout(() => {
112
+ console.error("WorkerScript: Maximum lifetime exceeded, exiting");
113
+ process.exit(1);
114
+ }, maxLifetime);
115
+
116
+ process.stdin.pause();
117
+
118
+ /** Log timing with consistent format */
119
+ const logTiming = debugWorkerTiming ? _logTiming : () => {};
120
+ function _logTiming(operation: string, duration?: number) {
121
+ if (duration === undefined) {
122
+ console.log(`[Worker] ${operation}`);
123
+ } else {
124
+ console.log(`[Worker] ${operation} ${duration.toFixed(1)}ms`);
125
+ }
126
+ }
127
+
128
+ /** Send message and exit with duration log */
129
+ function sendAndExit(msg: ResultMessage | ErrorMessage, exitCode: number) {
130
+ process.send!(msg, undefined, undefined, (err: Error | null): void => {
131
+ if (err) {
132
+ const kind = msg.type === "result" ? "results" : "error message";
133
+ console.error(`[Worker] Error sending ${kind}:`, err);
134
+ }
135
+ const suffix = exitCode === 0 ? "" : " (error)";
136
+ logTiming(`Total worker duration${suffix}:`, getElapsed(workerStartTime));
137
+ process.exit(exitCode);
138
+ });
139
+ }
140
+
141
+ interface BenchmarkImportResult {
142
+ fn: BenchmarkFunction;
143
+ params: unknown;
144
+ }
145
+
146
+ /** Resolve benchmark function from message (variant dir, module path, or fnCode) */
147
+ async function resolveBenchmarkFn(
148
+ message: RunMessage,
149
+ ): Promise<BenchmarkImportResult> {
150
+ if (message.variantDir && message.variantId) {
151
+ return importVariantModule(message);
152
+ }
153
+ if (message.modulePath) {
154
+ return importBenchmarkWithSetup(message);
155
+ }
156
+ return { fn: reconstructFunction(message.fnCode!), params: message.params };
157
+ }
158
+
159
+ /** Import variant from directory and prepare benchmark function */
160
+ async function importVariantModule(
161
+ message: RunMessage,
162
+ ): Promise<BenchmarkImportResult> {
163
+ const { variantDir, variantId, caseId, casesModule } = message;
164
+ let { caseData } = message;
165
+ const moduleUrl = variantModuleUrl(variantDir!, variantId!);
166
+ logTiming(`Importing variant ${variantId} from ${variantDir}`);
167
+
168
+ if (casesModule && caseId) {
169
+ caseData = (await loadCaseFromModule(casesModule, caseId)).data;
170
+ }
171
+
172
+ const module = await import(moduleUrl);
173
+ const { setup, run } = module;
174
+
175
+ if (typeof run !== "function") {
176
+ throw new Error(`Variant '${variantId}' must export 'run' function`);
177
+ }
178
+
179
+ // Stateful variant: setup returns state, run receives state
180
+ if (typeof setup === "function") {
181
+ logTiming(`Calling setup for ${variantId}`);
182
+ const state = await setup(caseData);
183
+ return { fn: () => run(state), params: undefined };
184
+ }
185
+
186
+ // Stateless variant: run receives caseData directly
187
+ return { fn: () => run(caseData), params: undefined };
188
+ }
189
+
190
+ /** Load case data from a cases module */
191
+ async function loadCaseFromModule(
192
+ casesModuleUrl: string,
193
+ caseId: string,
194
+ ): Promise<{ data: unknown; metadata?: Record<string, unknown> }> {
195
+ logTiming(`Loading case '${caseId}' from ${casesModuleUrl}`);
196
+ const module = await import(casesModuleUrl);
197
+ if (typeof module.loadCase === "function") {
198
+ return module.loadCase(caseId);
199
+ }
200
+ return { data: caseId };
201
+ }
202
+
203
+ /** Import benchmark function and optionally run setup */
204
+ async function importBenchmarkWithSetup(
205
+ message: RunMessage,
206
+ ): Promise<BenchmarkImportResult> {
207
+ const { modulePath, exportName, setupExportName, params } = message;
208
+ logTiming(
209
+ `Importing from ${modulePath}${exportName ? ` (${exportName})` : ""}`,
210
+ );
211
+ const module = await import(modulePath!);
212
+
213
+ const fn = getModuleExport(module, exportName, modulePath!);
214
+
215
+ if (setupExportName) {
216
+ logTiming(`Calling setup: ${setupExportName}`);
217
+ const setupFn = getModuleExport(module, setupExportName, modulePath!);
218
+ const setupResult = await setupFn(params);
219
+ return { fn, params: setupResult };
220
+ }
221
+
222
+ return { fn, params };
223
+ }
224
+
225
+ /** Get named or default export from module */
226
+ function getModuleExport(
227
+ module: any,
228
+ exportName: string | undefined,
229
+ modulePath: string,
230
+ ): BenchmarkFunction {
231
+ const fn = exportName ? module[exportName] : module.default || module;
232
+ if (typeof fn !== "function") {
233
+ const name = exportName || "default";
234
+ throw new Error(`Export '${name}' from ${modulePath} is not a function`);
235
+ }
236
+ return fn;
237
+ }
238
+
239
+ /** Reconstruct function from string code */
240
+ function reconstructFunction(fnCode: string): BenchmarkFunction {
241
+ // biome-ignore lint/security/noGlobalEval: Necessary for worker process isolation, code is from trusted source
242
+ const fn = eval(`(${fnCode})`); // eslint-disable-line no-eval
243
+ if (typeof fn !== "function") {
244
+ throw new Error("Reconstructed code is not a function");
245
+ }
246
+ return fn;
247
+ }
248
+
249
+ /** Create error message from exception */
250
+ function createErrorMessage(error: unknown): ErrorMessage {
251
+ return {
252
+ type: "error",
253
+ error: error instanceof Error ? error.message : String(error),
254
+ stack: error instanceof Error ? error.stack : undefined,
255
+ };
256
+ }
@@ -0,0 +1,19 @@
1
+ import pico from "picocolors";
2
+
3
+ const isTest = process.env.NODE_ENV === "test" || process.env.VITEST === "true";
4
+ const { red } = isTest ? { red: (str: string) => str } : pico;
5
+
6
+ const lowConfidence = 80;
7
+
8
+ /** @return convergence percentage with color for low values */
9
+ export function formatConvergence(v: unknown): string {
10
+ if (typeof v !== "number") return "—";
11
+ const pct = `${Math.round(v)}%`;
12
+ return v < lowConfidence ? red(pct) : pct;
13
+ }
14
+
15
+ /** @return coefficient of variation as ±percentage */
16
+ export function formatCV(v: unknown): string {
17
+ if (typeof v !== "number") return "";
18
+ return `±${(v * 100).toFixed(1)}%`;
19
+ }