@hydranium/cli 1.0.0-next.10

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 (267) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +240 -0
  3. package/heap-analysis/analyze-heap.mjs +798 -0
  4. package/heap-analysis/heap/core/aggregate.mjs +152 -0
  5. package/heap-analysis/heap/core/bucket-label.mjs +23 -0
  6. package/heap-analysis/heap/core/cutpoints.mjs +51 -0
  7. package/heap-analysis/heap/core/diff.mjs +56 -0
  8. package/heap-analysis/heap/core/format.mjs +47 -0
  9. package/heap-analysis/heap/core/holders.mjs +80 -0
  10. package/heap-analysis/heap/core/load.mjs +46 -0
  11. package/heap-analysis/heap/core/report.mjs +68 -0
  12. package/heap-analysis/heap/core/strings.mjs +80 -0
  13. package/heap-analysis/heap/langium/concepts.mjs +230 -0
  14. package/heap-analysis/heap/langium/glsp.classifier.mjs +80 -0
  15. package/heap-analysis/heap/langium/langium.classifier.mjs +203 -0
  16. package/heap-analysis/heap/renderer/renderer.mjs +122 -0
  17. package/lib/cli-version.d.ts +21 -0
  18. package/lib/cli-version.d.ts.map +1 -0
  19. package/lib/cli-version.js +30 -0
  20. package/lib/cli-version.js.map +1 -0
  21. package/lib/cli.d.ts +11 -0
  22. package/lib/cli.d.ts.map +1 -0
  23. package/lib/cli.js +135 -0
  24. package/lib/cli.js.map +1 -0
  25. package/lib/commands/analyze-heap-args.d.ts +46 -0
  26. package/lib/commands/analyze-heap-args.d.ts.map +1 -0
  27. package/lib/commands/analyze-heap-args.js +174 -0
  28. package/lib/commands/analyze-heap-args.js.map +1 -0
  29. package/lib/commands/analyze-heap.d.ts +37 -0
  30. package/lib/commands/analyze-heap.d.ts.map +1 -0
  31. package/lib/commands/analyze-heap.js +63 -0
  32. package/lib/commands/analyze-heap.js.map +1 -0
  33. package/lib/commands/ast-ground-truth-args.d.ts +23 -0
  34. package/lib/commands/ast-ground-truth-args.d.ts.map +1 -0
  35. package/lib/commands/ast-ground-truth-args.js +47 -0
  36. package/lib/commands/ast-ground-truth-args.js.map +1 -0
  37. package/lib/commands/ast-ground-truth-driver.d.ts +10 -0
  38. package/lib/commands/ast-ground-truth-driver.d.ts.map +1 -0
  39. package/lib/commands/ast-ground-truth-driver.js +72 -0
  40. package/lib/commands/ast-ground-truth-driver.js.map +1 -0
  41. package/lib/commands/ast-ground-truth.d.ts +31 -0
  42. package/lib/commands/ast-ground-truth.d.ts.map +1 -0
  43. package/lib/commands/ast-ground-truth.js +36 -0
  44. package/lib/commands/ast-ground-truth.js.map +1 -0
  45. package/lib/commands/generate-transfer-model-args.d.ts +38 -0
  46. package/lib/commands/generate-transfer-model-args.d.ts.map +1 -0
  47. package/lib/commands/generate-transfer-model-args.js +183 -0
  48. package/lib/commands/generate-transfer-model-args.js.map +1 -0
  49. package/lib/commands/generate-transfer-model-config.d.ts +32 -0
  50. package/lib/commands/generate-transfer-model-config.d.ts.map +1 -0
  51. package/lib/commands/generate-transfer-model-config.js +122 -0
  52. package/lib/commands/generate-transfer-model-config.js.map +1 -0
  53. package/lib/commands/generate-transfer-model.d.ts +110 -0
  54. package/lib/commands/generate-transfer-model.d.ts.map +1 -0
  55. package/lib/commands/generate-transfer-model.js +587 -0
  56. package/lib/commands/generate-transfer-model.js.map +1 -0
  57. package/lib/commands/harness-args.d.ts +198 -0
  58. package/lib/commands/harness-args.d.ts.map +1 -0
  59. package/lib/commands/harness-args.js +339 -0
  60. package/lib/commands/harness-args.js.map +1 -0
  61. package/lib/commands/headless-harness.d.ts +97 -0
  62. package/lib/commands/headless-harness.d.ts.map +1 -0
  63. package/lib/commands/headless-harness.js +121 -0
  64. package/lib/commands/headless-harness.js.map +1 -0
  65. package/lib/commands/init-args.d.ts +81 -0
  66. package/lib/commands/init-args.d.ts.map +1 -0
  67. package/lib/commands/init-args.js +250 -0
  68. package/lib/commands/init-args.js.map +1 -0
  69. package/lib/commands/init-prompt.d.ts +66 -0
  70. package/lib/commands/init-prompt.d.ts.map +1 -0
  71. package/lib/commands/init-prompt.js +71 -0
  72. package/lib/commands/init-prompt.js.map +1 -0
  73. package/lib/commands/init-templates.d.ts +42 -0
  74. package/lib/commands/init-templates.d.ts.map +1 -0
  75. package/lib/commands/init-templates.js +1902 -0
  76. package/lib/commands/init-templates.js.map +1 -0
  77. package/lib/commands/init-wizard.d.ts +65 -0
  78. package/lib/commands/init-wizard.d.ts.map +1 -0
  79. package/lib/commands/init-wizard.js +302 -0
  80. package/lib/commands/init-wizard.js.map +1 -0
  81. package/lib/commands/init-workspace.d.ts +168 -0
  82. package/lib/commands/init-workspace.d.ts.map +1 -0
  83. package/lib/commands/init-workspace.js +372 -0
  84. package/lib/commands/init-workspace.js.map +1 -0
  85. package/lib/commands/init.d.ts +273 -0
  86. package/lib/commands/init.d.ts.map +1 -0
  87. package/lib/commands/init.js +303 -0
  88. package/lib/commands/init.js.map +1 -0
  89. package/lib/commands/lint-grammar-args.d.ts +23 -0
  90. package/lib/commands/lint-grammar-args.d.ts.map +1 -0
  91. package/lib/commands/lint-grammar-args.js +63 -0
  92. package/lib/commands/lint-grammar-args.js.map +1 -0
  93. package/lib/commands/lint-grammar-driver.d.ts +10 -0
  94. package/lib/commands/lint-grammar-driver.d.ts.map +1 -0
  95. package/lib/commands/lint-grammar-driver.js +72 -0
  96. package/lib/commands/lint-grammar-driver.js.map +1 -0
  97. package/lib/commands/lint-grammar-report.d.ts +24 -0
  98. package/lib/commands/lint-grammar-report.d.ts.map +1 -0
  99. package/lib/commands/lint-grammar-report.js +65 -0
  100. package/lib/commands/lint-grammar-report.js.map +1 -0
  101. package/lib/commands/lint-grammar.d.ts +33 -0
  102. package/lib/commands/lint-grammar.d.ts.map +1 -0
  103. package/lib/commands/lint-grammar.js +42 -0
  104. package/lib/commands/lint-grammar.js.map +1 -0
  105. package/lib/commands/measure-memory-args.d.ts +23 -0
  106. package/lib/commands/measure-memory-args.d.ts.map +1 -0
  107. package/lib/commands/measure-memory-args.js +106 -0
  108. package/lib/commands/measure-memory-args.js.map +1 -0
  109. package/lib/commands/measure-memory-driver.d.ts +10 -0
  110. package/lib/commands/measure-memory-driver.d.ts.map +1 -0
  111. package/lib/commands/measure-memory-driver.js +110 -0
  112. package/lib/commands/measure-memory-driver.js.map +1 -0
  113. package/lib/commands/measure-memory.d.ts +68 -0
  114. package/lib/commands/measure-memory.d.ts.map +1 -0
  115. package/lib/commands/measure-memory.js +100 -0
  116. package/lib/commands/measure-memory.js.map +1 -0
  117. package/lib/commands/model-docs-args.d.ts +23 -0
  118. package/lib/commands/model-docs-args.d.ts.map +1 -0
  119. package/lib/commands/model-docs-args.js +53 -0
  120. package/lib/commands/model-docs-args.js.map +1 -0
  121. package/lib/commands/model-docs-driver.d.ts +10 -0
  122. package/lib/commands/model-docs-driver.d.ts.map +1 -0
  123. package/lib/commands/model-docs-driver.js +61 -0
  124. package/lib/commands/model-docs-driver.js.map +1 -0
  125. package/lib/commands/model-docs-report.d.ts +18 -0
  126. package/lib/commands/model-docs-report.d.ts.map +1 -0
  127. package/lib/commands/model-docs-report.js +123 -0
  128. package/lib/commands/model-docs-report.js.map +1 -0
  129. package/lib/commands/model-docs.d.ts +30 -0
  130. package/lib/commands/model-docs.d.ts.map +1 -0
  131. package/lib/commands/model-docs.js +37 -0
  132. package/lib/commands/model-docs.js.map +1 -0
  133. package/lib/commands/projects-args.d.ts +25 -0
  134. package/lib/commands/projects-args.d.ts.map +1 -0
  135. package/lib/commands/projects-args.js +49 -0
  136. package/lib/commands/projects-args.js.map +1 -0
  137. package/lib/commands/projects.d.ts +45 -0
  138. package/lib/commands/projects.d.ts.map +1 -0
  139. package/lib/commands/projects.js +39 -0
  140. package/lib/commands/projects.js.map +1 -0
  141. package/lib/commands/query-args.d.ts +25 -0
  142. package/lib/commands/query-args.d.ts.map +1 -0
  143. package/lib/commands/query-args.js +46 -0
  144. package/lib/commands/query-args.js.map +1 -0
  145. package/lib/commands/query.d.ts +32 -0
  146. package/lib/commands/query.d.ts.map +1 -0
  147. package/lib/commands/query.js +29 -0
  148. package/lib/commands/query.js.map +1 -0
  149. package/lib/commands/reflect-args.d.ts +23 -0
  150. package/lib/commands/reflect-args.d.ts.map +1 -0
  151. package/lib/commands/reflect-args.js +59 -0
  152. package/lib/commands/reflect-args.js.map +1 -0
  153. package/lib/commands/reflect-driver.d.ts +10 -0
  154. package/lib/commands/reflect-driver.d.ts.map +1 -0
  155. package/lib/commands/reflect-driver.js +66 -0
  156. package/lib/commands/reflect-driver.js.map +1 -0
  157. package/lib/commands/reflect-report.d.ts +20 -0
  158. package/lib/commands/reflect-report.d.ts.map +1 -0
  159. package/lib/commands/reflect-report.js +127 -0
  160. package/lib/commands/reflect-report.js.map +1 -0
  161. package/lib/commands/reflect.d.ts +30 -0
  162. package/lib/commands/reflect.d.ts.map +1 -0
  163. package/lib/commands/reflect.js +38 -0
  164. package/lib/commands/reflect.js.map +1 -0
  165. package/lib/commands/save-args.d.ts +26 -0
  166. package/lib/commands/save-args.d.ts.map +1 -0
  167. package/lib/commands/save-args.js +59 -0
  168. package/lib/commands/save-args.js.map +1 -0
  169. package/lib/commands/save.d.ts +38 -0
  170. package/lib/commands/save.d.ts.map +1 -0
  171. package/lib/commands/save.js +48 -0
  172. package/lib/commands/save.js.map +1 -0
  173. package/lib/commands/validate-args.d.ts +23 -0
  174. package/lib/commands/validate-args.d.ts.map +1 -0
  175. package/lib/commands/validate-args.js +60 -0
  176. package/lib/commands/validate-args.js.map +1 -0
  177. package/lib/commands/validate-driver.d.ts +10 -0
  178. package/lib/commands/validate-driver.d.ts.map +1 -0
  179. package/lib/commands/validate-driver.js +72 -0
  180. package/lib/commands/validate-driver.js.map +1 -0
  181. package/lib/commands/validate-report.d.ts +30 -0
  182. package/lib/commands/validate-report.d.ts.map +1 -0
  183. package/lib/commands/validate-report.js +87 -0
  184. package/lib/commands/validate-report.js.map +1 -0
  185. package/lib/commands/validate.d.ts +34 -0
  186. package/lib/commands/validate.d.ts.map +1 -0
  187. package/lib/commands/validate.js +41 -0
  188. package/lib/commands/validate.js.map +1 -0
  189. package/lib/commands/watch-args.d.ts +26 -0
  190. package/lib/commands/watch-args.d.ts.map +1 -0
  191. package/lib/commands/watch-args.js +59 -0
  192. package/lib/commands/watch-args.js.map +1 -0
  193. package/lib/commands/watch.d.ts +75 -0
  194. package/lib/commands/watch.d.ts.map +1 -0
  195. package/lib/commands/watch.js +112 -0
  196. package/lib/commands/watch.js.map +1 -0
  197. package/lib/index.d.ts +16 -0
  198. package/lib/index.d.ts.map +1 -0
  199. package/lib/index.js +16 -0
  200. package/lib/index.js.map +1 -0
  201. package/lib/log-level.d.ts +39 -0
  202. package/lib/log-level.d.ts.map +1 -0
  203. package/lib/log-level.js +47 -0
  204. package/lib/log-level.js.map +1 -0
  205. package/lib/package-name.d.ts +33 -0
  206. package/lib/package-name.d.ts.map +1 -0
  207. package/lib/package-name.js +37 -0
  208. package/lib/package-name.js.map +1 -0
  209. package/lib/spawn-data-server.d.ts +102 -0
  210. package/lib/spawn-data-server.d.ts.map +1 -0
  211. package/lib/spawn-data-server.js +154 -0
  212. package/lib/spawn-data-server.js.map +1 -0
  213. package/lib/testing/echo-server.d.ts +11 -0
  214. package/lib/testing/echo-server.d.ts.map +1 -0
  215. package/lib/testing/echo-server.js +79 -0
  216. package/lib/testing/echo-server.js.map +1 -0
  217. package/package.json +105 -0
  218. package/src/cli-version.ts +32 -0
  219. package/src/cli.ts +139 -0
  220. package/src/commands/analyze-heap-args.ts +184 -0
  221. package/src/commands/analyze-heap.ts +76 -0
  222. package/src/commands/ast-ground-truth-args.ts +63 -0
  223. package/src/commands/ast-ground-truth-driver.ts +81 -0
  224. package/src/commands/ast-ground-truth.ts +49 -0
  225. package/src/commands/generate-transfer-model-args.ts +203 -0
  226. package/src/commands/generate-transfer-model-config.ts +128 -0
  227. package/src/commands/generate-transfer-model.ts +716 -0
  228. package/src/commands/harness-args.ts +429 -0
  229. package/src/commands/headless-harness.ts +165 -0
  230. package/src/commands/init-args.ts +273 -0
  231. package/src/commands/init-prompt.ts +149 -0
  232. package/src/commands/init-templates.ts +2037 -0
  233. package/src/commands/init-wizard.ts +378 -0
  234. package/src/commands/init-workspace.ts +442 -0
  235. package/src/commands/init.ts +554 -0
  236. package/src/commands/lint-grammar-args.ts +79 -0
  237. package/src/commands/lint-grammar-driver.ts +83 -0
  238. package/src/commands/lint-grammar-report.ts +74 -0
  239. package/src/commands/lint-grammar.ts +57 -0
  240. package/src/commands/measure-memory-args.ts +123 -0
  241. package/src/commands/measure-memory-driver.ts +128 -0
  242. package/src/commands/measure-memory.ts +143 -0
  243. package/src/commands/model-docs-args.ts +69 -0
  244. package/src/commands/model-docs-driver.ts +68 -0
  245. package/src/commands/model-docs-report.ts +133 -0
  246. package/src/commands/model-docs.ts +48 -0
  247. package/src/commands/projects-args.ts +62 -0
  248. package/src/commands/projects.ts +76 -0
  249. package/src/commands/query-args.ts +61 -0
  250. package/src/commands/query.ts +58 -0
  251. package/src/commands/reflect-args.ts +75 -0
  252. package/src/commands/reflect-driver.ts +76 -0
  253. package/src/commands/reflect-report.ts +138 -0
  254. package/src/commands/reflect.ts +51 -0
  255. package/src/commands/save-args.ts +74 -0
  256. package/src/commands/save.ts +84 -0
  257. package/src/commands/validate-args.ts +76 -0
  258. package/src/commands/validate-driver.ts +84 -0
  259. package/src/commands/validate-report.ts +95 -0
  260. package/src/commands/validate.ts +58 -0
  261. package/src/commands/watch-args.ts +74 -0
  262. package/src/commands/watch.ts +185 -0
  263. package/src/index.ts +16 -0
  264. package/src/log-level.ts +49 -0
  265. package/src/package-name.ts +39 -0
  266. package/src/spawn-data-server.ts +247 -0
  267. package/src/testing/echo-server.ts +93 -0
