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,194 @@
1
+ /**
2
+ * Profile Parser Service
3
+ *
4
+ * Parses Chrome DevTools CPU profile format (*.cpuprofile files) generated by
5
+ * Node.js --cpu-prof flag. Extracts JavaScript function execution data
6
+ * including file paths, line numbers, hit counts, and percentages.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ import { readFile } from 'node:fs/promises';
12
+ import path from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ import type {
16
+ ProfiledFunction,
17
+ ProfileSummary,
18
+ RawProfileData,
19
+ } from '../../types/profiler.js';
20
+
21
+ interface CallFrame {
22
+ columnNumber: number;
23
+ functionName: string;
24
+ lineNumber: number;
25
+ scriptId: string;
26
+ url: string;
27
+ }
28
+
29
+ /**
30
+ * Chrome DevTools CPU profile format
31
+ */
32
+ interface CpuProfile {
33
+ endTime: number;
34
+ nodes: ProfileNode[];
35
+ samples?: number[];
36
+ startTime: number;
37
+ }
38
+
39
+ interface ProfileNode {
40
+ callFrame: CallFrame;
41
+ children?: number[];
42
+ hitCount?: number;
43
+ id: number;
44
+ }
45
+
46
+ /**
47
+ * Parse a CPU profile file
48
+ *
49
+ * @param profilePath - Path to *.cpuprofile file
50
+ * @returns Parsed profile data
51
+ */
52
+ export const parseProfile = async (
53
+ profilePath: string,
54
+ ): Promise<RawProfileData> => {
55
+ // Read and parse JSON profile
56
+ const content = await readFile(profilePath, 'utf-8');
57
+ const profile: CpuProfile = JSON.parse(content) as CpuProfile;
58
+
59
+ // Extract functions and calculate statistics
60
+ return parseCpuProfile(profile, profilePath);
61
+ };
62
+
63
+ /**
64
+ * Parse CPU profile data structure
65
+ */
66
+ const parseCpuProfile = (
67
+ profile: CpuProfile,
68
+ profilePath: string,
69
+ ): RawProfileData => {
70
+ const functions: ProfiledFunction[] = [];
71
+ let totalTicks = 0;
72
+
73
+ // Calculate total ticks from all node hit counts
74
+ for (const node of profile.nodes) {
75
+ totalTicks += node.hitCount || 0;
76
+ }
77
+
78
+ // Extract function information from nodes with hit counts
79
+ for (const node of profile.nodes) {
80
+ const hitCount = node.hitCount || 0;
81
+ if (hitCount === 0) {
82
+ continue; // Skip nodes with no samples
83
+ }
84
+
85
+ const { callFrame } = node;
86
+ const percentage = totalTicks > 0 ? (hitCount / totalTicks) * 100 : 0;
87
+
88
+ // Parse file path from URL
89
+ const filePath = parseFileUrl(callFrame.url);
90
+
91
+ // Determine category based on URL
92
+ const category = determineCategory(callFrame.url);
93
+
94
+ functions.push({
95
+ category,
96
+ file: filePath,
97
+ line: callFrame.lineNumber >= 0 ? callFrame.lineNumber + 1 : null, // DevTools uses 0-based line numbers
98
+ name: callFrame.functionName || '(anonymous)',
99
+ percentage,
100
+ ticks: hitCount,
101
+ });
102
+ }
103
+
104
+ // Calculate summary statistics
105
+ const javascriptTicks = functions
106
+ .filter((fn) => fn.category === 'JavaScript')
107
+ .reduce((sum, fn) => sum + fn.ticks, 0);
108
+
109
+ const cppTicks = functions
110
+ .filter((fn) => fn.category === 'C++')
111
+ .reduce((sum, fn) => sum + fn.ticks, 0);
112
+
113
+ const summary: ProfileSummary = {
114
+ cppTicks,
115
+ gcTicks: 0, // CPU profiles don't separate GC
116
+ javascriptTicks,
117
+ sharedLibraryTicks: 0,
118
+ totalTicks,
119
+ };
120
+
121
+ return {
122
+ functions,
123
+ logPath: profilePath,
124
+ summary,
125
+ totalTicks,
126
+ };
127
+ };
128
+
129
+ /**
130
+ * Parse file URL from call frame
131
+ */
132
+ const parseFileUrl = (url: string): string => {
133
+ if (!url) {
134
+ return '<unknown>';
135
+ }
136
+
137
+ // Handle file:// URLs
138
+ if (url.startsWith('file://')) {
139
+ try {
140
+ return fileURLToPath(url);
141
+ } catch {
142
+ // Fallback if URL is malformed
143
+ return path.normalize(url.replace('file://', ''));
144
+ }
145
+ }
146
+
147
+ // Handle node: internal modules
148
+ if (url.startsWith('node:')) {
149
+ return url;
150
+ }
151
+
152
+ // Handle plain paths
153
+ return path.normalize(url);
154
+ };
155
+
156
+ /**
157
+ * Determine function category based on URL
158
+ */
159
+ const determineCategory = (
160
+ url: string,
161
+ ): 'C++' | 'GC' | 'JavaScript' | 'Unknown' => {
162
+ if (!url) {
163
+ return 'Unknown';
164
+ }
165
+
166
+ // Node.js internals and native modules
167
+ if (url.startsWith('node:') || url.includes('[native code]')) {
168
+ return 'C++';
169
+ }
170
+
171
+ // eval'd code is JavaScript
172
+ if (url === '[eval]') {
173
+ return 'JavaScript';
174
+ }
175
+
176
+ // JavaScript files
177
+ if (
178
+ url.endsWith('.js') ||
179
+ url.endsWith('.mjs') ||
180
+ url.endsWith('.cjs') ||
181
+ url.endsWith('.ts') ||
182
+ url.endsWith('.mts') ||
183
+ url.endsWith('.cts')
184
+ ) {
185
+ return 'JavaScript';
186
+ }
187
+
188
+ // Default to JavaScript for file:// URLs
189
+ if (url.startsWith('file://')) {
190
+ return 'JavaScript';
191
+ }
192
+
193
+ return 'Unknown';
194
+ };
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Profile Runner Service
3
+ *
4
+ * Executes commands with Node.js CPU profiling enabled and captures profiler
5
+ * output to *.cpuprofile files in .modestbench/profiles/.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ import { glob } from 'glob';
11
+ import { spawn } from 'node:child_process';
12
+ import { mkdir, stat } from 'node:fs/promises';
13
+ import { join } from 'node:path';
14
+
15
+ /**
16
+ * Options for running with profiling
17
+ */
18
+ interface RunOptions {
19
+ /** Working directory */
20
+ cwd?: string;
21
+
22
+ /** Environment variables */
23
+ env?: NodeJS.ProcessEnv;
24
+
25
+ /** Timeout in milliseconds */
26
+ timeout?: number;
27
+ }
28
+
29
+ /**
30
+ * Run a command with Node.js profiling enabled
31
+ *
32
+ * @param command - Command to run (e.g., "npm test")
33
+ * @param options - Execution options
34
+ * @returns Path to generated *.cpuprofile file
35
+ */
36
+ export const runWithProfiling = async (
37
+ command: string,
38
+ options: RunOptions = {},
39
+ ): Promise<string> => {
40
+ const cwd = options.cwd || process.cwd();
41
+
42
+ // Create profiles directory
43
+ const profilesDir = join(cwd, '.modestbench', 'profiles');
44
+ await mkdir(profilesDir, { recursive: true });
45
+
46
+ // Run command with NODE_OPTIONS="--cpu-prof --cpu-prof-dir=..."
47
+ const proc = spawn(command, {
48
+ cwd,
49
+ env: {
50
+ ...process.env,
51
+ ...options.env,
52
+ NODE_OPTIONS: `--cpu-prof --cpu-prof-dir=${profilesDir}`,
53
+ },
54
+ shell: true,
55
+ stdio: 'inherit',
56
+ });
57
+
58
+ // Wait for process to complete
59
+ await new Promise<void>((resolve, reject) => {
60
+ const timeout = options.timeout
61
+ ? setTimeout(() => {
62
+ proc.kill();
63
+ reject(
64
+ new Error(`Profile command timed out after ${options.timeout}ms`),
65
+ );
66
+ }, options.timeout)
67
+ : null;
68
+
69
+ proc.on('close', (code) => {
70
+ if (timeout) {
71
+ clearTimeout(timeout);
72
+ }
73
+ if (code === 0 || code === null) {
74
+ resolve();
75
+ } else {
76
+ reject(new Error(`Profile command exited with code ${code}`));
77
+ }
78
+ });
79
+
80
+ proc.on('error', (err) => {
81
+ if (timeout) {
82
+ clearTimeout(timeout);
83
+ }
84
+ reject(err);
85
+ });
86
+ });
87
+
88
+ // Find generated *.cpuprofile file in profiles directory
89
+ const profileFiles = await glob('*.cpuprofile', { cwd: profilesDir });
90
+
91
+ if (profileFiles.length === 0) {
92
+ throw new Error(
93
+ 'No CPU profile generated. Ensure the command runs Node.js code.',
94
+ );
95
+ }
96
+
97
+ // Return most recent profile file
98
+ return await getMostRecentFile(profileFiles, profilesDir);
99
+ };
100
+
101
+ /**
102
+ * Get the most recently modified file from a list
103
+ */
104
+ const getMostRecentFile = async (
105
+ files: string[],
106
+ cwd: string,
107
+ ): Promise<string> => {
108
+ let mostRecent = files[0];
109
+ let mostRecentTime = (await stat(`${cwd}/${mostRecent}`)).mtimeMs;
110
+
111
+ for (const file of files.slice(1)) {
112
+ const filePath = `${cwd}/${file}`;
113
+ const fileTime = (await stat(filePath)).mtimeMs;
114
+ if (fileTime > mostRecentTime) {
115
+ mostRecent = file;
116
+ mostRecentTime = fileTime;
117
+ }
118
+ }
119
+
120
+ return `${cwd}/${mostRecent}`;
121
+ };
@@ -32,12 +32,16 @@ interface ProgressMetrics {
32
32
  export class ModestBenchProgressManager implements ProgressManager {
33
33
  private callbacks: ProgressCallback[] = [];
34
34
 
35
+ private lastMetricsUpdate = 0;
36
+
35
37
  private lastUpdate = 0;
36
38
 
37
39
  private readonly maxRecentTimings = 10;
38
40
 
39
41
  private metrics: null | ProgressMetrics = null;
40
42
 
43
+ private readonly metricsUpdateThrottleMs = 1000; // Throttle ETA calculations (1 second)
44
+
41
45
  private state: ProgressState;
42
46
 
43
47
  private readonly updateThrottleMs = 100; // Limit updates to avoid spam
@@ -53,6 +57,8 @@ export class ModestBenchProgressManager implements ProgressManager {
53
57
  this.callbacks = [];
54
58
  this.metrics = null;
55
59
  this.state = this.createInitialState();
60
+ this.lastUpdate = 0;
61
+ this.lastMetricsUpdate = 0;
56
62
  }
57
63
 
58
64
  /**
@@ -240,6 +246,7 @@ export class ModestBenchProgressManager implements ProgressManager {
240
246
  };
241
247
 
242
248
  this.lastUpdate = Date.now();
249
+ this.lastMetricsUpdate = Date.now();
243
250
  this.notifyCallbacks();
244
251
  }
245
252
 
@@ -314,8 +321,11 @@ export class ModestBenchProgressManager implements ProgressManager {
314
321
  percentage: this.calculatePercentage(updates),
315
322
  };
316
323
 
317
- // Update metrics for completion estimation
318
- this.updateMetrics(now);
324
+ // Update metrics for completion estimation (throttled separately for ETA)
325
+ if (now - this.lastMetricsUpdate >= this.metricsUpdateThrottleMs) {
326
+ this.updateMetrics(now);
327
+ this.lastMetricsUpdate = now;
328
+ }
319
329
 
320
330
  this.lastUpdate = now;
321
331
  this.notifyCallbacks();
@@ -33,74 +33,10 @@ export abstract class BaseReporter implements Reporter {
33
33
  this.options = options;
34
34
  }
35
35
 
36
- /**
37
- * Get reporter name
38
- */
39
- getName(): string {
40
- return this.name;
41
- }
42
-
43
- /**
44
- * Get reporter options
45
- */
46
- getOptions(): Record<string, unknown> {
47
- return { ...this.options };
48
- }
49
-
50
- /**
51
- * Called when benchmark run completes
52
- */
53
- abstract onEnd(run: BenchmarkRun): Promise<void> | void;
54
-
55
- /**
56
- * Called when an error occurs
57
- */
58
- abstract onError(error: Error): Promise<void> | void;
59
-
60
- /**
61
- * Called when a file completes
62
- */
63
- abstract onFileEnd(result: FileResult): Promise<void> | void;
64
-
65
- /**
66
- * Called when a file starts execution
67
- */
68
- abstract onFileStart(file: string): Promise<void> | void;
69
-
70
- /**
71
- * Called for progress updates
72
- */
73
- abstract onProgress(state: ProgressState): Promise<void> | void;
74
-
75
- /**
76
- * Called when benchmark run starts
77
- */
78
- abstract onStart(run: BenchmarkRun): Promise<void> | void;
79
-
80
- /**
81
- * Called when a suite completes
82
- */
83
- abstract onSuiteEnd(result: SuiteResult): Promise<void> | void;
84
-
85
- /**
86
- * Called when a suite starts execution
87
- */
88
- abstract onSuiteStart(suite: string): Promise<void> | void;
89
-
90
- /**
91
- * Called when a task completes
92
- */
93
- abstract onTaskResult(result: TaskResult): Promise<void> | void;
94
-
95
- /**
96
- * Called when a task starts execution
97
- */
98
- abstract onTaskStart(task: string): Promise<void> | void;
99
-
100
36
  /**
101
37
  * Utility method to format duration in human-readable format
102
38
  */
103
- protected formatDuration(nanoseconds: number): string {
39
+ protected static formatDuration(this: void, nanoseconds: number): string {
104
40
  if (nanoseconds < 1000) {
105
41
  return `${nanoseconds.toFixed(2)}ns`;
106
42
  } else if (nanoseconds < 1000000) {
@@ -115,7 +51,10 @@ export abstract class BaseReporter implements Reporter {
115
51
  /**
116
52
  * Utility method to format operations per second
117
53
  */
118
- protected formatOpsPerSecond(opsPerSecond: number): string {
54
+ protected static formatOpsPerSecond(
55
+ this: void,
56
+ opsPerSecond: number,
57
+ ): string {
119
58
  if (opsPerSecond < 1000) {
120
59
  return `${opsPerSecond.toFixed(2)} ops/sec`;
121
60
  } else if (opsPerSecond < 1000000) {
@@ -130,23 +69,43 @@ export abstract class BaseReporter implements Reporter {
130
69
  /**
131
70
  * Utility method to format percentage
132
71
  */
133
- protected formatPercentage(value: number): string {
72
+ protected static formatPercentage(this: void, value: number): string {
134
73
  return `${value.toFixed(2)}%`;
135
74
  }
136
75
 
137
76
  /**
138
- * Utility method to safely handle async operations
77
+ * Get reporter name
139
78
  */
140
- protected async safeAsync<T>(operation: () => Promise<T>): Promise<null | T> {
141
- try {
142
- return await operation();
143
- } catch (error) {
144
- await this.onError(
145
- error instanceof Error ? error : new Error(String(error)),
146
- );
147
- return null;
148
- }
79
+ getName(): string {
80
+ return this.name;
81
+ }
82
+
83
+ /**
84
+ * Get reporter options
85
+ */
86
+ getOptions(): Record<string, unknown> {
87
+ return { ...this.options };
149
88
  }
89
+
90
+ /**
91
+ * Called when benchmark run completes
92
+ */
93
+ abstract onEnd(run: BenchmarkRun): Promise<void> | void;
94
+
95
+ /**
96
+ * Called when an error occurs
97
+ */
98
+ abstract onError(error: Error): Promise<void> | void;
99
+
100
+ /**
101
+ * Called when benchmark run starts
102
+ */
103
+ abstract onStart(run: BenchmarkRun): Promise<void> | void;
104
+
105
+ /**
106
+ * Called when a task completes
107
+ */
108
+ abstract onTaskResult(result: TaskResult): Promise<void> | void;
150
109
  }
151
110
 
152
111
  /**
@@ -162,6 +121,8 @@ export class CompositeReporter extends BaseReporter {
162
121
 
163
122
  /**
164
123
  * Add a reporter to the composite
124
+ *
125
+ * Intended for programmatic usage.
165
126
  */
166
127
  addReporter(reporter: Reporter): void {
167
128
  this.reporters.push(reporter);
@@ -169,16 +130,18 @@ export class CompositeReporter extends BaseReporter {
169
130
 
170
131
  /**
171
132
  * Get all reporters in the composite
133
+ *
134
+ * Intended for programmatic usage.
172
135
  */
173
136
  getReporters(): Reporter[] {
174
137
  return [...this.reporters];
175
138
  }
176
139
 
177
- async onEnd(run: BenchmarkRun): Promise<void> {
140
+ override async onEnd(run: BenchmarkRun): Promise<void> {
178
141
  await this.broadcastAsync('onEnd', run);
179
142
  }
180
143
 
181
- async onError(error: Error): Promise<void> {
144
+ override async onError(error: Error): Promise<void> {
182
145
  await this.broadcastAsync('onError', error);
183
146
  }
184
147
 
@@ -194,7 +157,7 @@ export class CompositeReporter extends BaseReporter {
194
157
  await this.broadcastAsync('onProgress', state);
195
158
  }
196
159
 
197
- async onStart(run: BenchmarkRun): Promise<void> {
160
+ override async onStart(run: BenchmarkRun): Promise<void> {
198
161
  await this.broadcastAsync('onStart', run);
199
162
  }
200
163
 
@@ -206,7 +169,7 @@ export class CompositeReporter extends BaseReporter {
206
169
  await this.broadcastAsync('onSuiteStart', suite);
207
170
  }
208
171
 
209
- async onTaskResult(result: TaskResult): Promise<void> {
172
+ override async onTaskResult(result: TaskResult): Promise<void> {
210
173
  await this.broadcastAsync('onTaskResult', result);
211
174
  }
212
175
 
@@ -216,6 +179,8 @@ export class CompositeReporter extends BaseReporter {
216
179
 
217
180
  /**
218
181
  * Remove a reporter from the composite
182
+ *
183
+ * Intended for programmatic usage.
219
184
  */
220
185
  removeReporter(reporter: Reporter): boolean {
221
186
  const index = this.reporters.indexOf(reporter);