modestbench 0.0.1
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/CHANGELOG.md +45 -0
- package/LICENSE.md +55 -0
- package/README.md +699 -0
- package/dist/bootstrap.cjs +37 -0
- package/dist/bootstrap.cjs.map +1 -0
- package/dist/bootstrap.d.cts +17 -0
- package/dist/bootstrap.d.cts.map +1 -0
- package/dist/bootstrap.d.ts +17 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +33 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/cli/commands/history.cjs +459 -0
- package/dist/cli/commands/history.cjs.map +1 -0
- package/dist/cli/commands/history.d.cts +34 -0
- package/dist/cli/commands/history.d.cts.map +1 -0
- package/dist/cli/commands/history.d.ts +34 -0
- package/dist/cli/commands/history.d.ts.map +1 -0
- package/dist/cli/commands/history.js +422 -0
- package/dist/cli/commands/history.js.map +1 -0
- package/dist/cli/commands/init.cjs +566 -0
- package/dist/cli/commands/init.cjs.map +1 -0
- package/dist/cli/commands/init.d.cts +26 -0
- package/dist/cli/commands/init.d.cts.map +1 -0
- package/dist/cli/commands/init.d.ts +26 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +562 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/run.cjs +285 -0
- package/dist/cli/commands/run.cjs.map +1 -0
- package/dist/cli/commands/run.d.cts +37 -0
- package/dist/cli/commands/run.d.cts.map +1 -0
- package/dist/cli/commands/run.d.ts +37 -0
- package/dist/cli/commands/run.d.ts.map +1 -0
- package/dist/cli/commands/run.js +248 -0
- package/dist/cli/commands/run.js.map +1 -0
- package/dist/cli/index.cjs +523 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +58 -0
- package/dist/cli/index.d.cts.map +1 -0
- package/dist/cli/index.d.ts +58 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +515 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/config/manager.cjs +370 -0
- package/dist/config/manager.cjs.map +1 -0
- package/dist/config/manager.d.cts +46 -0
- package/dist/config/manager.d.cts.map +1 -0
- package/dist/config/manager.d.ts +46 -0
- package/dist/config/manager.d.ts.map +1 -0
- package/dist/config/manager.js +333 -0
- package/dist/config/manager.js.map +1 -0
- package/dist/config/schema.cjs +182 -0
- package/dist/config/schema.cjs.map +1 -0
- package/dist/config/schema.d.cts +51 -0
- package/dist/config/schema.d.cts.map +1 -0
- package/dist/config/schema.d.ts +51 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +145 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/constants.cjs +22 -0
- package/dist/constants.cjs.map +1 -0
- package/dist/constants.d.cts +10 -0
- package/dist/constants.d.cts.map +1 -0
- package/dist/constants.d.ts +10 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +19 -0
- package/dist/constants.js.map +1 -0
- package/dist/core/benchmark-schema.cjs +135 -0
- package/dist/core/benchmark-schema.cjs.map +1 -0
- package/dist/core/benchmark-schema.d.cts +139 -0
- package/dist/core/benchmark-schema.d.cts.map +1 -0
- package/dist/core/benchmark-schema.d.ts +139 -0
- package/dist/core/benchmark-schema.d.ts.map +1 -0
- package/dist/core/benchmark-schema.js +132 -0
- package/dist/core/benchmark-schema.js.map +1 -0
- package/dist/core/engine.cjs +669 -0
- package/dist/core/engine.cjs.map +1 -0
- package/dist/core/engine.d.cts +128 -0
- package/dist/core/engine.d.cts.map +1 -0
- package/dist/core/engine.d.ts +128 -0
- package/dist/core/engine.d.ts.map +1 -0
- package/dist/core/engine.js +632 -0
- package/dist/core/engine.js.map +1 -0
- package/dist/core/engines/accurate-engine.cjs +292 -0
- package/dist/core/engines/accurate-engine.cjs.map +1 -0
- package/dist/core/engines/accurate-engine.d.cts +63 -0
- package/dist/core/engines/accurate-engine.d.cts.map +1 -0
- package/dist/core/engines/accurate-engine.d.ts +63 -0
- package/dist/core/engines/accurate-engine.d.ts.map +1 -0
- package/dist/core/engines/accurate-engine.js +288 -0
- package/dist/core/engines/accurate-engine.js.map +1 -0
- package/dist/core/engines/index.cjs +21 -0
- package/dist/core/engines/index.cjs.map +1 -0
- package/dist/core/engines/index.d.cts +16 -0
- package/dist/core/engines/index.d.cts.map +1 -0
- package/dist/core/engines/index.d.ts +16 -0
- package/dist/core/engines/index.d.ts.map +1 -0
- package/dist/core/engines/index.js +16 -0
- package/dist/core/engines/index.js.map +1 -0
- package/dist/core/engines/tinybench-engine.cjs +286 -0
- package/dist/core/engines/tinybench-engine.cjs.map +1 -0
- package/dist/core/engines/tinybench-engine.d.cts +18 -0
- package/dist/core/engines/tinybench-engine.d.cts.map +1 -0
- package/dist/core/engines/tinybench-engine.d.ts +18 -0
- package/dist/core/engines/tinybench-engine.d.ts.map +1 -0
- package/dist/core/engines/tinybench-engine.js +282 -0
- package/dist/core/engines/tinybench-engine.js.map +1 -0
- package/dist/core/error-manager.cjs +303 -0
- package/dist/core/error-manager.cjs.map +1 -0
- package/dist/core/error-manager.d.cts +77 -0
- package/dist/core/error-manager.d.cts.map +1 -0
- package/dist/core/error-manager.d.ts +77 -0
- package/dist/core/error-manager.d.ts.map +1 -0
- package/dist/core/error-manager.js +299 -0
- package/dist/core/error-manager.js.map +1 -0
- package/dist/core/loader.cjs +287 -0
- package/dist/core/loader.cjs.map +1 -0
- package/dist/core/loader.d.cts +55 -0
- package/dist/core/loader.d.cts.map +1 -0
- package/dist/core/loader.d.ts +55 -0
- package/dist/core/loader.d.ts.map +1 -0
- package/dist/core/loader.js +250 -0
- package/dist/core/loader.js.map +1 -0
- package/dist/core/stats-utils.cjs +99 -0
- package/dist/core/stats-utils.cjs.map +1 -0
- package/dist/core/stats-utils.d.cts +50 -0
- package/dist/core/stats-utils.d.cts.map +1 -0
- package/dist/core/stats-utils.d.ts +50 -0
- package/dist/core/stats-utils.d.ts.map +1 -0
- package/dist/core/stats-utils.js +94 -0
- package/dist/core/stats-utils.js.map +1 -0
- package/dist/index.cjs +64 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +22 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/progress/manager.cjs +325 -0
- package/dist/progress/manager.cjs.map +1 -0
- package/dist/progress/manager.d.cts +125 -0
- package/dist/progress/manager.d.cts.map +1 -0
- package/dist/progress/manager.d.ts +125 -0
- package/dist/progress/manager.d.ts.map +1 -0
- package/dist/progress/manager.js +321 -0
- package/dist/progress/manager.js.map +1 -0
- package/dist/reporters/csv.cjs +250 -0
- package/dist/reporters/csv.cjs.map +1 -0
- package/dist/reporters/csv.d.cts +92 -0
- package/dist/reporters/csv.d.cts.map +1 -0
- package/dist/reporters/csv.d.ts +92 -0
- package/dist/reporters/csv.d.ts.map +1 -0
- package/dist/reporters/csv.js +246 -0
- package/dist/reporters/csv.js.map +1 -0
- package/dist/reporters/human.cjs +516 -0
- package/dist/reporters/human.cjs.map +1 -0
- package/dist/reporters/human.d.cts +86 -0
- package/dist/reporters/human.d.cts.map +1 -0
- package/dist/reporters/human.d.ts +86 -0
- package/dist/reporters/human.d.ts.map +1 -0
- package/dist/reporters/human.js +509 -0
- package/dist/reporters/human.js.map +1 -0
- package/dist/reporters/index.cjs +17 -0
- package/dist/reporters/index.cjs.map +1 -0
- package/dist/reporters/index.d.cts +10 -0
- package/dist/reporters/index.d.cts.map +1 -0
- package/dist/reporters/index.d.ts +10 -0
- package/dist/reporters/index.d.ts.map +1 -0
- package/dist/reporters/index.js +10 -0
- package/dist/reporters/index.js.map +1 -0
- package/dist/reporters/json.cjs +215 -0
- package/dist/reporters/json.cjs.map +1 -0
- package/dist/reporters/json.d.cts +79 -0
- package/dist/reporters/json.d.cts.map +1 -0
- package/dist/reporters/json.d.ts +79 -0
- package/dist/reporters/json.d.ts.map +1 -0
- package/dist/reporters/json.js +211 -0
- package/dist/reporters/json.js.map +1 -0
- package/dist/reporters/registry.cjs +255 -0
- package/dist/reporters/registry.cjs.map +1 -0
- package/dist/reporters/registry.d.cts +155 -0
- package/dist/reporters/registry.d.cts.map +1 -0
- package/dist/reporters/registry.d.ts +155 -0
- package/dist/reporters/registry.d.ts.map +1 -0
- package/dist/reporters/registry.js +249 -0
- package/dist/reporters/registry.js.map +1 -0
- package/dist/reporters/simple.cjs +328 -0
- package/dist/reporters/simple.cjs.map +1 -0
- package/dist/reporters/simple.d.cts +51 -0
- package/dist/reporters/simple.d.cts.map +1 -0
- package/dist/reporters/simple.d.ts +51 -0
- package/dist/reporters/simple.d.ts.map +1 -0
- package/dist/reporters/simple.js +321 -0
- package/dist/reporters/simple.js.map +1 -0
- package/dist/schema/modestbench-config.schema.json +162 -0
- package/dist/storage/history.cjs +456 -0
- package/dist/storage/history.cjs.map +1 -0
- package/dist/storage/history.d.cts +99 -0
- package/dist/storage/history.d.cts.map +1 -0
- package/dist/storage/history.d.ts +99 -0
- package/dist/storage/history.d.ts.map +1 -0
- package/dist/storage/history.js +452 -0
- package/dist/storage/history.js.map +1 -0
- package/dist/types/cli.cjs +21 -0
- package/dist/types/cli.cjs.map +1 -0
- package/dist/types/cli.d.cts +296 -0
- package/dist/types/cli.d.cts.map +1 -0
- package/dist/types/cli.d.ts +296 -0
- package/dist/types/cli.d.ts.map +1 -0
- package/dist/types/cli.js +18 -0
- package/dist/types/cli.js.map +1 -0
- package/dist/types/core.cjs +14 -0
- package/dist/types/core.cjs.map +1 -0
- package/dist/types/core.d.cts +380 -0
- package/dist/types/core.d.cts.map +1 -0
- package/dist/types/core.d.ts +380 -0
- package/dist/types/core.d.ts.map +1 -0
- package/dist/types/core.js +13 -0
- package/dist/types/core.js.map +1 -0
- package/dist/types/index.cjs +27 -0
- package/dist/types/index.cjs.map +1 -0
- package/dist/types/index.d.cts +11 -0
- package/dist/types/index.d.cts.map +1 -0
- package/dist/types/index.d.ts +11 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +11 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/interfaces.cjs +10 -0
- package/dist/types/interfaces.cjs.map +1 -0
- package/dist/types/interfaces.d.cts +381 -0
- package/dist/types/interfaces.d.cts.map +1 -0
- package/dist/types/interfaces.d.ts +381 -0
- package/dist/types/interfaces.d.ts.map +1 -0
- package/dist/types/interfaces.js +9 -0
- package/dist/types/interfaces.js.map +1 -0
- package/dist/types/utility.cjs +92 -0
- package/dist/types/utility.cjs.map +1 -0
- package/dist/types/utility.d.cts +330 -0
- package/dist/types/utility.d.cts.map +1 -0
- package/dist/types/utility.d.ts +330 -0
- package/dist/types/utility.d.ts.map +1 -0
- package/dist/types/utility.js +78 -0
- package/dist/types/utility.js.map +1 -0
- package/package.json +211 -0
- package/src/bootstrap.ts +35 -0
- package/src/cli/commands/history.ts +569 -0
- package/src/cli/commands/init.ts +658 -0
- package/src/cli/commands/run.ts +346 -0
- package/src/cli/index.ts +642 -0
- package/src/config/manager.ts +387 -0
- package/src/config/schema.ts +188 -0
- package/src/constants.ts +21 -0
- package/src/core/benchmark-schema.ts +185 -0
- package/src/core/engine.ts +888 -0
- package/src/core/engines/accurate-engine.ts +408 -0
- package/src/core/engines/index.ts +16 -0
- package/src/core/engines/tinybench-engine.ts +335 -0
- package/src/core/error-manager.ts +372 -0
- package/src/core/loader.ts +324 -0
- package/src/core/stats-utils.ts +135 -0
- package/src/index.ts +46 -0
- package/src/progress/manager.ts +415 -0
- package/src/reporters/csv.ts +368 -0
- package/src/reporters/human.ts +707 -0
- package/src/reporters/index.ts +10 -0
- package/src/reporters/json.ts +302 -0
- package/src/reporters/registry.ts +349 -0
- package/src/reporters/simple.ts +459 -0
- package/src/storage/history.ts +600 -0
- package/src/types/cli.ts +312 -0
- package/src/types/core.ts +414 -0
- package/src/types/index.ts +18 -0
- package/src/types/interfaces.ts +451 -0
- package/src/types/utility.ts +446 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TinybenchEngine - Tinybench-specific benchmark execution implementation
|
|
3
|
+
*
|
|
4
|
+
* Concrete implementation of ModestBenchEngine using the tinybench library for
|
|
5
|
+
* benchmark execution and measurement.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Bench } from 'tinybench';
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
BenchmarkTask,
|
|
12
|
+
ModestBenchConfig,
|
|
13
|
+
Reporter,
|
|
14
|
+
TaskResult,
|
|
15
|
+
} from '../../types/index.js';
|
|
16
|
+
|
|
17
|
+
import { ModestBenchEngine } from '../engine.js';
|
|
18
|
+
import { calculateStatistics, removeOutliersIQR } from '../stats-utils.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Tinybench-specific benchmark execution engine
|
|
22
|
+
*/
|
|
23
|
+
export class TinybenchEngine extends ModestBenchEngine {
|
|
24
|
+
/**
|
|
25
|
+
* Execute a single benchmark task using tinybench
|
|
26
|
+
*/
|
|
27
|
+
protected async executeBenchmarkTask(
|
|
28
|
+
taskName: string,
|
|
29
|
+
taskData: BenchmarkTask,
|
|
30
|
+
config: ModestBenchConfig,
|
|
31
|
+
_reporters: Reporter[] = [],
|
|
32
|
+
signal?: AbortSignal,
|
|
33
|
+
): Promise<TaskResult> {
|
|
34
|
+
try {
|
|
35
|
+
if (!taskData.fn || typeof taskData.fn !== 'function') {
|
|
36
|
+
throw new Error('Benchmark task must have a "fn" function property');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Determine effective time and iterations based on limitBy mode
|
|
40
|
+
let effectiveTime: number;
|
|
41
|
+
let effectiveIterations: number;
|
|
42
|
+
|
|
43
|
+
switch (config.limitBy) {
|
|
44
|
+
case 'all':
|
|
45
|
+
// Both must be met - tinybench default behavior
|
|
46
|
+
|
|
47
|
+
effectiveTime = Math.min(config.time || 1000, 2000);
|
|
48
|
+
effectiveIterations = config.iterations;
|
|
49
|
+
break;
|
|
50
|
+
|
|
51
|
+
case 'any':
|
|
52
|
+
// Stop at whichever comes first
|
|
53
|
+
// Since tinybench requires BOTH to be met, use iterations mode for faster completion
|
|
54
|
+
// This means if iterations completes before time, it stops (time=1ms ensures time completes fast)
|
|
55
|
+
effectiveTime = 1;
|
|
56
|
+
effectiveIterations = config.iterations;
|
|
57
|
+
break;
|
|
58
|
+
|
|
59
|
+
case 'iterations':
|
|
60
|
+
// Iterations is the limit, use minimal time
|
|
61
|
+
effectiveTime = 1;
|
|
62
|
+
effectiveIterations = config.iterations;
|
|
63
|
+
break;
|
|
64
|
+
|
|
65
|
+
case 'time':
|
|
66
|
+
// Time is the limit, iterations is a minimum (use small value)
|
|
67
|
+
effectiveTime = Math.min(config.time || 1000, 2000);
|
|
68
|
+
effectiveIterations = 1; // Minimal iterations so time is the limiting factor
|
|
69
|
+
break;
|
|
70
|
+
|
|
71
|
+
default:
|
|
72
|
+
// Fallback to iterations mode
|
|
73
|
+
effectiveTime = 1;
|
|
74
|
+
effectiveIterations = config.iterations;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const bench = new Bench({
|
|
78
|
+
iterations: effectiveIterations,
|
|
79
|
+
time: effectiveTime,
|
|
80
|
+
warmupIterations: config.warmup,
|
|
81
|
+
warmupTime: config.warmup > 0 ? Math.min(config.warmup || 0, 500) : 0,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Add the task with signal for task-level abort support
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
86
|
+
// @ts-ignore - Pending https://github.com/tinylibs/tinybench/pull/364
|
|
87
|
+
bench.add(taskName, taskData.fn, signal ? { signal } : undefined);
|
|
88
|
+
|
|
89
|
+
// Set up periodic progress updates during execution
|
|
90
|
+
const progressInterval = setInterval(() => {
|
|
91
|
+
// Force progress update to show current state with ETA
|
|
92
|
+
this.progressManager.forceUpdate();
|
|
93
|
+
}, 500); // Update every 500ms during execution
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
// Run the benchmark
|
|
97
|
+
await bench.run();
|
|
98
|
+
} catch (error) {
|
|
99
|
+
clearInterval(progressInterval);
|
|
100
|
+
// Handle array length errors for extremely fast operations
|
|
101
|
+
const errorMessage =
|
|
102
|
+
error instanceof Error ? error.message : String(error);
|
|
103
|
+
|
|
104
|
+
if (errorMessage.includes('Invalid array length')) {
|
|
105
|
+
// Retry with minimal time (1ms) for extremely fast operations
|
|
106
|
+
// Use same limiting logic but with minimal time for fast ops
|
|
107
|
+
let retryTime: number;
|
|
108
|
+
switch (config.limitBy) {
|
|
109
|
+
case 'all':
|
|
110
|
+
case 'any':
|
|
111
|
+
retryTime = 10;
|
|
112
|
+
break;
|
|
113
|
+
case 'iterations':
|
|
114
|
+
retryTime = 1;
|
|
115
|
+
break;
|
|
116
|
+
case 'time':
|
|
117
|
+
retryTime = 10;
|
|
118
|
+
break;
|
|
119
|
+
default:
|
|
120
|
+
retryTime = 1;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const minimalBench = new Bench({
|
|
124
|
+
iterations: config.iterations,
|
|
125
|
+
time: retryTime,
|
|
126
|
+
warmupIterations: config.warmup,
|
|
127
|
+
warmupTime: 0,
|
|
128
|
+
});
|
|
129
|
+
minimalBench.add(
|
|
130
|
+
taskName,
|
|
131
|
+
taskData.fn,
|
|
132
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
133
|
+
// @ts-ignore - Pending https://github.com/tinylibs/tinybench/pull/364
|
|
134
|
+
signal ? { signal } : undefined,
|
|
135
|
+
);
|
|
136
|
+
try {
|
|
137
|
+
await minimalBench.run();
|
|
138
|
+
} catch {
|
|
139
|
+
// If still failing, the operation is too fast even for tinybench
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Benchmark operation is too fast to measure reliably (execution time < 1ns)`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const minimalResults = minimalBench.results[0];
|
|
145
|
+
if (!minimalResults || minimalResults.error) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Benchmark too fast to measure reliably: ${minimalResults?.error?.message || 'unknown error'}`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
// Continue with minimal results - apply outlier removal
|
|
151
|
+
const minimalRawSamples = minimalResults.latency.samples || [];
|
|
152
|
+
const minimalSamplesInNs = minimalRawSamples.map((s) => s * 1e6);
|
|
153
|
+
const minimalCleanedSamples = removeOutliersIQR(minimalSamplesInNs);
|
|
154
|
+
const minimalStats = calculateStatistics(minimalCleanedSamples);
|
|
155
|
+
|
|
156
|
+
const taskResult: TaskResult = {
|
|
157
|
+
cv: minimalStats.cv,
|
|
158
|
+
iterations: minimalCleanedSamples.length,
|
|
159
|
+
marginOfError: minimalStats.marginOfError,
|
|
160
|
+
max: minimalStats.max,
|
|
161
|
+
mean: minimalStats.mean,
|
|
162
|
+
metadata: taskData.metadata ?? {},
|
|
163
|
+
min: minimalStats.min,
|
|
164
|
+
name: taskName,
|
|
165
|
+
opsPerSecond: minimalResults.throughput.mean || 0,
|
|
166
|
+
p95: minimalStats.p95,
|
|
167
|
+
p99: minimalStats.p99,
|
|
168
|
+
stdDev: minimalStats.stdDev,
|
|
169
|
+
...(taskData.tags ? { tags: taskData.tags } : {}),
|
|
170
|
+
variance: minimalStats.variance,
|
|
171
|
+
};
|
|
172
|
+
return taskResult;
|
|
173
|
+
}
|
|
174
|
+
throw error;
|
|
175
|
+
} finally {
|
|
176
|
+
// Always clear the progress interval
|
|
177
|
+
clearInterval(progressInterval);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Get results
|
|
181
|
+
const results = bench.results[0];
|
|
182
|
+
if (!results) {
|
|
183
|
+
throw new Error('No benchmark results returned');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Check if the task was aborted
|
|
187
|
+
if (results.aborted) {
|
|
188
|
+
// Task was aborted via signal - return minimal valid result with error
|
|
189
|
+
const taskResult: TaskResult = {
|
|
190
|
+
cv: 0,
|
|
191
|
+
error: new Error('Benchmark aborted by user signal'),
|
|
192
|
+
iterations: results.latency?.samples?.length || 0,
|
|
193
|
+
marginOfError: 0,
|
|
194
|
+
max: 0,
|
|
195
|
+
mean: 0,
|
|
196
|
+
metadata: taskData.metadata ?? {},
|
|
197
|
+
min: 0,
|
|
198
|
+
name: taskName,
|
|
199
|
+
opsPerSecond: 0,
|
|
200
|
+
p95: 0,
|
|
201
|
+
p99: 0,
|
|
202
|
+
stdDev: 0,
|
|
203
|
+
...(taskData.tags ? { tags: taskData.tags } : {}),
|
|
204
|
+
variance: 0,
|
|
205
|
+
};
|
|
206
|
+
return taskResult;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Check if tinybench detected an error during execution
|
|
210
|
+
if (results.error) {
|
|
211
|
+
const errorMessage =
|
|
212
|
+
results.error instanceof Error
|
|
213
|
+
? results.error.message
|
|
214
|
+
: String(results.error);
|
|
215
|
+
|
|
216
|
+
// Handle array length errors for extremely fast operations
|
|
217
|
+
if (errorMessage.includes('Invalid array length')) {
|
|
218
|
+
// Retry with minimal time for extremely fast operations
|
|
219
|
+
// Use same limiting logic but with minimal time for fast ops
|
|
220
|
+
let retryTime: number;
|
|
221
|
+
switch (config.limitBy) {
|
|
222
|
+
case 'all':
|
|
223
|
+
case 'any':
|
|
224
|
+
retryTime = 10;
|
|
225
|
+
break;
|
|
226
|
+
case 'iterations':
|
|
227
|
+
retryTime = 1;
|
|
228
|
+
break;
|
|
229
|
+
case 'time':
|
|
230
|
+
retryTime = 10;
|
|
231
|
+
break;
|
|
232
|
+
default:
|
|
233
|
+
retryTime = 1;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const minimalBench = new Bench({
|
|
237
|
+
iterations: config.iterations,
|
|
238
|
+
time: retryTime,
|
|
239
|
+
warmupIterations: config.warmup,
|
|
240
|
+
warmupTime: 0,
|
|
241
|
+
});
|
|
242
|
+
minimalBench.add(
|
|
243
|
+
taskName,
|
|
244
|
+
taskData.fn,
|
|
245
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
246
|
+
// @ts-ignore - Pending https://github.com/tinylibs/tinybench/pull/364
|
|
247
|
+
signal ? { signal } : undefined,
|
|
248
|
+
);
|
|
249
|
+
await minimalBench.run();
|
|
250
|
+
const minimalResults = minimalBench.results[0];
|
|
251
|
+
|
|
252
|
+
if (!minimalResults || minimalResults.error) {
|
|
253
|
+
// If retry also fails, just accept it failed
|
|
254
|
+
throw new Error(
|
|
255
|
+
`Benchmark operation is too fast to measure reliably`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Return minimal results - apply outlier removal
|
|
260
|
+
const minimalRawSamples2 = minimalResults.latency.samples || [];
|
|
261
|
+
const minimalSamplesInNs2 = minimalRawSamples2.map((s) => s * 1e6);
|
|
262
|
+
const minimalCleanedSamples2 = removeOutliersIQR(minimalSamplesInNs2);
|
|
263
|
+
const minimalStats2 = calculateStatistics(minimalCleanedSamples2);
|
|
264
|
+
|
|
265
|
+
const taskResult: TaskResult = {
|
|
266
|
+
cv: minimalStats2.cv,
|
|
267
|
+
iterations: minimalCleanedSamples2.length,
|
|
268
|
+
marginOfError: minimalStats2.marginOfError,
|
|
269
|
+
max: minimalStats2.max,
|
|
270
|
+
mean: minimalStats2.mean,
|
|
271
|
+
metadata: taskData.metadata ?? {},
|
|
272
|
+
min: minimalStats2.min,
|
|
273
|
+
name: taskName,
|
|
274
|
+
opsPerSecond: minimalResults.throughput.mean || 0,
|
|
275
|
+
p95: minimalStats2.p95,
|
|
276
|
+
p99: minimalStats2.p99,
|
|
277
|
+
stdDev: minimalStats2.stdDev,
|
|
278
|
+
...(taskData.tags ? { tags: taskData.tags } : {}),
|
|
279
|
+
variance: minimalStats2.variance,
|
|
280
|
+
};
|
|
281
|
+
return taskResult;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
throw results.error;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Apply IQR outlier removal to raw samples
|
|
288
|
+
const rawSamples = results.latency.samples || [];
|
|
289
|
+
const samplesInNs = rawSamples.map((s) => s * 1e6); // Convert ms to ns
|
|
290
|
+
const cleanedSamples = removeOutliersIQR(samplesInNs);
|
|
291
|
+
const stats = calculateStatistics(cleanedSamples);
|
|
292
|
+
|
|
293
|
+
const taskResult: TaskResult = {
|
|
294
|
+
cv: stats.cv,
|
|
295
|
+
iterations: cleanedSamples.length,
|
|
296
|
+
marginOfError: stats.marginOfError,
|
|
297
|
+
max: stats.max,
|
|
298
|
+
mean: stats.mean,
|
|
299
|
+
metadata: taskData.metadata ?? {},
|
|
300
|
+
min: stats.min,
|
|
301
|
+
name: taskName,
|
|
302
|
+
opsPerSecond: results.throughput.mean || 0, // Keep tinybench's ops/sec
|
|
303
|
+
p95: stats.p95,
|
|
304
|
+
p99: stats.p99,
|
|
305
|
+
stdDev: stats.stdDev,
|
|
306
|
+
...(taskData.tags ? { tags: taskData.tags } : {}),
|
|
307
|
+
variance: stats.variance,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
return taskResult;
|
|
311
|
+
} catch (error) {
|
|
312
|
+
const executionError =
|
|
313
|
+
error instanceof Error ? error : new Error(String(error));
|
|
314
|
+
|
|
315
|
+
const errorResult: TaskResult = {
|
|
316
|
+
cv: 0,
|
|
317
|
+
error: executionError,
|
|
318
|
+
iterations: 0,
|
|
319
|
+
marginOfError: 0,
|
|
320
|
+
max: 0,
|
|
321
|
+
mean: 0,
|
|
322
|
+
metadata: taskData.metadata ?? {},
|
|
323
|
+
min: 0,
|
|
324
|
+
name: taskName,
|
|
325
|
+
opsPerSecond: 0,
|
|
326
|
+
p95: 0,
|
|
327
|
+
p99: 0,
|
|
328
|
+
stdDev: 0,
|
|
329
|
+
...(taskData.tags ? { tags: taskData.tags } : {}),
|
|
330
|
+
variance: 0,
|
|
331
|
+
};
|
|
332
|
+
return errorResult;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModestBench Error Manager
|
|
3
|
+
*
|
|
4
|
+
* Handles execution errors with context tracking, categorization, and provides
|
|
5
|
+
* structured error information for graceful degradation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
ErrorContext,
|
|
10
|
+
ErrorManager,
|
|
11
|
+
ErrorStats,
|
|
12
|
+
ExecutionError,
|
|
13
|
+
ExecutionPhase,
|
|
14
|
+
} from '../types/index.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Error handler callback type
|
|
18
|
+
*/
|
|
19
|
+
type ErrorHandler = (error: ExecutionError) => void;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Error code mappings for different error types and contexts
|
|
23
|
+
*/
|
|
24
|
+
const ERROR_CODES = {
|
|
25
|
+
// Benchmark file errors
|
|
26
|
+
BENCH_001: 'Benchmark file syntax error',
|
|
27
|
+
BENCH_002: 'Invalid benchmark structure',
|
|
28
|
+
BENCH_003: 'Missing dependency',
|
|
29
|
+
BENCH_004: 'Timeout exceeded',
|
|
30
|
+
BENCH_005: 'Memory limit exceeded',
|
|
31
|
+
|
|
32
|
+
// Configuration errors
|
|
33
|
+
CONFIG_001: 'Invalid configuration file',
|
|
34
|
+
CONFIG_002: 'Missing required option',
|
|
35
|
+
|
|
36
|
+
// Execution errors
|
|
37
|
+
EXEC_001: 'Task execution failed',
|
|
38
|
+
EXEC_002: 'Setup function failed',
|
|
39
|
+
EXEC_003: 'Teardown function failed',
|
|
40
|
+
|
|
41
|
+
EXEC_004: 'Memory leak detected',
|
|
42
|
+
// File system errors
|
|
43
|
+
FILE_001: 'File not found',
|
|
44
|
+
FILE_002: 'Permission denied',
|
|
45
|
+
|
|
46
|
+
FILE_003: 'Invalid file format',
|
|
47
|
+
// History storage errors
|
|
48
|
+
HIST_001: 'History data corruption',
|
|
49
|
+
HIST_002: 'Disk space insufficient',
|
|
50
|
+
HIST_003: 'Index corruption',
|
|
51
|
+
|
|
52
|
+
// System errors
|
|
53
|
+
SYS_001: 'Out of memory',
|
|
54
|
+
SYS_002: 'Process crashed',
|
|
55
|
+
SYS_003: 'System resource unavailable',
|
|
56
|
+
|
|
57
|
+
// Unknown errors
|
|
58
|
+
UNKNOWN: 'Unknown error',
|
|
59
|
+
// Validation errors
|
|
60
|
+
VALID_001: 'Schema validation failed',
|
|
61
|
+
VALID_002: 'Type validation failed',
|
|
62
|
+
|
|
63
|
+
VALID_003: 'Range validation failed',
|
|
64
|
+
} as const;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Recoverable error types that shouldn't stop entire execution
|
|
68
|
+
*/
|
|
69
|
+
const RECOVERABLE_ERRORS = new Set([
|
|
70
|
+
'BENCH_003', // Missing dependency (can skip specific benchmark)
|
|
71
|
+
'EXEC_001', // Task execution failed (can continue with other tasks)
|
|
72
|
+
'FILE_001', // File not found (can continue with other files)
|
|
73
|
+
'VALID_002', // Type validation failed (can skip invalid items)
|
|
74
|
+
'VALID_003', // Range validation failed (can skip invalid items)
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Default error manager implementation
|
|
79
|
+
*/
|
|
80
|
+
export class ModestBenchErrorManager implements ErrorManager {
|
|
81
|
+
private errors: ExecutionError[] = [];
|
|
82
|
+
|
|
83
|
+
private handlers: ErrorHandler[] = [];
|
|
84
|
+
|
|
85
|
+
private readonly maxRecentErrors = 50;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Clear error history
|
|
89
|
+
*/
|
|
90
|
+
clearStats(): void {
|
|
91
|
+
this.errors = [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Format error for display
|
|
96
|
+
*/
|
|
97
|
+
formatError(error: ExecutionError): string {
|
|
98
|
+
const { code, context, message } = error;
|
|
99
|
+
|
|
100
|
+
let formatted = `[${code}] ${message}`;
|
|
101
|
+
|
|
102
|
+
// Add context information
|
|
103
|
+
const contextParts: string[] = [];
|
|
104
|
+
if (context.file) {
|
|
105
|
+
contextParts.push(`file: ${context.file}`);
|
|
106
|
+
}
|
|
107
|
+
if (context.suite) {
|
|
108
|
+
contextParts.push(`suite: ${context.suite}`);
|
|
109
|
+
}
|
|
110
|
+
if (context.task) {
|
|
111
|
+
contextParts.push(`task: ${context.task}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (contextParts.length > 0) {
|
|
115
|
+
formatted += ` (${contextParts.join(', ')})`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
formatted += ` at ${context.timestamp.toISOString()}`;
|
|
119
|
+
|
|
120
|
+
return formatted;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get error code for a given error
|
|
125
|
+
*/
|
|
126
|
+
getErrorCode(error: Error, context: ErrorContext): string {
|
|
127
|
+
// Check for specific error patterns
|
|
128
|
+
const message = error.message.toLowerCase();
|
|
129
|
+
const name = error.name.toLowerCase();
|
|
130
|
+
|
|
131
|
+
// File system errors
|
|
132
|
+
if (message.includes('enoent') || message.includes('no such file')) {
|
|
133
|
+
return 'FILE_001';
|
|
134
|
+
}
|
|
135
|
+
if (message.includes('eacces') || message.includes('permission denied')) {
|
|
136
|
+
return 'FILE_002';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Memory errors
|
|
140
|
+
if (message.includes('out of memory') || name.includes('rangeerror')) {
|
|
141
|
+
return 'SYS_001';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Timeout errors
|
|
145
|
+
if (message.includes('timeout') || name.includes('timeout')) {
|
|
146
|
+
return 'BENCH_004';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Syntax errors in benchmark files
|
|
150
|
+
if (name.includes('syntaxerror') && context.phase === 'loading') {
|
|
151
|
+
return 'BENCH_001';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Validation errors
|
|
155
|
+
if (context.phase === 'validation') {
|
|
156
|
+
if (message.includes('schema') || message.includes('structure')) {
|
|
157
|
+
return 'VALID_001';
|
|
158
|
+
}
|
|
159
|
+
if (message.includes('type')) {
|
|
160
|
+
return 'VALID_002';
|
|
161
|
+
}
|
|
162
|
+
if (message.includes('range') || message.includes('limit')) {
|
|
163
|
+
return 'VALID_003';
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Configuration errors
|
|
168
|
+
if (context.phase === 'discovery' && message.includes('config')) {
|
|
169
|
+
return 'CONFIG_001';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Execution phase errors
|
|
173
|
+
if (context.phase === 'execution') {
|
|
174
|
+
if (context.task) {
|
|
175
|
+
return 'EXEC_001';
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (context.phase === 'setup') {
|
|
180
|
+
return 'EXEC_002';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (context.phase === 'teardown') {
|
|
184
|
+
return 'EXEC_003';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Storage errors
|
|
188
|
+
if (message.includes('disk') && message.includes('space')) {
|
|
189
|
+
return 'HIST_002';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (message.includes('corrupt') || message.includes('invalid json')) {
|
|
193
|
+
return 'HIST_001';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Default to unknown
|
|
197
|
+
return 'UNKNOWN';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get error count by phase
|
|
202
|
+
*/
|
|
203
|
+
getErrorCountByPhase(phase: ExecutionPhase): number {
|
|
204
|
+
return this.errors.filter((error) => error.context.phase === phase).length;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get human-readable description for error code
|
|
209
|
+
*/
|
|
210
|
+
getErrorDescription(code: string): string {
|
|
211
|
+
return ERROR_CODES[code as keyof typeof ERROR_CODES] || 'Unknown error';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get all error handlers (for testing)
|
|
216
|
+
*/
|
|
217
|
+
getHandlers(): readonly ErrorHandler[] {
|
|
218
|
+
return [...this.handlers];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get recent errors for a specific phase
|
|
223
|
+
*/
|
|
224
|
+
getRecentErrorsForPhase(phase: ExecutionPhase, limit = 10): ExecutionError[] {
|
|
225
|
+
return this.errors
|
|
226
|
+
.filter((error) => error.context.phase === phase)
|
|
227
|
+
.slice(-limit);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get error statistics
|
|
232
|
+
*/
|
|
233
|
+
getStats(): ErrorStats {
|
|
234
|
+
const byPhase: Record<ExecutionPhase, number> = {
|
|
235
|
+
cleanup: 0,
|
|
236
|
+
discovery: 0,
|
|
237
|
+
execution: 0,
|
|
238
|
+
loading: 0,
|
|
239
|
+
reporting: 0,
|
|
240
|
+
setup: 0,
|
|
241
|
+
teardown: 0,
|
|
242
|
+
validation: 0,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const byType: Record<string, number> = {};
|
|
246
|
+
let firstError: Date | undefined;
|
|
247
|
+
let lastError: Date | undefined;
|
|
248
|
+
|
|
249
|
+
for (const error of this.errors) {
|
|
250
|
+
// Count by phase
|
|
251
|
+
byPhase[error.context.phase]++;
|
|
252
|
+
|
|
253
|
+
// Count by type (error code)
|
|
254
|
+
const type = error.code;
|
|
255
|
+
byType[type] = (byType[type] || 0) + 1;
|
|
256
|
+
|
|
257
|
+
// Track timestamps
|
|
258
|
+
const timestamp = error.processedAt;
|
|
259
|
+
if (!firstError || timestamp < firstError) {
|
|
260
|
+
firstError = timestamp;
|
|
261
|
+
}
|
|
262
|
+
if (!lastError || timestamp > lastError) {
|
|
263
|
+
lastError = timestamp;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const result: ErrorStats = {
|
|
268
|
+
byPhase,
|
|
269
|
+
byType,
|
|
270
|
+
...(firstError && { firstError }),
|
|
271
|
+
...(lastError && { lastError }),
|
|
272
|
+
recent: this.errors.slice(-this.maxRecentErrors),
|
|
273
|
+
total: this.errors.length,
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Handle an execution error
|
|
281
|
+
*/
|
|
282
|
+
handleError(error: Error, context: ErrorContext): ExecutionError {
|
|
283
|
+
const code = this.getErrorCode(error, context);
|
|
284
|
+
const recoverable = this.isRecoverableByCode(code);
|
|
285
|
+
|
|
286
|
+
const executionError: ExecutionError = {
|
|
287
|
+
code,
|
|
288
|
+
context,
|
|
289
|
+
message: this.createMessage(error, context, code),
|
|
290
|
+
originalError: error,
|
|
291
|
+
processedAt: new Date(),
|
|
292
|
+
recoverable,
|
|
293
|
+
...(error.stack && { stack: error.stack }),
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// Store error for statistics
|
|
297
|
+
this.errors.push(executionError);
|
|
298
|
+
|
|
299
|
+
// Keep only recent errors to prevent memory leaks
|
|
300
|
+
if (this.errors.length > this.maxRecentErrors * 2) {
|
|
301
|
+
this.errors = this.errors.slice(-this.maxRecentErrors);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Notify handlers
|
|
305
|
+
for (const handler of this.handlers) {
|
|
306
|
+
try {
|
|
307
|
+
handler(executionError);
|
|
308
|
+
} catch (handlerError) {
|
|
309
|
+
// Don't let handler errors break error handling
|
|
310
|
+
console.error('Error in error handler:', handlerError);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return executionError;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Check if an error is recoverable
|
|
319
|
+
*/
|
|
320
|
+
isRecoverable(error: ExecutionError): boolean {
|
|
321
|
+
return error.recoverable;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Register error handler callback
|
|
326
|
+
*/
|
|
327
|
+
onError(handler: ErrorHandler): void {
|
|
328
|
+
this.handlers.push(handler);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Remove error handler
|
|
333
|
+
*/
|
|
334
|
+
removeHandler(handler: ErrorHandler): boolean {
|
|
335
|
+
const index = this.handlers.indexOf(handler);
|
|
336
|
+
if (index >= 0) {
|
|
337
|
+
this.handlers.splice(index, 1);
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Create human-readable error message
|
|
345
|
+
*/
|
|
346
|
+
private createMessage(
|
|
347
|
+
error: Error,
|
|
348
|
+
context: ErrorContext,
|
|
349
|
+
code: string,
|
|
350
|
+
): string {
|
|
351
|
+
const baseMessage = this.getErrorDescription(code);
|
|
352
|
+
const originalMessage = error.message;
|
|
353
|
+
|
|
354
|
+
// If the original message is more descriptive, use it
|
|
355
|
+
if (
|
|
356
|
+
originalMessage &&
|
|
357
|
+
originalMessage !== baseMessage &&
|
|
358
|
+
!originalMessage.includes('[object')
|
|
359
|
+
) {
|
|
360
|
+
return `${baseMessage}: ${originalMessage}`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return baseMessage;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Check if error is recoverable by code
|
|
368
|
+
*/
|
|
369
|
+
private isRecoverableByCode(code: string): boolean {
|
|
370
|
+
return RECOVERABLE_ERRORS.has(code);
|
|
371
|
+
}
|
|
372
|
+
}
|