@@ -0,0 +1,152 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 CrossBreeze, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+
10
+ /*
11
+ * Heap-agnostic aggregation primitives.
12
+ *
13
+ * Three distinct size views, deliberately kept separate because they answer
14
+ * different questions and must never be conflated:
15
+ *
16
+ * - SHALLOW (self_size): the one clean additive partition of the heap. Sums
17
+ * to 100%. Use this for "share of heap".
18
+ * - RETAINED, OVERLAPPING (node.retainedSize summed per bucket): the
19
+ * DevTools "Summary" number. OVERLAPS across buckets (a parent's retained
20
+ * includes its children, which are their own rows) -> never sum these rows.
21
+ * - RETAINED, EXCLUSIVE (dominator attribution): each byte attributed once to
22
+ * the nearest anchor that dominates it. Non-overlapping; partitions the heap
23
+ * among the anchors + "(root/shared)". This is the honest "how much frees if
24
+ * this concept goes away" number.
25
+ */
26
+ import { bucketLabel } from './bucket-label.mjs';
27
+
28
+ /**
29
+ * One pass: shallow + overlapping-retained + count per label.
30
+ * Also returns flat typed arrays (indexed by node.nodeIndex) needed for the
31
+ * dominator-attribution pass, so callers don't iterate the heap twice.
32
+ *
33
+ * @param {import('@memlab/core').IHeapSnapshot} heap
34
+ * @param {(node: import('@memlab/core').IHeapNode) => string} [labelFn]
35
+ * maps a node to its bucket label (defaults to the heap-agnostic
36
+ * constructor/type label; the Langium layer passes a concept classifier)
37
+ */
38
+ export function aggregateByBucket(heap, labelFn = bucketLabel) {
39
+ const byBucket = new Map(); // label -> { count, shallow, retainedOverlapping }
40
+ const nodeCount = heap.nodes.length;
41
+ const selfSize = new Float64Array(nodeCount);
42
+ const domIndex = new Int32Array(nodeCount).fill(-1);
43
+ const labelOfIndex = new Array(nodeCount);
44
+
45
+ heap.nodes.forEach(node => {
46
+ const label = labelFn(node);
47
+ const entry = byBucket.get(label) ?? { count: 0, shallow: 0, retainedOverlapping: 0 };
48
+ entry.count++;
49
+ entry.shallow += node.self_size;
50
+ entry.retainedOverlapping += node.retainedSize;
51
+ byBucket.set(label, entry);
52
+
53
+ const idx = node.nodeIndex;
54
+ selfSize[idx] = node.self_size;
55
+ labelOfIndex[idx] = label;
56
+ domIndex[idx] = node.dominatorNode ? node.dominatorNode.nodeIndex : -1;
57
+ });
58
+
59
+ return { byBucket, selfSize, domIndex, labelOfIndex };
60
+ }
61
+
62
+ /**
63
+ * Non-overlapping retained attribution via the dominator tree.
64
+ *
65
+ * For every node, walk up its dominator chain to the first node whose bucket
66
+ * label is in `anchors`; attribute the node's self_size there. Nodes with no
67
+ * anchor ancestor land in "(root/shared)". The result partitions the entire
68
+ * heap among anchors + "(root/shared)" with no double counting.
69
+ *
70
+ * Memoized per node index, so total work is ~O(nodeCount).
71
+ *
72
+ * @param {{selfSize: Float64Array, domIndex: Int32Array, labelOfIndex: string[]}} arrays
73
+ * @param {Set<string>} anchors bucket labels treated as ownership boundaries
74
+ */
75
+ export function attributeExclusiveRetained({ selfSize, domIndex, labelOfIndex }, anchors) {
76
+ const nodeCount = selfSize.length;
77
+ const ROOT_SHARED = -1;
78
+ const UNVISITED = -2;
79
+ const IN_PROGRESS = -3;
80
+ const ownerOf = new Int32Array(nodeCount).fill(UNVISITED);
81
+
82
+ // Resolve the owning anchor index for a node, memoizing the whole chain.
83
+ // Robust against dominator-chain cycles (the synthetic GC root dominates
84
+ // itself; any node still IN_PROGRESS when revisited is treated as root).
85
+ const resolveOwner = startIdx => {
86
+ const chain = [];
87
+ let idx = startIdx;
88
+ let owner = ROOT_SHARED;
89
+ while (idx >= 0) {
90
+ const memo = ownerOf[idx];
91
+ if (memo === IN_PROGRESS) {
92
+ break; // cycle -> root/shared
93
+ }
94
+ if (memo !== UNVISITED) {
95
+ owner = memo; // already resolved; chain shares its owner
96
+ break;
97
+ }
98
+ if (anchors.has(labelOfIndex[idx])) {
99
+ ownerOf[idx] = idx; // anchor owns itself and the chain below it
100
+ owner = idx;
101
+ break;
102
+ }
103
+ ownerOf[idx] = IN_PROGRESS;
104
+ chain.push(idx);
105
+ idx = domIndex[idx];
106
+ }
107
+ for (const chainIdx of chain) {
108
+ ownerOf[chainIdx] = owner;
109
+ }
110
+ return owner;
111
+ };
112
+
113
+ for (let i = 0; i < nodeCount; i++) {
114
+ if (ownerOf[i] === UNVISITED) {
115
+ resolveOwner(i);
116
+ }
117
+ }
118
+
119
+ // Sum self_size into the owner's bucket label.
120
+ const exclusiveByLabel = new Map();
121
+ for (let i = 0; i < nodeCount; i++) {
122
+ const owner = ownerOf[i];
123
+ const label = owner === ROOT_SHARED ? '(root/shared)' : labelOfIndex[owner];
124
+ exclusiveByLabel.set(label, (exclusiveByLabel.get(label) ?? 0) + selfSize[i]);
125
+ }
126
+ return exclusiveByLabel;
127
+ }
128
+
129
+ /**
130
+ * Shortest retainer path from a node up to a GC root, using memlab's pathEdge
131
+ * (the incoming edge on the shortest path to root). Returns a list of hops
132
+ * [{ via, from }] from the node outward to the root.
133
+ *
134
+ * @param {import('@memlab/core').IHeapNode} node
135
+ * @param {number} [maxDepth]
136
+ */
137
+ export function shortestRetainerPath(node, maxDepth = 12) {
138
+ const hops = [];
139
+ let current = node;
140
+ let depth = 0;
141
+ while (current && current.pathEdge && depth < maxDepth) {
142
+ const edge = current.pathEdge;
143
+ const from = edge.fromNode;
144
+ hops.push({
145
+ via: edge.is_index ? `[${edge.name_or_index}]` : String(edge.name_or_index),
146
+ from: from.type === 'object' ? from.name || '(object)' : `(${from.type})`
147
+ });
148
+ current = from;
149
+ depth++;
150
+ }
151
+ return hops;
152
+ }
@@ -0,0 +1,23 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 CrossBreeze, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+
10
+ /*
11
+ * The default node bucket label. Kept in its own module — separate from the
12
+ * memlab-backed loader (load.mjs) — so the aggregation modules that need it
13
+ * (aggregate/holders/cutpoints) do not transitively pull @memlab/heap-analysis
14
+ * into the memlab-free `--diff` / `--help` paths.
15
+ */
16
+
17
+ /**
18
+ * Canonical bucket label for a node: a JS object is labelled by its
19
+ * constructor name; everything else by its V8 type in parentheses.
20
+ */
21
+ export function bucketLabel(node) {
22
+ return node.type === 'object' ? node.name || '(anonymous object)' : `(${node.type})`;
23
+ }
@@ -0,0 +1,51 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 CrossBreeze, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+
10
+ /*
11
+ * Cut points (heap-agnostic): lightweight SOLE owners of large subtrees.
12
+ *
13
+ * A node with tiny shallow size but huge retained size dominates a big subtree
14
+ * it alone keeps alive -- severing the few references to it frees the whole
15
+ * subtree. These are the highest-leverage things to drop. Few referrers = easy
16
+ * to cut; many referrers = the retained subtree is shared and won't free.
17
+ */
18
+ import { bucketLabel } from './bucket-label.mjs';
19
+
20
+ /**
21
+ * @param {import('@memlab/core').IHeapSnapshot} heap
22
+ * @param {string[]} labelOfIndex per-nodeIndex concept label (for readable names)
23
+ * @param {{ minRetained?: number, maxShallow?: number, topN?: number }} [opts]
24
+ */
25
+ export function findCutPoints(heap, labelOfIndex, { minRetained = 512 * 1024, maxShallow = 4096, topN = 20 } = {}) {
26
+ const candidates = [];
27
+ heap.nodes.forEach(node => {
28
+ // Skip the GC super-root(s): they "retain everything" but aren't a cut point.
29
+ if (node.type === 'synthetic') {
30
+ return;
31
+ }
32
+ if (node.retainedSize >= minRetained && node.self_size <= maxShallow) {
33
+ // The shortest-path-to-root edge names what holds this node — `Holder.edge`
34
+ // — so otherwise-identical cut points (e.g. several `Map`s) are told apart.
35
+ const pathEdge = node.pathEdge;
36
+ const from = pathEdge?.fromNode;
37
+ const via = pathEdge ? (pathEdge.is_index ? '[]' : pathEdge.name_or_index) : '';
38
+ const retainer = from ? `${labelOfIndex[from.nodeIndex] || bucketLabel(from)}.${via}` : '';
39
+ candidates.push({
40
+ node,
41
+ label: labelOfIndex[node.nodeIndex] || bucketLabel(node),
42
+ shallow: node.self_size,
43
+ retained: node.retainedSize,
44
+ referrers: node.numOfReferrers,
45
+ retainer
46
+ });
47
+ }
48
+ });
49
+ candidates.sort((a, b) => b.retained - a.retained);
50
+ return candidates.slice(0, topN);
51
+ }
@@ -0,0 +1,56 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 CrossBreeze, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+
10
+ /*
11
+ * Deterministic diff of two analysis JSONs (heap-agnostic). Pure arithmetic on
12
+ * already-computed analyses -- no snapshot or source needed, so it is instant
13
+ * and CI-safe. Baselines are the small JSON artifacts written by `--json`.
14
+ */
15
+
16
+ /** Union the keys of two count/size maps and emit per-key deltas, sorted by |delta|. */
17
+ function deltaRows(baseMap = {}, curMap = {}, pick) {
18
+ const keys = new Set([...Object.keys(baseMap), ...Object.keys(curMap)]);
19
+ const rows = [];
20
+ for (const key of keys) {
21
+ const base = pick(baseMap[key]);
22
+ const cur = pick(curMap[key]);
23
+ if (base !== cur) {
24
+ rows.push({ key, base, cur, delta: cur - base });
25
+ }
26
+ }
27
+ rows.sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta));
28
+ return rows;
29
+ }
30
+
31
+ /**
32
+ * @param {object} base baseline analysis JSON (from --json)
33
+ * @param {object} cur current analysis JSON
34
+ * @param {number} thresholdPct fraction of baseline total-shallow growth that
35
+ * counts as a regression (e.g. 0.05 = 5%)
36
+ */
37
+ export function diffAnalyses(base, cur, thresholdPct) {
38
+ const concepts = deltaRows(base.concepts, cur.concepts, e => e?.exclusive ?? 0);
39
+ const conceptsShallow = deltaRows(base.concepts, cur.concepts, e => e?.shallow ?? 0);
40
+ const ast = deltaRows(base.astByType, cur.astByType, v => v ?? 0);
41
+
42
+ const baseTotal = base.totalShallowBytes ?? 0;
43
+ const curTotal = cur.totalShallowBytes ?? 0;
44
+ const totalDelta = curTotal - baseTotal;
45
+ const growthPct = baseTotal ? totalDelta / baseTotal : 0;
46
+ const regressed = growthPct > thresholdPct;
47
+
48
+ return {
49
+ concepts,
50
+ conceptsShallow,
51
+ ast,
52
+ total: { base: baseTotal, cur: curTotal, delta: totalDelta, growthPct },
53
+ thresholdPct,
54
+ regressed
55
+ };
56
+ }
@@ -0,0 +1,47 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 CrossBreeze, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+
10
+ /* Tiny formatting helpers shared by the report renderers. */
11
+
12
+ export const mb = bytes => `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
13
+
14
+ /**
15
+ * Adaptive byte size: MB for ≥1 MB, KB for ≥1 KB, else bytes. Keeps sub-MB table
16
+ * rows rankable instead of collapsing dozens of them to `0.0 MB`.
17
+ */
18
+ export const humanBytes = bytes => {
19
+ const abs = Math.abs(bytes);
20
+ if (abs >= 1024 * 1024) {
21
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
22
+ }
23
+ if (abs >= 1024) {
24
+ return `${(bytes / 1024).toFixed(1)} KB`;
25
+ }
26
+ return `${Math.round(bytes)} B`;
27
+ };
28
+
29
+ export const pct = (part, whole) => (whole ? ((part / whole) * 100).toFixed(1) : '0.0') + '%';
30
+
31
+ /** Wall-clock `[HH:MM:SS]` stamp for progress lines on long-running work. */
32
+ export const nowStamp = () => `[${new Date().toTimeString().slice(0, 8)}]`;
33
+
34
+ /** Emit a timestamped progress line to stderr (keeps stdout clean for results). */
35
+ export function progress(message) {
36
+ process.stderr.write(`${nowStamp()} ${message}\n`);
37
+ }
38
+
39
+ /** Render rows [label, count, sizeBytes, ...extra] as an aligned table. */
40
+ export function sizeTable(rows, { totalShallow } = {}) {
41
+ const lines = [];
42
+ for (const [label, count, size] of rows) {
43
+ const share = totalShallow ? ` ${pct(size, totalShallow).padStart(6)}` : '';
44
+ lines.push(`${mb(size).padStart(10)}${share} ${String(count).padStart(10)} ${label}`);
45
+ }
46
+ return lines.join('\n');
47
+ }
@@ -0,0 +1,80 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 CrossBreeze, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+
10
+ /*
11
+ * "What holds X" retainer evidence (heap-agnostic).
12
+ *
13
+ * For every node matching a target predicate, tally two complementary views:
14
+ * - dominators: the concept of each node's single dominatorNode -> who
15
+ * EXCLUSIVELY owns it ("if I cut one thing, what frees").
16
+ * - referrers: every incoming edge as `edgeName@holderLabel` -> ALL holders,
17
+ * including shared references. Multiplicity here is why cutting one holder
18
+ * can free little (e.g. CST held by parent CST AND $cstNode AND a description).
19
+ *
20
+ * Labels are taken from the caller-supplied per-node label array, so core stays
21
+ * ignorant of what the labels mean; the caller groups them for display.
22
+ */
23
+ import { bucketLabel } from './bucket-label.mjs';
24
+
25
+ /**
26
+ * Profile holders for SEVERAL target groups in a SINGLE heap pass (loading the
27
+ * snapshot is the only expensive step, so multiple --holders share one pass).
28
+ *
29
+ * @param {import('@memlab/core').IHeapSnapshot} heap
30
+ * @param {string[]} labelOfIndex per-nodeIndex classification label
31
+ * @param {Int32Array} domIndex per-nodeIndex dominator nodeIndex (-1 = root)
32
+ * @param {Set<string>} targetGroups concept groups to profile
33
+ * @param {(label: string) => string} groupOf collapse a label to its group
34
+ * @returns {Map<string, {targetCount, targetRetained, dominators, referrers}>}
35
+ */
36
+ export function holderProfiles(heap, labelOfIndex, domIndex, targetGroups, groupOf) {
37
+ const results = new Map();
38
+ for (const group of targetGroups) {
39
+ results.set(group, { targetCount: 0, targetRetained: 0, dominators: new Map(), referrers: new Map() });
40
+ }
41
+
42
+ heap.nodes.forEach(node => {
43
+ const idx = node.nodeIndex;
44
+ const group = groupOf(labelOfIndex[idx]);
45
+ const result = results.get(group);
46
+ if (!result) {
47
+ return;
48
+ }
49
+ result.targetCount++;
50
+ result.targetRetained += node.retainedSize;
51
+
52
+ const dIdx = domIndex[idx];
53
+ const dLabel = dIdx >= 0 ? labelOfIndex[dIdx] : '(GC root)';
54
+ result.dominators.set(dLabel, (result.dominators.get(dLabel) ?? 0) + 1);
55
+
56
+ for (const edge of node.referrers) {
57
+ // __proto__ is the V8 prototype chain, not meaningful ownership -- skip it.
58
+ if (edge.name_or_index === '__proto__') {
59
+ continue;
60
+ }
61
+ const from = edge.fromNode;
62
+ const fromLabel = (from && labelOfIndex[from.nodeIndex]) || (from ? bucketLabel(from) : '(root)');
63
+ const name = edge.is_index ? '[]' : edge.name_or_index;
64
+ const key = `${name}@${fromLabel}`;
65
+ result.referrers.set(key, (result.referrers.get(key) ?? 0) + 1);
66
+ }
67
+ });
68
+
69
+ return results;
70
+ }
71
+
72
+ /** Regroup a tally Map's keys via a key-mapping function, summing collisions. */
73
+ export function regroupTally(tally, mapKey) {
74
+ const out = new Map();
75
+ for (const [key, count] of tally) {
76
+ const grouped = mapKey(key);
77
+ out.set(grouped, (out.get(grouped) ?? 0) + count);
78
+ }
79
+ return out;
80
+ }
@@ -0,0 +1,46 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 CrossBreeze, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+
10
+ /*
11
+ * Heap-agnostic loader. Wraps memlab's getFullHeapFromFile, which parses the
12
+ * .heapsnapshot and computes dominators + retained sizes natively.
13
+ *
14
+ * Nothing in core/ knows about Langium or any adopter's model.
15
+ */
16
+ import { getFullHeapFromFile } from '@memlab/heap-analysis';
17
+ import { progress } from './format.mjs';
18
+
19
+ const mb = bytes => bytes / (1024 * 1024);
20
+
21
+ /**
22
+ * Load a snapshot and return the memlab heap plus light meta.
23
+ * @param {string} file path to a .heapsnapshot
24
+ * @param {(msg: string) => void} [log] timestamped progress sink (defaults to stderr)
25
+ */
26
+ export async function loadHeap(file, log = progress) {
27
+ const t0 = Date.now();
28
+ log(`Loading ${file} ...`);
29
+ const heap = await getFullHeapFromFile(file);
30
+ const loadSeconds = (Date.now() - t0) / 1000;
31
+
32
+ let totalShallow = 0;
33
+ heap.nodes.forEach(node => {
34
+ totalShallow += node.self_size;
35
+ });
36
+
37
+ const meta = {
38
+ snapshot: file,
39
+ nodeCount: heap.nodes.length,
40
+ totalShallowBytes: totalShallow,
41
+ totalShallowMb: Number(mb(totalShallow).toFixed(1)),
42
+ loadSeconds: Number(loadSeconds.toFixed(1))
43
+ };
44
+ log(`Loaded ${meta.nodeCount} nodes, ${meta.totalShallowMb} MB shallow in ${meta.loadSeconds}s`);
45
+ return { heap, meta };
46
+ }
@@ -0,0 +1,68 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 CrossBreeze, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+
10
+ /*
11
+ * Minimal Markdown report builder. Accumulates sections (headings, prose,
12
+ * tables, code blocks) and renders to a single Markdown string -- readable as
13
+ * plain text in a terminal, saveable as a .md file, and cheap for a tool to
14
+ * parse (Markdown headings and tables).
15
+ */
16
+
17
+ /** Escape `|` so a heap-derived cell value (e.g. a `A | B` union string) cannot shift table columns (GFM). */
18
+ function escapeCell(value) {
19
+ return String(value).replace(/\|/g, '\\|');
20
+ }
21
+
22
+ export class Report {
23
+ constructor() {
24
+ this.lines = [];
25
+ }
26
+
27
+ /** Top-level title. */
28
+ title(text) {
29
+ this.lines.push(`# ${text}`, '');
30
+ return this;
31
+ }
32
+
33
+ /** Section heading (blank line before keeps sections separated). */
34
+ section(text) {
35
+ this.lines.push('', `## ${text}`, '');
36
+ return this;
37
+ }
38
+
39
+ /** A paragraph / note line. */
40
+ note(text) {
41
+ this.lines.push(text, '');
42
+ return this;
43
+ }
44
+
45
+ /**
46
+ * A Markdown table. `headers` is a string[]; `rows` is string[][]. Cells are
47
+ * stringified as-is (callers pre-format sizes/percentages).
48
+ */
49
+ table(headers, rows) {
50
+ this.lines.push(`| ${headers.map(escapeCell).join(' | ')} |`);
51
+ this.lines.push(`| ${headers.map(() => '---').join(' | ')} |`);
52
+ for (const row of rows) {
53
+ this.lines.push(`| ${row.map(escapeCell).join(' | ')} |`);
54
+ }
55
+ this.lines.push('');
56
+ return this;
57
+ }
58
+
59
+ /** A fenced code block (for retainer paths and other monospace content). */
60
+ code(text) {
61
+ this.lines.push('```', text, '```', '');
62
+ return this;
63
+ }
64
+
65
+ toString() {
66
+ return this.lines.join('\n');
67
+ }
68
+ }
@@ -0,0 +1,80 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 CrossBreeze, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+
10
+ /*
11
+ * Duplicate-string analysis (heap-agnostic) — quantifies the interning lever.
12
+ *
13
+ * V8 does not intern most runtime strings, so the same value (a name, a $type
14
+ * tag, a qualified id) can exist as many separate string nodes. Grouping string
15
+ * nodes by value shows how much could be reclaimed by interning to one copy.
16
+ *
17
+ * Only plain `string` nodes are analysed: memlab exposes their value via
18
+ * node.name. `concatenated string` (cons) nodes report a placeholder name, not
19
+ * their materialized text, so they cannot be value-deduped here -- they are a
20
+ * SEPARATE lever (compute the qualified/combined ids on demand instead of
21
+ * building them), quantified by their bucket size in the concept table.
22
+ */
23
+
24
+ /**
25
+ * @param {import('@memlab/core').IHeapSnapshot} heap
26
+ * @param {{ types?: string[], maxLen?: number }} [opts]
27
+ * types: which V8 string node types to include; maxLen: skip values longer
28
+ * than this as interning candidates (big unique blobs like source text).
29
+ */
30
+ export function duplicateStrings(heap, { types = ['string'], maxLen = 512 } = {}) {
31
+ const want = new Set(types);
32
+ const byValue = new Map(); // value -> { count, size, type }
33
+ let totalBytes = 0;
34
+ let totalNodes = 0;
35
+ let skippedLong = 0;
36
+
37
+ heap.nodes.forEach(node => {
38
+ if (!want.has(node.type)) {
39
+ return;
40
+ }
41
+ const value = node.name;
42
+ if (typeof value !== 'string') {
43
+ return;
44
+ }
45
+ totalNodes++;
46
+ totalBytes += node.self_size;
47
+ if (value.length > maxLen) {
48
+ skippedLong++;
49
+ return;
50
+ }
51
+ const entry = byValue.get(value);
52
+ if (entry) {
53
+ entry.count++;
54
+ } else {
55
+ byValue.set(value, { count: 1, size: node.self_size, type: node.type });
56
+ }
57
+ });
58
+
59
+ return { byValue, totalBytes, totalNodes, skippedLong };
60
+ }
61
+
62
+ /**
63
+ * Reduce a byValue map to ranked duplicates with reclaimable bytes
64
+ * (= (count - 1) * per-instance self_size, i.e. keep one copy).
65
+ */
66
+ export function rankReclaimable(byValue) {
67
+ const dups = [];
68
+ let reclaimableTotal = 0;
69
+ let duplicatedValues = 0;
70
+ for (const [value, { count, size, type }] of byValue) {
71
+ if (count > 1) {
72
+ const reclaimable = (count - 1) * size;
73
+ reclaimableTotal += reclaimable;
74
+ duplicatedValues++;
75
+ dups.push({ value, count, size, type, reclaimable });
76
+ }
77
+ }
78
+ dups.sort((a, b) => b.reclaimable - a.reclaimable);
79
+ return { dups, reclaimableTotal, duplicatedValues, distinctValues: byValue.size };
80
+ }