benchforge 0.1.9 → 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.
- package/README.md +40 -6
- package/dist/{BenchRunner-CSKN9zPy.d.mts → BenchRunner-BzyUfiyB.d.mts} +32 -8
- package/dist/{BrowserHeapSampler-DCeL42RE.mjs → BrowserHeapSampler-B6asLKWQ.mjs} +57 -57
- package/dist/BrowserHeapSampler-B6asLKWQ.mjs.map +1 -0
- package/dist/{GcStats-ByEovUi1.mjs → GcStats-wX7Xyblu.mjs} +15 -15
- package/dist/GcStats-wX7Xyblu.mjs.map +1 -0
- package/dist/HeapSampler-B8dtKHn1.mjs.map +1 -1
- package/dist/{TimingUtils-ClclVQ7E.mjs → TimingUtils-DwOwkc8G.mjs} +225 -225
- package/dist/TimingUtils-DwOwkc8G.mjs.map +1 -0
- package/dist/bin/benchforge.mjs +1 -1
- package/dist/browser/index.js +210 -210
- package/dist/index.d.mts +102 -46
- package/dist/index.mjs +3 -3
- package/dist/runners/WorkerScript.d.mts +1 -1
- package/dist/runners/WorkerScript.mjs +66 -66
- package/dist/runners/WorkerScript.mjs.map +1 -1
- package/dist/{src-Cf_LXwlp.mjs → src-B-DDaCa9.mjs} +1225 -990
- package/dist/src-B-DDaCa9.mjs.map +1 -0
- package/package.json +2 -1
- package/src/BenchMatrix.ts +125 -125
- package/src/BenchmarkReport.ts +50 -45
- package/src/HtmlDataPrep.ts +21 -21
- package/src/PermutationTest.ts +24 -24
- package/src/StandardSections.ts +45 -45
- package/src/StatisticalUtils.ts +60 -61
- package/src/browser/BrowserGcStats.ts +5 -5
- package/src/browser/BrowserHeapSampler.ts +63 -63
- package/src/cli/CliArgs.ts +6 -3
- package/src/cli/FilterBenchmarks.ts +5 -5
- package/src/cli/RunBenchCLI.ts +526 -498
- package/src/export/JsonExport.ts +10 -10
- package/src/export/PerfettoExport.ts +74 -74
- package/src/export/SpeedscopeExport.ts +202 -0
- package/src/heap-sample/HeapSampleReport.ts +143 -70
- package/src/heap-sample/HeapSampler.ts +55 -12
- package/src/heap-sample/ResolvedProfile.ts +89 -0
- package/src/html/HtmlReport.ts +33 -33
- package/src/html/HtmlTemplate.ts +67 -67
- package/src/html/browser/CIPlot.ts +50 -50
- package/src/html/browser/HistogramKde.ts +13 -13
- package/src/html/browser/LegendUtils.ts +48 -48
- package/src/html/browser/RenderPlots.ts +98 -98
- package/src/html/browser/SampleTimeSeries.ts +79 -79
- package/src/index.ts +6 -0
- package/src/matrix/MatrixFilter.ts +6 -6
- package/src/matrix/MatrixReport.ts +96 -96
- package/src/matrix/VariantLoader.ts +5 -5
- package/src/runners/AdaptiveWrapper.ts +151 -151
- package/src/runners/BasicRunner.ts +175 -175
- package/src/runners/BenchRunner.ts +8 -8
- package/src/runners/GcStats.ts +22 -22
- package/src/runners/RunnerOrchestrator.ts +168 -168
- package/src/runners/WorkerScript.ts +96 -96
- package/src/table-util/Formatters.ts +41 -36
- package/src/table-util/TableReport.ts +122 -122
- package/src/table-util/test/TableValueExtractor.ts +9 -9
- package/src/test/AdaptiveStatistics.integration.ts +7 -39
- package/src/test/HeapAttribution.test.ts +51 -0
- package/src/test/RunBenchCLI.test.ts +18 -18
- package/src/test/TestUtils.ts +24 -24
- package/src/tests/BenchMatrix.test.ts +12 -12
- package/src/tests/MatrixFilter.test.ts +15 -15
- package/dist/BrowserHeapSampler-DCeL42RE.mjs.map +0 -1
- package/dist/GcStats-ByEovUi1.mjs.map +0 -1
- package/dist/TimingUtils-ClclVQ7E.mjs.map +0 -1
- package/dist/src-Cf_LXwlp.mjs.map +0 -1
|
@@ -18,9 +18,16 @@ import type {
|
|
|
18
18
|
RunMessage,
|
|
19
19
|
} from "./WorkerScript.ts";
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
/**
|
|
170
|
-
function
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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();
|