benchforge 0.1.8 → 0.1.11

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 (68) hide show
  1. package/README.md +69 -42
  2. package/dist/{BenchRunner-CSKN9zPy.d.mts → BenchRunner-BzyUfiyB.d.mts} +32 -8
  3. package/dist/{BrowserHeapSampler-DCeL42RE.mjs → BrowserHeapSampler-B6asLKWQ.mjs} +57 -57
  4. package/dist/BrowserHeapSampler-B6asLKWQ.mjs.map +1 -0
  5. package/dist/{GcStats-ByEovUi1.mjs → GcStats-wX7Xyblu.mjs} +15 -15
  6. package/dist/GcStats-wX7Xyblu.mjs.map +1 -0
  7. package/dist/HeapSampler-B8dtKHn1.mjs.map +1 -1
  8. package/dist/{TimingUtils-ClclVQ7E.mjs → TimingUtils-DwOwkc8G.mjs} +225 -225
  9. package/dist/TimingUtils-DwOwkc8G.mjs.map +1 -0
  10. package/dist/bin/benchforge.mjs +1 -1
  11. package/dist/browser/index.js +210 -210
  12. package/dist/index.d.mts +106 -48
  13. package/dist/index.mjs +3 -3
  14. package/dist/runners/WorkerScript.d.mts +1 -1
  15. package/dist/runners/WorkerScript.mjs +66 -66
  16. package/dist/runners/WorkerScript.mjs.map +1 -1
  17. package/dist/{src-HfimYuW_.mjs → src-B-DDaCa9.mjs} +1250 -991
  18. package/dist/src-B-DDaCa9.mjs.map +1 -0
  19. package/package.json +4 -3
  20. package/src/BenchMatrix.ts +125 -125
  21. package/src/BenchmarkReport.ts +50 -45
  22. package/src/HtmlDataPrep.ts +21 -21
  23. package/src/PermutationTest.ts +24 -24
  24. package/src/StandardSections.ts +45 -45
  25. package/src/StatisticalUtils.ts +60 -61
  26. package/src/browser/BrowserGcStats.ts +5 -5
  27. package/src/browser/BrowserHeapSampler.ts +63 -63
  28. package/src/cli/CliArgs.ts +20 -6
  29. package/src/cli/FilterBenchmarks.ts +5 -5
  30. package/src/cli/RunBenchCLI.ts +533 -476
  31. package/src/export/JsonExport.ts +10 -10
  32. package/src/export/PerfettoExport.ts +74 -74
  33. package/src/export/SpeedscopeExport.ts +202 -0
  34. package/src/heap-sample/HeapSampleReport.ts +143 -70
  35. package/src/heap-sample/HeapSampler.ts +55 -12
  36. package/src/heap-sample/ResolvedProfile.ts +89 -0
  37. package/src/html/HtmlReport.ts +33 -33
  38. package/src/html/HtmlTemplate.ts +67 -67
  39. package/src/html/browser/CIPlot.ts +50 -50
  40. package/src/html/browser/HistogramKde.ts +13 -13
  41. package/src/html/browser/LegendUtils.ts +48 -48
  42. package/src/html/browser/RenderPlots.ts +98 -98
  43. package/src/html/browser/SampleTimeSeries.ts +79 -79
  44. package/src/index.ts +6 -0
  45. package/src/matrix/MatrixFilter.ts +6 -6
  46. package/src/matrix/MatrixReport.ts +96 -96
  47. package/src/matrix/VariantLoader.ts +5 -5
  48. package/src/runners/AdaptiveWrapper.ts +151 -151
  49. package/src/runners/BasicRunner.ts +175 -175
  50. package/src/runners/BenchRunner.ts +8 -8
  51. package/src/runners/GcStats.ts +22 -22
  52. package/src/runners/RunnerOrchestrator.ts +168 -168
  53. package/src/runners/WorkerScript.ts +96 -96
  54. package/src/table-util/Formatters.ts +41 -36
  55. package/src/table-util/TableReport.ts +122 -122
  56. package/src/table-util/test/TableValueExtractor.ts +9 -9
  57. package/src/test/AdaptiveStatistics.integration.ts +7 -39
  58. package/src/test/HeapAttribution.test.ts +51 -0
  59. package/src/test/RunBenchCLI.test.ts +36 -11
  60. package/src/test/TestUtils.ts +24 -24
  61. package/src/test/fixtures/fn-export-bench.ts +3 -0
  62. package/src/test/fixtures/suite-export-bench.ts +16 -0
  63. package/src/tests/BenchMatrix.test.ts +12 -12
  64. package/src/tests/MatrixFilter.test.ts +15 -15
  65. package/dist/BrowserHeapSampler-DCeL42RE.mjs.map +0 -1
  66. package/dist/GcStats-ByEovUi1.mjs.map +0 -1
  67. package/dist/TimingUtils-ClclVQ7E.mjs.map +0 -1
  68. package/dist/src-HfimYuW_.mjs.map +0 -1
