benchforge 0.1.11 → 0.2.4
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/LICENSE +20 -0
- package/README.md +99 -294
- package/bin/benchforge +1 -2
- package/dist/AnalyzeArchive-8NCJhmhS.mjs +145 -0
- package/dist/AnalyzeArchive-8NCJhmhS.mjs.map +1 -0
- package/dist/BenchMatrix-BZVrBB_h.mjs +1050 -0
- package/dist/BenchMatrix-BZVrBB_h.mjs.map +1 -0
- package/dist/{BenchRunner-BzyUfiyB.d.mts → BenchRunner-DglX1NOn.d.mts} +119 -66
- package/dist/CoverageSampler-D5T9DRqe.mjs +27 -0
- package/dist/CoverageSampler-D5T9DRqe.mjs.map +1 -0
- package/dist/Formatters-BWj3d4sv.mjs +95 -0
- package/dist/Formatters-BWj3d4sv.mjs.map +1 -0
- package/dist/{HeapSampler-B8dtKHn1.mjs → HeapSampler-Dq-hpXem.mjs} +4 -4
- package/dist/HeapSampler-Dq-hpXem.mjs.map +1 -0
- package/dist/RunBenchCLI-C17DrJz8.mjs +3075 -0
- package/dist/RunBenchCLI-C17DrJz8.mjs.map +1 -0
- package/dist/StatisticalUtils-BD92crgM.mjs +255 -0
- package/dist/StatisticalUtils-BD92crgM.mjs.map +1 -0
- package/dist/TimeSampler-Ds8n7l2B.mjs +29 -0
- package/dist/TimeSampler-Ds8n7l2B.mjs.map +1 -0
- package/dist/ViewerServer-BJhdnxlN.mjs +639 -0
- package/dist/ViewerServer-BJhdnxlN.mjs.map +1 -0
- package/dist/ViewerServer-CuMNdNBz.mjs +2 -0
- package/dist/bin/benchforge.mjs +4 -5
- package/dist/bin/benchforge.mjs.map +1 -1
- package/dist/index.d.mts +711 -558
- package/dist/index.mjs +98 -3
- package/dist/index.mjs.map +1 -0
- package/dist/runners/WorkerScript.d.mts +12 -4
- package/dist/runners/WorkerScript.mjs +77 -105
- package/dist/runners/WorkerScript.mjs.map +1 -1
- package/dist/viewer/assets/CIPlot-BkOvMoMa.js +1 -0
- package/dist/viewer/assets/HistogramKde-CmSyUFY0.js +1 -0
- package/dist/viewer/assets/LegendUtils-BJpbn_jr.js +55 -0
- package/dist/viewer/assets/SampleTimeSeries-C4VBhXr3.js +1 -0
- package/dist/viewer/assets/index-Br9bp_cX.js +153 -0
- package/dist/viewer/assets/index-NzXXe_CC.css +1 -0
- package/dist/viewer/index.html +19 -0
- package/dist/viewer/speedscope/LICENSE +21 -0
- package/dist/viewer/speedscope/SourceCodePro-Regular.ttf-ILST5JV6.woff2 +0 -0
- package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js +2 -0
- package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js.map +7 -0
- package/dist/viewer/speedscope/favicon-16x16-VSI62OPJ.png +0 -0
- package/dist/viewer/speedscope/favicon-32x32-3EB2YCUY.png +0 -0
- package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js +2 -0
- package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js.map +7 -0
- package/dist/viewer/speedscope/favicon-FOKUP5Y5.ico +0 -0
- package/dist/viewer/speedscope/favicon-M34RF7BI.js +2 -0
- package/dist/viewer/speedscope/favicon-M34RF7BI.js.map +7 -0
- package/dist/viewer/speedscope/file-format-schema.json +274 -0
- package/dist/viewer/speedscope/index.html +19 -0
- package/dist/viewer/speedscope/jfrview_bg-BLJXNNQB.wasm +0 -0
- package/dist/viewer/speedscope/perf-vertx-stacks-01-collapsed-all-ZNUIGAJL.txt +199 -0
- package/dist/viewer/speedscope/release.txt +3 -0
- package/dist/viewer/speedscope/source-code-pro.LICENSE.md +93 -0
- package/dist/viewer/speedscope/speedscope-GHPHNKXC.css +2 -0
- package/dist/viewer/speedscope/speedscope-GHPHNKXC.css.map +7 -0
- package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js +212 -0
- package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js.map +7 -0
- package/package.json +52 -27
- package/src/bin/benchforge.ts +2 -2
- package/src/cli/AnalyzeArchive.ts +232 -0
- package/src/cli/BrowserBench.ts +322 -0
- package/src/cli/CliArgs.ts +164 -51
- package/src/cli/CliExport.ts +179 -0
- package/src/cli/CliOptions.ts +147 -0
- package/src/cli/CliReport.ts +197 -0
- package/src/cli/FilterBenchmarks.ts +18 -30
- package/src/cli/RunBenchCLI.ts +132 -866
- package/src/cli/SuiteRunner.ts +160 -0
- package/src/cli/ViewerServer.ts +282 -0
- package/src/export/AllocExport.ts +121 -0
- package/src/export/ArchiveExport.ts +146 -0
- package/src/export/ArchiveFormat.ts +50 -0
- package/src/export/CoverageExport.ts +148 -0
- package/src/export/EditorUri.ts +10 -0
- package/src/export/PerfettoExport.ts +64 -99
- package/src/export/SpeedscopeTypes.ts +98 -0
- package/src/export/TimeExport.ts +115 -0
- package/src/index.ts +86 -67
- package/src/matrix/BenchMatrix.ts +230 -0
- package/src/matrix/CaseLoader.ts +8 -6
- package/src/matrix/MatrixDirRunner.ts +153 -0
- package/src/matrix/MatrixFilter.ts +49 -47
- package/src/matrix/MatrixInlineRunner.ts +50 -0
- package/src/matrix/MatrixReport.ts +90 -250
- package/src/matrix/VariantLoader.ts +5 -5
- package/src/profiling/browser/BenchLoop.ts +51 -0
- package/src/profiling/browser/BrowserCDP.ts +133 -0
- package/src/profiling/browser/BrowserGcStats.ts +33 -0
- package/src/profiling/browser/BrowserProfiler.ts +160 -0
- package/src/profiling/browser/CdpClient.ts +82 -0
- package/src/profiling/browser/CdpPage.ts +138 -0
- package/src/profiling/browser/ChromeLauncher.ts +158 -0
- package/src/profiling/browser/ChromeTraceEvent.ts +28 -0
- package/src/profiling/browser/PageLoadMode.ts +61 -0
- package/src/profiling/node/CoverageSampler.ts +27 -0
- package/src/profiling/node/CoverageTypes.ts +23 -0
- package/src/profiling/node/HeapSampleReport.ts +261 -0
- package/src/{heap-sample → profiling/node}/HeapSampler.ts +1 -2
- package/src/{heap-sample → profiling/node}/ResolvedProfile.ts +18 -9
- package/src/profiling/node/TimeSampler.ts +57 -0
- package/src/report/BenchmarkReport.ts +146 -0
- package/src/report/Colors.ts +9 -0
- package/src/report/Formatters.ts +110 -0
- package/src/report/GcSections.ts +151 -0
- package/src/{GitUtils.ts → report/GitUtils.ts} +18 -19
- package/src/report/HtmlReport.ts +223 -0
- package/src/report/ParseStats.ts +73 -0
- package/src/report/StandardSections.ts +147 -0
- package/src/report/ViewerSections.ts +286 -0
- package/src/report/text/TableReport.ts +253 -0
- package/src/report/text/TextReport.ts +123 -0
- package/src/runners/AdaptiveWrapper.ts +116 -236
- package/src/runners/BenchRunner.ts +20 -15
- package/src/{Benchmark.ts → runners/BenchmarkSpec.ts} +5 -6
- package/src/runners/CreateRunner.ts +5 -7
- package/src/runners/GcStats.ts +47 -50
- package/src/{MeasuredResults.ts → runners/MeasuredResults.ts} +43 -37
- package/src/runners/MergeBatches.ts +123 -0
- package/src/{NodeGC.ts → runners/NodeGC.ts} +2 -3
- package/src/runners/RunnerOrchestrator.ts +127 -243
- package/src/runners/RunnerUtils.ts +75 -1
- package/src/runners/SampleStats.ts +100 -0
- package/src/runners/TimingRunner.ts +244 -0
- package/src/runners/TimingUtils.ts +3 -2
- package/src/runners/WorkerScript.ts +135 -151
- package/src/stats/BootstrapDifference.ts +282 -0
- package/src/{PermutationTest.ts → stats/PermutationTest.ts} +8 -17
- package/src/stats/StatisticalUtils.ts +445 -0
- package/src/{tests → test}/AdaptiveConvergence.test.ts +10 -10
- package/src/test/AdaptiveRunner.test.ts +39 -41
- package/src/{tests → test}/AdaptiveSampling.test.ts +9 -9
- package/src/test/AdaptiveStatistics.integration.ts +2 -2
- package/src/{tests → test}/BenchMatrix.test.ts +19 -16
- package/src/test/BenchmarkReport.test.ts +63 -13
- package/src/test/BrowserBench.e2e.test.ts +186 -17
- package/src/test/BrowserBench.test.ts +10 -5
- package/src/test/BuildTimeSection.test.ts +130 -0
- package/src/test/CapSamples.test.ts +82 -0
- package/src/test/CoverageExport.test.ts +115 -0
- package/src/test/CoverageSampler.test.ts +33 -0
- package/src/test/HeapAttribution.test.ts +14 -14
- package/src/{tests → test}/MatrixFilter.test.ts +1 -1
- package/src/{tests → test}/MatrixReport.test.ts +1 -1
- package/src/test/PermutationTest.test.ts +1 -1
- package/src/{tests → test}/RealDataValidation.test.ts +6 -6
- package/src/test/RunBenchCLI.test.ts +39 -38
- package/src/test/RunnerOrchestrator.test.ts +12 -12
- package/src/test/StatisticalUtils.test.ts +48 -12
- package/src/{table-util/test → test}/TableReport.test.ts +2 -2
- package/src/test/TestUtils.ts +12 -7
- package/src/test/TimeExport.test.ts +139 -0
- package/src/test/TimeSampler.test.ts +37 -0
- package/src/test/ViewerLive.e2e.test.ts +159 -0
- package/src/test/ViewerStatic.static.e2e.test.ts +137 -0
- package/src/{tests → test}/fixtures/baseline/impl.ts +1 -1
- package/src/{tests → test}/fixtures/bevy30-samples.ts +3 -1
- package/src/test/fixtures/cases/asyncCases.ts +9 -0
- package/src/{tests → test}/fixtures/cases/cases.ts +5 -2
- package/src/test/fixtures/cases/variants/product.ts +2 -0
- package/src/test/fixtures/cases/variants/sum.ts +2 -0
- package/src/test/fixtures/discover/fast.ts +1 -0
- package/src/{tests → test}/fixtures/discover/slow.ts +1 -1
- package/src/test/fixtures/invalid/bad.ts +1 -0
- package/src/test/fixtures/loader/fast.ts +1 -0
- package/src/{tests → test}/fixtures/loader/slow.ts +1 -1
- package/src/test/fixtures/loader/stateful.ts +2 -0
- package/src/test/fixtures/stateful/stateful.ts +2 -0
- package/src/test/fixtures/variants/extra.ts +1 -0
- package/src/test/fixtures/variants/impl.ts +1 -0
- package/src/test/fixtures/worker/fast.ts +1 -0
- package/src/{tests → test}/fixtures/worker/slow.ts +1 -1
- package/src/viewer/DateFormat.ts +30 -0
- package/src/viewer/Helpers.ts +23 -0
- package/src/viewer/LineData.ts +120 -0
- package/src/viewer/Providers.ts +191 -0
- package/src/viewer/ReportData.ts +123 -0
- package/src/viewer/State.ts +49 -0
- package/src/viewer/Theme.ts +15 -0
- package/src/viewer/components/App.tsx +73 -0
- package/src/viewer/components/DropZone.tsx +71 -0
- package/src/viewer/components/LazyPlot.ts +33 -0
- package/src/viewer/components/SamplesPanel.tsx +214 -0
- package/src/viewer/components/Shell.tsx +26 -0
- package/src/viewer/components/SourcePanel.tsx +216 -0
- package/src/viewer/components/SummaryPanel.tsx +332 -0
- package/src/viewer/components/TabBar.tsx +131 -0
- package/src/viewer/components/TabContent.tsx +46 -0
- package/src/viewer/components/ThemeToggle.tsx +50 -0
- package/src/viewer/index.html +20 -0
- package/src/viewer/main.tsx +4 -0
- package/src/viewer/plots/CIPlot.ts +313 -0
- package/src/{html/browser → viewer/plots}/HistogramKde.ts +33 -38
- package/src/viewer/plots/LegendUtils.ts +134 -0
- package/src/viewer/plots/PlotTypes.ts +85 -0
- package/src/viewer/plots/RenderPlots.ts +230 -0
- package/src/viewer/plots/SampleTimeSeries.ts +306 -0
- package/src/viewer/plots/SvgHelpers.ts +136 -0
- package/src/viewer/plots/TimeSeriesMarks.ts +319 -0
- package/src/viewer/report.css +427 -0
- package/src/viewer/shell.css +357 -0
- package/src/viewer/tsconfig.json +11 -0
- package/dist/BrowserHeapSampler-B6asLKWQ.mjs +0 -202
- package/dist/BrowserHeapSampler-B6asLKWQ.mjs.map +0 -1
- package/dist/GcStats-wX7Xyblu.mjs +0 -77
- package/dist/GcStats-wX7Xyblu.mjs.map +0 -1
- package/dist/HeapSampler-B8dtKHn1.mjs.map +0 -1
- package/dist/TimingUtils-DwOwkc8G.mjs +0 -597
- package/dist/TimingUtils-DwOwkc8G.mjs.map +0 -1
- package/dist/browser/index.js +0 -914
- package/dist/src-B-DDaCa9.mjs +0 -3108
- package/dist/src-B-DDaCa9.mjs.map +0 -1
- package/src/BenchMatrix.ts +0 -380
- package/src/BenchmarkReport.ts +0 -161
- package/src/HtmlDataPrep.ts +0 -148
- package/src/StandardSections.ts +0 -261
- package/src/StatisticalUtils.ts +0 -175
- package/src/TypeUtil.ts +0 -8
- package/src/browser/BrowserGcStats.ts +0 -44
- package/src/browser/BrowserHeapSampler.ts +0 -271
- package/src/export/JsonExport.ts +0 -103
- package/src/export/JsonFormat.ts +0 -91
- package/src/export/SpeedscopeExport.ts +0 -202
- package/src/heap-sample/HeapSampleReport.ts +0 -269
- package/src/html/HtmlReport.ts +0 -131
- package/src/html/HtmlTemplate.ts +0 -284
- package/src/html/Types.ts +0 -88
- package/src/html/browser/CIPlot.ts +0 -287
- package/src/html/browser/LegendUtils.ts +0 -163
- package/src/html/browser/RenderPlots.ts +0 -263
- package/src/html/browser/SampleTimeSeries.ts +0 -389
- package/src/html/browser/Types.ts +0 -96
- package/src/html/browser/index.ts +0 -1
- package/src/html/index.ts +0 -17
- package/src/runners/BasicRunner.ts +0 -364
- package/src/table-util/ConvergenceFormatters.ts +0 -19
- package/src/table-util/Formatters.ts +0 -157
- package/src/table-util/README.md +0 -70
- package/src/table-util/TableReport.ts +0 -293
- package/src/tests/fixtures/cases/asyncCases.ts +0 -7
- package/src/tests/fixtures/cases/variants/product.ts +0 -2
- package/src/tests/fixtures/cases/variants/sum.ts +0 -2
- package/src/tests/fixtures/discover/fast.ts +0 -1
- package/src/tests/fixtures/invalid/bad.ts +0 -1
- package/src/tests/fixtures/loader/fast.ts +0 -1
- package/src/tests/fixtures/loader/stateful.ts +0 -2
- package/src/tests/fixtures/stateful/stateful.ts +0 -2
- package/src/tests/fixtures/variants/extra.ts +0 -1
- package/src/tests/fixtures/variants/impl.ts +0 -1
- package/src/tests/fixtures/worker/fast.ts +0 -1
- /package/src/{table-util/test → test}/TableValueExtractor.test.ts +0 -0
- /package/src/{table-util/test → test}/TableValueExtractor.ts +0 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { getHeapStatistics } from "node:v8";
|
|
2
|
+
import type { BenchmarkSpec } from "./BenchmarkSpec.ts";
|
|
3
|
+
import type { BenchRunner, RunnerOptions } from "./BenchRunner.ts";
|
|
4
|
+
import { executeBenchmark } from "./BenchRunner.ts";
|
|
5
|
+
import type {
|
|
6
|
+
MeasuredResults,
|
|
7
|
+
OptStatusInfo,
|
|
8
|
+
PausePoint,
|
|
9
|
+
} from "./MeasuredResults.ts";
|
|
10
|
+
import {
|
|
11
|
+
analyzeOptStatus,
|
|
12
|
+
computeStats,
|
|
13
|
+
createOptStatusGetter,
|
|
14
|
+
gcFunction,
|
|
15
|
+
} from "./SampleStats.ts";
|
|
16
|
+
|
|
17
|
+
type CollectParams<T = unknown> = {
|
|
18
|
+
benchmark: BenchmarkSpec<T>;
|
|
19
|
+
maxTime: number;
|
|
20
|
+
maxIterations: number;
|
|
21
|
+
warmup: number;
|
|
22
|
+
params?: T;
|
|
23
|
+
skipWarmup?: boolean;
|
|
24
|
+
traceOpt?: boolean;
|
|
25
|
+
pauseWarmup?: number;
|
|
26
|
+
pauseFirst?: number;
|
|
27
|
+
pauseInterval?: number;
|
|
28
|
+
pauseDuration?: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type CollectResult = {
|
|
32
|
+
samples: number[];
|
|
33
|
+
warmupSamples: number[];
|
|
34
|
+
heapGrowth: number;
|
|
35
|
+
heapSamples: number[];
|
|
36
|
+
startTime: number;
|
|
37
|
+
optStatus?: OptStatusInfo;
|
|
38
|
+
optSamples?: number[];
|
|
39
|
+
pausePoints: PausePoint[];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type SampleArrays = {
|
|
43
|
+
samples: number[];
|
|
44
|
+
heapSamples: number[];
|
|
45
|
+
optStatuses: number[];
|
|
46
|
+
pausePoints: PausePoint[];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const defaultCollectOptions = {
|
|
50
|
+
maxTime: 5000,
|
|
51
|
+
maxIterations: 1000000,
|
|
52
|
+
warmup: 0,
|
|
53
|
+
traceOpt: false,
|
|
54
|
+
pauseWarmup: 0,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Timing-based runner that collects samples within time/iteration limits.
|
|
59
|
+
* Handles warmup, heap tracking, V8 optimization tracing, and periodic pauses.
|
|
60
|
+
*/
|
|
61
|
+
export class TimingRunner implements BenchRunner {
|
|
62
|
+
async runBench<T = unknown>(
|
|
63
|
+
benchmark: BenchmarkSpec<T>,
|
|
64
|
+
options: RunnerOptions,
|
|
65
|
+
params?: T,
|
|
66
|
+
): Promise<MeasuredResults[]> {
|
|
67
|
+
const opts = { ...defaultCollectOptions, ...(options as any) };
|
|
68
|
+
const collected = await collectSamples({ benchmark, params, ...opts });
|
|
69
|
+
return [buildMeasuredResults(benchmark.name, collected)];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Collect timing samples with warmup, heap tracking, and optional V8 opt tracing. */
|
|
74
|
+
async function collectSamples<T>(
|
|
75
|
+
config: CollectParams<T>,
|
|
76
|
+
): Promise<CollectResult> {
|
|
77
|
+
if (!config.maxIterations && !config.maxTime) {
|
|
78
|
+
throw new Error(`At least one of maxIterations or maxTime must be set`);
|
|
79
|
+
}
|
|
80
|
+
const warmupSamples = config.skipWarmup ? [] : await runWarmup(config);
|
|
81
|
+
const heapBefore = process.memoryUsage().heapUsed;
|
|
82
|
+
const { samples, heapSamples, optStatuses, pausePoints, startTime } =
|
|
83
|
+
await runSampleLoop(config);
|
|
84
|
+
if (samples.length === 0)
|
|
85
|
+
throw new Error(
|
|
86
|
+
`No samples collected for benchmark: ${config.benchmark.name}`,
|
|
87
|
+
);
|
|
88
|
+
const heapAfter = process.memoryUsage().heapUsed;
|
|
89
|
+
const heapGrowth =
|
|
90
|
+
Math.max(0, heapAfter - heapBefore) / 1024 / samples.length;
|
|
91
|
+
const optStatus = config.traceOpt
|
|
92
|
+
? analyzeOptStatus(samples, optStatuses)
|
|
93
|
+
: undefined;
|
|
94
|
+
const optSamples =
|
|
95
|
+
config.traceOpt && optStatuses.length > 0 ? optStatuses : undefined;
|
|
96
|
+
return {
|
|
97
|
+
samples,
|
|
98
|
+
warmupSamples,
|
|
99
|
+
heapGrowth,
|
|
100
|
+
heapSamples,
|
|
101
|
+
startTime,
|
|
102
|
+
optStatus,
|
|
103
|
+
optSamples,
|
|
104
|
+
pausePoints,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Assemble CollectResult into a MeasuredResults record. */
|
|
109
|
+
function buildMeasuredResults(
|
|
110
|
+
name: string,
|
|
111
|
+
collected: CollectResult,
|
|
112
|
+
): MeasuredResults {
|
|
113
|
+
const { samples, warmupSamples, heapSamples } = collected;
|
|
114
|
+
const { optStatus, optSamples, pausePoints, heapGrowth, startTime } =
|
|
115
|
+
collected;
|
|
116
|
+
const time = computeStats(samples);
|
|
117
|
+
const heapSize = { avg: heapGrowth, min: heapGrowth, max: heapGrowth };
|
|
118
|
+
return {
|
|
119
|
+
name,
|
|
120
|
+
samples,
|
|
121
|
+
warmupSamples,
|
|
122
|
+
heapSamples,
|
|
123
|
+
time,
|
|
124
|
+
heapSize,
|
|
125
|
+
startTime,
|
|
126
|
+
optStatus,
|
|
127
|
+
optSamples,
|
|
128
|
+
pausePoints,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Run warmup iterations with gc + settle time for V8 optimization. Returns warmup timings.
|
|
134
|
+
*
|
|
135
|
+
* V8 has 4 compilation tiers: Ignition (interpreter) ==> Sparkplug (baseline) ==>
|
|
136
|
+
* Maglev (mid-tier optimizer) ==> TurboFan (full optimizer). Tiering thresholds:
|
|
137
|
+
* - Ignition ==> Sparkplug: 8 invocations
|
|
138
|
+
* - Sparkplug ==> Maglev: 500 invocations
|
|
139
|
+
* - Maglev ==> TurboFan: 6000 invocations
|
|
140
|
+
*
|
|
141
|
+
* Optimization compilation happens on background threads and requires idle time
|
|
142
|
+
* on the main thread to complete. Without sufficient warmup + settle time,
|
|
143
|
+
* benchmarks exhibit bimodal timing: slow Sparkplug samples (~30% slower) mixed
|
|
144
|
+
* with fast optimized samples.
|
|
145
|
+
*
|
|
146
|
+
* The warmup iterations trigger the optimization decision, then settle time
|
|
147
|
+
* provides idle time for background compilation to finish before measurement.
|
|
148
|
+
*
|
|
149
|
+
* @see https://v8.dev/blog/sparkplug
|
|
150
|
+
* @see https://v8.dev/blog/maglev
|
|
151
|
+
* @see https://v8.dev/blog/background-compilation
|
|
152
|
+
*/
|
|
153
|
+
async function runWarmup<T>(config: CollectParams<T>): Promise<number[]> {
|
|
154
|
+
const gc = gcFunction();
|
|
155
|
+
const samples = new Array<number>(config.warmup);
|
|
156
|
+
for (let i = 0; i < config.warmup; i++) {
|
|
157
|
+
const start = performance.now();
|
|
158
|
+
executeBenchmark(config.benchmark, config.params);
|
|
159
|
+
samples[i] = performance.now() - start;
|
|
160
|
+
}
|
|
161
|
+
gc();
|
|
162
|
+
if (config.pauseWarmup) {
|
|
163
|
+
await new Promise(r => setTimeout(r, config.pauseWarmup));
|
|
164
|
+
gc();
|
|
165
|
+
}
|
|
166
|
+
return samples;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Collect timing samples with optional periodic pauses for V8 background compilation to complete. */
|
|
170
|
+
async function runSampleLoop<T>(
|
|
171
|
+
config: CollectParams<T>,
|
|
172
|
+
): Promise<SampleArrays & { startTime: number }> {
|
|
173
|
+
const { maxTime, maxIterations, pauseFirst } = config;
|
|
174
|
+
const { pauseInterval = 0, pauseDuration = 100 } = config;
|
|
175
|
+
const getOptStatus = config.traceOpt ? createOptStatusGetter() : undefined;
|
|
176
|
+
const trackOpt = !!getOptStatus;
|
|
177
|
+
const estimated = maxIterations || Math.ceil(maxTime / 0.1);
|
|
178
|
+
const arrays = createSampleArrays(estimated, trackOpt);
|
|
179
|
+
|
|
180
|
+
let count = 0;
|
|
181
|
+
let elapsed = 0;
|
|
182
|
+
let totalPauseTime = 0;
|
|
183
|
+
const startTime = Number(process.hrtime.bigint() / 1000n);
|
|
184
|
+
const loopStart = performance.now();
|
|
185
|
+
|
|
186
|
+
while (
|
|
187
|
+
(!maxIterations || count < maxIterations) &&
|
|
188
|
+
(!maxTime || elapsed < maxTime)
|
|
189
|
+
) {
|
|
190
|
+
const start = performance.now();
|
|
191
|
+
executeBenchmark(config.benchmark, config.params);
|
|
192
|
+
const end = performance.now();
|
|
193
|
+
arrays.samples[count] = end - start;
|
|
194
|
+
arrays.heapSamples[count] = getHeapStatistics().used_heap_size;
|
|
195
|
+
if (getOptStatus)
|
|
196
|
+
arrays.optStatuses[count] = getOptStatus(config.benchmark.fn);
|
|
197
|
+
count++;
|
|
198
|
+
|
|
199
|
+
if (shouldPause(count, pauseFirst, pauseInterval)) {
|
|
200
|
+
const sampleIndex = count - 1;
|
|
201
|
+
arrays.pausePoints.push({ sampleIndex, durationMs: pauseDuration });
|
|
202
|
+
const pauseStart = performance.now();
|
|
203
|
+
await new Promise(r => setTimeout(r, pauseDuration));
|
|
204
|
+
totalPauseTime += performance.now() - pauseStart;
|
|
205
|
+
}
|
|
206
|
+
elapsed = performance.now() - loopStart - totalPauseTime;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
trimArrays(arrays, count, trackOpt);
|
|
210
|
+
return { ...arrays, startTime };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Pre-allocate sample arrays to reduce GC pressure during measurement. */
|
|
214
|
+
function createSampleArrays(n: number, trackOpt: boolean): SampleArrays {
|
|
215
|
+
const arr = () => new Array<number>(n);
|
|
216
|
+
return {
|
|
217
|
+
samples: arr(),
|
|
218
|
+
heapSamples: arr(),
|
|
219
|
+
optStatuses: trackOpt ? arr() : [],
|
|
220
|
+
pausePoints: [],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** @return true if this iteration should pause for V8 background compilation. */
|
|
225
|
+
function shouldPause(
|
|
226
|
+
iter: number,
|
|
227
|
+
first: number | undefined,
|
|
228
|
+
interval: number,
|
|
229
|
+
): boolean {
|
|
230
|
+
if (first !== undefined && iter === first) return true;
|
|
231
|
+
if (interval <= 0) return false;
|
|
232
|
+
if (first === undefined) return iter % interval === 0;
|
|
233
|
+
return (iter - first) % interval === 0;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Trim pre-allocated arrays to the actual sample count. */
|
|
237
|
+
function trimArrays(
|
|
238
|
+
arrays: SampleArrays,
|
|
239
|
+
count: number,
|
|
240
|
+
trackOpt: boolean,
|
|
241
|
+
): void {
|
|
242
|
+
arrays.samples.length = arrays.heapSamples.length = count;
|
|
243
|
+
if (trackOpt) arrays.optStatuses.length = count;
|
|
244
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
/** Toggle for worker process timing logs (manual, not exposed as CLI flag) */
|
|
1
2
|
export const debugWorkerTiming = false;
|
|
2
3
|
|
|
3
|
-
/**
|
|
4
|
+
/** Current time in ms, or 0 when debug timing is off (zero-cost no-op) */
|
|
4
5
|
export function getPerfNow(): number {
|
|
5
6
|
return debugWorkerTiming ? performance.now() : 0;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
|
-
/**
|
|
9
|
+
/** Elapsed ms between marks, or 0 when debug timing is off */
|
|
9
10
|
export function getElapsed(startMark: number, endMark?: number): number {
|
|
10
11
|
if (!debugWorkerTiming) return 0;
|
|
11
12
|
const end = endMark ?? performance.now();
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import type {
|
|
3
|
-
import type {
|
|
4
|
-
import type {
|
|
5
|
-
import {
|
|
2
|
+
import type { Session } from "node:inspector/promises";
|
|
3
|
+
import type { CoverageData } from "../profiling/node/CoverageTypes.ts";
|
|
4
|
+
import type { HeapProfile } from "../profiling/node/HeapSampler.ts";
|
|
5
|
+
import type { TimeProfile } from "../profiling/node/TimeSampler.ts";
|
|
6
|
+
import type { BenchmarkFunction, BenchmarkSpec } from "./BenchmarkSpec.ts";
|
|
7
|
+
import type { BenchRunner, RunnerOptions } from "./BenchRunner.ts";
|
|
8
|
+
import type { KnownRunner } from "./CreateRunner.ts";
|
|
9
|
+
import type { MeasuredResults } from "./MeasuredResults.ts";
|
|
6
10
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
import { debugWorkerTiming, getElapsed, getPerfNow } from "./TimingUtils.ts"; // 5 minutes
|
|
11
|
+
createBenchRunner,
|
|
12
|
+
importBenchFn,
|
|
13
|
+
resolveVariantFn,
|
|
14
|
+
} from "./RunnerUtils.ts";
|
|
15
|
+
import { debugWorkerTiming, getElapsed, getPerfNow } from "./TimingUtils.ts";
|
|
13
16
|
|
|
14
17
|
/** Message sent to worker process to start a benchmark run. */
|
|
15
18
|
export interface RunMessage {
|
|
@@ -17,27 +20,35 @@ export interface RunMessage {
|
|
|
17
20
|
spec: BenchmarkSpec;
|
|
18
21
|
runnerName: KnownRunner;
|
|
19
22
|
options: RunnerOptions;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
/** Serialized function body (mutually exclusive with modulePath) */
|
|
24
|
+
fnCode?: string;
|
|
25
|
+
modulePath?: string;
|
|
26
|
+
/** Defaults to default export */
|
|
27
|
+
exportName?: string;
|
|
28
|
+
/** Called once before benchmarking; result passed as params to fn */
|
|
29
|
+
setupExportName?: string;
|
|
24
30
|
params?: unknown;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
|
|
32
|
+
/** Directory URL containing variant .ts files (BenchMatrix mode) */
|
|
33
|
+
variantDir?: string;
|
|
34
|
+
/** Variant filename without .ts extension */
|
|
35
|
+
variantId?: string;
|
|
36
|
+
caseData?: unknown;
|
|
37
|
+
caseId?: string;
|
|
38
|
+
/** Module URL exporting cases[] and loadCase() */
|
|
39
|
+
casesModule?: string;
|
|
31
40
|
}
|
|
32
41
|
|
|
33
|
-
/**
|
|
42
|
+
/** Benchmark results returned from worker process. */
|
|
34
43
|
export interface ResultMessage {
|
|
35
44
|
type: "result";
|
|
36
45
|
results: MeasuredResults[];
|
|
37
46
|
heapProfile?: HeapProfile;
|
|
47
|
+
timeProfile?: TimeProfile;
|
|
48
|
+
coverage?: CoverageData;
|
|
38
49
|
}
|
|
39
50
|
|
|
40
|
-
/**
|
|
51
|
+
/** Error returned from worker process when benchmark fails. */
|
|
41
52
|
export interface ErrorMessage {
|
|
42
53
|
type: "error";
|
|
43
54
|
error: string;
|
|
@@ -51,20 +62,25 @@ interface BenchmarkImportResult {
|
|
|
51
62
|
params: unknown;
|
|
52
63
|
}
|
|
53
64
|
|
|
65
|
+
/** Profiling state accumulated during worker benchmark execution */
|
|
66
|
+
interface ProfilingState {
|
|
67
|
+
heapProfile?: HeapProfile;
|
|
68
|
+
timeProfile?: TimeProfile;
|
|
69
|
+
coverage?: CoverageData;
|
|
70
|
+
/** Shared session so TimeSampler doesn't reset coverage counters */
|
|
71
|
+
profilerSession?: Session;
|
|
72
|
+
}
|
|
73
|
+
|
|
54
74
|
const workerStartTime = getPerfNow();
|
|
55
75
|
const maxLifetime = 5 * 60 * 1000;
|
|
56
76
|
|
|
57
|
-
/** Log timing with consistent format */
|
|
58
77
|
const logTiming = debugWorkerTiming ? _logTiming : () => {};
|
|
59
78
|
function _logTiming(operation: string, duration?: number) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
} else {
|
|
63
|
-
console.log(`[Worker] ${operation} ${duration.toFixed(1)}ms`);
|
|
64
|
-
}
|
|
79
|
+
const suffix = duration !== undefined ? ` ${duration.toFixed(1)}ms` : "";
|
|
80
|
+
console.log(`[Worker] ${operation}${suffix}`);
|
|
65
81
|
}
|
|
66
82
|
|
|
67
|
-
/** Send message
|
|
83
|
+
/** Send IPC message to parent then exit the worker process */
|
|
68
84
|
function sendAndExit(msg: ResultMessage | ErrorMessage, exitCode: number) {
|
|
69
85
|
process.send!(msg, undefined, undefined, (err: Error | null): void => {
|
|
70
86
|
if (err) {
|
|
@@ -85,7 +101,12 @@ async function resolveBenchmarkFn(
|
|
|
85
101
|
return importVariantModule(message);
|
|
86
102
|
}
|
|
87
103
|
if (message.modulePath) {
|
|
88
|
-
|
|
104
|
+
const { modulePath, exportName, setupExportName, params } = message;
|
|
105
|
+
logTiming(
|
|
106
|
+
`Importing from ${modulePath}${exportName ? ` (${exportName})` : ""}`,
|
|
107
|
+
);
|
|
108
|
+
if (setupExportName) logTiming(`Calling setup: ${setupExportName}`);
|
|
109
|
+
return importBenchFn(modulePath, exportName, setupExportName, params);
|
|
89
110
|
}
|
|
90
111
|
return { fn: reconstructFunction(message.fnCode!), params: message.params };
|
|
91
112
|
}
|
|
@@ -94,56 +115,16 @@ async function resolveBenchmarkFn(
|
|
|
94
115
|
async function importVariantModule(
|
|
95
116
|
message: RunMessage,
|
|
96
117
|
): Promise<BenchmarkImportResult> {
|
|
97
|
-
const { variantDir, variantId
|
|
98
|
-
let { caseData } = message;
|
|
99
|
-
const moduleUrl = variantModuleUrl(variantDir!, variantId!);
|
|
118
|
+
const { variantDir, variantId } = message;
|
|
100
119
|
logTiming(`Importing variant ${variantId} from ${variantDir}`);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const module = await import(moduleUrl);
|
|
107
|
-
const { setup, run } = module;
|
|
108
|
-
|
|
109
|
-
if (typeof run !== "function") {
|
|
110
|
-
throw new Error(`Variant '${variantId}' must export 'run' function`);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Stateful variant: setup returns state, run receives state
|
|
114
|
-
if (typeof setup === "function") {
|
|
115
|
-
logTiming(`Calling setup for ${variantId}`);
|
|
116
|
-
const state = await setup(caseData);
|
|
117
|
-
return { fn: () => run(state), params: undefined };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Stateless variant: run receives caseData directly
|
|
121
|
-
return { fn: () => run(caseData), params: undefined };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** Import benchmark function and optionally run setup */
|
|
125
|
-
async function importBenchmarkWithSetup(
|
|
126
|
-
message: RunMessage,
|
|
127
|
-
): Promise<BenchmarkImportResult> {
|
|
128
|
-
const { modulePath, exportName, setupExportName, params } = message;
|
|
129
|
-
logTiming(
|
|
130
|
-
`Importing from ${modulePath}${exportName ? ` (${exportName})` : ""}`,
|
|
131
|
-
);
|
|
132
|
-
const module = await import(modulePath!);
|
|
133
|
-
|
|
134
|
-
const fn = getModuleExport(module, exportName, modulePath!);
|
|
135
|
-
|
|
136
|
-
if (setupExportName) {
|
|
137
|
-
logTiming(`Calling setup: ${setupExportName}`);
|
|
138
|
-
const setupFn = getModuleExport(module, setupExportName, modulePath!);
|
|
139
|
-
const setupResult = await setupFn(params);
|
|
140
|
-
return { fn, params: setupResult };
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return { fn, params };
|
|
120
|
+
return resolveVariantFn({
|
|
121
|
+
...message,
|
|
122
|
+
variantDir: variantDir!,
|
|
123
|
+
variantId: variantId!,
|
|
124
|
+
});
|
|
144
125
|
}
|
|
145
126
|
|
|
146
|
-
/**
|
|
127
|
+
/** Eval serialized function body back into a callable */
|
|
147
128
|
function reconstructFunction(fnCode: string): BenchmarkFunction {
|
|
148
129
|
// biome-ignore lint/security/noGlobalEval: Necessary for worker process isolation, code is from trusted source
|
|
149
130
|
const fn = eval(`(${fnCode})`); // eslint-disable-line no-eval
|
|
@@ -153,46 +134,75 @@ function reconstructFunction(fnCode: string): BenchmarkFunction {
|
|
|
153
134
|
return fn;
|
|
154
135
|
}
|
|
155
136
|
|
|
156
|
-
/**
|
|
157
|
-
async function
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
): Promise<
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
137
|
+
/** Run benchmark with optional heap, time, and coverage profiling */
|
|
138
|
+
async function runWithProfiling(
|
|
139
|
+
message: RunMessage,
|
|
140
|
+
runner: BenchRunner,
|
|
141
|
+
): Promise<ResultMessage> {
|
|
142
|
+
const state: ProfilingState = {};
|
|
143
|
+
const runBench = buildProfilingChain(message, runner, state);
|
|
144
|
+
|
|
145
|
+
if (!message.options.callCounts) {
|
|
146
|
+
const results = await runBench();
|
|
147
|
+
return { type: "result", results, ...state };
|
|
165
148
|
}
|
|
166
|
-
return { data: caseId };
|
|
167
|
-
}
|
|
168
149
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
throw new Error(`Export '${name}' from ${modulePath} is not a function`);
|
|
179
|
-
}
|
|
180
|
-
return fn;
|
|
150
|
+
const { withCoverageProfiling } = await import(
|
|
151
|
+
"../profiling/node/CoverageSampler.ts"
|
|
152
|
+
);
|
|
153
|
+
const r = await withCoverageProfiling(async session => {
|
|
154
|
+
state.profilerSession = session;
|
|
155
|
+
return runBench();
|
|
156
|
+
});
|
|
157
|
+
state.coverage = r.coverage;
|
|
158
|
+
return { type: "result", results: r.result, ...state };
|
|
181
159
|
}
|
|
182
160
|
|
|
183
|
-
/**
|
|
184
|
-
function
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
161
|
+
/** Build nested profiling wrappers: outer heap, inner time */
|
|
162
|
+
function buildProfilingChain(
|
|
163
|
+
message: RunMessage,
|
|
164
|
+
runner: BenchRunner,
|
|
165
|
+
state: ProfilingState,
|
|
166
|
+
): () => Promise<MeasuredResults[]> {
|
|
167
|
+
const { alloc, profile, profileInterval, allocInterval, allocDepth } =
|
|
168
|
+
message.options;
|
|
169
|
+
|
|
170
|
+
const run = async () => {
|
|
171
|
+
const { fn, params } = await resolveBenchmarkFn(message);
|
|
172
|
+
return runner.runBench({ ...message.spec, fn }, message.options, params);
|
|
189
173
|
};
|
|
174
|
+
|
|
175
|
+
const runMaybeWithTime = profile
|
|
176
|
+
? async () => {
|
|
177
|
+
const { withTimeProfiling } = await import(
|
|
178
|
+
"../profiling/node/TimeSampler.ts"
|
|
179
|
+
);
|
|
180
|
+
const opts = {
|
|
181
|
+
interval: profileInterval,
|
|
182
|
+
session: state.profilerSession,
|
|
183
|
+
};
|
|
184
|
+
const r = await withTimeProfiling(opts, run);
|
|
185
|
+
state.timeProfile = r.profile;
|
|
186
|
+
return r.result;
|
|
187
|
+
}
|
|
188
|
+
: run;
|
|
189
|
+
|
|
190
|
+
return alloc
|
|
191
|
+
? async () => {
|
|
192
|
+
const { withHeapSampling } = await import(
|
|
193
|
+
"../profiling/node/HeapSampler.ts"
|
|
194
|
+
);
|
|
195
|
+
const heapOpts = {
|
|
196
|
+
samplingInterval: allocInterval,
|
|
197
|
+
stackDepth: allocDepth,
|
|
198
|
+
};
|
|
199
|
+
const r = await withHeapSampling(heapOpts, runMaybeWithTime);
|
|
200
|
+
state.heapProfile = r.profile;
|
|
201
|
+
return r.result;
|
|
202
|
+
}
|
|
203
|
+
: runMaybeWithTime;
|
|
190
204
|
}
|
|
191
205
|
|
|
192
|
-
/**
|
|
193
|
-
* Worker process for isolated benchmark execution.
|
|
194
|
-
* Uses eval() safely in isolated child process with trusted code.
|
|
195
|
-
*/
|
|
196
206
|
process.on("message", async (message: RunMessage) => {
|
|
197
207
|
if (message.type !== "run") return;
|
|
198
208
|
|
|
@@ -200,57 +210,31 @@ process.on("message", async (message: RunMessage) => {
|
|
|
200
210
|
|
|
201
211
|
try {
|
|
202
212
|
const start = getPerfNow();
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
const runner = (message.options as any).adaptive
|
|
206
|
-
? createAdaptiveWrapper(baseRunner, message.options as AdaptiveOptions)
|
|
207
|
-
: baseRunner;
|
|
208
|
-
|
|
213
|
+
const runner = await createBenchRunner(message.runnerName, message.options);
|
|
209
214
|
logTiming("Runner created in", getElapsed(start));
|
|
210
215
|
|
|
211
216
|
const benchStart = getPerfNow();
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
}
|
|
217
|
+
const result = await runWithProfiling(message, runner);
|
|
218
|
+
logTiming("Benchmark execution took", getElapsed(benchStart));
|
|
219
|
+
sendAndExit(result, 0);
|
|
245
220
|
} catch (error) {
|
|
246
|
-
|
|
221
|
+
const err = error instanceof Error ? error : undefined;
|
|
222
|
+
sendAndExit(
|
|
223
|
+
{
|
|
224
|
+
type: "error",
|
|
225
|
+
error: err?.message ?? String(error),
|
|
226
|
+
stack: err?.stack,
|
|
227
|
+
},
|
|
228
|
+
1,
|
|
229
|
+
);
|
|
247
230
|
}
|
|
248
231
|
});
|
|
249
232
|
|
|
250
|
-
//
|
|
233
|
+
// Prevent zombie processes
|
|
251
234
|
setTimeout(() => {
|
|
252
235
|
console.error("WorkerScript: Maximum lifetime exceeded, exiting");
|
|
253
236
|
process.exit(1);
|
|
254
237
|
}, maxLifetime);
|
|
255
238
|
|
|
239
|
+
// Prevent stdin from keeping the worker process alive
|
|
256
240
|
process.stdin.pause();
|