modestbench 0.2.0 → 0.3.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 (357) hide show
  1. package/CHANGELOG.md +27 -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 +99 -166
  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 +99 -166
  30. package/dist/cli/commands/init.js.map +1 -1
  31. package/dist/cli/commands/run.cjs +146 -127
  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 +145 -93
  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 +114 -23
  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 +115 -24
  78. package/dist/core/engine.js.map +1 -1
  79. package/dist/core/engines/accurate-engine.cjs +171 -36
  80. package/dist/core/engines/accurate-engine.cjs.map +1 -1
  81. package/dist/core/engines/accurate-engine.d.cts +5 -0
  82. package/dist/core/engines/accurate-engine.d.cts.map +1 -1
  83. package/dist/core/engines/accurate-engine.d.ts +5 -0
  84. package/dist/core/engines/accurate-engine.d.ts.map +1 -1
  85. package/dist/core/engines/accurate-engine.js +171 -36
  86. package/dist/core/engines/accurate-engine.js.map +1 -1
  87. package/dist/core/engines/tinybench-engine.cjs +3 -2
  88. package/dist/core/engines/tinybench-engine.cjs.map +1 -1
  89. package/dist/core/engines/tinybench-engine.d.cts.map +1 -1
  90. package/dist/core/engines/tinybench-engine.d.ts.map +1 -1
  91. package/dist/core/engines/tinybench-engine.js +3 -2
  92. package/dist/core/engines/tinybench-engine.js.map +1 -1
  93. package/dist/core/output-path-resolver.cjs +8 -1
  94. package/dist/core/output-path-resolver.cjs.map +1 -1
  95. package/dist/core/output-path-resolver.d.cts.map +1 -1
  96. package/dist/core/output-path-resolver.d.ts.map +1 -1
  97. package/dist/core/output-path-resolver.js +9 -2
  98. package/dist/core/output-path-resolver.js.map +1 -1
  99. package/dist/errors/base.cjs +12 -3
  100. package/dist/errors/base.cjs.map +1 -1
  101. package/dist/errors/base.d.cts +7 -0
  102. package/dist/errors/base.d.cts.map +1 -1
  103. package/dist/errors/base.d.ts +7 -0
  104. package/dist/errors/base.d.ts.map +1 -1
  105. package/dist/errors/base.js +10 -2
  106. package/dist/errors/base.js.map +1 -1
  107. package/dist/errors/budget.cjs +37 -0
  108. package/dist/errors/budget.cjs.map +1 -0
  109. package/dist/errors/budget.d.cts +31 -0
  110. package/dist/errors/budget.d.cts.map +1 -0
  111. package/dist/errors/budget.d.ts +31 -0
  112. package/dist/errors/budget.d.ts.map +1 -0
  113. package/dist/errors/budget.js +33 -0
  114. package/dist/errors/budget.js.map +1 -0
  115. package/dist/errors/index.cjs +4 -1
  116. package/dist/errors/index.cjs.map +1 -1
  117. package/dist/errors/index.d.cts +1 -0
  118. package/dist/errors/index.d.cts.map +1 -1
  119. package/dist/errors/index.d.ts +1 -0
  120. package/dist/errors/index.d.ts.map +1 -1
  121. package/dist/errors/index.js +2 -0
  122. package/dist/errors/index.js.map +1 -1
  123. package/dist/index.cjs +13 -1
  124. package/dist/index.cjs.map +1 -1
  125. package/dist/index.d.cts +5 -0
  126. package/dist/index.d.cts.map +1 -1
  127. package/dist/index.d.ts +5 -0
  128. package/dist/index.d.ts.map +1 -1
  129. package/dist/index.js +7 -0
  130. package/dist/index.js.map +1 -1
  131. package/dist/reporters/csv.cjs +37 -17
  132. package/dist/reporters/csv.cjs.map +1 -1
  133. package/dist/reporters/csv.d.cts +3 -6
  134. package/dist/reporters/csv.d.cts.map +1 -1
  135. package/dist/reporters/csv.d.ts +3 -6
  136. package/dist/reporters/csv.d.ts.map +1 -1
  137. package/dist/reporters/csv.js +37 -17
  138. package/dist/reporters/csv.js.map +1 -1
  139. package/dist/reporters/human.cjs +290 -67
  140. package/dist/reporters/human.cjs.map +1 -1
  141. package/dist/reporters/human.d.cts +25 -13
  142. package/dist/reporters/human.d.cts.map +1 -1
  143. package/dist/reporters/human.d.ts +25 -13
  144. package/dist/reporters/human.d.ts.map +1 -1
  145. package/dist/reporters/human.js +290 -67
  146. package/dist/reporters/human.js.map +1 -1
  147. package/dist/reporters/json.cjs +23 -48
  148. package/dist/reporters/json.cjs.map +1 -1
  149. package/dist/reporters/json.d.cts +2 -28
  150. package/dist/reporters/json.d.cts.map +1 -1
  151. package/dist/reporters/json.d.ts +2 -28
  152. package/dist/reporters/json.d.ts.map +1 -1
  153. package/dist/reporters/json.js +25 -50
  154. package/dist/reporters/json.js.map +1 -1
  155. package/dist/reporters/profile-human.cjs +154 -0
  156. package/dist/reporters/profile-human.cjs.map +1 -0
  157. package/dist/reporters/profile-human.d.cts +44 -0
  158. package/dist/reporters/profile-human.d.cts.map +1 -0
  159. package/dist/reporters/profile-human.d.ts +44 -0
  160. package/dist/reporters/profile-human.d.ts.map +1 -0
  161. package/dist/reporters/profile-human.js +147 -0
  162. package/dist/reporters/profile-human.js.map +1 -0
  163. package/dist/reporters/simple.cjs +67 -45
  164. package/dist/reporters/simple.cjs.map +1 -1
  165. package/dist/reporters/simple.d.cts +14 -14
  166. package/dist/reporters/simple.d.cts.map +1 -1
  167. package/dist/reporters/simple.d.ts +14 -14
  168. package/dist/reporters/simple.d.ts.map +1 -1
  169. package/dist/reporters/simple.js +67 -45
  170. package/dist/reporters/simple.js.map +1 -1
  171. package/dist/schema/modestbench-config.schema.json +153 -0
  172. package/dist/services/baseline-storage.cjs +151 -0
  173. package/dist/services/baseline-storage.cjs.map +1 -0
  174. package/dist/services/baseline-storage.d.cts +55 -0
  175. package/dist/services/baseline-storage.d.cts.map +1 -0
  176. package/dist/services/baseline-storage.d.ts +55 -0
  177. package/dist/services/baseline-storage.d.ts.map +1 -0
  178. package/dist/services/baseline-storage.js +147 -0
  179. package/dist/services/baseline-storage.js.map +1 -0
  180. package/dist/services/budget-evaluator.cjs +146 -0
  181. package/dist/services/budget-evaluator.cjs.map +1 -0
  182. package/dist/services/budget-evaluator.d.cts +29 -0
  183. package/dist/services/budget-evaluator.d.cts.map +1 -0
  184. package/dist/services/budget-evaluator.d.ts +29 -0
  185. package/dist/services/budget-evaluator.d.ts.map +1 -0
  186. package/dist/services/budget-evaluator.js +142 -0
  187. package/dist/services/budget-evaluator.js.map +1 -0
  188. package/dist/services/config-manager.cjs +24 -10
  189. package/dist/services/config-manager.cjs.map +1 -1
  190. package/dist/services/config-manager.d.cts +6 -1
  191. package/dist/services/config-manager.d.cts.map +1 -1
  192. package/dist/services/config-manager.d.ts +6 -1
  193. package/dist/services/config-manager.d.ts.map +1 -1
  194. package/dist/services/config-manager.js +24 -10
  195. package/dist/services/config-manager.js.map +1 -1
  196. package/dist/services/file-loader.cjs +3 -6
  197. package/dist/services/file-loader.cjs.map +1 -1
  198. package/dist/services/file-loader.d.cts.map +1 -1
  199. package/dist/services/file-loader.d.ts.map +1 -1
  200. package/dist/services/file-loader.js +3 -6
  201. package/dist/services/file-loader.js.map +1 -1
  202. package/dist/services/profiler/profile-filter.cjs +116 -0
  203. package/dist/services/profiler/profile-filter.cjs.map +1 -0
  204. package/dist/services/profiler/profile-filter.d.cts +20 -0
  205. package/dist/services/profiler/profile-filter.d.cts.map +1 -0
  206. package/dist/services/profiler/profile-filter.d.ts +20 -0
  207. package/dist/services/profiler/profile-filter.d.ts.map +1 -0
  208. package/dist/services/profiler/profile-filter.js +112 -0
  209. package/dist/services/profiler/profile-filter.js.map +1 -0
  210. package/dist/services/profiler/profile-parser.cjs +139 -0
  211. package/dist/services/profiler/profile-parser.cjs.map +1 -0
  212. package/dist/services/profiler/profile-parser.d.cts +18 -0
  213. package/dist/services/profiler/profile-parser.d.cts.map +1 -0
  214. package/dist/services/profiler/profile-parser.d.ts +18 -0
  215. package/dist/services/profiler/profile-parser.d.ts.map +1 -0
  216. package/dist/services/profiler/profile-parser.js +132 -0
  217. package/dist/services/profiler/profile-parser.js.map +1 -0
  218. package/dist/services/profiler/profile-runner.cjs +90 -0
  219. package/dist/services/profiler/profile-runner.cjs.map +1 -0
  220. package/dist/services/profiler/profile-runner.d.cts +29 -0
  221. package/dist/services/profiler/profile-runner.d.cts.map +1 -0
  222. package/dist/services/profiler/profile-runner.d.ts +29 -0
  223. package/dist/services/profiler/profile-runner.d.ts.map +1 -0
  224. package/dist/services/profiler/profile-runner.js +86 -0
  225. package/dist/services/profiler/profile-runner.js.map +1 -0
  226. package/dist/services/progress-manager.cjs +10 -2
  227. package/dist/services/progress-manager.cjs.map +1 -1
  228. package/dist/services/progress-manager.d.cts +2 -0
  229. package/dist/services/progress-manager.d.cts.map +1 -1
  230. package/dist/services/progress-manager.d.ts +2 -0
  231. package/dist/services/progress-manager.d.ts.map +1 -1
  232. package/dist/services/progress-manager.js +10 -2
  233. package/dist/services/progress-manager.js.map +1 -1
  234. package/dist/services/reporter-registry.cjs +18 -24
  235. package/dist/services/reporter-registry.cjs.map +1 -1
  236. package/dist/services/reporter-registry.d.cts +18 -40
  237. package/dist/services/reporter-registry.d.cts.map +1 -1
  238. package/dist/services/reporter-registry.d.ts +18 -40
  239. package/dist/services/reporter-registry.d.ts.map +1 -1
  240. package/dist/services/reporter-registry.js +18 -24
  241. package/dist/services/reporter-registry.js.map +1 -1
  242. package/dist/types/budgets.cjs +8 -0
  243. package/dist/types/budgets.cjs.map +1 -0
  244. package/dist/types/budgets.d.cts +149 -0
  245. package/dist/types/budgets.d.cts.map +1 -0
  246. package/dist/types/budgets.d.ts +149 -0
  247. package/dist/types/budgets.d.ts.map +1 -0
  248. package/dist/types/budgets.js +7 -0
  249. package/dist/types/budgets.js.map +1 -0
  250. package/dist/types/cli.cjs +2 -11
  251. package/dist/types/cli.cjs.map +1 -1
  252. package/dist/types/cli.d.cts +3 -227
  253. package/dist/types/cli.d.cts.map +1 -1
  254. package/dist/types/cli.d.ts +3 -227
  255. package/dist/types/cli.d.ts.map +1 -1
  256. package/dist/types/cli.js +2 -11
  257. package/dist/types/cli.js.map +1 -1
  258. package/dist/types/core.cjs +6 -1
  259. package/dist/types/core.cjs.map +1 -1
  260. package/dist/types/core.d.cts +15 -2
  261. package/dist/types/core.d.cts.map +1 -1
  262. package/dist/types/core.d.ts +15 -2
  263. package/dist/types/core.d.ts.map +1 -1
  264. package/dist/types/core.js +2 -1
  265. package/dist/types/core.js.map +1 -1
  266. package/dist/types/index.cjs +5 -0
  267. package/dist/types/index.cjs.map +1 -1
  268. package/dist/types/index.d.cts +2 -0
  269. package/dist/types/index.d.cts.map +1 -1
  270. package/dist/types/index.d.ts +2 -0
  271. package/dist/types/index.d.ts.map +1 -1
  272. package/dist/types/index.js +2 -0
  273. package/dist/types/index.js.map +1 -1
  274. package/dist/types/interfaces.d.cts +19 -8
  275. package/dist/types/interfaces.d.cts.map +1 -1
  276. package/dist/types/interfaces.d.ts +19 -8
  277. package/dist/types/interfaces.d.ts.map +1 -1
  278. package/dist/types/profiler.cjs +11 -0
  279. package/dist/types/profiler.cjs.map +1 -0
  280. package/dist/types/profiler.d.cts +102 -0
  281. package/dist/types/profiler.d.cts.map +1 -0
  282. package/dist/types/profiler.d.ts +102 -0
  283. package/dist/types/profiler.d.ts.map +1 -0
  284. package/dist/types/profiler.js +10 -0
  285. package/dist/types/profiler.js.map +1 -0
  286. package/dist/types/utility.cjs.map +1 -1
  287. package/dist/types/utility.d.cts +0 -8
  288. package/dist/types/utility.d.cts.map +1 -1
  289. package/dist/types/utility.d.ts +0 -8
  290. package/dist/types/utility.d.ts.map +1 -1
  291. package/dist/types/utility.js.map +1 -1
  292. package/dist/utils/identifiers.cjs +32 -0
  293. package/dist/utils/identifiers.cjs.map +1 -0
  294. package/dist/utils/identifiers.d.cts +32 -0
  295. package/dist/utils/identifiers.d.cts.map +1 -0
  296. package/dist/utils/identifiers.d.ts +32 -0
  297. package/dist/utils/identifiers.d.ts.map +1 -0
  298. package/dist/utils/identifiers.js +27 -0
  299. package/dist/utils/identifiers.js.map +1 -0
  300. package/dist/utils/package.cjs +40 -0
  301. package/dist/utils/package.cjs.map +1 -0
  302. package/dist/utils/package.d.cts +15 -0
  303. package/dist/utils/package.d.cts.map +1 -0
  304. package/dist/utils/package.d.ts +15 -0
  305. package/dist/utils/package.d.ts.map +1 -0
  306. package/dist/utils/package.js +33 -0
  307. package/dist/utils/package.js.map +1 -0
  308. package/dist/utils/type-guards.cjs +48 -0
  309. package/dist/utils/type-guards.cjs.map +1 -0
  310. package/dist/utils/type-guards.d.cts +22 -0
  311. package/dist/utils/type-guards.d.cts.map +1 -0
  312. package/dist/utils/type-guards.d.ts +22 -0
  313. package/dist/utils/type-guards.d.ts.map +1 -0
  314. package/dist/utils/type-guards.js +43 -0
  315. package/dist/utils/type-guards.js.map +1 -0
  316. package/package.json +18 -19
  317. package/src/cli/commands/analyze.ts +101 -0
  318. package/src/cli/commands/baseline.ts +577 -0
  319. package/src/cli/commands/history.ts +1 -1
  320. package/src/cli/commands/init.ts +116 -194
  321. package/src/cli/commands/run.ts +183 -113
  322. package/src/cli/index.ts +425 -183
  323. package/src/config/budget-schema.ts +189 -0
  324. package/src/config/schema.ts +260 -1
  325. package/src/constants.ts +53 -1
  326. package/src/core/engine.ts +169 -22
  327. package/src/core/engines/accurate-engine.ts +195 -44
  328. package/src/core/engines/tinybench-engine.ts +3 -2
  329. package/src/core/output-path-resolver.ts +10 -2
  330. package/src/errors/base.ts +11 -2
  331. package/src/errors/budget.ts +38 -0
  332. package/src/errors/index.ts +3 -0
  333. package/src/index.ts +9 -0
  334. package/src/reporters/csv.ts +54 -25
  335. package/src/reporters/human.ts +434 -115
  336. package/src/reporters/json.ts +26 -71
  337. package/src/reporters/profile-human.ts +210 -0
  338. package/src/reporters/simple.ts +88 -54
  339. package/src/services/baseline-storage.ts +199 -0
  340. package/src/services/budget-evaluator.ts +182 -0
  341. package/src/services/config-manager.ts +24 -9
  342. package/src/services/file-loader.ts +3 -6
  343. package/src/services/profiler/profile-filter.ts +147 -0
  344. package/src/services/profiler/profile-parser.ts +194 -0
  345. package/src/services/profiler/profile-runner.ts +121 -0
  346. package/src/services/progress-manager.ts +12 -2
  347. package/src/services/reporter-registry.ts +46 -81
  348. package/src/types/budgets.ts +180 -0
  349. package/src/types/cli.ts +5 -238
  350. package/src/types/core.ts +52 -10
  351. package/src/types/index.ts +5 -0
  352. package/src/types/interfaces.ts +24 -6
  353. package/src/types/profiler.ts +135 -0
  354. package/src/types/utility.ts +0 -10
  355. package/src/utils/identifiers.ts +58 -0
  356. package/src/utils/package.ts +35 -0
  357. package/src/utils/type-guards.ts +51 -0