@@ -18,9 +18,16 @@ import type {
18
18
  RunMessage,
19
19
  } from "./WorkerScript.ts";
20
20
 
21
- const logTiming = debugWorkerTiming
22
- ? (message: string) => console.log(`[RunnerOrchestrator] ${message}`)
23
- : () => {};
21
+ /** Parameters for running a matrix variant in worker */
22
+ export interface RunMatrixVariantParams {
23
+ variantDir: string;
24
+ variantId: string;
25
+ caseId: string;
26
+ caseData?: unknown;
27
+ casesModule?: string;
28
+ runner: KnownRunner;
29
+ options: RunnerOptions;
30
+ }
24
31
 
25
32
  type WorkerParams<T = unknown> = {
26
33
  spec: BenchmarkSpec<T>;
@@ -42,6 +49,10 @@ interface RunBenchmarkParams<T = unknown> {
42
49
  params?: T;
43
50
  }
44
51
 
52
+ const logTiming = debugWorkerTiming
53
+ ? (message: string) => console.log(`[RunnerOrchestrator] ${message}`)
54
+ : () => {};
55
+
45
56
  /** Execute benchmarks directly or in worker process */
46
57
  export async function runBenchmark<T = unknown>({
47
58
  spec,
@@ -69,6 +80,34 @@ export async function runBenchmark<T = unknown>({
69
80
  return runInWorker({ spec, runner, options, params });
70
81
  }
71
82
 
83
+ /** Run a matrix variant benchmark in isolated worker process */
84
+ export async function runMatrixVariant(
85
+ params: RunMatrixVariantParams,
86
+ ): Promise<MeasuredResults[]> {
87
+ const {
88
+ variantDir,
89
+ variantId,
90
+ caseId,
91
+ caseData,
92
+ casesModule,
93
+ runner,
94
+ options,
95
+ } = params;
96
+ const name = `${variantId}/${caseId}`;
97
+ const message: RunMessage = {
98
+ type: "run",
99
+ spec: { name, fn: () => {} },
100
+ runnerName: runner,
101
+ options,
102
+ variantDir,
103
+ variantId,
104
+ caseId,
105
+ caseData,
106
+ casesModule,
107
+ };
108
+ return runWorkerWithMessage(name, options, message);
109
+ }
110
+
72
111
  /** Resolve modulePath/exportName to a real function for non-worker mode */
73
112
  async function resolveModuleSpec<T>(
74
113
  spec: BenchmarkSpec<T>,
@@ -109,38 +148,6 @@ async function runInWorker<T>(
109
148
  return runWorkerWithMessage(spec.name, options, msg);
110
149
  }
111
150
 
112
- /** Create worker process with timing logs */
113
- function createWorkerWithTiming(gcStats: boolean) {
114
- const workerStart = getPerfNow();
115
- const gcEvents: GcEvent[] = [];
116
- const worker = createWorkerProcess(gcStats);
117
- const createTime = getPerfNow();
118
- if (gcStats && worker.stdout) setupGcCapture(worker, gcEvents);
119
- logTiming(
120
- `Worker process created in ${getElapsed(workerStart, createTime).toFixed(1)}ms`,
121
- );
122
- return { worker, createTime, gcEvents };
123
- }
124
-
125
- /** Capture and parse GC lines from stdout (V8's --trace-gc-nvp outputs to stdout) */
126
- function setupGcCapture(worker: ChildProcess, gcEvents: GcEvent[]): void {
127
- let buffer = "";
128
- worker.stdout!.on("data", (data: Buffer) => {
129
- buffer += data.toString();
130
- const lines = buffer.split("\n");
131
- buffer = lines.pop() || ""; // Keep incomplete line in buffer
132
- for (const line of lines) {
133
- const event = parseGcLine(line);
134
- if (event) {
135
- gcEvents.push(event);
136
- } else if (line.trim()) {
137
- // Forward non-GC stdout to console (worker status messages)
138
- process.stdout.write(line + "\n");
139
- }
140
- }
141
- });
142
- }
143
-
144
151
  /** Spawn worker, wire handlers, send message, return results */
145
152
  function runWorkerWithMessage(
146
153
  name: string,
@@ -166,17 +173,69 @@ function runWorkerWithMessage(
166
173
  });
167
174
  }
168
175
 
169
- /** Send message to worker with timing log */
170
- function sendWorkerMessage(
171
- worker: ReturnType<typeof createWorkerProcess>,
172
- message: RunMessage,
173
- createTime: number,
174
- ): void {
175
- const messageTime = getPerfNow();
176
- worker.send(message);
176
+ /** Create message for worker execution */
177
+ function createRunMessage<T>(
178
+ spec: BenchmarkSpec<T>,
179
+ runnerName: KnownRunner,
180
+ options: RunnerOptions,
181
+ params?: T,
182
+ ): RunMessage {
183
+ const { fn, ...rest } = spec;
184
+ const message: RunMessage = {
185
+ type: "run",
186
+ spec: rest as BenchmarkSpec,
187
+ runnerName,
188
+ options,
189
+ params,
190
+ };
191
+ if (spec.modulePath) {
192
+ message.modulePath = spec.modulePath;
193
+ message.exportName = spec.exportName;
194
+ if (spec.setupExportName) message.setupExportName = spec.setupExportName;
195
+ } else {
196
+ message.fnCode = fn.toString();
197
+ }
198
+ return message;
199
+ }
200
+
201
+ /** Create worker process with timing logs */
202
+ function createWorkerWithTiming(gcStats: boolean) {
203
+ const workerStart = getPerfNow();
204
+ const gcEvents: GcEvent[] = [];
205
+ const worker = createWorkerProcess(gcStats);
206
+ const createTime = getPerfNow();
207
+ if (gcStats && worker.stdout) setupGcCapture(worker, gcEvents);
177
208
  logTiming(
178
- `Message sent to worker in ${getElapsed(createTime, messageTime).toFixed(1)}ms`,
209
+ `Worker process created in ${getElapsed(workerStart, createTime).toFixed(1)}ms`,
179
210
  );
211
+ return { worker, createTime, gcEvents };
212
+ }
213
+
214
+ // Consider: --no-compilation-cache, --max-old-space-size=512, --no-lazy
215
+ // for consistency (less realistic)
216
+
217
+ /** @return handlers that attach GC stats and heap profile to results */
218
+ function createWorkerHandlers(
219
+ specName: string,
220
+ startTime: number,
221
+ gcEvents: GcEvent[] | undefined,
222
+ resolve: (results: MeasuredResults[]) => void,
223
+ reject: (error: Error) => void,
224
+ ): WorkerHandlers {
225
+ return {
226
+ resolve: (results: MeasuredResults[], heapProfile?: HeapProfile) => {
227
+ logTiming(
228
+ `Total worker time for ${specName}: ${getElapsed(startTime).toFixed(1)}ms`,
229
+ );
230
+ if (gcEvents?.length) {
231
+ const gcStats = aggregateGcStats(gcEvents);
232
+ for (const r of results) r.gcStats = gcStats;
233
+ }
234
+ if (heapProfile) for (const r of results) r.heapProfile = heapProfile;
235
+ resolve(results);
236
+ },
237
+ reject,
238
+ };
180
239
  }
181
240
 
182
241
  /** Setup worker event handlers with cleanup */
@@ -195,6 +254,71 @@ function setupWorkerHandlers(
195
254
  worker.on("exit", createExitHandler(specName, cleanup, reject));
196
255
  }
197
256
 
257
+ /** Send message to worker with timing log */
258
+ function sendWorkerMessage(
259
+ worker: ReturnType<typeof createWorkerProcess>,
260
+ message: RunMessage,
261
+ createTime: number,
262
+ ): void {
263
+ const messageTime = getPerfNow();
264
+ worker.send(message);
265
+ logTiming(
266
+ `Message sent to worker in ${getElapsed(createTime, messageTime).toFixed(1)}ms`,
267
+ );
268
+ }
269
+
270
+ /** Create worker process with configuration */
271
+ function createWorkerProcess(gcStats: boolean) {
272
+ const workerPath = resolveWorkerPath();
273
+ const execArgv = ["--expose-gc", "--allow-natives-syntax"];
274
+ if (gcStats) execArgv.push("--trace-gc-nvp");
275
+
276
+ return fork(workerPath, [], {
277
+ execArgv,
278
+ silent: gcStats, // Capture stdout/stderr when collecting GC stats
279
+ env: {
280
+ ...process.env,
281
+ NODE_OPTIONS: "",
282
+ },
283
+ });
284
+ }
285
+
286
+ /** Capture and parse GC lines from stdout (V8's --trace-gc-nvp outputs to stdout) */
287
+ function setupGcCapture(worker: ChildProcess, gcEvents: GcEvent[]): void {
288
+ let buffer = "";
289
+ worker.stdout!.on("data", (data: Buffer) => {
290
+ buffer += data.toString();
291
+ const lines = buffer.split("\n");
292
+ buffer = lines.pop() || ""; // Keep incomplete line in buffer
293
+ for (const line of lines) {
294
+ const event = parseGcLine(line);
295
+ if (event) {
296
+ gcEvents.push(event);
297
+ } else if (line.trim()) {
298
+ // Forward non-GC stdout to console (worker status messages)
299
+ process.stdout.write(line + "\n");
300
+ }
301
+ }
302
+ });
303
+ }
304
+
305
+ /** Create cleanup for timeout and termination */
306
+ function createCleanup(
307
+ worker: ReturnType<typeof createWorkerProcess>,
308
+ specName: string,
309
+ reject: (error: Error) => void,
310
+ ) {
311
+ const timeoutId = setTimeout(() => {
312
+ cleanup();
313
+ reject(new Error(`Benchmark "${specName}" timed out after 60 seconds`));
314
+ }, 60000);
315
+ const cleanup = () => {
316
+ clearTimeout(timeoutId);
317
+ if (!worker.killed) worker.kill("SIGTERM");
318
+ };
319
+ return cleanup;
320
+ }
321
+
198
322
  /** Handle worker messages (results or errors) */
199
323
  function createMessageHandler(
200
324
  specName: string,
@@ -245,39 +369,6 @@ function createExitHandler(
245
369
  };
246
370
  }
247
371
 
248
- /** Create cleanup for timeout and termination */
249
- function createCleanup(
250
- worker: ReturnType<typeof createWorkerProcess>,
251
- specName: string,
252
- reject: (error: Error) => void,
253
- ) {
254
- const timeoutId = setTimeout(() => {
255
- cleanup();
256
- reject(new Error(`Benchmark "${specName}" timed out after 60 seconds`));
257
- }, 60000);
258
- const cleanup = () => {
259
- clearTimeout(timeoutId);
260
- if (!worker.killed) worker.kill("SIGTERM");
261
- };
262
- return cleanup;
263
- }
264
-
265
- /** Create worker process with configuration */
266
- function createWorkerProcess(gcStats: boolean) {
267
- const workerPath = resolveWorkerPath();
268
- const execArgv = ["--expose-gc", "--allow-natives-syntax"];
269
- if (gcStats) execArgv.push("--trace-gc-nvp");
270
-
271
- return fork(workerPath, [], {
272
- execArgv,
273
- silent: gcStats, // Capture stdout/stderr when collecting GC stats
274
- env: {
275
- ...process.env,
276
- NODE_OPTIONS: "",
277
- },
278
- });
279
- }
280
-
281
372
  /** Resolve WorkerScript path for dev (.ts) or dist (.mjs) */
282
373
  function resolveWorkerPath(): string {
283
374
  const dir = import.meta.dirname!;
@@ -285,94 +376,3 @@ function resolveWorkerPath(): string {
285
376
  if (existsSync(tsPath)) return tsPath;
286
377
  return path.join(dir, "runners", "WorkerScript.mjs");
287
378
  }
288
-
289
- // Consider: --no-compilation-cache, --max-old-space-size=512, --no-lazy
290
- // for consistency (less realistic)
291
-
292
- /** @return handlers that attach GC stats and heap profile to results */
293
- function createWorkerHandlers(
294
- specName: string,
295
- startTime: number,
296
- gcEvents: GcEvent[] | undefined,
297
- resolve: (results: MeasuredResults[]) => void,
298
- reject: (error: Error) => void,
299
- ): WorkerHandlers {
300
- return {
301
- resolve: (results: MeasuredResults[], heapProfile?: HeapProfile) => {
302
- logTiming(
303
- `Total worker time for ${specName}: ${getElapsed(startTime).toFixed(1)}ms`,
304
- );
305
- if (gcEvents?.length) {
306
- const gcStats = aggregateGcStats(gcEvents);
307
- for (const r of results) r.gcStats = gcStats;
308
- }
309
- if (heapProfile) for (const r of results) r.heapProfile = heapProfile;
310
- resolve(results);
311
- },
312
- reject,
313
- };
314
- }
315
-
316
- /** Create message for worker execution */
317
- function createRunMessage<T>(
318
- spec: BenchmarkSpec<T>,
319
- runnerName: KnownRunner,
320
- options: RunnerOptions,
321
- params?: T,
322
- ): RunMessage {
323
- const { fn, ...rest } = spec;
324
- const message: RunMessage = {
325
- type: "run",
326
- spec: rest as BenchmarkSpec,
327
- runnerName,
328
- options,
329
- params,
330
- };
331
- if (spec.modulePath) {
332
- message.modulePath = spec.modulePath;
333
- message.exportName = spec.exportName;
334
- if (spec.setupExportName) message.setupExportName = spec.setupExportName;
335
- } else {
336
- message.fnCode = fn.toString();
337
- }
338
- return message;
339
- }
340
-
341
- /** Parameters for running a matrix variant in worker */
342
- export interface RunMatrixVariantParams {
343
- variantDir: string;
344
- variantId: string;
345
- caseId: string;
346
- caseData?: unknown;
347
- casesModule?: string;
348
- runner: KnownRunner;
349
- options: RunnerOptions;
350
- }
351
-
352
- /** Run a matrix variant benchmark in isolated worker process */
353
- export async function runMatrixVariant(
354
- params: RunMatrixVariantParams,
355
- ): Promise<MeasuredResults[]> {
356
- const {
357
- variantDir,
358
- variantId,
359
- caseId,
360
- caseData,
361
- casesModule,
362
- runner,
363
- options,
364
- } = params;
365
- const name = `${variantId}/${caseId}`;
366
- const message: RunMessage = {
367
- type: "run",
368
- spec: { name, fn: () => {} },
369
- runnerName: runner,
370
- options,
371
- variantDir,
372
- variantId,
373
- caseId,
374
- caseData,
375
- casesModule,
376
- };
377
- return runWorkerWithMessage(name, options, message);
378
- }
@@ -9,10 +9,7 @@ import {
9
9
  } from "./AdaptiveWrapper.ts";
10
10
  import type { RunnerOptions } from "./BenchRunner.ts";
11
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
12
+ import { debugWorkerTiming, getElapsed, getPerfNow } from "./TimingUtils.ts"; // 5 minutes
16
13
 
17
14
  /** Message sent to worker process to start a benchmark run. */
18
15
  export interface RunMessage {
@@ -49,71 +46,13 @@ export interface ErrorMessage {
49
46
 
50
47
  export type WorkerMessage = RunMessage | ResultMessage | ErrorMessage;
51
48
 
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);
49
+ interface BenchmarkImportResult {
50
+ fn: BenchmarkFunction;
51
+ params: unknown;
52
+ }
115
53
 
116
- process.stdin.pause();
54
+ const workerStartTime = getPerfNow();
55
+ const maxLifetime = 5 * 60 * 1000;
117
56
 
118
57
  /** Log timing with consistent format */
119
58
  const logTiming = debugWorkerTiming ? _logTiming : () => {};
@@ -138,11 +77,6 @@ function sendAndExit(msg: ResultMessage | ErrorMessage, exitCode: number) {
138
77
  });
139
78
  }
140
79
 
141
- interface BenchmarkImportResult {
142
- fn: BenchmarkFunction;
143
- params: unknown;
144
- }
145
-
146
80
  /** Resolve benchmark function from message (variant dir, module path, or fnCode) */
147
81
  async function resolveBenchmarkFn(
148
82
  message: RunMessage,
@@ -187,19 +121,6 @@ async function importVariantModule(
187
121
  return { fn: () => run(caseData), params: undefined };
188
122
  }
189
123
 
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
124
  /** Import benchmark function and optionally run setup */
204
125
  async function importBenchmarkWithSetup(
205
126
  message: RunMessage,
@@ -222,6 +143,29 @@ async function importBenchmarkWithSetup(
222
143
  return { fn, params };
223
144
  }
224
145
 
146
+ /** Reconstruct function from string code */
147
+ function reconstructFunction(fnCode: string): BenchmarkFunction {
148
+ // biome-ignore lint/security/noGlobalEval: Necessary for worker process isolation, code is from trusted source
149
+ const fn = eval(`(${fnCode})`); // eslint-disable-line no-eval
150
+ if (typeof fn !== "function") {
151
+ throw new Error("Reconstructed code is not a function");
152
+ }
153
+ return fn;
154
+ }
155
+
156
+ /** Load case data from a cases module */
157
+ async function loadCaseFromModule(
158
+ casesModuleUrl: string,
159
+ caseId: string,
160
+ ): Promise<{ data: unknown; metadata?: Record<string, unknown> }> {
161
+ logTiming(`Loading case '${caseId}' from ${casesModuleUrl}`);
162
+ const module = await import(casesModuleUrl);
163
+ if (typeof module.loadCase === "function") {
164
+ return module.loadCase(caseId);
165
+ }
166
+ return { data: caseId };
167
+ }
168
+
225
169
  /** Get named or default export from module */
226
170
  function getModuleExport(
227
171
  module: any,
@@ -236,16 +180,6 @@ function getModuleExport(
236
180
  return fn;
237
181
  }
238
182
 
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
183
  /** Create error message from exception */
250
184
  function createErrorMessage(error: unknown): ErrorMessage {
251
185
  return {
@@ -254,3 +188,69 @@ function createErrorMessage(error: unknown): ErrorMessage {
254
188
  stack: error instanceof Error ? error.stack : undefined,
255
189
  };
256
190
  }
191
+
192
+ /**
193
+ * Worker process for isolated benchmark execution.
194
+ * Uses eval() safely in isolated child process with trusted code.
195
+ */
196
+ process.on("message", async (message: RunMessage) => {
197
+ if (message.type !== "run") return;
198
+
199
+ logTiming(`Processing ${message.spec.name} with ${message.runnerName}`);
200
+
201
+ try {
202
+ const start = getPerfNow();
203
+ const baseRunner = await createRunner(message.runnerName);
204
+
205
+ const runner = (message.options as any).adaptive
206
+ ? createAdaptiveWrapper(baseRunner, message.options as AdaptiveOptions)
207
+ : baseRunner;
208
+
209
+ logTiming("Runner created in", getElapsed(start));
210
+
211
+ const benchStart = getPerfNow();
212
+
213
+ // Run with heap sampling if enabled (covers module import + execution)
214
+ if (message.options.heapSample) {
215
+ const { withHeapSampling } = await import(
216
+ "../heap-sample/HeapSampler.ts"
217
+ );
218
+ const heapOpts = {
219
+ samplingInterval: message.options.heapInterval,
220
+ stackDepth: message.options.heapDepth,
221
+ };
222
+ const { result: results, profile: heapProfile } = await withHeapSampling(
223
+ heapOpts,
224
+ async () => {
225
+ const { fn, params } = await resolveBenchmarkFn(message);
226
+ return runner.runBench(
227
+ { ...message.spec, fn },
228
+ message.options,
229
+ params,
230
+ );
231
+ },
232
+ );
233
+ logTiming("Benchmark execution took", getElapsed(benchStart));
234
+ sendAndExit({ type: "result", results, heapProfile }, 0);
235
+ } else {
236
+ const { fn, params } = await resolveBenchmarkFn(message);
237
+ const results = await runner.runBench(
238
+ { ...message.spec, fn },
239
+ message.options,
240
+ params,
241
+ );
242
+ logTiming("Benchmark execution took", getElapsed(benchStart));
243
+ sendAndExit({ type: "result", results }, 0);
244
+ }
245
+ } catch (error) {
246
+ sendAndExit(createErrorMessage(error), 1);
247
+ }
248
+ });
249
+
250
+ // Exit after 5 minutes to prevent zombie processes
251
+ setTimeout(() => {
252
+ console.error("WorkerScript: Maximum lifetime exceeded, exiting");
253
+ process.exit(1);
254
+ }, maxLifetime);
255
+
256
+ process.stdin.pause();