modestbench 0.2.0 → 0.3.0

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 (332) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +131 -34
  3. package/dist/cli/commands/analyze.cjs +60 -0
  4. package/dist/cli/commands/analyze.cjs.map +1 -0
  5. package/dist/cli/commands/analyze.d.cts +35 -0
  6. package/dist/cli/commands/analyze.d.cts.map +1 -0
  7. package/dist/cli/commands/analyze.d.ts +35 -0
  8. package/dist/cli/commands/analyze.d.ts.map +1 -0
  9. package/dist/cli/commands/analyze.js +56 -0
  10. package/dist/cli/commands/analyze.js.map +1 -0
  11. package/dist/cli/commands/baseline.cjs +404 -0
  12. package/dist/cli/commands/baseline.cjs.map +1 -0
  13. package/dist/cli/commands/baseline.d.cts +72 -0
  14. package/dist/cli/commands/baseline.d.cts.map +1 -0
  15. package/dist/cli/commands/baseline.d.ts +72 -0
  16. package/dist/cli/commands/baseline.d.ts.map +1 -0
  17. package/dist/cli/commands/baseline.js +396 -0
  18. package/dist/cli/commands/baseline.js.map +1 -0
  19. package/dist/cli/commands/history.d.cts +1 -1
  20. package/dist/cli/commands/history.d.cts.map +1 -1
  21. package/dist/cli/commands/history.d.ts +1 -1
  22. package/dist/cli/commands/history.d.ts.map +1 -1
  23. package/dist/cli/commands/init.cjs +88 -155
  24. package/dist/cli/commands/init.cjs.map +1 -1
  25. package/dist/cli/commands/init.d.cts +4 -4
  26. package/dist/cli/commands/init.d.cts.map +1 -1
  27. package/dist/cli/commands/init.d.ts +4 -4
  28. package/dist/cli/commands/init.d.ts.map +1 -1
  29. package/dist/cli/commands/init.js +88 -155
  30. package/dist/cli/commands/init.js.map +1 -1
  31. package/dist/cli/commands/run.cjs +132 -114
  32. package/dist/cli/commands/run.cjs.map +1 -1
  33. package/dist/cli/commands/run.d.cts +16 -3
  34. package/dist/cli/commands/run.d.cts.map +1 -1
  35. package/dist/cli/commands/run.d.ts +16 -3
  36. package/dist/cli/commands/run.d.ts.map +1 -1
  37. package/dist/cli/commands/run.js +131 -80
  38. package/dist/cli/commands/run.js.map +1 -1
  39. package/dist/cli/index.cjs +583 -394
  40. package/dist/cli/index.cjs.map +1 -1
  41. package/dist/cli/index.d.cts +4 -16
  42. package/dist/cli/index.d.cts.map +1 -1
  43. package/dist/cli/index.d.ts +4 -16
  44. package/dist/cli/index.d.ts.map +1 -1
  45. package/dist/cli/index.js +575 -386
  46. package/dist/cli/index.js.map +1 -1
  47. package/dist/config/budget-schema.cjs +172 -0
  48. package/dist/config/budget-schema.cjs.map +1 -0
  49. package/dist/config/budget-schema.d.cts +59 -0
  50. package/dist/config/budget-schema.d.cts.map +1 -0
  51. package/dist/config/budget-schema.d.ts +59 -0
  52. package/dist/config/budget-schema.d.ts.map +1 -0
  53. package/dist/config/budget-schema.js +166 -0
  54. package/dist/config/budget-schema.js.map +1 -0
  55. package/dist/config/schema.cjs +182 -2
  56. package/dist/config/schema.cjs.map +1 -1
  57. package/dist/config/schema.d.cts +122 -3
  58. package/dist/config/schema.d.cts.map +1 -1
  59. package/dist/config/schema.d.ts +122 -3
  60. package/dist/config/schema.d.ts.map +1 -1
  61. package/dist/config/schema.js +180 -1
  62. package/dist/config/schema.js.map +1 -1
  63. package/dist/constants.cjs +45 -2
  64. package/dist/constants.cjs.map +1 -1
  65. package/dist/constants.d.cts +41 -0
  66. package/dist/constants.d.cts.map +1 -1
  67. package/dist/constants.d.ts +41 -0
  68. package/dist/constants.d.ts.map +1 -1
  69. package/dist/constants.js +44 -1
  70. package/dist/constants.js.map +1 -1
  71. package/dist/core/engine.cjs +103 -21
  72. package/dist/core/engine.cjs.map +1 -1
  73. package/dist/core/engine.d.cts +7 -7
  74. package/dist/core/engine.d.cts.map +1 -1
  75. package/dist/core/engine.d.ts +7 -7
  76. package/dist/core/engine.d.ts.map +1 -1
  77. package/dist/core/engine.js +104 -22
  78. package/dist/core/engine.js.map +1 -1
  79. package/dist/core/output-path-resolver.cjs +8 -1
  80. package/dist/core/output-path-resolver.cjs.map +1 -1
  81. package/dist/core/output-path-resolver.d.cts.map +1 -1
  82. package/dist/core/output-path-resolver.d.ts.map +1 -1
  83. package/dist/core/output-path-resolver.js +9 -2
  84. package/dist/core/output-path-resolver.js.map +1 -1
  85. package/dist/errors/base.cjs +12 -3
  86. package/dist/errors/base.cjs.map +1 -1
  87. package/dist/errors/base.d.cts +7 -0
  88. package/dist/errors/base.d.cts.map +1 -1
  89. package/dist/errors/base.d.ts +7 -0
  90. package/dist/errors/base.d.ts.map +1 -1
  91. package/dist/errors/base.js +10 -2
  92. package/dist/errors/base.js.map +1 -1
  93. package/dist/errors/budget.cjs +37 -0
  94. package/dist/errors/budget.cjs.map +1 -0
  95. package/dist/errors/budget.d.cts +31 -0
  96. package/dist/errors/budget.d.cts.map +1 -0
  97. package/dist/errors/budget.d.ts +31 -0
  98. package/dist/errors/budget.d.ts.map +1 -0
  99. package/dist/errors/budget.js +33 -0
  100. package/dist/errors/budget.js.map +1 -0
  101. package/dist/errors/index.cjs +4 -1
  102. package/dist/errors/index.cjs.map +1 -1
  103. package/dist/errors/index.d.cts +1 -0
  104. package/dist/errors/index.d.cts.map +1 -1
  105. package/dist/errors/index.d.ts +1 -0
  106. package/dist/errors/index.d.ts.map +1 -1
  107. package/dist/errors/index.js +2 -0
  108. package/dist/errors/index.js.map +1 -1
  109. package/dist/index.cjs +13 -1
  110. package/dist/index.cjs.map +1 -1
  111. package/dist/index.d.cts +5 -0
  112. package/dist/index.d.cts.map +1 -1
  113. package/dist/index.d.ts +5 -0
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.js +7 -0
  116. package/dist/index.js.map +1 -1
  117. package/dist/reporters/csv.cjs +37 -17
  118. package/dist/reporters/csv.cjs.map +1 -1
  119. package/dist/reporters/csv.d.cts +3 -6
  120. package/dist/reporters/csv.d.cts.map +1 -1
  121. package/dist/reporters/csv.d.ts +3 -6
  122. package/dist/reporters/csv.d.ts.map +1 -1
  123. package/dist/reporters/csv.js +37 -17
  124. package/dist/reporters/csv.js.map +1 -1
  125. package/dist/reporters/human.cjs +66 -40
  126. package/dist/reporters/human.cjs.map +1 -1
  127. package/dist/reporters/human.d.cts +14 -13
  128. package/dist/reporters/human.d.cts.map +1 -1
  129. package/dist/reporters/human.d.ts +14 -13
  130. package/dist/reporters/human.d.ts.map +1 -1
  131. package/dist/reporters/human.js +66 -40
  132. package/dist/reporters/human.js.map +1 -1
  133. package/dist/reporters/json.cjs +23 -48
  134. package/dist/reporters/json.cjs.map +1 -1
  135. package/dist/reporters/json.d.cts +2 -28
  136. package/dist/reporters/json.d.cts.map +1 -1
  137. package/dist/reporters/json.d.ts +2 -28
  138. package/dist/reporters/json.d.ts.map +1 -1
  139. package/dist/reporters/json.js +25 -50
  140. package/dist/reporters/json.js.map +1 -1
  141. package/dist/reporters/profile-human.cjs +149 -0
  142. package/dist/reporters/profile-human.cjs.map +1 -0
  143. package/dist/reporters/profile-human.d.cts +44 -0
  144. package/dist/reporters/profile-human.d.cts.map +1 -0
  145. package/dist/reporters/profile-human.d.ts +44 -0
  146. package/dist/reporters/profile-human.d.ts.map +1 -0
  147. package/dist/reporters/profile-human.js +142 -0
  148. package/dist/reporters/profile-human.js.map +1 -0
  149. package/dist/reporters/simple.cjs +64 -44
  150. package/dist/reporters/simple.cjs.map +1 -1
  151. package/dist/reporters/simple.d.cts +14 -14
  152. package/dist/reporters/simple.d.cts.map +1 -1
  153. package/dist/reporters/simple.d.ts +14 -14
  154. package/dist/reporters/simple.d.ts.map +1 -1
  155. package/dist/reporters/simple.js +64 -44
  156. package/dist/reporters/simple.js.map +1 -1
  157. package/dist/schema/modestbench-config.schema.json +153 -0
  158. package/dist/services/baseline-storage.cjs +151 -0
  159. package/dist/services/baseline-storage.cjs.map +1 -0
  160. package/dist/services/baseline-storage.d.cts +55 -0
  161. package/dist/services/baseline-storage.d.cts.map +1 -0
  162. package/dist/services/baseline-storage.d.ts +55 -0
  163. package/dist/services/baseline-storage.d.ts.map +1 -0
  164. package/dist/services/baseline-storage.js +147 -0
  165. package/dist/services/baseline-storage.js.map +1 -0
  166. package/dist/services/budget-evaluator.cjs +146 -0
  167. package/dist/services/budget-evaluator.cjs.map +1 -0
  168. package/dist/services/budget-evaluator.d.cts +29 -0
  169. package/dist/services/budget-evaluator.d.cts.map +1 -0
  170. package/dist/services/budget-evaluator.d.ts +29 -0
  171. package/dist/services/budget-evaluator.d.ts.map +1 -0
  172. package/dist/services/budget-evaluator.js +142 -0
  173. package/dist/services/budget-evaluator.js.map +1 -0
  174. package/dist/services/config-manager.cjs +23 -9
  175. package/dist/services/config-manager.cjs.map +1 -1
  176. package/dist/services/config-manager.d.cts +6 -1
  177. package/dist/services/config-manager.d.cts.map +1 -1
  178. package/dist/services/config-manager.d.ts +6 -1
  179. package/dist/services/config-manager.d.ts.map +1 -1
  180. package/dist/services/config-manager.js +23 -9
  181. package/dist/services/config-manager.js.map +1 -1
  182. package/dist/services/file-loader.cjs +3 -6
  183. package/dist/services/file-loader.cjs.map +1 -1
  184. package/dist/services/file-loader.d.cts.map +1 -1
  185. package/dist/services/file-loader.d.ts.map +1 -1
  186. package/dist/services/file-loader.js +3 -6
  187. package/dist/services/file-loader.js.map +1 -1
  188. package/dist/services/profiler/profile-filter.cjs +113 -0
  189. package/dist/services/profiler/profile-filter.cjs.map +1 -0
  190. package/dist/services/profiler/profile-filter.d.cts +20 -0
  191. package/dist/services/profiler/profile-filter.d.cts.map +1 -0
  192. package/dist/services/profiler/profile-filter.d.ts +20 -0
  193. package/dist/services/profiler/profile-filter.d.ts.map +1 -0
  194. package/dist/services/profiler/profile-filter.js +109 -0
  195. package/dist/services/profiler/profile-filter.js.map +1 -0
  196. package/dist/services/profiler/profile-parser.cjs +139 -0
  197. package/dist/services/profiler/profile-parser.cjs.map +1 -0
  198. package/dist/services/profiler/profile-parser.d.cts +18 -0
  199. package/dist/services/profiler/profile-parser.d.cts.map +1 -0
  200. package/dist/services/profiler/profile-parser.d.ts +18 -0
  201. package/dist/services/profiler/profile-parser.d.ts.map +1 -0
  202. package/dist/services/profiler/profile-parser.js +132 -0
  203. package/dist/services/profiler/profile-parser.js.map +1 -0
  204. package/dist/services/profiler/profile-runner.cjs +90 -0
  205. package/dist/services/profiler/profile-runner.cjs.map +1 -0
  206. package/dist/services/profiler/profile-runner.d.cts +29 -0
  207. package/dist/services/profiler/profile-runner.d.cts.map +1 -0
  208. package/dist/services/profiler/profile-runner.d.ts +29 -0
  209. package/dist/services/profiler/profile-runner.d.ts.map +1 -0
  210. package/dist/services/profiler/profile-runner.js +86 -0
  211. package/dist/services/profiler/profile-runner.js.map +1 -0
  212. package/dist/services/reporter-registry.cjs +18 -24
  213. package/dist/services/reporter-registry.cjs.map +1 -1
  214. package/dist/services/reporter-registry.d.cts +18 -40
  215. package/dist/services/reporter-registry.d.cts.map +1 -1
  216. package/dist/services/reporter-registry.d.ts +18 -40
  217. package/dist/services/reporter-registry.d.ts.map +1 -1
  218. package/dist/services/reporter-registry.js +18 -24
  219. package/dist/services/reporter-registry.js.map +1 -1
  220. package/dist/types/budgets.cjs +8 -0
  221. package/dist/types/budgets.cjs.map +1 -0
  222. package/dist/types/budgets.d.cts +149 -0
  223. package/dist/types/budgets.d.cts.map +1 -0
  224. package/dist/types/budgets.d.ts +149 -0
  225. package/dist/types/budgets.d.ts.map +1 -0
  226. package/dist/types/budgets.js +7 -0
  227. package/dist/types/budgets.js.map +1 -0
  228. package/dist/types/cli.cjs +2 -11
  229. package/dist/types/cli.cjs.map +1 -1
  230. package/dist/types/cli.d.cts +3 -227
  231. package/dist/types/cli.d.cts.map +1 -1
  232. package/dist/types/cli.d.ts +3 -227
  233. package/dist/types/cli.d.ts.map +1 -1
  234. package/dist/types/cli.js +2 -11
  235. package/dist/types/cli.js.map +1 -1
  236. package/dist/types/core.cjs +6 -1
  237. package/dist/types/core.cjs.map +1 -1
  238. package/dist/types/core.d.cts +13 -2
  239. package/dist/types/core.d.cts.map +1 -1
  240. package/dist/types/core.d.ts +13 -2
  241. package/dist/types/core.d.ts.map +1 -1
  242. package/dist/types/core.js +2 -1
  243. package/dist/types/core.js.map +1 -1
  244. package/dist/types/index.cjs +5 -0
  245. package/dist/types/index.cjs.map +1 -1
  246. package/dist/types/index.d.cts +2 -0
  247. package/dist/types/index.d.cts.map +1 -1
  248. package/dist/types/index.d.ts +2 -0
  249. package/dist/types/index.d.ts.map +1 -1
  250. package/dist/types/index.js +2 -0
  251. package/dist/types/index.js.map +1 -1
  252. package/dist/types/interfaces.d.cts +15 -8
  253. package/dist/types/interfaces.d.cts.map +1 -1
  254. package/dist/types/interfaces.d.ts +15 -8
  255. package/dist/types/interfaces.d.ts.map +1 -1
  256. package/dist/types/profiler.cjs +11 -0
  257. package/dist/types/profiler.cjs.map +1 -0
  258. package/dist/types/profiler.d.cts +100 -0
  259. package/dist/types/profiler.d.cts.map +1 -0
  260. package/dist/types/profiler.d.ts +100 -0
  261. package/dist/types/profiler.d.ts.map +1 -0
  262. package/dist/types/profiler.js +10 -0
  263. package/dist/types/profiler.js.map +1 -0
  264. package/dist/types/utility.cjs.map +1 -1
  265. package/dist/types/utility.d.cts +0 -8
  266. package/dist/types/utility.d.cts.map +1 -1
  267. package/dist/types/utility.d.ts +0 -8
  268. package/dist/types/utility.d.ts.map +1 -1
  269. package/dist/types/utility.js.map +1 -1
  270. package/dist/utils/identifiers.cjs +32 -0
  271. package/dist/utils/identifiers.cjs.map +1 -0
  272. package/dist/utils/identifiers.d.cts +32 -0
  273. package/dist/utils/identifiers.d.cts.map +1 -0
  274. package/dist/utils/identifiers.d.ts +32 -0
  275. package/dist/utils/identifiers.d.ts.map +1 -0
  276. package/dist/utils/identifiers.js +27 -0
  277. package/dist/utils/identifiers.js.map +1 -0
  278. package/dist/utils/package.cjs +40 -0
  279. package/dist/utils/package.cjs.map +1 -0
  280. package/dist/utils/package.d.cts +15 -0
  281. package/dist/utils/package.d.cts.map +1 -0
  282. package/dist/utils/package.d.ts +15 -0
  283. package/dist/utils/package.d.ts.map +1 -0
  284. package/dist/utils/package.js +33 -0
  285. package/dist/utils/package.js.map +1 -0
  286. package/dist/utils/type-guards.cjs +48 -0
  287. package/dist/utils/type-guards.cjs.map +1 -0
  288. package/dist/utils/type-guards.d.cts +22 -0
  289. package/dist/utils/type-guards.d.cts.map +1 -0
  290. package/dist/utils/type-guards.d.ts +22 -0
  291. package/dist/utils/type-guards.d.ts.map +1 -0
  292. package/dist/utils/type-guards.js +43 -0
  293. package/dist/utils/type-guards.js.map +1 -0
  294. package/package.json +10 -10
  295. package/src/cli/commands/analyze.ts +101 -0
  296. package/src/cli/commands/baseline.ts +577 -0
  297. package/src/cli/commands/history.ts +1 -1
  298. package/src/cli/commands/init.ts +105 -183
  299. package/src/cli/commands/run.ts +167 -98
  300. package/src/cli/index.ts +425 -183
  301. package/src/config/budget-schema.ts +189 -0
  302. package/src/config/schema.ts +260 -1
  303. package/src/constants.ts +53 -1
  304. package/src/core/engine.ts +151 -20
  305. package/src/core/output-path-resolver.ts +10 -2
  306. package/src/errors/base.ts +11 -2
  307. package/src/errors/budget.ts +38 -0
  308. package/src/errors/index.ts +3 -0
  309. package/src/index.ts +9 -0
  310. package/src/reporters/csv.ts +54 -25
  311. package/src/reporters/human.ts +88 -47
  312. package/src/reporters/json.ts +26 -71
  313. package/src/reporters/profile-human.ts +204 -0
  314. package/src/reporters/simple.ts +84 -53
  315. package/src/services/baseline-storage.ts +199 -0
  316. package/src/services/budget-evaluator.ts +182 -0
  317. package/src/services/config-manager.ts +23 -8
  318. package/src/services/file-loader.ts +3 -6
  319. package/src/services/profiler/profile-filter.ts +143 -0
  320. package/src/services/profiler/profile-parser.ts +194 -0
  321. package/src/services/profiler/profile-runner.ts +121 -0
  322. package/src/services/reporter-registry.ts +46 -81
  323. package/src/types/budgets.ts +180 -0
  324. package/src/types/cli.ts +5 -238
  325. package/src/types/core.ts +50 -10
  326. package/src/types/index.ts +5 -0
  327. package/src/types/interfaces.ts +16 -6
  328. package/src/types/profiler.ts +132 -0
  329. package/src/types/utility.ts +0 -10
  330. package/src/utils/identifiers.ts +58 -0
  331. package/src/utils/package.ts +35 -0
  332. package/src/utils/type-guards.ts +51 -0
