@memlab/mcp-server 2.0.3 → 2.1.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.
- package/README.md +28 -13
- package/dist/heap-state.d.ts +12 -1
- package/dist/heap-state.d.ts.map +1 -1
- package/dist/heap-state.js +10 -4
- package/dist/heap-state.js.map +1 -1
- package/dist/index.js +27 -1
- package/dist/index.js.map +1 -1
- package/dist/tools/aggregate-nodes.d.ts.map +1 -1
- package/dist/tools/aggregate-nodes.js +2 -2
- package/dist/tools/aggregate-nodes.js.map +1 -1
- package/dist/tools/auto-investigate.d.ts +12 -0
- package/dist/tools/auto-investigate.d.ts.map +1 -0
- package/dist/tools/auto-investigate.js +683 -0
- package/dist/tools/auto-investigate.js.map +1 -0
- package/dist/tools/cache-analysis.d.ts +12 -0
- package/dist/tools/cache-analysis.d.ts.map +1 -0
- package/dist/tools/cache-analysis.js +341 -0
- package/dist/tools/cache-analysis.js.map +1 -0
- package/dist/tools/check-health.d.ts +12 -0
- package/dist/tools/check-health.d.ts.map +1 -0
- package/dist/tools/check-health.js +289 -0
- package/dist/tools/check-health.js.map +1 -0
- package/dist/tools/class-histogram.d.ts.map +1 -1
- package/dist/tools/class-histogram.js +70 -14
- package/dist/tools/class-histogram.js.map +1 -1
- package/dist/tools/closure-inspection.d.ts.map +1 -1
- package/dist/tools/closure-inspection.js +5 -5
- package/dist/tools/closure-inspection.js.map +1 -1
- package/dist/tools/detached-dom.d.ts.map +1 -1
- package/dist/tools/detached-dom.js +151 -5
- package/dist/tools/detached-dom.js.map +1 -1
- package/dist/tools/diff-snapshots.d.ts +12 -0
- package/dist/tools/diff-snapshots.d.ts.map +1 -0
- package/dist/tools/diff-snapshots.js +224 -0
- package/dist/tools/diff-snapshots.js.map +1 -0
- package/dist/tools/dominator-subtree.d.ts.map +1 -1
- package/dist/tools/dominator-subtree.js +29 -4
- package/dist/tools/dominator-subtree.js.map +1 -1
- package/dist/tools/duplicated-strings.d.ts.map +1 -1
- package/dist/tools/duplicated-strings.js +62 -9
- package/dist/tools/duplicated-strings.js.map +1 -1
- package/dist/tools/eval.d.ts.map +1 -1
- package/dist/tools/eval.js +111 -7
- package/dist/tools/eval.js.map +1 -1
- package/dist/tools/event-listener-leaks.d.ts +12 -0
- package/dist/tools/event-listener-leaks.d.ts.map +1 -0
- package/dist/tools/event-listener-leaks.js +266 -0
- package/dist/tools/event-listener-leaks.js.map +1 -0
- package/dist/tools/find-by-property.d.ts.map +1 -1
- package/dist/tools/find-by-property.js +38 -4
- package/dist/tools/find-by-property.js.map +1 -1
- package/dist/tools/find-nodes-by-class.d.ts.map +1 -1
- package/dist/tools/find-nodes-by-class.js +4 -4
- package/dist/tools/find-nodes-by-class.js.map +1 -1
- package/dist/tools/for-each.d.ts.map +1 -1
- package/dist/tools/for-each.js +2 -2
- package/dist/tools/for-each.js.map +1 -1
- package/dist/tools/get-node.d.ts.map +1 -1
- package/dist/tools/get-node.js +2 -2
- package/dist/tools/get-node.js.map +1 -1
- package/dist/tools/get-property.d.ts.map +1 -1
- package/dist/tools/get-property.js +4 -4
- package/dist/tools/get-property.js.map +1 -1
- package/dist/tools/get-references.d.ts.map +1 -1
- package/dist/tools/get-references.js +11 -5
- package/dist/tools/get-references.js.map +1 -1
- package/dist/tools/get-referrers.d.ts.map +1 -1
- package/dist/tools/get-referrers.js +11 -5
- package/dist/tools/get-referrers.js.map +1 -1
- package/dist/tools/global-variables.d.ts.map +1 -1
- package/dist/tools/global-variables.js +10 -6
- package/dist/tools/global-variables.js.map +1 -1
- package/dist/tools/largest-objects.d.ts.map +1 -1
- package/dist/tools/largest-objects.js +78 -8
- package/dist/tools/largest-objects.js.map +1 -1
- package/dist/tools/load-snapshot.d.ts.map +1 -1
- package/dist/tools/load-snapshot.js +118 -4
- package/dist/tools/load-snapshot.js.map +1 -1
- package/dist/tools/object-shape.d.ts.map +1 -1
- package/dist/tools/object-shape.js +66 -47
- package/dist/tools/object-shape.js.map +1 -1
- package/dist/tools/pinch-points.d.ts +12 -0
- package/dist/tools/pinch-points.d.ts.map +1 -0
- package/dist/tools/pinch-points.js +124 -0
- package/dist/tools/pinch-points.js.map +1 -0
- package/dist/tools/quick-diagnosis.d.ts +12 -0
- package/dist/tools/quick-diagnosis.d.ts.map +1 -0
- package/dist/tools/quick-diagnosis.js +213 -0
- package/dist/tools/quick-diagnosis.js.map +1 -0
- package/dist/tools/referrer-summary.d.ts +12 -0
- package/dist/tools/referrer-summary.d.ts.map +1 -0
- package/dist/tools/referrer-summary.js +119 -0
- package/dist/tools/referrer-summary.js.map +1 -0
- package/dist/tools/reports.d.ts.map +1 -1
- package/dist/tools/reports.js +5 -5
- package/dist/tools/reports.js.map +1 -1
- package/dist/tools/retainer-summary.d.ts +12 -0
- package/dist/tools/retainer-summary.d.ts.map +1 -0
- package/dist/tools/retainer-summary.js +307 -0
- package/dist/tools/retainer-summary.js.map +1 -0
- package/dist/tools/retainer-trace.d.ts.map +1 -1
- package/dist/tools/retainer-trace.js +88 -14
- package/dist/tools/retainer-trace.js.map +1 -1
- package/dist/tools/search-nodes.d.ts.map +1 -1
- package/dist/tools/search-nodes.js +2 -2
- package/dist/tools/search-nodes.js.map +1 -1
- package/dist/tools/shape-histogram.d.ts +12 -0
- package/dist/tools/shape-histogram.d.ts.map +1 -0
- package/dist/tools/shape-histogram.js +119 -0
- package/dist/tools/shape-histogram.js.map +1 -0
- package/dist/tools/sliced-strings.d.ts +12 -0
- package/dist/tools/sliced-strings.d.ts.map +1 -0
- package/dist/tools/sliced-strings.js +126 -0
- package/dist/tools/sliced-strings.js.map +1 -0
- package/dist/tools/snapshot-summary.d.ts.map +1 -1
- package/dist/tools/snapshot-summary.js +42 -3
- package/dist/tools/snapshot-summary.js.map +1 -1
- package/dist/tools/stale-collections.d.ts.map +1 -1
- package/dist/tools/stale-collections.js +30 -6
- package/dist/tools/stale-collections.js.map +1 -1
- package/dist/tools/string-patterns.d.ts +12 -0
- package/dist/tools/string-patterns.d.ts.map +1 -0
- package/dist/tools/string-patterns.js +115 -0
- package/dist/tools/string-patterns.js.map +1 -0
- package/dist/tools/trace-dominators.d.ts +12 -0
- package/dist/tools/trace-dominators.d.ts.map +1 -0
- package/dist/tools/trace-dominators.js +237 -0
- package/dist/tools/trace-dominators.js.map +1 -0
- package/dist/utils.d.ts +9 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +67 -5
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* @format
|
|
8
|
+
* @oncall memory_lab
|
|
9
|
+
*/
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { getSnapshot, getSnapshotMetadata } from '../heap-state.js';
|
|
12
|
+
import { filterLargestObjects, isNodeWorthInspecting, formatBytes, formatNumber, truncateNodeName, errorResult, toolResult, } from '../utils.js';
|
|
13
|
+
function classifySeverity(retainedSize, totalSize) {
|
|
14
|
+
if (totalSize === 0)
|
|
15
|
+
return 'LOW';
|
|
16
|
+
const pct = retainedSize / totalSize;
|
|
17
|
+
if (pct >= 0.1)
|
|
18
|
+
return 'CRITICAL';
|
|
19
|
+
if (pct >= 0.05)
|
|
20
|
+
return 'HIGH';
|
|
21
|
+
if (pct >= 0.01)
|
|
22
|
+
return 'MEDIUM';
|
|
23
|
+
return 'LOW';
|
|
24
|
+
}
|
|
25
|
+
function extractSourceHints(traces) {
|
|
26
|
+
const paths = new Set();
|
|
27
|
+
const filePathRegex = /(?:\/[\w.-]+){2,}\.\w+/g;
|
|
28
|
+
for (const trace of traces) {
|
|
29
|
+
for (const step of trace) {
|
|
30
|
+
const matches = step.name.match(filePathRegex);
|
|
31
|
+
if (matches) {
|
|
32
|
+
for (const m of matches) {
|
|
33
|
+
if (!m.includes('/node_modules/') ||
|
|
34
|
+
m.includes('/node_modules/@') ||
|
|
35
|
+
m.split('/node_modules/').length <= 2) {
|
|
36
|
+
paths.add(m);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (step.edgeName) {
|
|
41
|
+
const edgeMatches = step.edgeName.match(filePathRegex);
|
|
42
|
+
if (edgeMatches) {
|
|
43
|
+
for (const m of edgeMatches) {
|
|
44
|
+
if (!m.includes('/node_modules/') ||
|
|
45
|
+
m.includes('/node_modules/@') ||
|
|
46
|
+
m.split('/node_modules/').length <= 2) {
|
|
47
|
+
paths.add(m);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return [...paths];
|
|
55
|
+
}
|
|
56
|
+
function getRetainerPath(node) {
|
|
57
|
+
const visited = new Set([node.id]);
|
|
58
|
+
let cur = node;
|
|
59
|
+
const reverse = [
|
|
60
|
+
{
|
|
61
|
+
nodeId: cur.id,
|
|
62
|
+
name: cur.name,
|
|
63
|
+
type: cur.type,
|
|
64
|
+
retainedSize: cur.retainedSize,
|
|
65
|
+
selfSize: cur.self_size,
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
while (cur && cur.hasPathEdge) {
|
|
69
|
+
const edge = cur.pathEdge;
|
|
70
|
+
if (!edge)
|
|
71
|
+
break;
|
|
72
|
+
const from = edge.fromNode;
|
|
73
|
+
if (visited.has(from.id))
|
|
74
|
+
break;
|
|
75
|
+
visited.add(from.id);
|
|
76
|
+
reverse.push({
|
|
77
|
+
nodeId: from.id,
|
|
78
|
+
name: from.name,
|
|
79
|
+
type: from.type,
|
|
80
|
+
retainedSize: from.retainedSize,
|
|
81
|
+
selfSize: from.self_size,
|
|
82
|
+
edgeName: String(edge.name_or_index),
|
|
83
|
+
});
|
|
84
|
+
cur = from;
|
|
85
|
+
}
|
|
86
|
+
reverse.reverse();
|
|
87
|
+
return reverse;
|
|
88
|
+
}
|
|
89
|
+
function findPinchPoint(trace) {
|
|
90
|
+
let best = null;
|
|
91
|
+
for (const step of trace) {
|
|
92
|
+
if (step.selfSize === 0)
|
|
93
|
+
continue;
|
|
94
|
+
const ratio = step.retainedSize / step.selfSize;
|
|
95
|
+
if (ratio < 100)
|
|
96
|
+
continue;
|
|
97
|
+
if (step.type === 'hidden' ||
|
|
98
|
+
step.type === 'array' ||
|
|
99
|
+
step.type === 'native' ||
|
|
100
|
+
step.type === 'synthetic') {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (!best || ratio > best.ratio) {
|
|
104
|
+
best = {
|
|
105
|
+
nodeId: step.nodeId,
|
|
106
|
+
name: step.name,
|
|
107
|
+
type: step.type,
|
|
108
|
+
selfSize: step.selfSize,
|
|
109
|
+
retainedSize: step.retainedSize,
|
|
110
|
+
ratio,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return best;
|
|
115
|
+
}
|
|
116
|
+
function findUnboundedCaches(snapshot, limit) {
|
|
117
|
+
const caches = [];
|
|
118
|
+
snapshot.nodes.forEach(node => {
|
|
119
|
+
if (node.id <= 3)
|
|
120
|
+
return;
|
|
121
|
+
if (node.name !== 'Map' && node.name !== 'Set')
|
|
122
|
+
return;
|
|
123
|
+
if (node.type !== 'object')
|
|
124
|
+
return;
|
|
125
|
+
if (node.retainedSize < 1024 * 1024)
|
|
126
|
+
return;
|
|
127
|
+
let entryCount = 0;
|
|
128
|
+
for (const edge of node.references) {
|
|
129
|
+
if (edge.name_or_index === 'table' && edge.toNode.type === 'array') {
|
|
130
|
+
entryCount = edge.toNode.edge_count;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (entryCount < 50)
|
|
135
|
+
return;
|
|
136
|
+
const size = node.retainedSize;
|
|
137
|
+
let inserted = false;
|
|
138
|
+
for (let i = 0; i < caches.length; i++) {
|
|
139
|
+
if (size > caches[i].retainedSize) {
|
|
140
|
+
caches.splice(i, 0, {
|
|
141
|
+
nodeId: node.id,
|
|
142
|
+
name: node.name,
|
|
143
|
+
type: node.type,
|
|
144
|
+
entryCount,
|
|
145
|
+
retainedSize: size,
|
|
146
|
+
});
|
|
147
|
+
inserted = true;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!inserted)
|
|
152
|
+
caches.push({
|
|
153
|
+
nodeId: node.id,
|
|
154
|
+
name: node.name,
|
|
155
|
+
type: node.type,
|
|
156
|
+
entryCount,
|
|
157
|
+
retainedSize: size,
|
|
158
|
+
});
|
|
159
|
+
if (caches.length > limit)
|
|
160
|
+
caches.length = limit;
|
|
161
|
+
});
|
|
162
|
+
return caches;
|
|
163
|
+
}
|
|
164
|
+
function scanPromises(snapshot) {
|
|
165
|
+
const summary = {
|
|
166
|
+
pendingCount: 0,
|
|
167
|
+
pendingRetained: 0,
|
|
168
|
+
resolvedCount: 0,
|
|
169
|
+
topPending: [],
|
|
170
|
+
};
|
|
171
|
+
snapshot.nodes.forEach(node => {
|
|
172
|
+
if (node.name !== 'Promise' || node.type !== 'object')
|
|
173
|
+
return;
|
|
174
|
+
if (node.id <= 3)
|
|
175
|
+
return;
|
|
176
|
+
let isPending = false;
|
|
177
|
+
for (const edge of node.references) {
|
|
178
|
+
if (edge.type === 'internal' &&
|
|
179
|
+
String(edge.name_or_index) === 'reactions_or_result' &&
|
|
180
|
+
edge.toNode.name === 'PromiseReaction') {
|
|
181
|
+
isPending = true;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (isPending) {
|
|
186
|
+
summary.pendingCount++;
|
|
187
|
+
summary.pendingRetained += node.retainedSize;
|
|
188
|
+
const size = node.retainedSize;
|
|
189
|
+
let inserted = false;
|
|
190
|
+
for (let i = 0; i < summary.topPending.length; i++) {
|
|
191
|
+
if (size > summary.topPending[i].retainedSize) {
|
|
192
|
+
summary.topPending.splice(i, 0, {
|
|
193
|
+
nodeId: node.id,
|
|
194
|
+
retainedSize: size,
|
|
195
|
+
});
|
|
196
|
+
inserted = true;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (!inserted)
|
|
201
|
+
summary.topPending.push({ nodeId: node.id, retainedSize: size });
|
|
202
|
+
if (summary.topPending.length > 5)
|
|
203
|
+
summary.topPending.length = 5;
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
summary.resolvedCount++;
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
return summary;
|
|
210
|
+
}
|
|
211
|
+
function scanTopShapes(snapshot, limit) {
|
|
212
|
+
const shapeMap = new Map();
|
|
213
|
+
snapshot.nodes.forEach(node => {
|
|
214
|
+
if (node.type !== 'object' || node.id <= 3)
|
|
215
|
+
return;
|
|
216
|
+
if (node.name !== 'Object')
|
|
217
|
+
return;
|
|
218
|
+
const names = [];
|
|
219
|
+
for (const edge of node.references) {
|
|
220
|
+
if (edge.type === 'property') {
|
|
221
|
+
names.push(String(edge.name_or_index));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (names.length === 0)
|
|
225
|
+
return;
|
|
226
|
+
names.sort();
|
|
227
|
+
const key = names.join(',');
|
|
228
|
+
const existing = shapeMap.get(key);
|
|
229
|
+
if (existing) {
|
|
230
|
+
existing.count++;
|
|
231
|
+
existing.totalSelfSize += node.self_size;
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
shapeMap.set(key, {
|
|
235
|
+
properties: names,
|
|
236
|
+
count: 1,
|
|
237
|
+
totalSelfSize: node.self_size,
|
|
238
|
+
exampleNodeId: node.id,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
return [...shapeMap.values()]
|
|
243
|
+
.filter(s => s.count >= 100)
|
|
244
|
+
.sort((a, b) => b.totalSelfSize - a.totalSelfSize)
|
|
245
|
+
.slice(0, limit);
|
|
246
|
+
}
|
|
247
|
+
const LISTENER_SHAPE_KEYWORDS = new Set([
|
|
248
|
+
'callback',
|
|
249
|
+
'handler',
|
|
250
|
+
'listener',
|
|
251
|
+
'onError',
|
|
252
|
+
'onSuccess',
|
|
253
|
+
'onChange',
|
|
254
|
+
'onUpdate',
|
|
255
|
+
'onLoad',
|
|
256
|
+
'onDone',
|
|
257
|
+
'fn',
|
|
258
|
+
]);
|
|
259
|
+
const LISTENER_CONTEXT_KEYWORDS = new Set([
|
|
260
|
+
'context',
|
|
261
|
+
'ctx',
|
|
262
|
+
'this',
|
|
263
|
+
'target',
|
|
264
|
+
'scope',
|
|
265
|
+
'self',
|
|
266
|
+
]);
|
|
267
|
+
function scanSubscriptionAccumulation(snapshot, threshold) {
|
|
268
|
+
const shapeMap = new Map();
|
|
269
|
+
snapshot.nodes.forEach(node => {
|
|
270
|
+
if (node.type !== 'object' || node.id <= 3)
|
|
271
|
+
return;
|
|
272
|
+
if (node.name !== 'Object')
|
|
273
|
+
return;
|
|
274
|
+
const names = [];
|
|
275
|
+
for (const edge of node.references) {
|
|
276
|
+
if (edge.type === 'property') {
|
|
277
|
+
names.push(String(edge.name_or_index));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (names.length === 0 || names.length > 10)
|
|
281
|
+
return;
|
|
282
|
+
names.sort();
|
|
283
|
+
const key = names.join(',');
|
|
284
|
+
const existing = shapeMap.get(key);
|
|
285
|
+
if (existing) {
|
|
286
|
+
existing.count++;
|
|
287
|
+
existing.totalSelfSize += node.self_size;
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
const hasListener = names.some(n => LISTENER_SHAPE_KEYWORDS.has(n) || n.startsWith('on'));
|
|
291
|
+
const hasContext = names.some(n => LISTENER_CONTEXT_KEYWORDS.has(n));
|
|
292
|
+
shapeMap.set(key, {
|
|
293
|
+
properties: names,
|
|
294
|
+
count: 1,
|
|
295
|
+
totalSelfSize: node.self_size,
|
|
296
|
+
exampleNodeId: node.id,
|
|
297
|
+
hasListenerProp: hasListener,
|
|
298
|
+
hasContextProp: hasContext,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
return [...shapeMap.values()]
|
|
303
|
+
.filter(s => s.count >= threshold && s.hasListenerProp && s.hasContextProp)
|
|
304
|
+
.sort((a, b) => b.totalSelfSize - a.totalSelfSize);
|
|
305
|
+
}
|
|
306
|
+
const ERROR_CLASS_NAMES = new Set([
|
|
307
|
+
'Error',
|
|
308
|
+
'SyntaxError',
|
|
309
|
+
'TypeError',
|
|
310
|
+
'ReferenceError',
|
|
311
|
+
'RangeError',
|
|
312
|
+
'URIError',
|
|
313
|
+
'EvalError',
|
|
314
|
+
'AggregateError',
|
|
315
|
+
]);
|
|
316
|
+
function scanErrorAccumulation(snapshot, minCount) {
|
|
317
|
+
const errorMap = new Map();
|
|
318
|
+
snapshot.nodes.forEach(node => {
|
|
319
|
+
if (node.type !== 'object' || node.id <= 3)
|
|
320
|
+
return;
|
|
321
|
+
if (!ERROR_CLASS_NAMES.has(node.name))
|
|
322
|
+
return;
|
|
323
|
+
let message = '';
|
|
324
|
+
for (const edge of node.references) {
|
|
325
|
+
if (String(edge.name_or_index) === 'message' && edge.toNode.isString) {
|
|
326
|
+
const strNode = edge.toNode.toStringNode();
|
|
327
|
+
if (strNode)
|
|
328
|
+
message = strNode.stringValue;
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (!message)
|
|
333
|
+
return;
|
|
334
|
+
const key = `${node.name}::${message}`;
|
|
335
|
+
const existing = errorMap.get(key);
|
|
336
|
+
if (existing) {
|
|
337
|
+
existing.count++;
|
|
338
|
+
existing.totalRetained += node.retainedSize;
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
errorMap.set(key, {
|
|
342
|
+
errorType: node.name,
|
|
343
|
+
message,
|
|
344
|
+
count: 1,
|
|
345
|
+
totalRetained: node.retainedSize,
|
|
346
|
+
exampleNodeId: node.id,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
return [...errorMap.values()]
|
|
351
|
+
.filter(e => e.count >= minCount)
|
|
352
|
+
.sort((a, b) => b.count - a.count);
|
|
353
|
+
}
|
|
354
|
+
function tracePrefix(trace) {
|
|
355
|
+
return trace.map(s => `${s.name}(${s.type})`).join('->');
|
|
356
|
+
}
|
|
357
|
+
function deduplicateFindings(findings) {
|
|
358
|
+
if (findings.length <= 1)
|
|
359
|
+
return findings;
|
|
360
|
+
const prefixMap = new Map();
|
|
361
|
+
for (const f of findings) {
|
|
362
|
+
const collapsed = f.trace.filter(s => s.type !== 'hidden' &&
|
|
363
|
+
s.type !== 'array' &&
|
|
364
|
+
s.type !== 'native' &&
|
|
365
|
+
s.type !== 'synthetic' &&
|
|
366
|
+
s.type !== 'code');
|
|
367
|
+
const prefix = collapsed.length >= 3
|
|
368
|
+
? collapsed
|
|
369
|
+
.slice(0, -1)
|
|
370
|
+
.map(s => `${s.name}(${s.type})`)
|
|
371
|
+
.join('->')
|
|
372
|
+
: tracePrefix(collapsed);
|
|
373
|
+
const existing = prefixMap.get(prefix);
|
|
374
|
+
if (existing) {
|
|
375
|
+
if (f.node.retainedSize > existing.representative.node.retainedSize) {
|
|
376
|
+
existing.others.push(existing.representative);
|
|
377
|
+
existing.representative = f;
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
existing.others.push(f);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
prefixMap.set(prefix, { representative: f, others: [] });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const result = [];
|
|
388
|
+
for (const { representative, others } of prefixMap.values()) {
|
|
389
|
+
representative.collapsed_siblings = others;
|
|
390
|
+
result.push(representative);
|
|
391
|
+
}
|
|
392
|
+
return result;
|
|
393
|
+
}
|
|
394
|
+
function formatTrace(trace, maxSteps) {
|
|
395
|
+
const collapsed = [];
|
|
396
|
+
for (const step of trace) {
|
|
397
|
+
const isInternal = step.type === 'hidden' ||
|
|
398
|
+
step.type === 'array' ||
|
|
399
|
+
step.type === 'native' ||
|
|
400
|
+
step.type === 'synthetic' ||
|
|
401
|
+
step.type === 'code';
|
|
402
|
+
if (isInternal && collapsed.length > 0)
|
|
403
|
+
continue;
|
|
404
|
+
collapsed.push(step);
|
|
405
|
+
}
|
|
406
|
+
const shown = collapsed.length <= maxSteps
|
|
407
|
+
? collapsed
|
|
408
|
+
: [...collapsed.slice(0, 3), ...collapsed.slice(-2)];
|
|
409
|
+
const parts = [];
|
|
410
|
+
for (let i = 0; i < shown.length; i++) {
|
|
411
|
+
const s = shown[i];
|
|
412
|
+
const name = truncateNodeName(s.name, s.type, s.selfSize, 50);
|
|
413
|
+
parts.push(`@${s.nodeId} ${name} (${s.type}) [${formatBytes(s.retainedSize)}]`);
|
|
414
|
+
if (i === 2 && collapsed.length > maxSteps) {
|
|
415
|
+
parts.push(` … ${collapsed.length - 5} more nodes …`);
|
|
416
|
+
}
|
|
417
|
+
if (i < shown.length - 1 && s.edgeName != null) {
|
|
418
|
+
parts[parts.length - 1] += ` --${s.edgeName}-->`;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return parts.join('\n → ');
|
|
422
|
+
}
|
|
423
|
+
export function registerAutoInvestigate(server) {
|
|
424
|
+
server.tool('memlab_auto_investigate', 'One-shot deep analysis: finds the top retained objects, traces each retainer chain to the GC root, identifies pinch points (small objects retaining large subtrees), and detects unbounded caches. Returns a structured report with root causes and suggested fixes. Use this as the first tool after loading a snapshot to get immediate actionable findings.', {
|
|
425
|
+
top_n: z
|
|
426
|
+
.number()
|
|
427
|
+
.optional()
|
|
428
|
+
.default(5)
|
|
429
|
+
.describe('Number of top retained objects to analyze (default 5)'),
|
|
430
|
+
focus: z
|
|
431
|
+
.enum(['all', 'caches', 'dom', 'strings', 'closures'])
|
|
432
|
+
.optional()
|
|
433
|
+
.default('all')
|
|
434
|
+
.describe('Focus the analysis on a specific category: "caches" (Maps/Sets), "dom" (detached DOM), "strings" (string waste), "closures" (closure leaks), or "all" (default).'),
|
|
435
|
+
}, async ({ top_n, focus }) => {
|
|
436
|
+
try {
|
|
437
|
+
const snapshot = getSnapshot();
|
|
438
|
+
const meta = getSnapshotMetadata();
|
|
439
|
+
const totalSize = meta?.totalSize ?? 0;
|
|
440
|
+
const focusFilter = (node) => {
|
|
441
|
+
if (!isNodeWorthInspecting(node))
|
|
442
|
+
return false;
|
|
443
|
+
switch (focus) {
|
|
444
|
+
case 'caches':
|
|
445
|
+
return ((node.name === 'Map' ||
|
|
446
|
+
node.name === 'Set' ||
|
|
447
|
+
node.name === 'Array') &&
|
|
448
|
+
node.type === 'object');
|
|
449
|
+
case 'dom':
|
|
450
|
+
return node.is_detached || node.name.startsWith('Detached ');
|
|
451
|
+
case 'strings':
|
|
452
|
+
return (node.type === 'string' || node.type === 'concatenated string');
|
|
453
|
+
case 'closures':
|
|
454
|
+
return node.type === 'closure';
|
|
455
|
+
default:
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
const largest = filterLargestObjects(snapshot, focusFilter, top_n);
|
|
460
|
+
if (largest.length === 0) {
|
|
461
|
+
return toolResult(focus === 'all'
|
|
462
|
+
? 'No significant objects found in the snapshot.'
|
|
463
|
+
: `No significant ${focus} objects found. Try focus: "all" for a broader analysis.`);
|
|
464
|
+
}
|
|
465
|
+
const rawFindings = [];
|
|
466
|
+
for (const node of largest) {
|
|
467
|
+
const trace = getRetainerPath(node);
|
|
468
|
+
const pinch = findPinchPoint(trace);
|
|
469
|
+
const severity = classifySeverity(node.retainedSize, totalSize);
|
|
470
|
+
rawFindings.push({ node, trace, pinchPoint: pinch, severity });
|
|
471
|
+
}
|
|
472
|
+
const findings = deduplicateFindings(rawFindings);
|
|
473
|
+
const caches = focus === 'all' || focus === 'caches'
|
|
474
|
+
? findUnboundedCaches(snapshot, 5)
|
|
475
|
+
: [];
|
|
476
|
+
const promises = focus === 'all'
|
|
477
|
+
? scanPromises(snapshot)
|
|
478
|
+
: {
|
|
479
|
+
pendingCount: 0,
|
|
480
|
+
pendingRetained: 0,
|
|
481
|
+
resolvedCount: 0,
|
|
482
|
+
topPending: [],
|
|
483
|
+
};
|
|
484
|
+
const shapes = focus === 'all' ? scanTopShapes(snapshot, 5) : [];
|
|
485
|
+
const subscriptions = focus === 'all' ? scanSubscriptionAccumulation(snapshot, 1000) : [];
|
|
486
|
+
const errorAccum = focus === 'all' ? scanErrorAccumulation(snapshot, 10) : [];
|
|
487
|
+
const lines = [`# Auto-Investigation Report`, ''];
|
|
488
|
+
const focusLabel = focus === 'all' ? '' : ` (focus: ${focus})`;
|
|
489
|
+
lines.push(`## Top ${findings.length} Retained Objects${focusLabel}`);
|
|
490
|
+
lines.push('');
|
|
491
|
+
const pinchPointMap = new Map();
|
|
492
|
+
for (let i = 0; i < findings.length; i++) {
|
|
493
|
+
const f = findings[i];
|
|
494
|
+
if (f.pinchPoint) {
|
|
495
|
+
const existing = pinchPointMap.get(f.pinchPoint.nodeId);
|
|
496
|
+
if (existing) {
|
|
497
|
+
existing.findingIndices.push(i);
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
pinchPointMap.set(f.pinchPoint.nodeId, {
|
|
501
|
+
pp: f.pinchPoint,
|
|
502
|
+
findingIndices: [i],
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
for (let i = 0; i < findings.length; i++) {
|
|
508
|
+
const f = findings[i];
|
|
509
|
+
const siblings = f.collapsed_siblings ?? [];
|
|
510
|
+
const totalRetained = f.node.retainedSize +
|
|
511
|
+
siblings.reduce((s, sib) => s + sib.node.retainedSize, 0);
|
|
512
|
+
const pct = totalSize > 0
|
|
513
|
+
? ` (${((totalRetained / totalSize) * 100).toFixed(1)}% of heap)`
|
|
514
|
+
: '';
|
|
515
|
+
const name = truncateNodeName(f.node.name, f.node.type, f.node.self_size, 60);
|
|
516
|
+
const sevIcon = f.severity === 'CRITICAL'
|
|
517
|
+
? '🔴'
|
|
518
|
+
: f.severity === 'HIGH'
|
|
519
|
+
? '🟠'
|
|
520
|
+
: f.severity === 'MEDIUM'
|
|
521
|
+
? '🟡'
|
|
522
|
+
: '🔵';
|
|
523
|
+
if (siblings.length > 0) {
|
|
524
|
+
lines.push(`### ${i + 1}. ${sevIcon} [${f.severity}] \`${name}\` chain — ${formatBytes(totalRetained)} total across ${1 + siblings.length} nodes${pct}`);
|
|
525
|
+
lines.push('');
|
|
526
|
+
lines.push(`**Retainer chain:** ${formatTrace(f.trace, 8)}`);
|
|
527
|
+
lines.push('');
|
|
528
|
+
lines.push(`**Same chain contains:** @${f.node.id} (${formatBytes(f.node.retainedSize)})` +
|
|
529
|
+
siblings
|
|
530
|
+
.map(sib => `, @${sib.node.id} ${truncateNodeName(sib.node.name, sib.node.type, sib.node.self_size, 30)} (${formatBytes(sib.node.retainedSize)})`)
|
|
531
|
+
.join(''));
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
lines.push(`### ${i + 1}. ${sevIcon} [${f.severity}] @${f.node.id} \`${name}\` (${f.node.type}) — ${formatBytes(f.node.retainedSize)}${pct}`);
|
|
535
|
+
lines.push('');
|
|
536
|
+
lines.push(`**Retainer chain:** ${formatTrace(f.trace, 8)}`);
|
|
537
|
+
}
|
|
538
|
+
lines.push('');
|
|
539
|
+
if (f.pinchPoint) {
|
|
540
|
+
const pp = f.pinchPoint;
|
|
541
|
+
const ppEntry = pinchPointMap.get(pp.nodeId);
|
|
542
|
+
const sharedWith = ppEntry?.findingIndices.filter(j => j !== i);
|
|
543
|
+
if (sharedWith && sharedWith.length > 0) {
|
|
544
|
+
lines.push(`**Pinch point:** @${pp.nodeId} (shared with findings ${sharedWith.map(j => `#${j + 1}`).join(', ')} — see below)`);
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
const ppName = truncateNodeName(pp.name, pp.type, pp.selfSize, 50);
|
|
548
|
+
lines.push(`**Pinch point:** @${pp.nodeId} \`${ppName}\` — self: ${formatBytes(pp.selfSize)}, retains: ${formatBytes(pp.retainedSize)} (${formatNumber(Math.round(pp.ratio))}:1 ratio). Freeing this single object would reclaim ${formatBytes(pp.retainedSize)}.`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
lines.push('**Pinch point:** none found (retained size is distributed)');
|
|
553
|
+
}
|
|
554
|
+
lines.push('');
|
|
555
|
+
}
|
|
556
|
+
const sharedPinchPoints = [...pinchPointMap.values()].filter(e => e.findingIndices.length > 1);
|
|
557
|
+
if (sharedPinchPoints.length > 0) {
|
|
558
|
+
lines.push('## Shared Pinch Points');
|
|
559
|
+
lines.push('');
|
|
560
|
+
for (const { pp, findingIndices } of sharedPinchPoints) {
|
|
561
|
+
const ppName = truncateNodeName(pp.name, pp.type, pp.selfSize, 50);
|
|
562
|
+
lines.push(`- @${pp.nodeId} \`${ppName}\` — self: ${formatBytes(pp.selfSize)}, retains: ${formatBytes(pp.retainedSize)} (${formatNumber(Math.round(pp.ratio))}:1 ratio). Shared by findings ${findingIndices.map(j => `#${j + 1}`).join(', ')}. Freeing this single object would reclaim ${formatBytes(pp.retainedSize)}.`);
|
|
563
|
+
}
|
|
564
|
+
lines.push('');
|
|
565
|
+
}
|
|
566
|
+
if (promises.pendingCount > 0 && promises.pendingRetained > 100_000) {
|
|
567
|
+
lines.push(`## Pending Promises`);
|
|
568
|
+
lines.push('');
|
|
569
|
+
lines.push(`${formatNumber(promises.pendingCount)} pending, ${formatNumber(promises.resolvedCount)} resolved. Pending promises retain **${formatBytes(promises.pendingRetained)}** total.`);
|
|
570
|
+
if (promises.topPending.length > 0) {
|
|
571
|
+
lines.push('');
|
|
572
|
+
lines.push('Top pending by retained size:');
|
|
573
|
+
for (const p of promises.topPending) {
|
|
574
|
+
lines.push(`- @${p.nodeId} — ${formatBytes(p.retainedSize)} retained`);
|
|
575
|
+
}
|
|
576
|
+
lines.push('', '_Pending promises retaining large subtrees are often stuck async operations (HTTP requests, DB queries). Use `memlab_trace_dominators` to trace from the top pending promise to the data it holds._');
|
|
577
|
+
}
|
|
578
|
+
lines.push('');
|
|
579
|
+
}
|
|
580
|
+
if (caches.length > 0) {
|
|
581
|
+
lines.push(`## Unbounded Caches Detected`);
|
|
582
|
+
lines.push('');
|
|
583
|
+
for (const c of caches) {
|
|
584
|
+
lines.push(`- @${c.nodeId} \`${c.name}\` — ${formatNumber(c.entryCount)} entries, ${formatBytes(c.retainedSize)} retained. Check if this cache has eviction logic.`);
|
|
585
|
+
}
|
|
586
|
+
lines.push('');
|
|
587
|
+
}
|
|
588
|
+
if (shapes.length > 0) {
|
|
589
|
+
lines.push(`## Object Shape Summary`);
|
|
590
|
+
lines.push('');
|
|
591
|
+
lines.push('Top data record shapes (generic `Object` instances grouped by property structure):');
|
|
592
|
+
lines.push('');
|
|
593
|
+
for (const s of shapes) {
|
|
594
|
+
const propsDisplay = s.properties.length <= 6
|
|
595
|
+
? `{${s.properties.join(', ')}}`
|
|
596
|
+
: `{${s.properties.slice(0, 5).join(', ')}, … +${s.properties.length - 5}}`;
|
|
597
|
+
const shapeSev = classifySeverity(s.totalSelfSize, totalSize);
|
|
598
|
+
const shapeSevIcon = shapeSev === 'CRITICAL'
|
|
599
|
+
? '🔴'
|
|
600
|
+
: shapeSev === 'HIGH'
|
|
601
|
+
? '🟠'
|
|
602
|
+
: shapeSev === 'MEDIUM'
|
|
603
|
+
? '🟡'
|
|
604
|
+
: '';
|
|
605
|
+
const sevLabel = shapeSev !== 'LOW' ? ` ${shapeSevIcon} [${shapeSev}]` : '';
|
|
606
|
+
lines.push(`- ${propsDisplay} — ${formatNumber(s.count)} instances, ${formatBytes(s.totalSelfSize)} self (example: @${s.exampleNodeId})${sevLabel}`);
|
|
607
|
+
}
|
|
608
|
+
lines.push('', '_Use `memlab_shape_histogram` for full shape analysis with retained sizes._');
|
|
609
|
+
lines.push('');
|
|
610
|
+
}
|
|
611
|
+
if (subscriptions.length > 0) {
|
|
612
|
+
lines.push(`## Subscription/Listener Accumulation`);
|
|
613
|
+
lines.push('');
|
|
614
|
+
lines.push('Object shapes matching listener/subscription patterns (properties containing callback/handler + context/target):');
|
|
615
|
+
lines.push('');
|
|
616
|
+
for (const s of subscriptions.slice(0, 5)) {
|
|
617
|
+
const propsDisplay = `{${s.properties.join(', ')}}`;
|
|
618
|
+
const subSev = classifySeverity(s.totalSelfSize, totalSize);
|
|
619
|
+
const subSevIcon = subSev === 'CRITICAL'
|
|
620
|
+
? '🔴'
|
|
621
|
+
: subSev === 'HIGH'
|
|
622
|
+
? '🟠'
|
|
623
|
+
: subSev === 'MEDIUM'
|
|
624
|
+
? '🟡'
|
|
625
|
+
: '🔵';
|
|
626
|
+
lines.push(`- ${subSevIcon} [${subSev}] ${propsDisplay} — **${formatNumber(s.count)}** instances, ${formatBytes(s.totalSelfSize)} self (example: @${s.exampleNodeId})`);
|
|
627
|
+
}
|
|
628
|
+
lines.push('', '_These may be event listeners, signal subscriptions, or observer registrations that are not being cleaned up. Use `memlab_retainer_summary` with node_ids from the examples to trace the retention pattern._');
|
|
629
|
+
lines.push('');
|
|
630
|
+
}
|
|
631
|
+
if (errorAccum.length > 0) {
|
|
632
|
+
lines.push(`## Repeated Error Accumulation`);
|
|
633
|
+
lines.push('');
|
|
634
|
+
lines.push('Identical Error instances appearing many times — often indicates a module being evaluated repeatedly and failing each time:');
|
|
635
|
+
lines.push('');
|
|
636
|
+
for (const e of errorAccum.slice(0, 5)) {
|
|
637
|
+
const msgDisplay = e.message.length > 80
|
|
638
|
+
? e.message.slice(0, 77) + '...'
|
|
639
|
+
: e.message;
|
|
640
|
+
lines.push(`- **${formatNumber(e.count)}x** \`${e.errorType}\`: "${msgDisplay}" — ${formatBytes(e.totalRetained)} total retained (example: @${e.exampleNodeId})`);
|
|
641
|
+
}
|
|
642
|
+
lines.push('', '_Repeated identical errors are almost always a bug — a module failing to load, a misconfigured import, or a retry loop that never succeeds._');
|
|
643
|
+
lines.push('');
|
|
644
|
+
}
|
|
645
|
+
const sourceHints = extractSourceHints(findings.map(f => f.trace));
|
|
646
|
+
if (sourceHints.length > 0) {
|
|
647
|
+
lines.push('## Source Hints');
|
|
648
|
+
lines.push('');
|
|
649
|
+
lines.push('File paths extracted from retainer chains (use to locate relevant source code):');
|
|
650
|
+
for (const hint of sourceHints.slice(0, 10)) {
|
|
651
|
+
lines.push(`- \`${hint}\``);
|
|
652
|
+
}
|
|
653
|
+
if (sourceHints.length > 10) {
|
|
654
|
+
lines.push(`- … and ${sourceHints.length - 10} more`);
|
|
655
|
+
}
|
|
656
|
+
lines.push('');
|
|
657
|
+
}
|
|
658
|
+
lines.push('## Suggested Next Steps');
|
|
659
|
+
lines.push('');
|
|
660
|
+
let stepNum = 1;
|
|
661
|
+
if (findings[0]?.pinchPoint) {
|
|
662
|
+
const pp = findings[0].pinchPoint;
|
|
663
|
+
lines.push(`${stepNum++}. Inspect pinch point: \`memlab_get_node(${pp.nodeId})\` then \`memlab_dominator_subtree(${pp.nodeId})\``);
|
|
664
|
+
}
|
|
665
|
+
lines.push(`${stepNum++}. Trace top retainer: \`memlab_trace_dominators(${findings[0].node.id})\` for full dominator chain in one call`);
|
|
666
|
+
lines.push(`${stepNum++}. Check for patterns: \`memlab_retainer_summary\` with class name of top objects`);
|
|
667
|
+
if (promises.pendingCount > 0 && promises.topPending.length > 0) {
|
|
668
|
+
lines.push(`${stepNum++}. Investigate pending promises: \`memlab_trace_dominators(${promises.topPending[0].nodeId})\``);
|
|
669
|
+
}
|
|
670
|
+
if (caches.length > 0) {
|
|
671
|
+
lines.push(`${stepNum++}. Analyze caches: \`memlab_cache_analysis\` for detailed cache inspection`);
|
|
672
|
+
}
|
|
673
|
+
if (shapes.length > 0) {
|
|
674
|
+
lines.push(`${stepNum++}. Explore data shapes: \`memlab_shape_histogram\` for detailed shape clustering`);
|
|
675
|
+
}
|
|
676
|
+
return toolResult(lines.join('\n'));
|
|
677
|
+
}
|
|
678
|
+
catch (err) {
|
|
679
|
+
return errorResult(err);
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
//# sourceMappingURL=auto-investigate.js.map
|