@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.
- package/LICENSE +21 -0
- package/README.md +240 -0
- package/heap-analysis/analyze-heap.mjs +798 -0
- package/heap-analysis/heap/core/aggregate.mjs +152 -0
- package/heap-analysis/heap/core/bucket-label.mjs +23 -0
- package/heap-analysis/heap/core/cutpoints.mjs +51 -0
- package/heap-analysis/heap/core/diff.mjs +56 -0
- package/heap-analysis/heap/core/format.mjs +47 -0
- package/heap-analysis/heap/core/holders.mjs +80 -0
- package/heap-analysis/heap/core/load.mjs +46 -0
- package/heap-analysis/heap/core/report.mjs +68 -0
- package/heap-analysis/heap/core/strings.mjs +80 -0
- package/heap-analysis/heap/langium/concepts.mjs +230 -0
- package/heap-analysis/heap/langium/glsp.classifier.mjs +80 -0
- package/heap-analysis/heap/langium/langium.classifier.mjs +203 -0
- package/heap-analysis/heap/renderer/renderer.mjs +122 -0
- package/lib/cli-version.d.ts +21 -0
- package/lib/cli-version.d.ts.map +1 -0
- package/lib/cli-version.js +30 -0
- package/lib/cli-version.js.map +1 -0
- package/lib/cli.d.ts +11 -0
- package/lib/cli.d.ts.map +1 -0
- package/lib/cli.js +135 -0
- package/lib/cli.js.map +1 -0
- package/lib/commands/analyze-heap-args.d.ts +46 -0
- package/lib/commands/analyze-heap-args.d.ts.map +1 -0
- package/lib/commands/analyze-heap-args.js +174 -0
- package/lib/commands/analyze-heap-args.js.map +1 -0
- package/lib/commands/analyze-heap.d.ts +37 -0
- package/lib/commands/analyze-heap.d.ts.map +1 -0
- package/lib/commands/analyze-heap.js +63 -0
- package/lib/commands/analyze-heap.js.map +1 -0
- package/lib/commands/ast-ground-truth-args.d.ts +23 -0
- package/lib/commands/ast-ground-truth-args.d.ts.map +1 -0
- package/lib/commands/ast-ground-truth-args.js +47 -0
- package/lib/commands/ast-ground-truth-args.js.map +1 -0
- package/lib/commands/ast-ground-truth-driver.d.ts +10 -0
- package/lib/commands/ast-ground-truth-driver.d.ts.map +1 -0
- package/lib/commands/ast-ground-truth-driver.js +72 -0
- package/lib/commands/ast-ground-truth-driver.js.map +1 -0
- package/lib/commands/ast-ground-truth.d.ts +31 -0
- package/lib/commands/ast-ground-truth.d.ts.map +1 -0
- package/lib/commands/ast-ground-truth.js +36 -0
- package/lib/commands/ast-ground-truth.js.map +1 -0
- package/lib/commands/generate-transfer-model-args.d.ts +38 -0
- package/lib/commands/generate-transfer-model-args.d.ts.map +1 -0
- package/lib/commands/generate-transfer-model-args.js +183 -0
- package/lib/commands/generate-transfer-model-args.js.map +1 -0
- package/lib/commands/generate-transfer-model-config.d.ts +32 -0
- package/lib/commands/generate-transfer-model-config.d.ts.map +1 -0
- package/lib/commands/generate-transfer-model-config.js +122 -0
- package/lib/commands/generate-transfer-model-config.js.map +1 -0
- package/lib/commands/generate-transfer-model.d.ts +110 -0
- package/lib/commands/generate-transfer-model.d.ts.map +1 -0
- package/lib/commands/generate-transfer-model.js +587 -0
- package/lib/commands/generate-transfer-model.js.map +1 -0
- package/lib/commands/harness-args.d.ts +198 -0
- package/lib/commands/harness-args.d.ts.map +1 -0
- package/lib/commands/harness-args.js +339 -0
- package/lib/commands/harness-args.js.map +1 -0
- package/lib/commands/headless-harness.d.ts +97 -0
- package/lib/commands/headless-harness.d.ts.map +1 -0
- package/lib/commands/headless-harness.js +121 -0
- package/lib/commands/headless-harness.js.map +1 -0
- package/lib/commands/init-args.d.ts +81 -0
- package/lib/commands/init-args.d.ts.map +1 -0
- package/lib/commands/init-args.js +250 -0
- package/lib/commands/init-args.js.map +1 -0
- package/lib/commands/init-prompt.d.ts +66 -0
- package/lib/commands/init-prompt.d.ts.map +1 -0
- package/lib/commands/init-prompt.js +71 -0
- package/lib/commands/init-prompt.js.map +1 -0
- package/lib/commands/init-templates.d.ts +42 -0
- package/lib/commands/init-templates.d.ts.map +1 -0
- package/lib/commands/init-templates.js +1902 -0
- package/lib/commands/init-templates.js.map +1 -0
- package/lib/commands/init-wizard.d.ts +65 -0
- package/lib/commands/init-wizard.d.ts.map +1 -0
- package/lib/commands/init-wizard.js +302 -0
- package/lib/commands/init-wizard.js.map +1 -0
- package/lib/commands/init-workspace.d.ts +168 -0
- package/lib/commands/init-workspace.d.ts.map +1 -0
- package/lib/commands/init-workspace.js +372 -0
- package/lib/commands/init-workspace.js.map +1 -0
- package/lib/commands/init.d.ts +273 -0
- package/lib/commands/init.d.ts.map +1 -0
- package/lib/commands/init.js +303 -0
- package/lib/commands/init.js.map +1 -0
- package/lib/commands/lint-grammar-args.d.ts +23 -0
- package/lib/commands/lint-grammar-args.d.ts.map +1 -0
- package/lib/commands/lint-grammar-args.js +63 -0
- package/lib/commands/lint-grammar-args.js.map +1 -0
- package/lib/commands/lint-grammar-driver.d.ts +10 -0
- package/lib/commands/lint-grammar-driver.d.ts.map +1 -0
- package/lib/commands/lint-grammar-driver.js +72 -0
- package/lib/commands/lint-grammar-driver.js.map +1 -0
- package/lib/commands/lint-grammar-report.d.ts +24 -0
- package/lib/commands/lint-grammar-report.d.ts.map +1 -0
- package/lib/commands/lint-grammar-report.js +65 -0
- package/lib/commands/lint-grammar-report.js.map +1 -0
- package/lib/commands/lint-grammar.d.ts +33 -0
- package/lib/commands/lint-grammar.d.ts.map +1 -0
- package/lib/commands/lint-grammar.js +42 -0
- package/lib/commands/lint-grammar.js.map +1 -0
- package/lib/commands/measure-memory-args.d.ts +23 -0
- package/lib/commands/measure-memory-args.d.ts.map +1 -0
- package/lib/commands/measure-memory-args.js +106 -0
- package/lib/commands/measure-memory-args.js.map +1 -0
- package/lib/commands/measure-memory-driver.d.ts +10 -0
- package/lib/commands/measure-memory-driver.d.ts.map +1 -0
- package/lib/commands/measure-memory-driver.js +110 -0
- package/lib/commands/measure-memory-driver.js.map +1 -0
- package/lib/commands/measure-memory.d.ts +68 -0
- package/lib/commands/measure-memory.d.ts.map +1 -0
- package/lib/commands/measure-memory.js +100 -0
- package/lib/commands/measure-memory.js.map +1 -0
- package/lib/commands/model-docs-args.d.ts +23 -0
- package/lib/commands/model-docs-args.d.ts.map +1 -0
- package/lib/commands/model-docs-args.js +53 -0
- package/lib/commands/model-docs-args.js.map +1 -0
- package/lib/commands/model-docs-driver.d.ts +10 -0
- package/lib/commands/model-docs-driver.d.ts.map +1 -0
- package/lib/commands/model-docs-driver.js +61 -0
- package/lib/commands/model-docs-driver.js.map +1 -0
- package/lib/commands/model-docs-report.d.ts +18 -0
- package/lib/commands/model-docs-report.d.ts.map +1 -0
- package/lib/commands/model-docs-report.js +123 -0
- package/lib/commands/model-docs-report.js.map +1 -0
- package/lib/commands/model-docs.d.ts +30 -0
- package/lib/commands/model-docs.d.ts.map +1 -0
- package/lib/commands/model-docs.js +37 -0
- package/lib/commands/model-docs.js.map +1 -0
- package/lib/commands/projects-args.d.ts +25 -0
- package/lib/commands/projects-args.d.ts.map +1 -0
- package/lib/commands/projects-args.js +49 -0
- package/lib/commands/projects-args.js.map +1 -0
- package/lib/commands/projects.d.ts +45 -0
- package/lib/commands/projects.d.ts.map +1 -0
- package/lib/commands/projects.js +39 -0
- package/lib/commands/projects.js.map +1 -0
- package/lib/commands/query-args.d.ts +25 -0
- package/lib/commands/query-args.d.ts.map +1 -0
- package/lib/commands/query-args.js +46 -0
- package/lib/commands/query-args.js.map +1 -0
- package/lib/commands/query.d.ts +32 -0
- package/lib/commands/query.d.ts.map +1 -0
- package/lib/commands/query.js +29 -0
- package/lib/commands/query.js.map +1 -0
- package/lib/commands/reflect-args.d.ts +23 -0
- package/lib/commands/reflect-args.d.ts.map +1 -0
- package/lib/commands/reflect-args.js +59 -0
- package/lib/commands/reflect-args.js.map +1 -0
- package/lib/commands/reflect-driver.d.ts +10 -0
- package/lib/commands/reflect-driver.d.ts.map +1 -0
- package/lib/commands/reflect-driver.js +66 -0
- package/lib/commands/reflect-driver.js.map +1 -0
- package/lib/commands/reflect-report.d.ts +20 -0
- package/lib/commands/reflect-report.d.ts.map +1 -0
- package/lib/commands/reflect-report.js +127 -0
- package/lib/commands/reflect-report.js.map +1 -0
- package/lib/commands/reflect.d.ts +30 -0
- package/lib/commands/reflect.d.ts.map +1 -0
- package/lib/commands/reflect.js +38 -0
- package/lib/commands/reflect.js.map +1 -0
- package/lib/commands/save-args.d.ts +26 -0
- package/lib/commands/save-args.d.ts.map +1 -0
- package/lib/commands/save-args.js +59 -0
- package/lib/commands/save-args.js.map +1 -0
- package/lib/commands/save.d.ts +38 -0
- package/lib/commands/save.d.ts.map +1 -0
- package/lib/commands/save.js +48 -0
- package/lib/commands/save.js.map +1 -0
- package/lib/commands/validate-args.d.ts +23 -0
- package/lib/commands/validate-args.d.ts.map +1 -0
- package/lib/commands/validate-args.js +60 -0
- package/lib/commands/validate-args.js.map +1 -0
- package/lib/commands/validate-driver.d.ts +10 -0
- package/lib/commands/validate-driver.d.ts.map +1 -0
- package/lib/commands/validate-driver.js +72 -0
- package/lib/commands/validate-driver.js.map +1 -0
- package/lib/commands/validate-report.d.ts +30 -0
- package/lib/commands/validate-report.d.ts.map +1 -0
- package/lib/commands/validate-report.js +87 -0
- package/lib/commands/validate-report.js.map +1 -0
- package/lib/commands/validate.d.ts +34 -0
- package/lib/commands/validate.d.ts.map +1 -0
- package/lib/commands/validate.js +41 -0
- package/lib/commands/validate.js.map +1 -0
- package/lib/commands/watch-args.d.ts +26 -0
- package/lib/commands/watch-args.d.ts.map +1 -0
- package/lib/commands/watch-args.js +59 -0
- package/lib/commands/watch-args.js.map +1 -0
- package/lib/commands/watch.d.ts +75 -0
- package/lib/commands/watch.d.ts.map +1 -0
- package/lib/commands/watch.js +112 -0
- package/lib/commands/watch.js.map +1 -0
- package/lib/index.d.ts +16 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +16 -0
- package/lib/index.js.map +1 -0
- package/lib/log-level.d.ts +39 -0
- package/lib/log-level.d.ts.map +1 -0
- package/lib/log-level.js +47 -0
- package/lib/log-level.js.map +1 -0
- package/lib/package-name.d.ts +33 -0
- package/lib/package-name.d.ts.map +1 -0
- package/lib/package-name.js +37 -0
- package/lib/package-name.js.map +1 -0
- package/lib/spawn-data-server.d.ts +102 -0
- package/lib/spawn-data-server.d.ts.map +1 -0
- package/lib/spawn-data-server.js +154 -0
- package/lib/spawn-data-server.js.map +1 -0
- package/lib/testing/echo-server.d.ts +11 -0
- package/lib/testing/echo-server.d.ts.map +1 -0
- package/lib/testing/echo-server.js +79 -0
- package/lib/testing/echo-server.js.map +1 -0
- package/package.json +105 -0
- package/src/cli-version.ts +32 -0
- package/src/cli.ts +139 -0
- package/src/commands/analyze-heap-args.ts +184 -0
- package/src/commands/analyze-heap.ts +76 -0
- package/src/commands/ast-ground-truth-args.ts +63 -0
- package/src/commands/ast-ground-truth-driver.ts +81 -0
- package/src/commands/ast-ground-truth.ts +49 -0
- package/src/commands/generate-transfer-model-args.ts +203 -0
- package/src/commands/generate-transfer-model-config.ts +128 -0
- package/src/commands/generate-transfer-model.ts +716 -0
- package/src/commands/harness-args.ts +429 -0
- package/src/commands/headless-harness.ts +165 -0
- package/src/commands/init-args.ts +273 -0
- package/src/commands/init-prompt.ts +149 -0
- package/src/commands/init-templates.ts +2037 -0
- package/src/commands/init-wizard.ts +378 -0
- package/src/commands/init-workspace.ts +442 -0
- package/src/commands/init.ts +554 -0
- package/src/commands/lint-grammar-args.ts +79 -0
- package/src/commands/lint-grammar-driver.ts +83 -0
- package/src/commands/lint-grammar-report.ts +74 -0
- package/src/commands/lint-grammar.ts +57 -0
- package/src/commands/measure-memory-args.ts +123 -0
- package/src/commands/measure-memory-driver.ts +128 -0
- package/src/commands/measure-memory.ts +143 -0
- package/src/commands/model-docs-args.ts +69 -0
- package/src/commands/model-docs-driver.ts +68 -0
- package/src/commands/model-docs-report.ts +133 -0
- package/src/commands/model-docs.ts +48 -0
- package/src/commands/projects-args.ts +62 -0
- package/src/commands/projects.ts +76 -0
- package/src/commands/query-args.ts +61 -0
- package/src/commands/query.ts +58 -0
- package/src/commands/reflect-args.ts +75 -0
- package/src/commands/reflect-driver.ts +76 -0
- package/src/commands/reflect-report.ts +138 -0
- package/src/commands/reflect.ts +51 -0
- package/src/commands/save-args.ts +74 -0
- package/src/commands/save.ts +84 -0
- package/src/commands/validate-args.ts +76 -0
- package/src/commands/validate-driver.ts +84 -0
- package/src/commands/validate-report.ts +95 -0
- package/src/commands/validate.ts +58 -0
- package/src/commands/watch-args.ts +74 -0
- package/src/commands/watch.ts +185 -0
- package/src/index.ts +16 -0
- package/src/log-level.ts +49 -0
- package/src/package-name.ts +39 -0
- package/src/spawn-data-server.ts +247 -0
- 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
|
+
}
|