modestbench 0.9.1 → 0.9.2

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 (151) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/cli/builder.cjs +259 -0
  3. package/dist/cli/builder.cjs.map +1 -0
  4. package/dist/cli/builder.d.cts +37 -0
  5. package/dist/cli/builder.d.cts.map +1 -0
  6. package/dist/cli/builder.d.ts +37 -0
  7. package/dist/cli/builder.d.ts.map +1 -0
  8. package/dist/cli/builder.js +255 -0
  9. package/dist/cli/builder.js.map +1 -0
  10. package/dist/cli/commands/baseline.cjs +4 -33
  11. package/dist/cli/commands/baseline.cjs.map +1 -1
  12. package/dist/cli/commands/baseline.d.cts.map +1 -1
  13. package/dist/cli/commands/baseline.d.ts.map +1 -1
  14. package/dist/cli/commands/baseline.js +2 -31
  15. package/dist/cli/commands/baseline.js.map +1 -1
  16. package/dist/cli/commands/history.cjs +2 -14
  17. package/dist/cli/commands/history.cjs.map +1 -1
  18. package/dist/cli/commands/history.d.cts.map +1 -1
  19. package/dist/cli/commands/history.d.ts.map +1 -1
  20. package/dist/cli/commands/history.js +1 -13
  21. package/dist/cli/commands/history.js.map +1 -1
  22. package/dist/cli/context.cjs +60 -0
  23. package/dist/cli/context.cjs.map +1 -0
  24. package/dist/cli/context.d.cts +28 -0
  25. package/dist/cli/context.d.cts.map +1 -0
  26. package/dist/cli/context.d.ts +28 -0
  27. package/dist/cli/context.d.ts.map +1 -0
  28. package/dist/cli/context.js +56 -0
  29. package/dist/cli/context.js.map +1 -0
  30. package/dist/cli/handlers.cjs +74 -0
  31. package/dist/cli/handlers.cjs.map +1 -0
  32. package/dist/cli/handlers.d.cts +13 -0
  33. package/dist/cli/handlers.d.cts.map +1 -0
  34. package/dist/cli/handlers.d.ts +13 -0
  35. package/dist/cli/handlers.d.ts.map +1 -0
  36. package/dist/cli/handlers.js +70 -0
  37. package/dist/cli/handlers.js.map +1 -0
  38. package/dist/cli/index.cjs +12 -724
  39. package/dist/cli/index.cjs.map +1 -1
  40. package/dist/cli/index.d.cts +4 -39
  41. package/dist/cli/index.d.cts.map +1 -1
  42. package/dist/cli/index.d.ts +4 -39
  43. package/dist/cli/index.d.ts.map +1 -1
  44. package/dist/cli/index.js +9 -722
  45. package/dist/cli/index.js.map +1 -1
  46. package/dist/cli/parsers/analyze.cjs +54 -0
  47. package/dist/cli/parsers/analyze.cjs.map +1 -0
  48. package/dist/cli/parsers/analyze.d.cts +37 -0
  49. package/dist/cli/parsers/analyze.d.cts.map +1 -0
  50. package/dist/cli/parsers/analyze.d.ts +37 -0
  51. package/dist/cli/parsers/analyze.d.ts.map +1 -0
  52. package/dist/cli/parsers/analyze.js +51 -0
  53. package/dist/cli/parsers/analyze.js.map +1 -0
  54. package/dist/cli/parsers/baseline.cjs +75 -0
  55. package/dist/cli/parsers/baseline.cjs.map +1 -0
  56. package/dist/cli/parsers/baseline.d.cts +59 -0
  57. package/dist/cli/parsers/baseline.d.cts.map +1 -0
  58. package/dist/cli/parsers/baseline.d.ts +59 -0
  59. package/dist/cli/parsers/baseline.d.ts.map +1 -0
  60. package/dist/cli/parsers/baseline.js +72 -0
  61. package/dist/cli/parsers/baseline.js.map +1 -0
  62. package/dist/cli/parsers/global.cjs +49 -0
  63. package/dist/cli/parsers/global.cjs.map +1 -0
  64. package/dist/cli/parsers/global.d.cts +45 -0
  65. package/dist/cli/parsers/global.d.cts.map +1 -0
  66. package/dist/cli/parsers/global.d.ts +45 -0
  67. package/dist/cli/parsers/global.d.ts.map +1 -0
  68. package/dist/cli/parsers/global.js +46 -0
  69. package/dist/cli/parsers/global.js.map +1 -0
  70. package/dist/cli/parsers/history.cjs +138 -0
  71. package/dist/cli/parsers/history.cjs.map +1 -0
  72. package/dist/cli/parsers/history.d.cts +108 -0
  73. package/dist/cli/parsers/history.d.cts.map +1 -0
  74. package/dist/cli/parsers/history.d.ts +108 -0
  75. package/dist/cli/parsers/history.d.ts.map +1 -0
  76. package/dist/cli/parsers/history.js +135 -0
  77. package/dist/cli/parsers/history.js.map +1 -0
  78. package/dist/cli/parsers/index.cjs +35 -0
  79. package/dist/cli/parsers/index.cjs.map +1 -0
  80. package/dist/cli/parsers/index.d.cts +15 -0
  81. package/dist/cli/parsers/index.d.cts.map +1 -0
  82. package/dist/cli/parsers/index.d.ts +15 -0
  83. package/dist/cli/parsers/index.d.ts.map +1 -0
  84. package/dist/cli/parsers/index.js +15 -0
  85. package/dist/cli/parsers/index.js.map +1 -0
  86. package/dist/cli/parsers/init.cjs +39 -0
  87. package/dist/cli/parsers/init.cjs.map +1 -0
  88. package/dist/cli/parsers/init.d.cts +32 -0
  89. package/dist/cli/parsers/init.d.cts.map +1 -0
  90. package/dist/cli/parsers/init.d.ts +32 -0
  91. package/dist/cli/parsers/init.d.ts.map +1 -0
  92. package/dist/cli/parsers/init.js +36 -0
  93. package/dist/cli/parsers/init.js.map +1 -0
  94. package/dist/cli/parsers/run.cjs +99 -0
  95. package/dist/cli/parsers/run.cjs.map +1 -0
  96. package/dist/cli/parsers/run.d.cts +62 -0
  97. package/dist/cli/parsers/run.d.cts.map +1 -0
  98. package/dist/cli/parsers/run.d.ts +62 -0
  99. package/dist/cli/parsers/run.d.ts.map +1 -0
  100. package/dist/cli/parsers/run.js +96 -0
  101. package/dist/cli/parsers/run.js.map +1 -0
  102. package/dist/cli/parsers/test.cjs +42 -0
  103. package/dist/cli/parsers/test.cjs.map +1 -0
  104. package/dist/cli/parsers/test.d.cts +31 -0
  105. package/dist/cli/parsers/test.d.cts.map +1 -0
  106. package/dist/cli/parsers/test.d.ts +31 -0
  107. package/dist/cli/parsers/test.d.ts.map +1 -0
  108. package/dist/cli/parsers/test.js +39 -0
  109. package/dist/cli/parsers/test.js.map +1 -0
  110. package/dist/cli/theme.cjs +35 -0
  111. package/dist/cli/theme.cjs.map +1 -0
  112. package/dist/cli/theme.d.cts +31 -0
  113. package/dist/cli/theme.d.cts.map +1 -0
  114. package/dist/cli/theme.d.ts +31 -0
  115. package/dist/cli/theme.d.ts.map +1 -0
  116. package/dist/cli/theme.js +32 -0
  117. package/dist/cli/theme.js.map +1 -0
  118. package/dist/errors/base.cjs +3 -12
  119. package/dist/errors/base.cjs.map +1 -1
  120. package/dist/errors/base.d.cts +0 -7
  121. package/dist/errors/base.d.cts.map +1 -1
  122. package/dist/errors/base.d.ts +0 -7
  123. package/dist/errors/base.d.ts.map +1 -1
  124. package/dist/errors/base.js +1 -9
  125. package/dist/errors/base.js.map +1 -1
  126. package/dist/services/profiler/profile-runner.cjs +11 -0
  127. package/dist/services/profiler/profile-runner.cjs.map +1 -1
  128. package/dist/services/profiler/profile-runner.d.cts +2 -0
  129. package/dist/services/profiler/profile-runner.d.cts.map +1 -1
  130. package/dist/services/profiler/profile-runner.d.ts +2 -0
  131. package/dist/services/profiler/profile-runner.d.ts.map +1 -1
  132. package/dist/services/profiler/profile-runner.js +11 -0
  133. package/dist/services/profiler/profile-runner.js.map +1 -1
  134. package/package.json +1 -1
  135. package/src/cli/builder.ts +387 -0
  136. package/src/cli/commands/baseline.ts +7 -33
  137. package/src/cli/commands/history.ts +1 -16
  138. package/src/cli/context.ts +117 -0
  139. package/src/cli/handlers.ts +76 -0
  140. package/src/cli/index.ts +10 -1012
  141. package/src/cli/parsers/analyze.ts +61 -0
  142. package/src/cli/parsers/baseline.ts +92 -0
  143. package/src/cli/parsers/global.ts +51 -0
  144. package/src/cli/parsers/history.ts +168 -0
  145. package/src/cli/parsers/index.ts +28 -0
  146. package/src/cli/parsers/init.ts +45 -0
  147. package/src/cli/parsers/run.ts +118 -0
  148. package/src/cli/parsers/test.ts +46 -0
  149. package/src/cli/theme.ts +33 -0
  150. package/src/errors/base.ts +1 -10
  151. package/src/services/profiler/profile-runner.ts +15 -0
