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.
Files changed (275) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/LICENSE.md +55 -0
  3. package/README.md +699 -0
  4. package/dist/bootstrap.cjs +37 -0
  5. package/dist/bootstrap.cjs.map +1 -0
  6. package/dist/bootstrap.d.cts +17 -0
  7. package/dist/bootstrap.d.cts.map +1 -0
  8. package/dist/bootstrap.d.ts +17 -0
  9. package/dist/bootstrap.d.ts.map +1 -0
  10. package/dist/bootstrap.js +33 -0
  11. package/dist/bootstrap.js.map +1 -0
  12. package/dist/cli/commands/history.cjs +459 -0
  13. package/dist/cli/commands/history.cjs.map +1 -0
  14. package/dist/cli/commands/history.d.cts +34 -0
  15. package/dist/cli/commands/history.d.cts.map +1 -0
  16. package/dist/cli/commands/history.d.ts +34 -0
  17. package/dist/cli/commands/history.d.ts.map +1 -0
  18. package/dist/cli/commands/history.js +422 -0
  19. package/dist/cli/commands/history.js.map +1 -0
  20. package/dist/cli/commands/init.cjs +566 -0
  21. package/dist/cli/commands/init.cjs.map +1 -0
  22. package/dist/cli/commands/init.d.cts +26 -0
  23. package/dist/cli/commands/init.d.cts.map +1 -0
  24. package/dist/cli/commands/init.d.ts +26 -0
  25. package/dist/cli/commands/init.d.ts.map +1 -0
  26. package/dist/cli/commands/init.js +562 -0
  27. package/dist/cli/commands/init.js.map +1 -0
  28. package/dist/cli/commands/run.cjs +285 -0
  29. package/dist/cli/commands/run.cjs.map +1 -0
  30. package/dist/cli/commands/run.d.cts +37 -0
  31. package/dist/cli/commands/run.d.cts.map +1 -0
  32. package/dist/cli/commands/run.d.ts +37 -0
  33. package/dist/cli/commands/run.d.ts.map +1 -0
  34. package/dist/cli/commands/run.js +248 -0
  35. package/dist/cli/commands/run.js.map +1 -0
  36. package/dist/cli/index.cjs +523 -0
  37. package/dist/cli/index.cjs.map +1 -0
  38. package/dist/cli/index.d.cts +58 -0
  39. package/dist/cli/index.d.cts.map +1 -0
  40. package/dist/cli/index.d.ts +58 -0
  41. package/dist/cli/index.d.ts.map +1 -0
  42. package/dist/cli/index.js +515 -0
  43. package/dist/cli/index.js.map +1 -0
  44. package/dist/config/manager.cjs +370 -0
  45. package/dist/config/manager.cjs.map +1 -0
  46. package/dist/config/manager.d.cts +46 -0
  47. package/dist/config/manager.d.cts.map +1 -0
  48. package/dist/config/manager.d.ts +46 -0
  49. package/dist/config/manager.d.ts.map +1 -0
  50. package/dist/config/manager.js +333 -0
  51. package/dist/config/manager.js.map +1 -0
  52. package/dist/config/schema.cjs +182 -0
  53. package/dist/config/schema.cjs.map +1 -0
  54. package/dist/config/schema.d.cts +51 -0
  55. package/dist/config/schema.d.cts.map +1 -0
  56. package/dist/config/schema.d.ts +51 -0
  57. package/dist/config/schema.d.ts.map +1 -0
  58. package/dist/config/schema.js +145 -0
  59. package/dist/config/schema.js.map +1 -0
  60. package/dist/constants.cjs +22 -0
  61. package/dist/constants.cjs.map +1 -0
  62. package/dist/constants.d.cts +10 -0
  63. package/dist/constants.d.cts.map +1 -0
  64. package/dist/constants.d.ts +10 -0
  65. package/dist/constants.d.ts.map +1 -0
  66. package/dist/constants.js +19 -0
  67. package/dist/constants.js.map +1 -0
  68. package/dist/core/benchmark-schema.cjs +135 -0
  69. package/dist/core/benchmark-schema.cjs.map +1 -0
  70. package/dist/core/benchmark-schema.d.cts +139 -0
  71. package/dist/core/benchmark-schema.d.cts.map +1 -0
  72. package/dist/core/benchmark-schema.d.ts +139 -0
  73. package/dist/core/benchmark-schema.d.ts.map +1 -0
  74. package/dist/core/benchmark-schema.js +132 -0
  75. package/dist/core/benchmark-schema.js.map +1 -0
  76. package/dist/core/engine.cjs +669 -0
  77. package/dist/core/engine.cjs.map +1 -0
  78. package/dist/core/engine.d.cts +128 -0
  79. package/dist/core/engine.d.cts.map +1 -0
  80. package/dist/core/engine.d.ts +128 -0
  81. package/dist/core/engine.d.ts.map +1 -0
  82. package/dist/core/engine.js +632 -0
  83. package/dist/core/engine.js.map +1 -0
  84. package/dist/core/engines/accurate-engine.cjs +292 -0
  85. package/dist/core/engines/accurate-engine.cjs.map +1 -0
  86. package/dist/core/engines/accurate-engine.d.cts +63 -0
  87. package/dist/core/engines/accurate-engine.d.cts.map +1 -0
  88. package/dist/core/engines/accurate-engine.d.ts +63 -0
  89. package/dist/core/engines/accurate-engine.d.ts.map +1 -0
  90. package/dist/core/engines/accurate-engine.js +288 -0
  91. package/dist/core/engines/accurate-engine.js.map +1 -0
  92. package/dist/core/engines/index.cjs +21 -0
  93. package/dist/core/engines/index.cjs.map +1 -0
  94. package/dist/core/engines/index.d.cts +16 -0
  95. package/dist/core/engines/index.d.cts.map +1 -0
  96. package/dist/core/engines/index.d.ts +16 -0
  97. package/dist/core/engines/index.d.ts.map +1 -0
  98. package/dist/core/engines/index.js +16 -0
  99. package/dist/core/engines/index.js.map +1 -0
  100. package/dist/core/engines/tinybench-engine.cjs +286 -0
  101. package/dist/core/engines/tinybench-engine.cjs.map +1 -0
  102. package/dist/core/engines/tinybench-engine.d.cts +18 -0
  103. package/dist/core/engines/tinybench-engine.d.cts.map +1 -0
  104. package/dist/core/engines/tinybench-engine.d.ts +18 -0
  105. package/dist/core/engines/tinybench-engine.d.ts.map +1 -0
  106. package/dist/core/engines/tinybench-engine.js +282 -0
  107. package/dist/core/engines/tinybench-engine.js.map +1 -0
  108. package/dist/core/error-manager.cjs +303 -0
  109. package/dist/core/error-manager.cjs.map +1 -0
  110. package/dist/core/error-manager.d.cts +77 -0
  111. package/dist/core/error-manager.d.cts.map +1 -0
  112. package/dist/core/error-manager.d.ts +77 -0
  113. package/dist/core/error-manager.d.ts.map +1 -0
  114. package/dist/core/error-manager.js +299 -0
  115. package/dist/core/error-manager.js.map +1 -0
  116. package/dist/core/loader.cjs +287 -0
  117. package/dist/core/loader.cjs.map +1 -0
  118. package/dist/core/loader.d.cts +55 -0
  119. package/dist/core/loader.d.cts.map +1 -0
  120. package/dist/core/loader.d.ts +55 -0
  121. package/dist/core/loader.d.ts.map +1 -0
  122. package/dist/core/loader.js +250 -0
  123. package/dist/core/loader.js.map +1 -0
  124. package/dist/core/stats-utils.cjs +99 -0
  125. package/dist/core/stats-utils.cjs.map +1 -0
  126. package/dist/core/stats-utils.d.cts +50 -0
  127. package/dist/core/stats-utils.d.cts.map +1 -0
  128. package/dist/core/stats-utils.d.ts +50 -0
  129. package/dist/core/stats-utils.d.ts.map +1 -0
  130. package/dist/core/stats-utils.js +94 -0
  131. package/dist/core/stats-utils.js.map +1 -0
  132. package/dist/index.cjs +64 -0
  133. package/dist/index.cjs.map +1 -0
  134. package/dist/index.d.cts +22 -0
  135. package/dist/index.d.cts.map +1 -0
  136. package/dist/index.d.ts +22 -0
  137. package/dist/index.d.ts.map +1 -0
  138. package/dist/index.js +30 -0
  139. package/dist/index.js.map +1 -0
  140. package/dist/progress/manager.cjs +325 -0
  141. package/dist/progress/manager.cjs.map +1 -0
  142. package/dist/progress/manager.d.cts +125 -0
  143. package/dist/progress/manager.d.cts.map +1 -0
  144. package/dist/progress/manager.d.ts +125 -0
  145. package/dist/progress/manager.d.ts.map +1 -0
  146. package/dist/progress/manager.js +321 -0
  147. package/dist/progress/manager.js.map +1 -0
  148. package/dist/reporters/csv.cjs +250 -0
  149. package/dist/reporters/csv.cjs.map +1 -0
  150. package/dist/reporters/csv.d.cts +92 -0
  151. package/dist/reporters/csv.d.cts.map +1 -0
  152. package/dist/reporters/csv.d.ts +92 -0
  153. package/dist/reporters/csv.d.ts.map +1 -0
  154. package/dist/reporters/csv.js +246 -0
  155. package/dist/reporters/csv.js.map +1 -0
  156. package/dist/reporters/human.cjs +516 -0
  157. package/dist/reporters/human.cjs.map +1 -0
  158. package/dist/reporters/human.d.cts +86 -0
  159. package/dist/reporters/human.d.cts.map +1 -0
  160. package/dist/reporters/human.d.ts +86 -0
  161. package/dist/reporters/human.d.ts.map +1 -0
  162. package/dist/reporters/human.js +509 -0
  163. package/dist/reporters/human.js.map +1 -0
  164. package/dist/reporters/index.cjs +17 -0
  165. package/dist/reporters/index.cjs.map +1 -0
  166. package/dist/reporters/index.d.cts +10 -0
  167. package/dist/reporters/index.d.cts.map +1 -0
  168. package/dist/reporters/index.d.ts +10 -0
  169. package/dist/reporters/index.d.ts.map +1 -0
  170. package/dist/reporters/index.js +10 -0
  171. package/dist/reporters/index.js.map +1 -0
  172. package/dist/reporters/json.cjs +215 -0
  173. package/dist/reporters/json.cjs.map +1 -0
  174. package/dist/reporters/json.d.cts +79 -0
  175. package/dist/reporters/json.d.cts.map +1 -0
  176. package/dist/reporters/json.d.ts +79 -0
  177. package/dist/reporters/json.d.ts.map +1 -0
  178. package/dist/reporters/json.js +211 -0
  179. package/dist/reporters/json.js.map +1 -0
  180. package/dist/reporters/registry.cjs +255 -0
  181. package/dist/reporters/registry.cjs.map +1 -0
  182. package/dist/reporters/registry.d.cts +155 -0
  183. package/dist/reporters/registry.d.cts.map +1 -0
  184. package/dist/reporters/registry.d.ts +155 -0
  185. package/dist/reporters/registry.d.ts.map +1 -0
  186. package/dist/reporters/registry.js +249 -0
  187. package/dist/reporters/registry.js.map +1 -0
  188. package/dist/reporters/simple.cjs +328 -0
  189. package/dist/reporters/simple.cjs.map +1 -0
  190. package/dist/reporters/simple.d.cts +51 -0
  191. package/dist/reporters/simple.d.cts.map +1 -0
  192. package/dist/reporters/simple.d.ts +51 -0
  193. package/dist/reporters/simple.d.ts.map +1 -0
  194. package/dist/reporters/simple.js +321 -0
  195. package/dist/reporters/simple.js.map +1 -0
  196. package/dist/schema/modestbench-config.schema.json +162 -0
  197. package/dist/storage/history.cjs +456 -0
  198. package/dist/storage/history.cjs.map +1 -0
  199. package/dist/storage/history.d.cts +99 -0
  200. package/dist/storage/history.d.cts.map +1 -0
  201. package/dist/storage/history.d.ts +99 -0
  202. package/dist/storage/history.d.ts.map +1 -0
  203. package/dist/storage/history.js +452 -0
  204. package/dist/storage/history.js.map +1 -0
  205. package/dist/types/cli.cjs +21 -0
  206. package/dist/types/cli.cjs.map +1 -0
  207. package/dist/types/cli.d.cts +296 -0
  208. package/dist/types/cli.d.cts.map +1 -0
  209. package/dist/types/cli.d.ts +296 -0
  210. package/dist/types/cli.d.ts.map +1 -0
  211. package/dist/types/cli.js +18 -0
  212. package/dist/types/cli.js.map +1 -0
  213. package/dist/types/core.cjs +14 -0
  214. package/dist/types/core.cjs.map +1 -0
  215. package/dist/types/core.d.cts +380 -0
  216. package/dist/types/core.d.cts.map +1 -0
  217. package/dist/types/core.d.ts +380 -0
  218. package/dist/types/core.d.ts.map +1 -0
  219. package/dist/types/core.js +13 -0
  220. package/dist/types/core.js.map +1 -0
  221. package/dist/types/index.cjs +27 -0
  222. package/dist/types/index.cjs.map +1 -0
  223. package/dist/types/index.d.cts +11 -0
  224. package/dist/types/index.d.cts.map +1 -0
  225. package/dist/types/index.d.ts +11 -0
  226. package/dist/types/index.d.ts.map +1 -0
  227. package/dist/types/index.js +11 -0
  228. package/dist/types/index.js.map +1 -0
  229. package/dist/types/interfaces.cjs +10 -0
  230. package/dist/types/interfaces.cjs.map +1 -0
  231. package/dist/types/interfaces.d.cts +381 -0
  232. package/dist/types/interfaces.d.cts.map +1 -0
  233. package/dist/types/interfaces.d.ts +381 -0
  234. package/dist/types/interfaces.d.ts.map +1 -0
  235. package/dist/types/interfaces.js +9 -0
  236. package/dist/types/interfaces.js.map +1 -0
  237. package/dist/types/utility.cjs +92 -0
  238. package/dist/types/utility.cjs.map +1 -0
  239. package/dist/types/utility.d.cts +330 -0
  240. package/dist/types/utility.d.cts.map +1 -0
  241. package/dist/types/utility.d.ts +330 -0
  242. package/dist/types/utility.d.ts.map +1 -0
  243. package/dist/types/utility.js +78 -0
  244. package/dist/types/utility.js.map +1 -0
  245. package/package.json +211 -0
  246. package/src/bootstrap.ts +35 -0
  247. package/src/cli/commands/history.ts +569 -0
  248. package/src/cli/commands/init.ts +658 -0
  249. package/src/cli/commands/run.ts +346 -0
  250. package/src/cli/index.ts +642 -0
  251. package/src/config/manager.ts +387 -0
  252. package/src/config/schema.ts +188 -0
  253. package/src/constants.ts +21 -0
  254. package/src/core/benchmark-schema.ts +185 -0
  255. package/src/core/engine.ts +888 -0
  256. package/src/core/engines/accurate-engine.ts +408 -0
  257. package/src/core/engines/index.ts +16 -0
  258. package/src/core/engines/tinybench-engine.ts +335 -0
  259. package/src/core/error-manager.ts +372 -0
  260. package/src/core/loader.ts +324 -0
  261. package/src/core/stats-utils.ts +135 -0
  262. package/src/index.ts +46 -0
  263. package/src/progress/manager.ts +415 -0
  264. package/src/reporters/csv.ts +368 -0
  265. package/src/reporters/human.ts +707 -0
  266. package/src/reporters/index.ts +10 -0
  267. package/src/reporters/json.ts +302 -0
  268. package/src/reporters/registry.ts +349 -0
  269. package/src/reporters/simple.ts +459 -0
  270. package/src/storage/history.ts +600 -0
  271. package/src/types/cli.ts +312 -0
  272. package/src/types/core.ts +414 -0
  273. package/src/types/index.ts +18 -0
  274. package/src/types/interfaces.ts +451 -0
  275. 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
+ }