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,10 @@
1
+ /**
2
+ * ModestBench Reporters
3
+ *
4
+ * Export all available reporters and registry functionality.
5
+ */
6
+
7
+ export { CsvReporter } from './csv.js';
8
+ export { HumanReporter } from './human.js';
9
+ export { JsonReporter } from './json.js';
10
+ export { SimpleReporter } from './simple.js';
@@ -0,0 +1,302 @@
1
+ /**
2
+ * ModestBench JSON Reporter
3
+ *
4
+ * Outputs benchmark results in structured JSON format. Suitable for machine
5
+ * processing, CI/CD integration, and data analysis.
6
+ */
7
+
8
+ import { mkdirSync, writeFileSync } from 'node:fs';
9
+ import { dirname } from 'node:path';
10
+
11
+ import type {
12
+ BenchmarkRun,
13
+ FileResult,
14
+ ProgressState,
15
+ SuiteResult,
16
+ TaskResult,
17
+ } from '../types/index.js';
18
+
19
+ import { BaseReporter } from './registry.js';
20
+
21
+ /**
22
+ * JSON output structure for benchmark results
23
+ */
24
+ interface JsonOutput {
25
+ /** ModestBench metadata */
26
+ readonly meta: {
27
+ readonly format: 'modestbench-json';
28
+ readonly timestamp: string;
29
+ readonly version: string;
30
+ };
31
+ /** Complete benchmark run data */
32
+ readonly run: BenchmarkRun;
33
+ /** Additional computed statistics */
34
+ statistics?: {
35
+ averageOpsPerSecond?: number;
36
+ fastestTask?: TaskResult;
37
+ slowestTask?: TaskResult;
38
+ totalIterations?: number;
39
+ };
40
+ }
41
+
42
+ /**
43
+ * JSON reporter for structured output
44
+ */
45
+ export class JsonReporter extends BaseReporter {
46
+ private currentRun?: BenchmarkRun;
47
+
48
+ private readonly includeMetadata: boolean;
49
+
50
+ private readonly includeStatistics: boolean;
51
+
52
+ private readonly outputPath?: string | undefined;
53
+
54
+ private readonly prettyPrint: boolean;
55
+
56
+ private readonly quiet: boolean;
57
+
58
+ private statistics: {
59
+ fastestTask?: TaskResult;
60
+ slowestTask?: TaskResult;
61
+ taskCount: number;
62
+ totalIterations: number;
63
+ totalOpsPerSecond: number;
64
+ } = {
65
+ taskCount: 0,
66
+ totalIterations: 0,
67
+ totalOpsPerSecond: 0,
68
+ };
69
+
70
+ constructor(
71
+ options: {
72
+ includeMetadata?: boolean;
73
+ includeStatistics?: boolean;
74
+ outputPath?: string;
75
+ prettyPrint?: boolean;
76
+ quiet?: boolean;
77
+ verbose?: boolean;
78
+ } = {},
79
+ ) {
80
+ super('json', options);
81
+
82
+ this.outputPath = options.outputPath;
83
+ this.prettyPrint = options.prettyPrint ?? true;
84
+ this.includeStatistics = options.includeStatistics ?? true;
85
+ this.includeMetadata = options.includeMetadata ?? true;
86
+ this.quiet = options.quiet ?? false;
87
+ }
88
+
89
+ /**
90
+ * Check if statistics are included
91
+ */
92
+ areStatisticsIncluded(): boolean {
93
+ return this.includeStatistics;
94
+ }
95
+
96
+ /**
97
+ * Get the output path (if configured)
98
+ */
99
+ getOutputPath(): string | undefined {
100
+ return this.outputPath;
101
+ }
102
+
103
+ /**
104
+ * Check if metadata is included
105
+ */
106
+ isMetadataIncluded(): boolean {
107
+ return this.includeMetadata;
108
+ }
109
+
110
+ /**
111
+ * Check if pretty printing is enabled
112
+ */
113
+ isPrettyPrintEnabled(): boolean {
114
+ return this.prettyPrint;
115
+ }
116
+
117
+ async onEnd(run: BenchmarkRun): Promise<void> {
118
+ const output = this.buildJsonOutput(run);
119
+
120
+ if (this.outputPath) {
121
+ await this.writeToFile(output);
122
+ } else {
123
+ this.writeToStdout(output);
124
+ }
125
+ }
126
+
127
+ onError(error: Error): void {
128
+ // For JSON reporter, we'll include errors in the final output
129
+ // but we can also log to stderr for immediate feedback
130
+ console.error('JSON Reporter Error:', error.message);
131
+ }
132
+
133
+ onFileEnd(_result: FileResult): void {
134
+ // No-op for JSON reporter
135
+ }
136
+
137
+ onFileStart(_file: string): void {
138
+ // No-op for JSON reporter
139
+ }
140
+
141
+ onProgress(_state: ProgressState): void {
142
+ // No-op for JSON reporter - we don't output progress in JSON format
143
+ }
144
+
145
+ onStart(run: BenchmarkRun): void {
146
+ this.currentRun = run;
147
+ this.resetStatistics();
148
+ }
149
+
150
+ onSuiteEnd(_result: SuiteResult): void {
151
+ // No-op for JSON reporter
152
+ }
153
+
154
+ onSuiteStart(_suite: string): void {
155
+ // No-op for JSON reporter
156
+ }
157
+
158
+ onTaskResult(result: TaskResult): void {
159
+ if (!result.error) {
160
+ this.updateStatistics(result);
161
+ }
162
+ }
163
+
164
+ onTaskStart(_task: string): void {
165
+ // No-op for JSON reporter
166
+ }
167
+
168
+ /**
169
+ * Build the complete JSON output structure
170
+ */
171
+ private buildJsonOutput(run: BenchmarkRun): JsonOutput {
172
+ const output: JsonOutput = {
173
+ meta: {
174
+ format: 'modestbench-json',
175
+ timestamp: new Date().toISOString(),
176
+ version: '0.1.0', // TODO: Get from package.json
177
+ },
178
+ run: this.includeMetadata ? run : this.sanitizeRun(run),
179
+ };
180
+
181
+ if (this.includeStatistics) {
182
+ const stats = {
183
+ averageOpsPerSecond:
184
+ this.statistics.taskCount > 0
185
+ ? this.statistics.totalOpsPerSecond / this.statistics.taskCount
186
+ : 0,
187
+ totalIterations: this.statistics.totalIterations,
188
+ ...(this.statistics.fastestTask && {
189
+ fastestTask: this.statistics.fastestTask,
190
+ }),
191
+ ...(this.statistics.slowestTask && {
192
+ slowestTask: this.statistics.slowestTask,
193
+ }),
194
+ };
195
+
196
+ output.statistics = stats;
197
+ }
198
+
199
+ return output;
200
+ }
201
+
202
+ /**
203
+ * Reset statistics tracking
204
+ */
205
+ private resetStatistics(): void {
206
+ this.statistics = {
207
+ taskCount: 0,
208
+ totalIterations: 0,
209
+ totalOpsPerSecond: 0,
210
+ };
211
+ }
212
+
213
+ /**
214
+ * Remove potentially sensitive metadata from run data
215
+ */
216
+ private sanitizeRun(run: BenchmarkRun): BenchmarkRun {
217
+ let sanitized = {
218
+ ...run,
219
+ environment: {
220
+ ...run.environment,
221
+ env: {}, // Remove environment variables
222
+ hostname: 'redacted', // Remove hostname
223
+ },
224
+ } as BenchmarkRun;
225
+
226
+ if (run.git) {
227
+ sanitized = {
228
+ ...sanitized,
229
+ git: {
230
+ ...run.git,
231
+ author: 'redacted', // Remove author info
232
+ },
233
+ };
234
+ }
235
+
236
+ return sanitized;
237
+ }
238
+
239
+ /**
240
+ * Update running statistics with a task result
241
+ */
242
+ private updateStatistics(result: TaskResult): void {
243
+ this.statistics.totalIterations += result.iterations;
244
+ this.statistics.totalOpsPerSecond += result.opsPerSecond;
245
+ this.statistics.taskCount++;
246
+
247
+ // Track fastest task
248
+ if (
249
+ !this.statistics.fastestTask ||
250
+ result.mean < this.statistics.fastestTask.mean
251
+ ) {
252
+ this.statistics.fastestTask = result;
253
+ }
254
+
255
+ // Track slowest task
256
+ if (
257
+ !this.statistics.slowestTask ||
258
+ result.mean > this.statistics.slowestTask.mean
259
+ ) {
260
+ this.statistics.slowestTask = result;
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Write JSON output to file
266
+ */
267
+ private async writeToFile(output: JsonOutput): Promise<void> {
268
+ if (!this.outputPath) {
269
+ throw new Error('Output path not specified');
270
+ }
271
+
272
+ try {
273
+ // Ensure directory exists
274
+ const dir = dirname(this.outputPath);
275
+ mkdirSync(dir, { recursive: true });
276
+
277
+ // Write JSON file
278
+ const jsonString = this.prettyPrint
279
+ ? JSON.stringify(output, null, 2)
280
+ : JSON.stringify(output);
281
+
282
+ writeFileSync(this.outputPath, jsonString, 'utf8');
283
+ } catch (error) {
284
+ throw new Error(
285
+ `Failed to write JSON output to ${this.outputPath}: ${error instanceof Error ? error.message : String(error)}`,
286
+ );
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Write JSON output to stdout
292
+ */
293
+ private writeToStdout(output: JsonOutput): void {
294
+ // Always write to stdout when no output path is specified
295
+ // The quiet flag only affects progress messages (stderr), not data output
296
+ const jsonString = this.prettyPrint
297
+ ? JSON.stringify(output, null, 2)
298
+ : JSON.stringify(output);
299
+
300
+ console.log(jsonString);
301
+ }
302
+ }
@@ -0,0 +1,349 @@
1
+ /**
2
+ * ModestBench Reporter Registry
3
+ *
4
+ * Plugin-based system for managing benchmark output formatters. Supports
5
+ * registration, retrieval, and lifecycle management of reporters.
6
+ */
7
+
8
+ import type {
9
+ BenchmarkRun,
10
+ FileResult,
11
+ ProgressState,
12
+ Reporter,
13
+ ReporterRegistry,
14
+ SuiteResult,
15
+ TaskResult,
16
+ } from '../types/index.js';
17
+
18
+ /**
19
+ * Base abstract reporter class providing common functionality
20
+ */
21
+ export abstract class BaseReporter implements Reporter {
22
+ protected readonly name: string;
23
+
24
+ protected readonly options: Record<string, unknown>;
25
+
26
+ constructor(name: string, options: Record<string, unknown> = {}) {
27
+ this.name = name;
28
+ this.options = options;
29
+ }
30
+
31
+ /**
32
+ * Get reporter name
33
+ */
34
+ getName(): string {
35
+ return this.name;
36
+ }
37
+
38
+ /**
39
+ * Get reporter options
40
+ */
41
+ getOptions(): Record<string, unknown> {
42
+ return { ...this.options };
43
+ }
44
+
45
+ /**
46
+ * Called when benchmark run completes
47
+ */
48
+ abstract onEnd(run: BenchmarkRun): Promise<void> | void;
49
+
50
+ /**
51
+ * Called when an error occurs
52
+ */
53
+ abstract onError(error: Error): Promise<void> | void;
54
+
55
+ /**
56
+ * Called when a file completes
57
+ */
58
+ abstract onFileEnd(result: FileResult): Promise<void> | void;
59
+
60
+ /**
61
+ * Called when a file starts execution
62
+ */
63
+ abstract onFileStart(file: string): Promise<void> | void;
64
+
65
+ /**
66
+ * Called for progress updates
67
+ */
68
+ abstract onProgress(state: ProgressState): Promise<void> | void;
69
+
70
+ /**
71
+ * Called when benchmark run starts
72
+ */
73
+ abstract onStart(run: BenchmarkRun): Promise<void> | void;
74
+
75
+ /**
76
+ * Called when a suite completes
77
+ */
78
+ abstract onSuiteEnd(result: SuiteResult): Promise<void> | void;
79
+
80
+ /**
81
+ * Called when a suite starts execution
82
+ */
83
+ abstract onSuiteStart(suite: string): Promise<void> | void;
84
+
85
+ /**
86
+ * Called when a task completes
87
+ */
88
+ abstract onTaskResult(result: TaskResult): Promise<void> | void;
89
+
90
+ /**
91
+ * Called when a task starts execution
92
+ */
93
+ abstract onTaskStart(task: string): Promise<void> | void;
94
+
95
+ /**
96
+ * Utility method to format duration in human-readable format
97
+ */
98
+ protected formatDuration(nanoseconds: number): string {
99
+ if (nanoseconds < 1000) {
100
+ return `${nanoseconds.toFixed(2)}ns`;
101
+ } else if (nanoseconds < 1000000) {
102
+ return `${(nanoseconds / 1000).toFixed(2)}μs`;
103
+ } else if (nanoseconds < 1000000000) {
104
+ return `${(nanoseconds / 1000000).toFixed(2)}ms`;
105
+ } else {
106
+ return `${(nanoseconds / 1000000000).toFixed(2)}s`;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Utility method to format operations per second
112
+ */
113
+ protected formatOpsPerSecond(opsPerSecond: number): string {
114
+ if (opsPerSecond < 1000) {
115
+ return `${opsPerSecond.toFixed(2)} ops/sec`;
116
+ } else if (opsPerSecond < 1000000) {
117
+ return `${(opsPerSecond / 1000).toFixed(2)}K ops/sec`;
118
+ } else if (opsPerSecond < 1000000000) {
119
+ return `${(opsPerSecond / 1000000).toFixed(2)}M ops/sec`;
120
+ } else {
121
+ return `${(opsPerSecond / 1000000000).toFixed(2)}B ops/sec`;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Utility method to format percentage
127
+ */
128
+ protected formatPercentage(value: number): string {
129
+ return `${value.toFixed(2)}%`;
130
+ }
131
+
132
+ /**
133
+ * Utility method to safely handle async operations
134
+ */
135
+ protected async safeAsync<T>(operation: () => Promise<T>): Promise<null | T> {
136
+ try {
137
+ return await operation();
138
+ } catch (error) {
139
+ await this.onError(
140
+ error instanceof Error ? error : new Error(String(error)),
141
+ );
142
+ return null;
143
+ }
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Composite reporter that broadcasts events to multiple reporters
149
+ */
150
+ export class CompositeReporter extends BaseReporter {
151
+ private readonly reporters: Reporter[];
152
+
153
+ constructor(reporters: Reporter[]) {
154
+ super('composite', {});
155
+ this.reporters = [...reporters];
156
+ }
157
+
158
+ /**
159
+ * Add a reporter to the composite
160
+ */
161
+ addReporter(reporter: Reporter): void {
162
+ this.reporters.push(reporter);
163
+ }
164
+
165
+ /**
166
+ * Get all reporters in the composite
167
+ */
168
+ getReporters(): Reporter[] {
169
+ return [...this.reporters];
170
+ }
171
+
172
+ async onEnd(run: BenchmarkRun): Promise<void> {
173
+ await this.broadcastAsync('onEnd', run);
174
+ }
175
+
176
+ async onError(error: Error): Promise<void> {
177
+ await this.broadcastAsync('onError', error);
178
+ }
179
+
180
+ async onFileEnd(result: FileResult): Promise<void> {
181
+ await this.broadcastAsync('onFileEnd', result);
182
+ }
183
+
184
+ async onFileStart(file: string): Promise<void> {
185
+ await this.broadcastAsync('onFileStart', file);
186
+ }
187
+
188
+ async onProgress(state: ProgressState): Promise<void> {
189
+ await this.broadcastAsync('onProgress', state);
190
+ }
191
+
192
+ async onStart(run: BenchmarkRun): Promise<void> {
193
+ await this.broadcastAsync('onStart', run);
194
+ }
195
+
196
+ async onSuiteEnd(result: SuiteResult): Promise<void> {
197
+ await this.broadcastAsync('onSuiteEnd', result);
198
+ }
199
+
200
+ async onSuiteStart(suite: string): Promise<void> {
201
+ await this.broadcastAsync('onSuiteStart', suite);
202
+ }
203
+
204
+ async onTaskResult(result: TaskResult): Promise<void> {
205
+ await this.broadcastAsync('onTaskResult', result);
206
+ }
207
+
208
+ async onTaskStart(task: string): Promise<void> {
209
+ await this.broadcastAsync('onTaskStart', task);
210
+ }
211
+
212
+ /**
213
+ * Remove a reporter from the composite
214
+ */
215
+ removeReporter(reporter: Reporter): boolean {
216
+ const index = this.reporters.indexOf(reporter);
217
+ if (index >= 0) {
218
+ this.reporters.splice(index, 1);
219
+ return true;
220
+ }
221
+ return false;
222
+ }
223
+
224
+ /**
225
+ * Broadcast an event to all reporters with error handling
226
+ */
227
+ private async broadcastAsync(
228
+ method: keyof Reporter,
229
+
230
+ ...args: any[]
231
+ ): Promise<void> {
232
+ const promises = this.reporters.map(async (reporter) => {
233
+ try {
234
+ const reporterMethod = reporter[method];
235
+ if (typeof reporterMethod === 'function') {
236
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
237
+ const result = (reporterMethod as any)(...args);
238
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
239
+ if (result && typeof result.then === 'function') {
240
+ await result;
241
+ }
242
+ }
243
+ } catch (error) {
244
+ // Handle reporter-specific errors without affecting others
245
+ console.error(
246
+ `Reporter error in ${reporter.constructor.name}.${method}:`,
247
+ error,
248
+ );
249
+ }
250
+ });
251
+
252
+ await Promise.all(promises);
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Reporter registry implementation for managing multiple reporters
258
+ */
259
+ export class ModestBenchReporterRegistry implements ReporterRegistry {
260
+ private readonly reporters: Map<string, Reporter> = new Map();
261
+
262
+ /**
263
+ * Clear all registered reporters
264
+ */
265
+ clear(): void {
266
+ this.reporters.clear();
267
+ }
268
+
269
+ /**
270
+ * Get a reporter by name
271
+ */
272
+ get(name: string): Reporter | undefined {
273
+ return this.reporters.get(name);
274
+ }
275
+
276
+ /**
277
+ * Get all registered reporters
278
+ */
279
+ getAll(): Record<string, Reporter> {
280
+ const result: Record<string, Reporter> = {};
281
+ this.reporters.forEach((reporter, name) => {
282
+ result[name] = reporter;
283
+ });
284
+ return result;
285
+ }
286
+
287
+ /**
288
+ * Get multiple reporters by names
289
+ */
290
+ getByNames(names: string[]): Reporter[] {
291
+ const result: Reporter[] = [];
292
+ const missing: string[] = [];
293
+
294
+ for (const name of names) {
295
+ const reporter = this.reporters.get(name);
296
+ if (reporter) {
297
+ result.push(reporter);
298
+ } else {
299
+ missing.push(name);
300
+ }
301
+ }
302
+
303
+ if (missing.length > 0) {
304
+ throw new Error(
305
+ `Unknown reporters: ${missing.join(', ')}. Available: ${Array.from(this.reporters.keys()).join(', ')}`,
306
+ );
307
+ }
308
+
309
+ return result;
310
+ }
311
+
312
+ /**
313
+ * Get list of registered reporter names
314
+ */
315
+ getNames(): string[] {
316
+ return Array.from(this.reporters.keys());
317
+ }
318
+
319
+ /**
320
+ * Check if a reporter is registered
321
+ */
322
+ has(name: string): boolean {
323
+ return this.reporters.has(name);
324
+ }
325
+
326
+ /**
327
+ * Register a reporter with a unique name
328
+ */
329
+ register(name: string, reporter: Reporter): void {
330
+ if (this.reporters.has(name)) {
331
+ throw new Error(`Reporter with name "${name}" is already registered`);
332
+ }
333
+ this.reporters.set(name, reporter);
334
+ }
335
+
336
+ /**
337
+ * Get count of registered reporters
338
+ */
339
+ size(): number {
340
+ return this.reporters.size;
341
+ }
342
+
343
+ /**
344
+ * Unregister a reporter
345
+ */
346
+ unregister(name: string): boolean {
347
+ return this.reporters.delete(name);
348
+ }
349
+ }