@@ -0,0 +1,387 @@
1
+ /**
2
+ * CLI Builder
3
+ *
4
+ * Constructs the CLI using bargs, registering all commands, subcommands, and
5
+ * their handlers.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ import { bargs } from '@boneskull/bargs';
11
+
12
+ import type { CliContext } from './context.js';
13
+
14
+ import {
15
+ handleAnalyzeCommand as analyzeCommand,
16
+ type AnalyzeOptions,
17
+ } from './commands/analyze.js';
18
+ import {
19
+ handleAnalyzeCommand as handleBaselineAnalyzeCommand,
20
+ handleDeleteCommand as handleBaselineDeleteCommand,
21
+ handleListCommand as handleBaselineListCommand,
22
+ handleSetCommand as handleBaselineSetCommand,
23
+ handleShowCommand as handleBaselineShowCommand,
24
+ } from './commands/baseline.js';
25
+ import {
26
+ handleCleanCommand,
27
+ handleCompareCommand,
28
+ handleExportCommand,
29
+ handleListCommand,
30
+ handleShowCommand,
31
+ handleTrendsCommand,
32
+ } from './commands/history.js';
33
+ import { handleInitCommand as initCommand } from './commands/init.js';
34
+ import { handleRunCommand as runCommand } from './commands/run.js';
35
+ import {
36
+ handleTestCommand as testCommand,
37
+ type TestOptions,
38
+ } from './commands/test.js';
39
+ import { createCliContext } from './context.js';
40
+ import {
41
+ analyzeParser,
42
+ baselineAnalyzeParser,
43
+ baselineDeleteParser,
44
+ baselineListParser,
45
+ baselineSetParser,
46
+ baselineShowParser,
47
+ globalOptions,
48
+ historyCleanParser,
49
+ historyCompareParser,
50
+ historyExportParser,
51
+ historyListParser,
52
+ historyShowParser,
53
+ historyTrendsParser,
54
+ initParser,
55
+ quietOption,
56
+ runParser,
57
+ testParser,
58
+ } from './parsers/index.js';
59
+ import { synthwaveTheme } from './theme.js';
60
+
61
+ /**
62
+ * Create the CLI builder with all commands registered
63
+ *
64
+ * @param abortController - Controller for aborting benchmark runs
65
+ * @returns Configured bargs CLI builder
66
+ */
67
+ export const createCli = (abortController: AbortController) => {
68
+ return bargs('modestbench', {
69
+ description: 'A modern benchmark runner for Node.js',
70
+ theme: synthwaveTheme,
71
+ })
72
+ .globals(globalOptions)
73
+ .command(
74
+ 'run',
75
+ runParser,
76
+ async ({ positionals, values }) => {
77
+ const [pattern] = positionals;
78
+ const context = await createCliContext(
79
+ values,
80
+ abortController,
81
+ values.engine,
82
+ );
83
+ const exitCode = await runCommand(context, {
84
+ bail: values.bail,
85
+ config: values.config,
86
+ cwd: values.cwd,
87
+ engine: values.engine,
88
+ exclude: values.exclude,
89
+ excludeTags: values.excludeTag,
90
+ iterations: values.iterations,
91
+ json: values.json,
92
+ jsonPretty: values.jsonPretty,
93
+ noColor: values.noColor,
94
+ outputDir: values.output,
95
+ outputFile: values.outputFile,
96
+ pattern,
97
+ progress: values.progress,
98
+ quiet: values.quiet,
99
+ reporters: values.reporter,
100
+ tags: values.tag,
101
+ time: values.time,
102
+ timeout: values.timeout,
103
+ verbose: values.verbose,
104
+ warmup: values.warmup,
105
+ });
106
+ process.exitCode = exitCode;
107
+ },
108
+ 'Run benchmark files',
109
+ )
110
+ .command(
111
+ 'history',
112
+ (history) =>
113
+ history
114
+ .globals(quietOption)
115
+ .command(
116
+ 'list',
117
+ historyListParser,
118
+ async ({ values }) => {
119
+ const context = await createCliContext(values, abortController);
120
+ const exitCode = await handleListCommand(context, {
121
+ cwd: values.cwd,
122
+ format: values.format,
123
+ limit: values.limit,
124
+ pattern: values.pattern,
125
+ since: values.since,
126
+ tags: values.tag,
127
+ until: values.until,
128
+ verbose: values.verbose,
129
+ });
130
+ process.exitCode = exitCode;
131
+ },
132
+ 'List recent benchmark runs',
133
+ )
134
+ .command(
135
+ 'show',
136
+ historyShowParser,
137
+ async ({ positionals, values }) => {
138
+ const [runId] = positionals;
139
+ const context = await createCliContext(values, abortController);
140
+ const exitCode = await handleShowCommand(context, {
141
+ cwd: values.cwd,
142
+ format: values.format,
143
+ runId,
144
+ verbose: values.verbose,
145
+ });
146
+ process.exitCode = exitCode;
147
+ },
148
+ 'Show detailed results for a specific run',
149
+ )
150
+ .command(
151
+ 'compare',
152
+ historyCompareParser,
153
+ async ({ positionals, values }) => {
154
+ const [runId1, runId2] = positionals;
155
+ const context = await createCliContext(values, abortController);
156
+ const exitCode = await handleCompareCommand(context, {
157
+ cwd: values.cwd,
158
+ format: values.format,
159
+ runId1,
160
+ runId2,
161
+ verbose: values.verbose,
162
+ });
163
+ process.exitCode = exitCode;
164
+ },
165
+ 'Compare two benchmark runs',
166
+ )
167
+ .command(
168
+ 'trends',
169
+ historyTrendsParser,
170
+ async ({ positionals, values }) => {
171
+ const [pattern] = positionals;
172
+ const context = await createCliContext(values, abortController);
173
+ const exitCode = await handleTrendsCommand(context, {
174
+ all: values.all,
175
+ cwd: values.cwd,
176
+ format: values.format,
177
+ limit: values.limit,
178
+ pattern,
179
+ since: values.since,
180
+ tags: values.tag,
181
+ until: values.until,
182
+ verbose: values.verbose,
183
+ });
184
+ process.exitCode = exitCode;
185
+ },
186
+ 'Show performance trends over time',
187
+ )
188
+ .command(
189
+ 'clean',
190
+ historyCleanParser,
191
+ async ({ values }) => {
192
+ const context = await createCliContext(values, abortController);
193
+ const exitCode = await handleCleanCommand(context, {
194
+ confirm: values.yes,
195
+ cwd: values.cwd,
196
+ maxAge: values.maxAge,
197
+ maxRuns: values.maxRuns,
198
+ maxSize: values.maxSize,
199
+ quiet: values.quiet,
200
+ verbose: values.verbose,
201
+ });
202
+ process.exitCode = exitCode;
203
+ },
204
+ 'Clean up old benchmark history',
205
+ )
206
+ .command(
207
+ 'export',
208
+ historyExportParser,
209
+ async ({ values }) => {
210
+ const context = await createCliContext(values, abortController);
211
+ const exitCode = await handleExportCommand(context, {
212
+ cwd: values.cwd,
213
+ format: values.format,
214
+ outputPath: values.output,
215
+ quiet: Boolean(values.quiet),
216
+ since: values.since,
217
+ until: values.until,
218
+ verbose: values.verbose,
219
+ });
220
+ process.exitCode = exitCode;
221
+ },
222
+ 'Export benchmark history to a file',
223
+ ),
224
+ 'View and manage benchmark history',
225
+ )
226
+ .command(
227
+ 'baseline',
228
+ (baseline) =>
229
+ baseline
230
+ .globals(quietOption)
231
+ .command(
232
+ 'set',
233
+ baselineSetParser,
234
+ async ({ positionals, values }) => {
235
+ const [name] = positionals;
236
+ const context = await createCliContext(values, abortController);
237
+ const exitCode = await handleBaselineSetCommand(context, {
238
+ branch: values.branch,
239
+ commit: values.commit,
240
+ cwd: values.cwd,
241
+ default: values.default,
242
+ name,
243
+ quiet: Boolean(values.quiet),
244
+ runId: values.runId,
245
+ verbose: values.verbose,
246
+ });
247
+ process.exitCode = exitCode;
248
+ },
249
+ 'Save a benchmark run as a baseline',
250
+ )
251
+ .command(
252
+ 'list',
253
+ baselineListParser,
254
+ async ({ values }) => {
255
+ const context = await createCliContext(values, abortController);
256
+ const exitCode = await handleBaselineListCommand(context, {
257
+ cwd: values.cwd,
258
+ format: values.format,
259
+ quiet: Boolean(values.quiet),
260
+ verbose: values.verbose,
261
+ });
262
+ process.exitCode = exitCode;
263
+ },
264
+ 'List all saved baselines',
265
+ )
266
+ .command(
267
+ 'show',
268
+ baselineShowParser,
269
+ async ({ positionals, values }) => {
270
+ const [name] = positionals;
271
+ const context = await createCliContext(values, abortController);
272
+ const exitCode = await handleBaselineShowCommand(context, {
273
+ cwd: values.cwd,
274
+ format: values.format,
275
+ name,
276
+ quiet: Boolean(values.quiet),
277
+ verbose: values.verbose,
278
+ });
279
+ process.exitCode = exitCode;
280
+ },
281
+ 'Show baseline details',
282
+ )
283
+ .command(
284
+ 'delete',
285
+ baselineDeleteParser,
286
+ async ({ positionals, values }) => {
287
+ const [name] = positionals;
288
+ const context = await createCliContext(values, abortController);
289
+ const exitCode = await handleBaselineDeleteCommand(context, {
290
+ cwd: values.cwd,
291
+ name,
292
+ quiet: Boolean(values.quiet),
293
+ verbose: values.verbose,
294
+ });
295
+ process.exitCode = exitCode;
296
+ },
297
+ 'Delete a baseline',
298
+ )
299
+ .command(
300
+ 'analyze',
301
+ baselineAnalyzeParser,
302
+ async ({ values }) => {
303
+ const context = await createCliContext(values, abortController);
304
+ const exitCode = await handleBaselineAnalyzeCommand(context, {
305
+ confidence: values.confidence,
306
+ cwd: values.cwd,
307
+ quiet: Boolean(values.quiet),
308
+ runs: values.runs,
309
+ verbose: values.verbose,
310
+ });
311
+ process.exitCode = exitCode;
312
+ },
313
+ 'Analyze history and suggest performance budgets',
314
+ ),
315
+ 'Manage performance baselines',
316
+ )
317
+ .command(
318
+ 'init',
319
+ initParser,
320
+ async ({ positionals, values }) => {
321
+ const [type] = positionals;
322
+ const context = await createCliContext(values, abortController);
323
+ const exitCode = await initCommand(context, {
324
+ configType: values.configType,
325
+ cwd: values.cwd,
326
+ examples: values.examples,
327
+ force: values.force,
328
+ quiet: values.quiet,
329
+ type,
330
+ verbose: values.verbose,
331
+ yes: values.yes,
332
+ });
333
+ process.exitCode = exitCode;
334
+ },
335
+ 'Initialize a new benchmark project',
336
+ )
337
+ .command(
338
+ 'analyze',
339
+ analyzeParser,
340
+ async ({ positionals, values }) => {
341
+ const [command] = positionals;
342
+ // Context not needed for analyze command currently
343
+ const context = {} as CliContext;
344
+
345
+ const options: AnalyzeOptions = {
346
+ color: !values.noColor,
347
+ command,
348
+ cwd: values.cwd || process.cwd(),
349
+ filterFile: values.filterFile,
350
+ groupByFile: values.groupByFile,
351
+ input: values.input,
352
+ minPercent: values.minPercent,
353
+ top: values.top,
354
+ };
355
+
356
+ process.exitCode = await analyzeCommand(context, options);
357
+ },
358
+ {
359
+ aliases: ['profile'],
360
+ description: 'Analyze code execution and identify benchmark candidates',
361
+ },
362
+ )
363
+ .command(
364
+ 'test',
365
+ testParser,
366
+ async ({ positionals, values }) => {
367
+ const [framework, files] = positionals;
368
+ const context = await createCliContext(values, abortController);
369
+ const options: TestOptions = {
370
+ bail: values.bail,
371
+ cwd: values.cwd,
372
+ framework,
373
+ iterations: values.iterations,
374
+ json: values.json,
375
+ noColor: values.noColor,
376
+ pattern: files,
377
+ quiet: values.quiet,
378
+ verbose: values.verbose,
379
+ warmup: values.warmup,
380
+ };
381
+ const exitCode = await testCommand(context, options);
382
+ process.exitCode = exitCode;
383
+ },
384
+ 'Run test files as benchmarks',
385
+ )
386
+ .defaultCommand('run');
387
+ };
@@ -11,6 +11,10 @@ import type { CliContext } from '../index.js';
11
11
 