@@ -9,8 +9,8 @@ import path from 'node:path';
9
9
 
10
10
  import type {
11
11
  BenchmarkRun,
12
+ BudgetSummary,
12
13
  FileResult,
13
- ProgressState,
14
14
  SuiteResult,
15
15
  TaskResult,
16
16
  } from '../types/index.js';
@@ -62,6 +62,78 @@ export class SimpleReporter extends BaseReporter {
62
62
  this.quiet = options.quiet ?? false;
63
63
  }
64
64
 
65
+ /**
66
+ * Format bytes in human-readable format
67
+ */
68
+ private static formatBytes(this: void, bytes: number): string {
69
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
70
+ let size = bytes;
71
+ let unitIndex = 0;
72
+
73
+ while (size >= 1024 && unitIndex < units.length - 1) {
74
+ size /= 1024;
75
+ unitIndex++;
76
+ }
77
+
78
+ return `${size.toFixed(1)} ${units[unitIndex]}`;
79
+ }
80
+
81
+ /**
82
+ * Format file path - show relative path if within CWD, otherwise absolute
83
+ */
84
+ private static formatPath(this: void, filePath: string): string {
85
+ const cwd = process.cwd();
86
+ const absolutePath = path.resolve(filePath);
87
+
88
+ // Check if the file is within the current working directory
89
+ if (absolutePath.startsWith(cwd + path.sep) || absolutePath === cwd) {
90
+ return path.relative(cwd, absolutePath);
91
+ }
92
+
93
+ return absolutePath;
94
+ }
95
+
96
+ /**
97
+ * Simple pluralization helper
98
+ */
99
+ private static pluralize(this: void, str: string, count: number): string {
100
+ return count === 1 ? str : `${str}s`;
101
+ }
102
+
103
+ onBudgetResult(summary: BudgetSummary): void {
104
+ if (summary.total === 0 || this.quiet) {
105
+ return;
106
+ }
107
+
108
+ console.log('== Performance Budgets');
109
+ console.log();
110
+
111
+ for (const result of summary.results) {
112
+ const icon = result.passed ? symbols.checkmark : symbols.cross;
113
+ console.log(` ${icon} ${result.taskId}`);
114
+
115
+ if (!result.passed && result.violations.length > 0) {
116
+ for (const violation of result.violations) {
117
+ console.log(` ${violation.message}`);
118
+ }
119
+ }
120
+ }
121
+
122
+ console.log();
123
+
124
+ if (summary.failed === 0) {
125
+ console.log(
126
+ ` ${symbols.checkmark} All ${summary.total} budget(s) passed`,
127
+ );
128
+ } else {
129
+ console.log(
130
+ ` ${symbols.cross} ${summary.failed} of ${summary.total} budget(s) failed`,
131
+ );
132
+ }
133
+
134
+ console.log();
135
+ }
136
+
65
137
  onEnd(run: BenchmarkRun): void {
66
138
  if (this.quiet) {
67
139
  return;
@@ -97,7 +169,7 @@ export class SimpleReporter extends BaseReporter {
97
169
  console.log(`- Files: ${totalFiles}`);
98
170
  console.log(`- Suites: ${totalSuites}`);
99
171
  console.log(
100
- `${symbols.approx} Duration: ${this.formatDuration(duration * 1e6)}`,
172
+ `${symbols.approx} Duration: ${BaseReporter.formatDuration(duration * 1e6)}`,
101
173
  );
102
174
  console.log();
103
175
 
@@ -111,7 +183,7 @@ export class SimpleReporter extends BaseReporter {
111
183
  console.log();
112
184
 
113
185
  for (const failure of this.failures) {
114
- const displayPath = this.formatPath(failure.file);
186
+ const displayPath = SimpleReporter.formatPath(failure.file);
115
187
  console.log(` ${displayPath} > ${failure.suite} > ${failure.task}`);
116
188
  console.log(` ${failure.error}`);
117
189
  console.log();
@@ -155,7 +227,7 @@ export class SimpleReporter extends BaseReporter {
155
227
  );
156
228
  } else {
157
229
  console.log(
158
- ` ${symbols.checkmark} ${totalPassed > 1 ? 'All ' : ''}${totalPassed} ${this.pluralize('task', totalPassed)} passed`,
230
+ ` ${symbols.checkmark} ${totalPassed > 1 ? 'All ' : ''}${totalPassed} ${SimpleReporter.pluralize('task', totalPassed)} passed`,
159
231
  );
160
232
  }
161
233
 
@@ -169,15 +241,10 @@ export class SimpleReporter extends BaseReporter {
169
241
  return;
170
242
  }
171
243
 
172
- const displayPath = this.formatPath(file);
244
+ const displayPath = SimpleReporter.formatPath(file);
173
245
  console.log(`-- ${displayPath}`);
174
246
  }
175
247
 
176
- onProgress(_state: ProgressState): void {
177
- // Simple reporter does not display progress bars
178
- return;
179
- }
180
-
181
248
  onStart(run: BenchmarkRun): void {
182
249
  this.startTime = Date.now();
183
250
  this.failures = []; // Reset failures for new run
@@ -197,7 +264,9 @@ export class SimpleReporter extends BaseReporter {
197
264
  console.log(
198
265
  ` cpu: ${run.environment.cpu.model} (${run.environment.cpu.cores} cores)`,
199
266
  );
200
- console.log(` mem: ${this.formatBytes(run.environment.memory.total)}`);
267
+ console.log(
268
+ ` mem: ${SimpleReporter.formatBytes(run.environment.memory.total)}`,
269
+ );
201
270
  console.log();
202
271
  }
203
272
 
@@ -231,7 +300,7 @@ export class SimpleReporter extends BaseReporter {
231
300
  console.log(` ${symbols.cross} ${failed} failed, ${passed} passed`);
232
301
  } else {
233
302
  console.log(
234
- ` ${symbols.checkmark} ${passed} ${this.pluralize('task', passed)} passed`,
303
+ ` ${symbols.checkmark} ${passed} ${SimpleReporter.pluralize('task', passed)} passed`,
235
304
  );
236
305
  }
237
306
  console.log();
@@ -275,44 +344,6 @@ export class SimpleReporter extends BaseReporter {
275
344
  }
276
345
  }
277
346
 
278
- /**
279
- * Format bytes in human-readable format
280
- */
281
- private formatBytes(bytes: number): string {
282
- const units = ['B', 'KB', 'MB', 'GB', 'TB'];
283
- let size = bytes;
284
- let unitIndex = 0;
285
-
286
- while (size >= 1024 && unitIndex < units.length - 1) {
287
- size /= 1024;
288
- unitIndex++;
289
- }
290
-
291
- return `${size.toFixed(1)} ${units[unitIndex]}`;
292
- }
293
-
294
- /**
295
- * Format file path - show relative path if within CWD, otherwise absolute
296
- */
297
- private formatPath(filePath: string): string {
298
- const cwd = process.cwd();
299
- const absolutePath = path.resolve(filePath);
300
-
301
- // Check if the file is within the current working directory
302
- if (absolutePath.startsWith(cwd + path.sep) || absolutePath === cwd) {
303
- return path.relative(cwd, absolutePath);
304
- }
305
-
306
- return absolutePath;
307
- }
308
-
309
- /**
310
- * Simple pluralization helper
311
- */
312
- private pluralize(str: string, count: number): string {
313
- return count === 1 ? str : `${str}s`;
314
- }
315
-
316
347
  /**
317
348
  * Print all task results in a suite with aligned columns
318
349
  */
@@ -364,9 +395,9 @@ export class SimpleReporter extends BaseReporter {
364
395
  };
365
396
  }
366
397
 
367
- const duration = this.formatDuration(result.mean * 1e9);
368
- const opsPerSec = this.formatOpsPerSecond(result.opsPerSecond);
369
- const rme = this.formatPercentage(result.marginOfError * 100);
398
+ const duration = BaseReporter.formatDuration(result.mean * 1e9);
399
+ const opsPerSec = BaseReporter.formatOpsPerSecond(result.opsPerSecond);
400
+ const rme = BaseReporter.formatPercentage(result.marginOfError * 100);
370
401
 
371
402
  return {
372
403
  durationLen: duration.length,
@@ -0,0 +1,199 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+
5
+ import type {
6
+ BaselineReference,
7
+ BaselineStorage,
8
+ BaselineSummaryData,
9
+ BenchmarkRun,
10
+ TaskId,
11
+ } from '../types/core.js';
12
+
13
+ import { validateBaselineStorage } from '../config/budget-schema.js';
14
+ import { StorageError } from '../errors/storage.js';
15
+ import { createTaskId } from '../types/core.js';
16
+
17
+ /**
18
+ * Service for managing named baselines
19
+ *
20
+ * @packageDocumentation
21
+ */
22
+ export class BaselineStorageService {
23
+ private readonly storageDir: string;
24
+
25
+ private readonly storageFile: string;
26
+
27
+ constructor(storageDir: string = '.') {
28
+ this.storageDir = storageDir;
29
+ this.storageFile = join(storageDir, '.modestbench.baselines.json');
30
+ }
31
+
32
+ /**
33
+ * Delete a baseline
34
+ */
35
+ async deleteBaseline(name: string): Promise<void> {
36
+ let storage = await this.loadStorage();
37
+
38
+ if (storage.baselines[name]) {
39
+ delete storage.baselines[name];
40
+
41
+ // Clear default if it was the deleted baseline
42
+ if (storage.default === name) {
43
+ storage = { ...storage, default: undefined };
44
+ }
45
+
46
+ await this.saveStorage(storage);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Get a baseline by name
52
+ */
53
+ async getBaseline(name: string): Promise<BaselineReference | null> {
54
+ const storage = await this.loadStorage();
55
+ return storage.baselines[name] ?? null;
56
+ }
57
+
58
+ /**
59
+ * Get default baseline name
60
+ */
61
+ async getDefault(): Promise<null | string> {
62
+ const storage = await this.loadStorage();
63
+ return storage.default ?? null;
64
+ }
65
+
66
+ /**
67
+ * List all baselines
68
+ */
69
+ async listBaselines(): Promise<BaselineReference[]> {
70
+ const storage = await this.loadStorage();
71
+ return Object.values(storage.baselines).sort(
72
+ (a, b) => b.date.getTime() - a.date.getTime(),
73
+ );
74
+ }
75
+
76
+ /**
77
+ * Resolve baseline name (use provided or fall back to default)
78
+ */
79
+ async resolveBaselineName(name?: string): Promise<null | string> {
80
+ if (name) {
81
+ return name;
82
+ }
83
+
84
+ return await this.getDefault();
85
+ }
86
+
87
+ /**
88
+ * Save a benchmark run as a named baseline
89
+ */
90
+ async saveBaseline(
91
+ name: string,
92
+ run: BenchmarkRun,
93
+ metadata?: {
94
+ branch?: string;
95
+ commit?: string;
96
+ },
97
+ ): Promise<void> {
98
+ const storage = await this.loadStorage();
99
+
100
+ const baseline: BaselineReference = {
101
+ branch: metadata?.branch,
102
+ commit: metadata?.commit,
103
+ date:
104
+ run.startTime instanceof Date ? run.startTime : new Date(run.startTime),
105
+ name,
106
+ runId: run.id,
107
+ summary: this.extractSummary(run),
108
+ };
109
+
110
+ storage.baselines[name] = baseline;
111
+
112
+ await this.saveStorage(storage);
113
+ }
114
+
115
+ /**
116
+ * Set default baseline
117
+ */
118
+ async setDefault(name: string): Promise<void> {
119
+ let storage = await this.loadStorage();
120
+
121
+ if (!storage.baselines[name]) {
122
+ throw new StorageError(
123
+ `Baseline "${name}" does not exist. Cannot set as default.`,
124
+ );
125
+ }
126
+
127
+ storage = { ...storage, default: name };
128
+ await this.saveStorage(storage);
129
+ }
130
+
131
+ /**
132
+ * Extract task summary from benchmark run
133
+ */
134
+ private extractSummary(
135
+ run: BenchmarkRun,
136
+ ): Record<TaskId, BaselineSummaryData> {
137
+ const summary: Record<TaskId, BaselineSummaryData> = {};
138
+
139
+ for (const file of run.files) {
140
+ for (const suite of file.suites) {
141
+ for (const task of suite.tasks) {
142
+ if (!task.error) {
143
+ const taskId = createTaskId(file.filePath, suite.name, task.name);
144
+ summary[taskId] = {
145
+ mean: task.mean,
146
+ opsPerSecond: task.opsPerSecond,
147
+ p99: task.p99,
148
+ };
149
+ }
150
+ }
151
+ }
152
+ }
153
+
154
+ return summary;
155
+ }
156
+
157
+ /**
158
+ * Load baseline storage from disk
159
+ */
160
+ private async loadStorage(): Promise<BaselineStorage> {
161
+ if (!existsSync(this.storageFile)) {
162
+ return {
163
+ baselines: {},
164
+ version: '1.0.0',
165
+ };
166
+ }
167
+
168
+ try {
169
+ const content = await readFile(this.storageFile, 'utf-8');
170
+ const data = JSON.parse(content) as unknown;
171
+ return validateBaselineStorage(data);
172
+ } catch (error) {
173
+ throw new StorageError(
174
+ `Failed to load baseline storage from ${this.storageFile}`,
175
+ { cause: error },
176
+ );
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Save baseline storage to disk
182
+ */
183
+ private async saveStorage(storage: BaselineStorage): Promise<void> {
184
+ try {
185
+ // Ensure directory exists
186
+ if (!existsSync(this.storageDir)) {
187
+ await mkdir(this.storageDir, { recursive: true });
188
+ }
189
+
190
+ const content = JSON.stringify(storage, null, 2);
191
+ await writeFile(this.storageFile, content, 'utf-8');
192
+ } catch (error) {
193
+ throw new StorageError(
194
+ `Failed to save baseline storage to ${this.storageFile}`,
195
+ { cause: error },
196
+ );
197
+ }
198
+ }
199
+ }
@@ -0,0 +1,182 @@
1
+ import type {
2
+ BaselineSummaryData,
3
+ Budget,
4
+ BudgetResult,
5
+ BudgetSummary,
6
+ BudgetViolation,
7
+ TaskId,
8
+ TaskResult,
9
+ } from '../types/core.js';
10
+
11
+ /**
12
+ * Service for evaluating performance budgets
13
+ *
14
+ * @packageDocumentation
15
+ */
16
+ export class BudgetEvaluator {
17
+ /**
18
+ * Format number with thousands separators
19
+ */
20
+ private static formatNumber(this: void, value: number): string {
21
+ return value.toLocaleString('en-US', {
22
+ maximumFractionDigits: 0,
23
+ });
24
+ }
25
+
26
+ /**
27
+ * Format decimal as percentage
28
+ */
29
+ private static formatPercentage(this: void, value: number): string {
30
+ return `${(value * 100).toFixed(1)}%`;
31
+ }
32
+
33
+ /**
34
+ * Format time in nanoseconds to human-readable string
35
+ */
36
+ private static formatTime(this: void, nanoseconds: number): string {
37
+ if (nanoseconds < 1_000) {
38
+ return `${nanoseconds.toFixed(0)}ns`;
39
+ } else if (nanoseconds < 1_000_000) {
40
+ return `${(nanoseconds / 1_000).toFixed(2)}μs`;
41
+ } else if (nanoseconds < 1_000_000_000) {
42
+ return `${(nanoseconds / 1_000_000).toFixed(2)}ms`;
43
+ } else {
44
+ return `${(nanoseconds / 1_000_000_000).toFixed(2)}s`;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Evaluate budgets for an entire benchmark run
50
+ */
51
+ evaluateRun(
52
+ budgets: Record<string, Budget>,
53
+ taskResults: Map<TaskId, TaskResult>,
54
+ baselineData?: Map<TaskId, BaselineSummaryData>,
55
+ ): BudgetSummary {
56
+ const results: BudgetResult[] = [];
57
+
58
+ for (const [taskId, budget] of Object.entries(budgets)) {
59
+ const taskResult = taskResults.get(taskId as TaskId);
60
+
61
+ // Skip if no result for this task
62
+ if (!taskResult) {
63
+ continue;
64
+ }
65
+
66
+ // Skip relative budgets if no baseline data
67
+ if (budget.relative && !baselineData) {
68
+ continue;
69
+ }
70
+
71
+ const budgetResult = this.evaluateTask(
72
+ taskId as TaskId,
73
+ budget,
74
+ taskResult,
75
+ baselineData?.get(taskId as TaskId),
76
+ );
77
+
78
+ results.push(budgetResult);
79
+ }
80
+
81
+ const passed = results.filter((r) => r.passed).length;
82
+ const failed = results.filter((r) => !r.passed).length;
83
+
84
+ return {
85
+ failed,
86
+ passed,
87
+ results,
88
+ total: results.length,
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Evaluate budgets for a single task
94
+ */
95
+ private evaluateTask(
96
+ taskId: TaskId,
97
+ budget: Budget,
98
+ actual: TaskResult,
99
+ baseline?: BaselineSummaryData,
100
+ ): BudgetResult {
101
+ const violations: BudgetViolation[] = [];
102
+
103
+ // Evaluate absolute budgets
104
+ if (budget.absolute) {
105
+ if (budget.absolute.maxTime !== undefined) {
106
+ if (actual.mean > budget.absolute.maxTime) {
107
+ violations.push({
108
+ actual: actual.mean,
109
+ delta:
110
+ (actual.mean - budget.absolute.maxTime) / budget.absolute.maxTime,
111
+ message: `Mean execution time ${BudgetEvaluator.formatTime(actual.mean)} exceeded budget of ${BudgetEvaluator.formatTime(budget.absolute.maxTime)} by ${BudgetEvaluator.formatPercentage((actual.mean - budget.absolute.maxTime) / budget.absolute.maxTime)}`,
112
+ threshold: budget.absolute.maxTime,
113
+ type: 'maxTime',
114
+ });
115
+ }
116
+ }
117
+
118
+ if (budget.absolute.minOpsPerSec !== undefined) {
119
+ if (actual.opsPerSecond < budget.absolute.minOpsPerSec) {
120
+ violations.push({
121
+ actual: actual.opsPerSecond,
122
+ delta:
123
+ (budget.absolute.minOpsPerSec - actual.opsPerSecond) /
124
+ budget.absolute.minOpsPerSec,
125
+ message: `Operations per second ${BudgetEvaluator.formatNumber(actual.opsPerSecond)} is below minimum of ${BudgetEvaluator.formatNumber(budget.absolute.minOpsPerSec)} by ${BudgetEvaluator.formatPercentage((budget.absolute.minOpsPerSec - actual.opsPerSecond) / budget.absolute.minOpsPerSec)}`,
126
+ threshold: budget.absolute.minOpsPerSec,
127
+ type: 'minOpsPerSec',
128
+ });
129
+ }
130
+ }
131
+
132
+ if (budget.absolute.maxP99 !== undefined && actual.p99 !== undefined) {
133
+ if (actual.p99 > budget.absolute.maxP99) {
134
+ violations.push({
135
+ actual: actual.p99,
136
+ delta:
137
+ (actual.p99 - budget.absolute.maxP99) / budget.absolute.maxP99,
138
+ message: `P99 latency ${BudgetEvaluator.formatTime(actual.p99)} exceeded budget of ${BudgetEvaluator.formatTime(budget.absolute.maxP99)} by ${BudgetEvaluator.formatPercentage((actual.p99 - budget.absolute.maxP99) / budget.absolute.maxP99)}`,
139
+ threshold: budget.absolute.maxP99,
140
+ type: 'maxP99',
141
+ });
142
+ }
143
+ }
144
+ }
145
+
146
+ // Evaluate relative budgets
147
+ if (budget.relative && baseline) {
148
+ if (budget.relative.maxRegression !== undefined) {
149
+ const regression = (actual.mean - baseline.mean) / baseline.mean;
150
+
151
+ if (regression > budget.relative.maxRegression) {
152
+ violations.push({
153
+ actual: regression,
154
+ delta: regression - budget.relative.maxRegression,
155
+ message: `Performance regressed by ${BudgetEvaluator.formatPercentage(regression)} exceeding maximum allowed regression of ${BudgetEvaluator.formatPercentage(budget.relative.maxRegression)}`,
156
+ threshold: budget.relative.maxRegression,
157
+ type: 'maxRegression',
158
+ });
159
+ }
160
+ }
161
+ }
162
+
163
+ return {
164
+ actual: {
165
+ mean: actual.mean,
166
+ opsPerSecond: actual.opsPerSecond,
167
+ p99: actual.p99,
168
+ },
169
+ baseline: baseline
170
+ ? {
171
+ mean: baseline.mean,
172
+ opsPerSecond: baseline.opsPerSecond,
173
+ p99: baseline.p99,
174
+ }
175
+ : undefined,
176
+ budget,
177
+ passed: violations.length === 0,
178
+ taskId,
179
+ violations,
180
+ };
181
+ }
182
+ }
@@ -56,7 +56,7 @@ const DEFAULT_CONFIG: ModestBenchConfig = {
56
56
  limitBy: 'iterations', // Default to limiting by iteration count
57
57
  metadata: {},
58
58
  outputDir: './benchmark-results',
59
- pattern: '**/*.bench.{js,ts,mjs,cjs,mts,cts}',
59
+ pattern: 'bench/**/*.bench.{js,ts,mjs,cjs,mts,cts}', // Search bench/ directory recursively
60
60
  quiet: false,
61
61
  reporterConfig: {},
62
62
  reporters: [getDefaultReporter()],
@@ -121,10 +121,16 @@ export class ModestBenchConfigurationManager implements ConfigurationManager {
121
121
 
122
122
  /**
123
123
  * Load configuration from various sources with precedence
124
+ *
125
+ * @param configPath - Optional path to configuration file
126
+ * @param cliArgs - Optional CLI arguments to merge
127
+ * @param commandDefaults - Command-specific defaults (fallback to
128
+ * DEFAULT_CONFIG)
124
129
  */
125
130
  async load(
126
131
  configPath?: string,
127
132
  cliArgs?: Record<string, unknown>,
133
+ commandDefaults?: Partial<ModestBenchConfig>,
128
134
  ): Promise<ModestBenchConfig> {
129
135
  try {
130
136
  // Create a fresh explorer for each load to avoid module caching issues
@@ -164,9 +170,13 @@ export class ModestBenchConfigurationManager implements ConfigurationManager {
164
170
 
165
171
  const fileConfig = (result?.config || {}) as Partial<ModestBenchConfig>;
166
172
 
167
- // 2. Merge: defaults <- file <- CLI args
173
+ // 2. Merge: command defaults <- file <- CLI args
174
+ // Use command-specific defaults if provided, otherwise use DEFAULT_CONFIG
175
+ const baseDefaults = commandDefaults
176
+ ? this.merge(DEFAULT_CONFIG, commandDefaults)
177
+ : DEFAULT_CONFIG;
168
178
  const normalizedCliArgs = cliArgs ? this.normalizeCliArgs(cliArgs) : {};
169
- const merged = this.merge(DEFAULT_CONFIG, fileConfig, normalizedCliArgs);
179
+ const merged = this.merge(baseDefaults, fileConfig, normalizedCliArgs);
170
180
 
171
181
  // 2.5. Apply smart defaults for limitBy if not explicitly provided
172
182
  const finalConfig = ModestBenchConfigurationManager.applySmartDefaults(
@@ -175,15 +185,20 @@ export class ModestBenchConfigurationManager implements ConfigurationManager {
175
185
  fileConfig,
176
186
  );
177
187
 
178
- // 3. Validate final configuration
179
- const validation = this.validate(finalConfig);
180
- if (!validation.valid) {
188
+ // 3. Validate final configuration and get transformed config
189
+ // The validation also transforms budgets from nested to flat format
190
+ const validation = safeParseConfig(finalConfig);
191
+ if (!validation.success) {
192
+ const errors = validation.error.issues.map((issue) => {
193
+ const path = issue.path.join('.');
194
+ return `${path ? `${path}: ` : ''}${issue.message}`;
195
+ });
181
196
  throw new ConfigValidationError(
182
- `Configuration validation failed: ${validation.errors.map((e) => e.message).join(', ')}`,
197
+ `Configuration validation failed: ${errors.join(', ')}`,
183
198
  );
184
199
  }
185
200
 
186
- return finalConfig;
201
+ return validation.data;
187
202
  } catch (error) {
188
203
  // Re-throw our custom errors
189
204
  if (
@@ -64,8 +64,7 @@ export class BenchmarkFileLoader implements FileLoader {
64
64
  // Handle empty patterns - use sensible defaults
65
65
  if (patterns.length === 0) {
66
66
  patterns = [
67
- `*${BENCHMARK_FILE_PATTERN}`, // top-level current directory
68
- `bench/*${BENCHMARK_FILE_PATTERN}`, // top-level bench/ directory
67
+ `bench/**/*${BENCHMARK_FILE_PATTERN}`, // bench/ directory (recursive)
69
68
  ];
70
69
  }
71
70
 
@@ -152,10 +151,8 @@ export class BenchmarkFileLoader implements FileLoader {
152
151
  default?: unknown;
153
152
  };
154
153
  } else {
155
- // Use native dynamic import for JavaScript files with cache busting
156
- // Add timestamp to prevent module caching issues across multiple loads
157
- const timestamp = Date.now();
158
- module = (await import(`${filePath}?t=${timestamp}`)) as {
154
+ // Use native dynamic import for JavaScript files
155
+ module = (await import(filePath)) as {
159
156
  [key: string]: unknown;
160
157
  default?: unknown;
161
158
  };