@@ -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
+ }
@@ -55,8 +55,8 @@ const DEFAULT_CONFIG: ModestBenchConfig = {
55
55
  iterations: 100, // Sufficient iterations for reliable statistics
56
56
  limitBy: 'iterations', // Default to limiting by iteration count
57
57
  metadata: {},
58
- outputDir: './benchmark-results',
59
- pattern: '**/*.bench.{js,ts,mjs,cjs,mts,cts}',
58
+ outputDir: '.modestbench',
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
  };
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Profile Filter Service
3
+ *
4
+ * Filters and sorts profiled functions based on configuration. Implements smart
5
+ * detection to focus on user code by excluding node_modules and Node.js
6
+ * internals.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ import { minimatch } from 'minimatch';
12
+
13
+ import type {
14
+ FilteredProfileData,
15
+ ProfileConfig,
16
+ ProfiledFunction,
17
+ RawProfileData,
18
+ } from '../../types/profiler.js';
19
+
20
+ /**
21
+ * Filter profile data based on configuration
22
+ *
23
+ * @param data - Raw profile data
24
+ * @param config - Filter configuration
25
+ * @param packageRoot - Package root directory for smart detection
26
+ * @returns Filtered profile data
27
+ */
28
+ export const filterProfile = (
29
+ data: RawProfileData,
30
+ config: ProfileConfig,
31
+ packageRoot: string,
32
+ ): FilteredProfileData => {
33
+ let filtered = data.functions.filter((fn) => {
34
+ // Only JavaScript functions
35
+ if (fn.category !== 'JavaScript') {
36
+ return false;
37
+ }
38
+
39
+ // Apply smart detection if enabled
40
+ if (config.smartDetection && !config.focus?.length) {
41
+ if (!isUserCode(fn.file, packageRoot)) {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ // Apply focus patterns (if provided, overrides smart detection)
47
+ if (config.focus?.length) {
48
+ if (!matchesAnyPattern(fn.file, config.focus)) {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ // Apply exclude patterns (always applied if provided)
54
+ if (config.exclude?.length) {
55
+ if (matchesAnyPattern(fn.file, config.exclude)) {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ return true;
61
+ });
62
+
63
+ // Save count of user functions before percentage filtering
64
+ const totalUserFunctions = filtered.length;
65
+
66
+ // Apply percentage threshold
67
+ const minPercent = config.minExecutionPercent ?? 0.5;
68
+ filtered = filtered.filter((fn) => fn.percentage >= minPercent);
69
+
70
+ // Sort by percentage (highest first)
71
+ filtered.sort((a, b) => b.percentage - a.percentage);
72
+
73
+ // Limit to topN
74
+ const topN = config.topN ?? 25;
75
+ filtered = filtered.slice(0, topN);
76
+
77
+ // Group by file if requested
78
+ let groupedByFile: Map<string, ProfiledFunction[]> | undefined;
79
+ if (config.groupByFile) {
80
+ groupedByFile = groupByFile(filtered);
81
+ }
82
+
83
+ return {
84
+ functions: filtered,
85
+ groupedByFile,
86
+ minExecutionPercent: minPercent,
87
+ summary: data.summary,
88
+ totalFiltered: totalUserFunctions,
89
+ totalShown: filtered.length,
90
+ totalTicks: data.totalTicks,
91
+ };
92
+ };
93
+
94
+ /**
95
+ * Check if a file path is user code (not node_modules or internals)
96
+ */
97
+ const isUserCode = (filePath: string, packageRoot: string): boolean => {
98
+ // Exclude node_modules
99
+ if (
100
+ filePath.includes('/node_modules/') ||
101
+ filePath.includes('\\node_modules\\')
102
+ ) {
103
+ return false;
104
+ }
105
+
106
+ // Exclude Node.js internals
107
+ if (filePath.startsWith('node:') || filePath.startsWith('internal/')) {
108
+ return false;
109
+ }
110
+
111
+ // Allow <unknown> files (could be eval'd code or other user code without file paths)
112
+ if (filePath === '<unknown>' || filePath === '[eval]') {
113
+ return true;
114
+ }
115
+
116
+ // Must be within package root
117
+ return filePath.startsWith(packageRoot);
118
+ };
119
+
120
+ /**
121
+ * Check if a file path matches any of the given glob patterns
122
+ */
123
+ const matchesAnyPattern = (filePath: string, patterns: string[]): boolean => {
124
+ return patterns.some((pattern) => minimatch(filePath, pattern));
125
+ };
126
+
127
+ /**
128
+ * Group functions by file path
129
+ */
130
+ const groupByFile = (
131
+ functions: ProfiledFunction[],
132
+ ): Map<string, ProfiledFunction[]> => {
133
+ const grouped = new Map<string, ProfiledFunction[]>();
134
+
135
+ for (const fn of functions) {
136
+ const existing = grouped.get(fn.file) || [];
137
+ existing.push(fn);
138
+ grouped.set(fn.file, existing);
139
+ }
140
+
141
+ // Sort functions within each file by percentage
142
+ for (const [, fns] of grouped.entries()) {
143
+ fns.sort((a, b) => b.percentage - a.percentage);
144
+ }
145
+
146
+ return grouped;
147
+ };