12
12
  import { BaselineStorageService } from '../../services/baseline-storage.js';
13
13
  import { createTaskId } from '../../types/index.js';
14
+ import {
15
+ formatDuration,
16
+ formatOpsPerSecond,
17
+ } from '../../utils/reporter-utils.js';
14
18
 
15
19
  /**
16
20
  * Options for baseline analyze command
@@ -62,38 +66,6 @@ interface BaselineShowOptions extends BaselineBaseOptions {
62
66
  name: string;
63
67
  }
64
68
 
65
- /**
66
- * Format duration in human-readable format
67
- */
68
- const formatDuration = (nanoseconds: number): string => {
69
- if (nanoseconds < 1000) {
70
- return `${nanoseconds.toFixed(2)}ns`;
71
- }
72
- if (nanoseconds < 1_000_000) {
73
- return `${(nanoseconds / 1000).toFixed(2)}μs`;
74
- }
75
- if (nanoseconds < 1_000_000_000) {
76
- return `${(nanoseconds / 1_000_000).toFixed(2)}ms`;
77
- }
78
- return `${(nanoseconds / 1_000_000_000).toFixed(2)}s`;
79
- };
80
-
81
- /**
82
- * Format operations per second
83
- */
84
- const formatOpsPerSec = (ops: number): string => {
85
- if (ops < 1000) {
86
- return `${ops.toFixed(2)} ops/sec`;
87
- }
88
- if (ops < 1_000_000) {
89
- return `${(ops / 1000).toFixed(2)}K ops/sec`;
90
- }
91
- if (ops < 1_000_000_000) {
92
- return `${(ops / 1_000_000).toFixed(2)}M ops/sec`;
93
- }
94
- return `${(ops / 1_000_000_000).toFixed(2)}B ops/sec`;
95
- };
96
-
97
69
  /**
98
70
  * Format date in readable format
99
71
  */
@@ -337,7 +309,9 @@ export const handleShowCommand = async (
337
309
  for (const [taskId, data] of tasks) {
338
310
  console.log(` ${taskId}`);
339
311
  console.log(` Mean: ${formatDuration(data.mean)}`);
340
- console.log(` Ops/sec: ${formatOpsPerSec(data.opsPerSecond)}`);
312
+ console.log(
313
+ ` Ops/sec: ${formatOpsPerSecond(data.opsPerSecond)}`,
314
+ );
341
315
  if (data.p99) {
342
316
  console.log(` P99: ${formatDuration(data.p99)}`);
343
317
  }
@@ -22,6 +22,7 @@ import {
22
22
  parseDate,
23
23
  } from '../../services/history/query.js';
24
24
  import { TrendAnalysisService } from '../../services/history/trend-analysis.js';
25
+ import { formatBytes } from '../../utils/reporter-utils.js';
25
26
 
26
27
  /**
27
28
  * Base options shared by all history subcommands
@@ -94,22 +95,6 @@ interface HistoryTrendsOptions extends BaseHistoryOptions {
94
95
  until?: string | undefined;
95
96
  }
96
97
 
97
- /**
98
- * Format bytes in human-readable format
99
- */
100
- const formatBytes = (bytes: number): string => {
101
- const units = ['B', 'KB', 'MB', 'GB'];
102
- let size = bytes;
103
- let unitIndex = 0;
104
-
105
- while (size >= 1024 && unitIndex < units.length - 1) {
106
- size /= 1024;
107
- unitIndex++;
108
- }
109
-
110
- return `${size.toFixed(1)} ${units[unitIndex]}`;
111
- };
112
-
113
98
  /**
114
99
  * Resolve a partial run ID to a full ID by checking prefix match
115
100
  *
@@ -0,0 +1,117 @@
1
+ /**
2
+ * CLI Context
3
+ *
4
+ * Provides dependency injection container for CLI commands, including
5
+ * configuration manager, benchmark engine, history storage, and reporters.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ import type { InferParserValues } from '@boneskull/bargs';
11
+
12
+ import type {
13
+ BenchmarkEngine,
14
+ ConfigurationManager,
15
+ Engine,
16
+ HistoryStorage,
17
+ ProgressManager,
18
+ ReporterRegistry,
19
+ } from '../types/index.js';
20
+ import type { globalOptions } from './parsers/global.js';
21
+
22
+ import { bootstrap } from '../bootstrap.js';
23
+ import { DEFAULT_ENGINE, Engines, ExitCodes, Reporters } from '../constants.js';
24
+ import { AccurateEngine, TinybenchEngine } from '../core/engines/index.js';
25
+ import {
26
+ CsvReporter,
27
+ HumanReporter,
28
+ JsonReporter,
29
+ NyanReporter,
30
+ SimpleReporter,
31
+ } from '../reporters/index.js';
32
+
33
+ /**
34
+ * CLI context with initialized services
35
+ */
36
+ export interface CliContext {
37
+ readonly abortController: AbortController;
38
+ readonly configManager: ConfigurationManager;
39
+ readonly engine: BenchmarkEngine;
40
+ readonly historyStorage: HistoryStorage;
41
+ readonly options: InferParserValues<typeof globalOptions>;
42
+ readonly progressManager: ProgressManager;
43
+ readonly reporterRegistry: ReporterRegistry;
44
+ }
45
+
46
+ /**
47
+ * Create CLI context with dependency injection
48
+ */
49
+ export const createCliContext = async (
50
+ options: InferParserValues<typeof globalOptions>,
51
+ abortController: AbortController,
52
+ engineType: Engine = DEFAULT_ENGINE,
53
+ ): Promise<CliContext> => {
54
+ try {
55
+ const dependencies = bootstrap();
56
+
57
+ // Select engine based on type
58
+ const engine =
59
+ engineType === Engines.ACCURATE
60
+ ? new AccurateEngine(dependencies)
61
+ : new TinybenchEngine(dependencies);
62
+
63
+ // Register built-in reporters
64
+ engine.registerReporter(
65
+ Reporters.HUMAN,
66
+ new HumanReporter({
67
+ color: !options.noColor,
68
+ verbose: options.verbose,
69
+ }),
70
+ );
71
+
72
+ engine.registerReporter(
73
+ 'json',
74
+ new JsonReporter({
75
+ prettyPrint: false,
76
+ }),
77
+ );
78
+
79
+ engine.registerReporter(
80
+ 'csv',
81
+ new CsvReporter({
82
+ includeHeaders: true,
83
+ includeMetadata: true,
84
+ }),
85
+ );
86
+
87
+ engine.registerReporter(
88
+ 'simple',
89
+ new SimpleReporter({
90
+ verbose: options.verbose,
91
+ }),
92
+ );
93
+
94
+ engine.registerReporter(
95
+ 'nyan',
96
+ new NyanReporter({
97
+ color: !options.noColor,
98
+ }),
99
+ );
100
+
101
+ return {
102
+ abortController,
103
+ configManager: engine.configManager,
104
+ engine,
105
+ historyStorage: engine.historyStorage,
106
+ options,
107
+ progressManager: engine.progressManager,
108
+ reporterRegistry: engine.reporterRegistry,
109
+ };
110
+ } catch (error) {
111
+ console.error(
112
+ 'Failed to initialize ModestBench:',
113
+ error instanceof Error ? error.message : String(error),
114
+ );
115
+ process.exit(ExitCodes.CONFIG_ERROR);
116
+ }
117
+ };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * CLI Signal Handlers
3
+ *
4
+ * Handles process signals (SIGINT, SIGTERM, etc.) and uncaught errors for
5
+ * graceful shutdown of benchmark runs.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ import { ABORT_TIMEOUT, ExitCodes } from '../constants.js';
11
+ import { isModestBenchError, UnknownError } from '../errors/index.js';
12
+ import { isError } from '../utils/type-guards.js';
13
+
14
+ /**
15
+ * Handle process signals gracefully
16
+ */
17
+ export const setupSignalHandlers = (abortController: AbortController): void => {
18
+ let abortRequested = false;
19
+
20
+ const handleSignal = (signal: NodeJS.Signals) => {
21
+ if (abortRequested) {
22
+ // Second signal, force exit
23
+ console.log(`\nReceived ${signal} again, forcing exit...`);
24
+ process.exit(computeExitCode(signal));
25
+ }
26
+
27
+ console.log(`\nReceived ${signal}, aborting benchmarks...`);
28
+ abortRequested = true;
29
+ abortController.abort();
30
+
31
+ // Give a short grace period for cleanup, then exit
32
+ setTimeout(() => {
33
+ console.log('\nBenchmark aborted.');
34
+ process.exit(computeExitCode(signal));
35
+ }, ABORT_TIMEOUT);
36
+ };
37
+
38
+ process
39
+ .once('SIGINT', handleSignal)
40
+ .once('SIGQUIT', handleSignal)
41
+ .once('SIGTERM', handleSignal)
42
+ .once('uncaughtException', (error) => {
43
+ // Wrap non-ModestBench errors with UnknownError
44
+ const wrappedError: Error = isModestBenchError(error)
45
+ ? error
46
+ : new UnknownError(error.message, { cause: error });
47
+ console.error(`${wrappedError}`);
48
+ process.exit(ExitCodes.RUNTIME_ERROR);
49
+ })
50
+ .once('unhandledRejection', (reason) => {
51
+ const wrappedError: Error = isModestBenchError(reason)
52
+ ? reason
53
+ : new UnknownError(isError(reason) ? reason.message : String(reason), {
54
+ cause: reason,
55
+ });
56
+ console.error(`${wrappedError}`);
57
+ process.exit(ExitCodes.RUNTIME_ERROR);
58
+ });
59
+ };
60
+
61
+ /**
62
+ * Compute the exit code based on the signal
63
+ *
64
+ * @param signal - The signal that caused the exit
65
+ * @returns The exit code
66
+ */
67
+ const computeExitCode = (signal: NodeJS.Signals): number => {
68
+ switch (signal) {
69
+ case 'SIGINT':
70
+ return 130; // 128 + 2
71
+ case 'SIGQUIT':
72
+ return 131; // 128 + 3
73
+ default:
74
+ return 143; // 128 + 15 (SIGTERM)
75
+ }
76
+ };