@memlab/mcp-server 2.18.1 → 2.52.0
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 +8 -14
- package/bin/.eslintrc.js +26 -0
- package/bin/memlab-cli.js +313 -0
- package/dist/artifact-classes.d.ts +86 -0
- package/dist/artifact-classes.d.ts.map +1 -0
- package/dist/artifact-classes.js +261 -0
- package/dist/artifact-classes.js.map +1 -0
- package/dist/capture-mode.d.ts +60 -0
- package/dist/capture-mode.d.ts.map +1 -0
- package/dist/capture-mode.js +39 -0
- package/dist/capture-mode.js.map +1 -0
- package/dist/guardrail.d.ts.map +1 -1
- package/dist/guardrail.js +13 -0
- package/dist/guardrail.js.map +1 -1
- package/dist/heap-state.d.ts +89 -2
- package/dist/heap-state.d.ts.map +1 -1
- package/dist/heap-state.js +186 -2
- package/dist/heap-state.js.map +1 -1
- package/dist/index.js +39 -1
- package/dist/index.js.map +1 -1
- package/dist/progress.d.ts +25 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +46 -0
- package/dist/progress.js.map +1 -0
- package/dist/result-handles.d.ts +11 -0
- package/dist/result-handles.d.ts.map +1 -0
- package/dist/result-handles.js +77 -0
- package/dist/result-handles.js.map +1 -0
- package/dist/string-index.d.ts +55 -0
- package/dist/string-index.d.ts.map +1 -0
- package/dist/string-index.js +91 -0
- package/dist/string-index.js.map +1 -0
- package/dist/tool-registry.d.ts +42 -0
- package/dist/tool-registry.d.ts.map +1 -0
- package/dist/tool-registry.js +20 -0
- package/dist/tool-registry.js.map +1 -0
- package/dist/tools/analyze-run.d.ts +12 -0
- package/dist/tools/analyze-run.d.ts.map +1 -0
- package/dist/tools/analyze-run.js +343 -0
- package/dist/tools/analyze-run.js.map +1 -0
- package/dist/tools/app-heap.d.ts +12 -0
- package/dist/tools/app-heap.d.ts.map +1 -0
- package/dist/tools/app-heap.js +140 -0
- package/dist/tools/app-heap.js.map +1 -0
- package/dist/tools/batch.d.ts +12 -0
- package/dist/tools/batch.d.ts.map +1 -0
- package/dist/tools/batch.js +168 -0
- package/dist/tools/batch.js.map +1 -0
- package/dist/tools/cache-analysis.d.ts.map +1 -1
- package/dist/tools/cache-analysis.js +156 -5
- package/dist/tools/cache-analysis.js.map +1 -1
- package/dist/tools/class-histogram.d.ts.map +1 -1
- package/dist/tools/class-histogram.js +14 -3
- package/dist/tools/class-histogram.js.map +1 -1
- package/dist/tools/collection-trend.d.ts +55 -0
- package/dist/tools/collection-trend.d.ts.map +1 -0
- package/dist/tools/collection-trend.js +315 -0
- package/dist/tools/collection-trend.js.map +1 -0
- package/dist/tools/detached-dom.d.ts +2 -0
- package/dist/tools/detached-dom.d.ts.map +1 -1
- package/dist/tools/detached-dom.js +291 -20
- package/dist/tools/detached-dom.js.map +1 -1
- package/dist/tools/dev-artifacts.d.ts +62 -6
- package/dist/tools/dev-artifacts.d.ts.map +1 -1
- package/dist/tools/dev-artifacts.js +466 -23
- package/dist/tools/dev-artifacts.js.map +1 -1
- package/dist/tools/dominator-attribution.d.ts +12 -0
- package/dist/tools/dominator-attribution.d.ts.map +1 -0
- package/dist/tools/dominator-attribution.js +189 -0
- package/dist/tools/dominator-attribution.js.map +1 -0
- package/dist/tools/dominator-chain.d.ts +12 -0
- package/dist/tools/dominator-chain.d.ts.map +1 -0
- package/dist/tools/dominator-chain.js +92 -0
- package/dist/tools/dominator-chain.js.map +1 -0
- package/dist/tools/duplicate-objects.d.ts +12 -0
- package/dist/tools/duplicate-objects.d.ts.map +1 -0
- package/dist/tools/duplicate-objects.js +172 -0
- package/dist/tools/duplicate-objects.js.map +1 -0
- package/dist/tools/duplicated-strings.d.ts.map +1 -1
- package/dist/tools/duplicated-strings.js +46 -38
- package/dist/tools/duplicated-strings.js.map +1 -1
- package/dist/tools/eval.d.ts +1 -0
- package/dist/tools/eval.d.ts.map +1 -1
- package/dist/tools/eval.js +785 -28
- package/dist/tools/eval.js.map +1 -1
- package/dist/tools/explain-delta.d.ts +12 -0
- package/dist/tools/explain-delta.d.ts.map +1 -0
- package/dist/tools/explain-delta.js +170 -0
- package/dist/tools/explain-delta.js.map +1 -0
- package/dist/tools/find-nodes-by-class.d.ts.map +1 -1
- package/dist/tools/find-nodes-by-class.js +112 -13
- package/dist/tools/find-nodes-by-class.js.map +1 -1
- package/dist/tools/finding-index.d.ts +22 -0
- package/dist/tools/finding-index.d.ts.map +1 -0
- package/dist/tools/finding-index.js +247 -0
- package/dist/tools/finding-index.js.map +1 -0
- package/dist/tools/for-each.d.ts.map +1 -1
- package/dist/tools/for-each.js +15 -3
- package/dist/tools/for-each.js.map +1 -1
- package/dist/tools/get-referrers.d.ts.map +1 -1
- package/dist/tools/get-referrers.js +17 -0
- package/dist/tools/get-referrers.js.map +1 -1
- package/dist/tools/hunt-report.d.ts +12 -0
- package/dist/tools/hunt-report.d.ts.map +1 -0
- package/dist/tools/hunt-report.js +186 -0
- package/dist/tools/hunt-report.js.map +1 -0
- package/dist/tools/hypothesis.d.ts +12 -0
- package/dist/tools/hypothesis.d.ts.map +1 -0
- package/dist/tools/hypothesis.js +221 -0
- package/dist/tools/hypothesis.js.map +1 -0
- package/dist/tools/intern-opportunities.d.ts.map +1 -1
- package/dist/tools/intern-opportunities.js +112 -47
- package/dist/tools/intern-opportunities.js.map +1 -1
- package/dist/tools/ladder.d.ts +33 -0
- package/dist/tools/ladder.d.ts.map +1 -0
- package/dist/tools/ladder.js +266 -0
- package/dist/tools/ladder.js.map +1 -0
- package/dist/tools/largest-objects.d.ts.map +1 -1
- package/dist/tools/largest-objects.js +6 -2
- package/dist/tools/largest-objects.js.map +1 -1
- package/dist/tools/leak-report.d.ts +12 -0
- package/dist/tools/leak-report.d.ts.map +1 -0
- package/dist/tools/leak-report.js +256 -0
- package/dist/tools/leak-report.js.map +1 -0
- package/dist/tools/load-snapshot.d.ts +18 -0
- package/dist/tools/load-snapshot.d.ts.map +1 -1
- package/dist/tools/load-snapshot.js +211 -13
- package/dist/tools/load-snapshot.js.map +1 -1
- package/dist/tools/map-entries.d.ts +12 -0
- package/dist/tools/map-entries.d.ts.map +1 -0
- package/dist/tools/map-entries.js +139 -0
- package/dist/tools/map-entries.js.map +1 -0
- package/dist/tools/match-object.d.ts +12 -0
- package/dist/tools/match-object.d.ts.map +1 -0
- package/dist/tools/match-object.js +165 -0
- package/dist/tools/match-object.js.map +1 -0
- package/dist/tools/object-shape.d.ts.map +1 -1
- package/dist/tools/object-shape.js +53 -5
- package/dist/tools/object-shape.js.map +1 -1
- package/dist/tools/population-diff.d.ts +57 -0
- package/dist/tools/population-diff.d.ts.map +1 -0
- package/dist/tools/population-diff.js +338 -0
- package/dist/tools/population-diff.js.map +1 -0
- package/dist/tools/retainer-layers.d.ts +23 -0
- package/dist/tools/retainer-layers.d.ts.map +1 -0
- package/dist/tools/retainer-layers.js +179 -0
- package/dist/tools/retainer-layers.js.map +1 -0
- package/dist/tools/retainer-summary.d.ts +1 -0
- package/dist/tools/retainer-summary.d.ts.map +1 -1
- package/dist/tools/retainer-summary.js +128 -32
- package/dist/tools/retainer-summary.js.map +1 -1
- package/dist/tools/retainer-trace.d.ts.map +1 -1
- package/dist/tools/retainer-trace.js +4 -2
- 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 +5 -1
- package/dist/tools/search-nodes.js.map +1 -1
- package/dist/tools/search-strings.d.ts.map +1 -1
- package/dist/tools/search-strings.js +53 -7
- package/dist/tools/search-strings.js.map +1 -1
- package/dist/tools/sequence-analysis.d.ts +49 -0
- package/dist/tools/sequence-analysis.d.ts.map +1 -1
- package/dist/tools/sequence-analysis.js +266 -86
- package/dist/tools/sequence-analysis.js.map +1 -1
- package/dist/tools/shape-histogram.d.ts.map +1 -1
- package/dist/tools/shape-histogram.js +9 -2
- package/dist/tools/shape-histogram.js.map +1 -1
- package/dist/tools/string-patterns.d.ts.map +1 -1
- package/dist/tools/string-patterns.js +5 -4
- package/dist/tools/string-patterns.js.map +1 -1
- package/dist/tools/tools-index.d.ts +12 -0
- package/dist/tools/tools-index.d.ts.map +1 -0
- package/dist/tools/tools-index.js +342 -0
- package/dist/tools/tools-index.js.map +1 -0
- package/dist/tools/verify-fix.d.ts +23 -0
- package/dist/tools/verify-fix.d.ts.map +1 -0
- package/dist/tools/verify-fix.js +211 -0
- package/dist/tools/verify-fix.js.map +1 -0
- package/dist/tools/weakmap-entries.d.ts.map +1 -1
- package/dist/tools/weakmap-entries.js +6 -1
- package/dist/tools/weakmap-entries.js.map +1 -1
- package/dist/utils.d.ts +146 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +335 -10
- package/dist/utils.js.map +1 -1
- package/package.json +5 -4
|
@@ -0,0 +1,256 @@
|
|
|
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 memlabHeapAnalysis from '@memlab/heap-analysis';
|
|
12
|
+
const { getFullHeapFromFile } = memlabHeapAnalysis;
|
|
13
|
+
import { formatBytes, formatNumber, markdownTable, errorResult, toolResult, } from '../utils.js';
|
|
14
|
+
import { artifactLabel } from '../artifact-classes.js';
|
|
15
|
+
import { computeSequenceTrends, normalizeClassName, } from './sequence-analysis.js';
|
|
16
|
+
import { collectDevRoots, computeReachableWithoutDevRoots, } from './dev-artifacts.js';
|
|
17
|
+
import { getFirstNonFrameworkRetainer } from './detached-dom.js';
|
|
18
|
+
// Nodes kept per candidate class for the retainer sample. Small on purpose: the
|
|
19
|
+
// question is "what shape of thing holds these", and a handful of instances
|
|
20
|
+
// answers it — walking thousands would cost more than the trend pass itself.
|
|
21
|
+
const MAX_SAMPLES_PER_CLASS = 8;
|
|
22
|
+
const RETAINER_SAMPLES = 3;
|
|
23
|
+
// A class whose instances are overwhelmingly dev-root-retained is a measurement
|
|
24
|
+
// artifact in production terms, whatever its growth curve looks like. Matches
|
|
25
|
+
// the majority-not-any threshold intern_opportunities uses, for the same reason:
|
|
26
|
+
// a class can legitimately mix a few console-held instances with real data.
|
|
27
|
+
const DEV_ONLY_SHARE = 0.8;
|
|
28
|
+
// Deliberately NOT a sum of retainedSize across the class: retained sizes
|
|
29
|
+
// overlap wherever instances nest, so summing them reports figures larger than
|
|
30
|
+
// the heap (measured: 5.9 GB of `Object` in a 200 MB heap). Net self-size delta
|
|
31
|
+
// across the ladder is additive and is the growth the report is about anyway.
|
|
32
|
+
function displayName(row) {
|
|
33
|
+
return row.name.length > 0 ? row.name : `(unnamed ${row.type})`;
|
|
34
|
+
}
|
|
35
|
+
function topRetainerLabel(ev) {
|
|
36
|
+
const counts = new Map();
|
|
37
|
+
for (const node of ev.samples.slice(0, RETAINER_SAMPLES)) {
|
|
38
|
+
const label = getFirstNonFrameworkRetainer(node);
|
|
39
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
40
|
+
}
|
|
41
|
+
let best = '(unknown)';
|
|
42
|
+
let bestCount = 0;
|
|
43
|
+
for (const [label, c] of counts) {
|
|
44
|
+
if (c > bestCount) {
|
|
45
|
+
best = label;
|
|
46
|
+
bestCount = c;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return best;
|
|
50
|
+
}
|
|
51
|
+
export function registerLeakReport(server) {
|
|
52
|
+
server.tool('memlab_leak_report', 'One-call leak triage across an ORDERED ladder of >=2 heap snapshots: runs the growth-trend pass, then gathers per-class EVIDENCE from the final snapshot and returns a single table — class, per-rung counts, Δ and Δ/cycle, how much of it is dev/automation-retained, the dominant retainer, and a verdict hint. ' +
|
|
53
|
+
'Exists because the trend pass alone cannot tell a leak from an artifact: every hunt then ran memlab_dev_artifacts and a retainer trace by hand on each grower and joined the three outputs mentally, which is the step that gets skipped right before something is reported as a production leak. Composes memlab_sequence_analysis with memlab_dev_artifacts and a retainer sample; costs one extra snapshot load (the last rung) on top of the ladder pass. ' +
|
|
54
|
+
'The verdict column is a HINT, not a conclusion — confirm a candidate with memlab_retainer_trace on the example node before calling it a leak. Paths may be local, manifold:// URLs, or bare filenames.', {
|
|
55
|
+
paths: z
|
|
56
|
+
.array(z.string())
|
|
57
|
+
.min(1)
|
|
58
|
+
.describe('Ordered list of >=2 snapshot paths (oldest first): local absolute paths, manifold:// URLs, or bare snapshot filenames. A single ["ladder:<name>"] entry expands to a ladder saved with memlab_ladder.'),
|
|
59
|
+
cycles: z
|
|
60
|
+
.number()
|
|
61
|
+
.optional()
|
|
62
|
+
.describe('Number of interaction cycles driven between the FIRST and LAST snapshot. When provided, a "Δ/cycle" column is reported — a per-cycle rate is what says whether growth scales with interaction, which a total cannot.'),
|
|
63
|
+
limit: z
|
|
64
|
+
.number()
|
|
65
|
+
.optional()
|
|
66
|
+
.default(10)
|
|
67
|
+
.describe('Maximum number of growing classes to gather evidence for (default 10).'),
|
|
68
|
+
min_growth_count: z
|
|
69
|
+
.number()
|
|
70
|
+
.optional()
|
|
71
|
+
.default(50)
|
|
72
|
+
.describe('Only consider classes whose net instance-count growth is at least this (default 50).'),
|
|
73
|
+
monotonic_only: z
|
|
74
|
+
.boolean()
|
|
75
|
+
.optional()
|
|
76
|
+
.default(false)
|
|
77
|
+
.describe('Only consider classes that grew at EVERY step (default false: grew-net classes are included and flagged as noisy).'),
|
|
78
|
+
include_artifacts: z
|
|
79
|
+
.boolean()
|
|
80
|
+
.optional()
|
|
81
|
+
.default(false)
|
|
82
|
+
.describe('Include classes that are known measurement artifacts (CDP inspector retention, V8 JIT warmup, Blink a11y caches, captured Error stacks). Default false: they are counted in a one-line summary instead of consuming evidence slots, since none of them is an app leak.'),
|
|
83
|
+
max_file_size_mb: z
|
|
84
|
+
.number()
|
|
85
|
+
.optional()
|
|
86
|
+
.describe('Per-file size ceiling in MB, matching memlab_load_snapshot / memlab_sequence_analysis defaults. Snapshots are loaded one at a time and dropped before the next.'),
|
|
87
|
+
}, async ({ paths, cycles, limit, min_growth_count, monotonic_only, include_artifacts, max_file_size_mb, }) => {
|
|
88
|
+
try {
|
|
89
|
+
const { steps, rows } = await computeSequenceTrends(paths, {
|
|
90
|
+
minGrowthCount: min_growth_count,
|
|
91
|
+
monotonicOnly: monotonic_only,
|
|
92
|
+
maxFileSizeMB: max_file_size_mb,
|
|
93
|
+
toolName: 'memlab_leak_report',
|
|
94
|
+
});
|
|
95
|
+
const n = steps.length;
|
|
96
|
+
const artifactRows = rows.filter(r => r.artifact != null);
|
|
97
|
+
const candidates = (include_artifacts ? rows : rows.filter(r => r.artifact == null)).slice(0, limit);
|
|
98
|
+
const heapNet = steps[n - 1].totalSize - steps[0].totalSize;
|
|
99
|
+
const lines = [
|
|
100
|
+
`## Leak report (${n} snapshots)`,
|
|
101
|
+
'',
|
|
102
|
+
`Heap self-size ${heapNet >= 0 ? 'grew' : 'shrank'} by ${formatBytes(Math.abs(heapNet))} across the ladder (${formatNumber(steps[0].nodeCount)} → ${formatNumber(steps[n - 1].nodeCount)} nodes).`,
|
|
103
|
+
'',
|
|
104
|
+
];
|
|
105
|
+
if (n === 2) {
|
|
106
|
+
lines.push('> ⚠️ **Only 2 snapshots: "↑ every step" is monotonic by construction** and cannot separate a real trend from a single GC-band sample. Capture at least a third rung before treating anything here as a leak.', '');
|
|
107
|
+
}
|
|
108
|
+
if (candidates.length === 0) {
|
|
109
|
+
lines.push(`No non-artifact class grew by >= ${formatNumber(min_growth_count)} instances across the ladder — no unbounded-growth signal to report.`);
|
|
110
|
+
if (artifactRows.length > 0) {
|
|
111
|
+
lines.push('', `> 🧹 ${formatNumber(artifactRows.length)} growing class(es) were known measurement artifacts (pass \`include_artifacts: true\` to see them).`);
|
|
112
|
+
}
|
|
113
|
+
return toolResult(lines.join('\n'));
|
|
114
|
+
}
|
|
115
|
+
// Evidence pass. The trend loop drops each graph before loading the
|
|
116
|
+
// next, so the final rung is re-opened here — one extra load, and the
|
|
117
|
+
// only way to ask retention questions about the classes the trend pass
|
|
118
|
+
// just identified.
|
|
119
|
+
const snapshot = await getFullHeapFromFile(steps[n - 1].localPath);
|
|
120
|
+
const devRoots = collectDevRoots(snapshot);
|
|
121
|
+
const reached = devRoots.byId.size > 0
|
|
122
|
+
? computeReachableWithoutDevRoots(snapshot, devRoots)
|
|
123
|
+
: null;
|
|
124
|
+
const evidence = new Map();
|
|
125
|
+
for (const c of candidates) {
|
|
126
|
+
evidence.set(c.key, {
|
|
127
|
+
total: 0,
|
|
128
|
+
devOnly: 0,
|
|
129
|
+
samples: [],
|
|
130
|
+
example: null,
|
|
131
|
+
exampleRetained: -1,
|
|
132
|
+
anyExample: null,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
snapshot.nodes.forEach(node => {
|
|
136
|
+
if (node.id <= 3)
|
|
137
|
+
return;
|
|
138
|
+
// Must key exactly as buildHistogram does, normalization included, or
|
|
139
|
+
// per-instance Context/scope classes never match their trend row.
|
|
140
|
+
const ev = evidence.get(`${node.type}::${normalizeClassName(node.name)}`);
|
|
141
|
+
if (!ev)
|
|
142
|
+
return;
|
|
143
|
+
ev.total++;
|
|
144
|
+
if (reached != null && reached[node.nodeIndex] === 0)
|
|
145
|
+
ev.devOnly++;
|
|
146
|
+
if (ev.anyExample == null)
|
|
147
|
+
ev.anyExample = node;
|
|
148
|
+
if (!node.hasPathEdge)
|
|
149
|
+
return;
|
|
150
|
+
if (ev.samples.length < MAX_SAMPLES_PER_CLASS)
|
|
151
|
+
ev.samples.push(node);
|
|
152
|
+
if (node.retainedSize > ev.exampleRetained) {
|
|
153
|
+
ev.exampleRetained = node.retainedSize;
|
|
154
|
+
ev.example = node;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
// The biggest traceable instance is the most informative one to walk, so
|
|
158
|
+
// put it at the head of every retainer sample.
|
|
159
|
+
for (const ev of evidence.values()) {
|
|
160
|
+
if (ev.example != null) {
|
|
161
|
+
ev.samples = [
|
|
162
|
+
ev.example,
|
|
163
|
+
...ev.samples.filter(s => s.id !== ev.example.id),
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const perCycle = cycles != null && cycles > 0;
|
|
168
|
+
const showDevOnly = reached != null;
|
|
169
|
+
const headers = [
|
|
170
|
+
'Class',
|
|
171
|
+
'Type',
|
|
172
|
+
...steps.map((_, i) => `#${i + 1}`),
|
|
173
|
+
'Δ count',
|
|
174
|
+
...(perCycle ? ['Δ/cycle'] : []),
|
|
175
|
+
'Δ size',
|
|
176
|
+
...(showDevOnly ? ['Dev-only'] : []),
|
|
177
|
+
'Top retainer',
|
|
178
|
+
'Verdict hint',
|
|
179
|
+
];
|
|
180
|
+
const rightCols = new Set();
|
|
181
|
+
for (let i = 2; i < headers.length - 2; i++)
|
|
182
|
+
rightCols.add(i);
|
|
183
|
+
let leakCandidates = 0;
|
|
184
|
+
let devOnlyClasses = 0;
|
|
185
|
+
const tableRows = candidates.map(r => {
|
|
186
|
+
const ev = evidence.get(r.key);
|
|
187
|
+
const devShare = ev.total > 0 ? ev.devOnly / ev.total : 0;
|
|
188
|
+
const isDevOnly = showDevOnly && devShare >= DEV_ONLY_SHARE;
|
|
189
|
+
if (isDevOnly)
|
|
190
|
+
devOnlyClasses++;
|
|
191
|
+
let verdict;
|
|
192
|
+
if (r.artifact != null) {
|
|
193
|
+
verdict = artifactLabel(r.artifact);
|
|
194
|
+
}
|
|
195
|
+
else if (isDevOnly) {
|
|
196
|
+
verdict = '🛠 dev/automation-retained (not production)';
|
|
197
|
+
}
|
|
198
|
+
else if (r.trend === 'monotonic-up') {
|
|
199
|
+
verdict = '↑ every step — LEAK candidate';
|
|
200
|
+
leakCandidates++;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
verdict = 'grew net (noisy)';
|
|
204
|
+
}
|
|
205
|
+
const label = displayName(r);
|
|
206
|
+
return [
|
|
207
|
+
label.length > 34 ? label.slice(0, 31) + '…' : label,
|
|
208
|
+
r.type,
|
|
209
|
+
...r.counts.map(c => formatNumber(c)),
|
|
210
|
+
`+${formatNumber(r.netCount)}`,
|
|
211
|
+
...(perCycle ? [(r.netCount / cycles).toFixed(2)] : []),
|
|
212
|
+
`${r.netSize >= 0 ? '+' : ''}${formatBytes(r.netSize)}`,
|
|
213
|
+
...(showDevOnly
|
|
214
|
+
? [
|
|
215
|
+
ev.total === 0
|
|
216
|
+
? '—'
|
|
217
|
+
: ev.devOnly === 0
|
|
218
|
+
? '—'
|
|
219
|
+
: `${(devShare * 100).toFixed(0)}%`,
|
|
220
|
+
]
|
|
221
|
+
: []),
|
|
222
|
+
(label => (label.length > 44 ? label.slice(0, 41) + '…' : label))(topRetainerLabel(ev)),
|
|
223
|
+
verdict,
|
|
224
|
+
];
|
|
225
|
+
});
|
|
226
|
+
lines.push(markdownTable(headers, tableRows, rightCols));
|
|
227
|
+
lines.push('', `**${formatNumber(leakCandidates)} leak candidate(s)** of ${formatNumber(candidates.length)} growing class(es) examined` +
|
|
228
|
+
(devOnlyClasses > 0
|
|
229
|
+
? `; ${formatNumber(devOnlyClasses)} ruled out as dev/automation-retained`
|
|
230
|
+
: '') +
|
|
231
|
+
(artifactRows.length > 0 && !include_artifacts
|
|
232
|
+
? `; ${formatNumber(artifactRows.length)} known measurement artifact(s) not shown (\`include_artifacts: true\`)`
|
|
233
|
+
: '') +
|
|
234
|
+
'.');
|
|
235
|
+
if (!showDevOnly) {
|
|
236
|
+
lines.push('', '_No dev/automation roots were found in the final snapshot, so the dev-only column is omitted — nothing here is being attributed to the inspector._');
|
|
237
|
+
}
|
|
238
|
+
// Point at the single next call for the strongest candidate rather than
|
|
239
|
+
// a menu: the failure mode this tool exists to fix is a plausible
|
|
240
|
+
// grower being reported without anyone tracing it.
|
|
241
|
+
const strongest = candidates.find(r => r.artifact == null &&
|
|
242
|
+
r.trend === 'monotonic-up' &&
|
|
243
|
+
evidence.get(r.key)?.example != null);
|
|
244
|
+
if (strongest) {
|
|
245
|
+
const ev = evidence.get(strongest.key);
|
|
246
|
+
const example = ev.example;
|
|
247
|
+
lines.push('', `**Next:** confirm the top candidate before reporting it — \`memlab_retainer_trace({node_id: ${example.id}})\` on the largest traceable \`${displayName(strongest)}\` instance in the final rung, then \`memlab_dominator_chain\` on whatever owns it. Counts alone cannot distinguish a leak from a cache that grew and will be evicted.`);
|
|
248
|
+
}
|
|
249
|
+
return toolResult(lines.join('\n'));
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
return errorResult(err);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
//# sourceMappingURL=leak-report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"leak-report.js","sourceRoot":"","sources":["../../src/tools/leak-report.ts"],"names":[],"mappings":"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"}
|
|
@@ -16,6 +16,24 @@ export declare const DEFAULT_MAX_EDGES = 24000000;
|
|
|
16
16
|
export declare const WARN_NODE_COUNT = 2000000;
|
|
17
17
|
export declare const NODES_PER_HEAP_MB = 1700;
|
|
18
18
|
export declare const EDGES_PER_HEAP_MB = 6800;
|
|
19
|
+
export declare const LOAD_BYTES_PER_NODE = 200;
|
|
20
|
+
export declare const LOAD_BYTES_PER_EDGE = 40;
|
|
21
|
+
export declare const HEADROOM_SAFETY_FRACTION = 0.8;
|
|
22
|
+
/**
|
|
23
|
+
* Projected additional heap (MB) a full load of this graph will occupy.
|
|
24
|
+
* Light loads skip the dominator/retained-size arrays; the parsed graph still
|
|
25
|
+
* dominates, so the discount is modest and intentionally conservative.
|
|
26
|
+
*/
|
|
27
|
+
export declare function estimateLoadHeapMB(counts: {
|
|
28
|
+
nodeCount: number;
|
|
29
|
+
edgeCount: number;
|
|
30
|
+
}, light: boolean): number;
|
|
31
|
+
/**
|
|
32
|
+
* Old-space headroom (MB) left for a new load: the configured limit minus what
|
|
33
|
+
* is already committed. Returns null when the limit can't be read, in which case
|
|
34
|
+
* the caller skips the guard rather than guessing.
|
|
35
|
+
*/
|
|
36
|
+
export declare function getHeapHeadroomMB(): number | null;
|
|
19
37
|
/**
|
|
20
38
|
* Configured V8 old-space limit in MB (reflects --max-old-space-size). Returns 0
|
|
21
39
|
* when it can't be read, so callers fall back to the fixed floors.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-snapshot.d.ts","sourceRoot":"","sources":["../../src/tools/load-snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"load-snapshot.d.ts","sourceRoot":"","sources":["../../src/tools/load-snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,yCAAyC,CAAC;AAuCvE,eAAO,MAAM,uBAAuB,sCAAsC,CAAC;AAU3E,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAC5C,eAAO,MAAM,4BAA4B,OAAO,CAAC;AAmBjD,eAAO,MAAM,iBAAiB,WAAa,CAAC;AAC5C,eAAO,MAAM,iBAAiB,WAAa,CAAC;AAC5C,eAAO,MAAM,eAAe,UAAY,CAAC;AAazC,eAAO,MAAM,iBAAiB,OAAO,CAAC;AACtC,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAStC,eAAO,MAAM,mBAAmB,MAAM,CAAC;AACvC,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAMtC,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAC,EAC9C,KAAK,EAAE,OAAO,GACb,MAAM,CAKR;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,IAAI,CAUjD;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAM3C;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,IAAI;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB,CAoBA;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,GAChB;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAC,GAAG,IAAI,CAgB/C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAC,EAC9C,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAC,GAC7C,MAAM,GAAG,IAAI,CASf;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,mBAAmB,EAAE,OAAO,GAC3B,MAAM,CAKR;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG;IACrD,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,CAsEA;AAmCD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAWhE;AA4HD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAmgB5D"}
|
|
@@ -13,10 +13,16 @@ import path from 'path';
|
|
|
13
13
|
import v8 from 'v8';
|
|
14
14
|
import { execFileSync } from 'child_process';
|
|
15
15
|
import { z } from 'zod';
|
|
16
|
+
import memlabCore from '@memlab/core';
|
|
16
17
|
import memlabHeapAnalysis from '@memlab/heap-analysis';
|
|
17
18
|
const { getFullHeapFromFile } = memlabHeapAnalysis;
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
// `getFullHeapFromFile` = parse + `analysis.preparePathFinder`. Light mode calls
|
|
20
|
+
// only the parse half, so the two pieces are reached directly here. Measured on
|
|
21
|
+
// a 380 MB / 4.19M-node browser snapshot: parse 7.9s, preparePathFinder 9.9s.
|
|
22
|
+
const { utils: memlabUtils } = memlabCore;
|
|
23
|
+
import { setSnapshot, getSnapshotMetadata, setSessionConfig, listSnapshots, clearAllSnapshots, evictLeastRecentlyUsed, findResidentByPath, retainOnlySnapshot, setCurrentSnapshot, } from '../heap-state.js';
|
|
24
|
+
import { makeProgressReporter } from '../progress.js';
|
|
25
|
+
import { formatBytes, formatNumber, truncateNodeName, errorResult, textResult, suggestionsSuppressed, } from '../utils.js';
|
|
20
26
|
// The bucket the Nest auto-capture + the Manifold links in diffs use, so a
|
|
21
27
|
// bare snapshot filename resolves predictably (Feedback §7).
|
|
22
28
|
export const DEFAULT_MANIFOLD_BUCKET = 'nest_server_nodejs_heap_snapshots';
|
|
@@ -63,6 +69,48 @@ export const WARN_NODE_COUNT = 2_000_000;
|
|
|
63
69
|
// 45–49M-edge band, still rejects the 89M-edge / 2 GB band).
|
|
64
70
|
export const NODES_PER_HEAP_MB = 1700;
|
|
65
71
|
export const EDGES_PER_HEAP_MB = 6800;
|
|
72
|
+
// Per-node / per-edge working-set estimate for a full load, fitted to a
|
|
73
|
+
// measured browser capture: 4,185,734 nodes + 18,755,739 edges peaked at
|
|
74
|
+
// ~1,500 MB RSS (200 B/node + 40 B/edge predicts ~1,588 MB). Deliberately a
|
|
75
|
+
// slight OVER-estimate — the failure this guards against is an OOM that kills
|
|
76
|
+
// every resident snapshot, so erring toward "not enough headroom" is the cheap
|
|
77
|
+
// direction to be wrong in. File size is not used: it correlates poorly with
|
|
78
|
+
// graph cost, which is why the node/edge ceilings exist at all.
|
|
79
|
+
export const LOAD_BYTES_PER_NODE = 200;
|
|
80
|
+
export const LOAD_BYTES_PER_EDGE = 40;
|
|
81
|
+
// Fraction of remaining old-space a single load may be projected to consume
|
|
82
|
+
// before the guard fires. Well below 1.0 because the estimate is approximate,
|
|
83
|
+
// the parse allocates in bursts the GC cannot always keep up with, and analysis
|
|
84
|
+
// after the load needs room of its own.
|
|
85
|
+
export const HEADROOM_SAFETY_FRACTION = 0.8;
|
|
86
|
+
/**
|
|
87
|
+
* Projected additional heap (MB) a full load of this graph will occupy.
|
|
88
|
+
* Light loads skip the dominator/retained-size arrays; the parsed graph still
|
|
89
|
+
* dominates, so the discount is modest and intentionally conservative.
|
|
90
|
+
*/
|
|
91
|
+
export function estimateLoadHeapMB(counts, light) {
|
|
92
|
+
const bytes = counts.nodeCount * LOAD_BYTES_PER_NODE +
|
|
93
|
+
counts.edgeCount * LOAD_BYTES_PER_EDGE;
|
|
94
|
+
return Math.round((bytes / (1024 * 1024)) * (light ? 0.85 : 1));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Old-space headroom (MB) left for a new load: the configured limit minus what
|
|
98
|
+
* is already committed. Returns null when the limit can't be read, in which case
|
|
99
|
+
* the caller skips the guard rather than guessing.
|
|
100
|
+
*/
|
|
101
|
+
export function getHeapHeadroomMB() {
|
|
102
|
+
try {
|
|
103
|
+
const s = v8.getHeapStatistics();
|
|
104
|
+
const limit = s.heap_size_limit / (1024 * 1024);
|
|
105
|
+
const used = s.used_heap_size / (1024 * 1024);
|
|
106
|
+
if (!Number.isFinite(limit) || limit <= 0)
|
|
107
|
+
return null;
|
|
108
|
+
return Math.max(0, Math.round(limit - used));
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
66
114
|
/**
|
|
67
115
|
* Configured V8 old-space limit in MB (reflects --max-old-space-size). Returns 0
|
|
68
116
|
* when it can't be read, so callers fall back to the fixed floors.
|
|
@@ -285,7 +333,7 @@ function detectEnv(snapshot) {
|
|
|
285
333
|
return 'node';
|
|
286
334
|
return 'unknown';
|
|
287
335
|
}
|
|
288
|
-
function quickDiagnosis(snapshot, totalSelfSize) {
|
|
336
|
+
function quickDiagnosis(snapshot, totalSelfSize, light = false) {
|
|
289
337
|
const warnings = [];
|
|
290
338
|
const largeStrings = [];
|
|
291
339
|
const stringCounts = new Map();
|
|
@@ -322,7 +370,10 @@ function quickDiagnosis(snapshot, totalSelfSize) {
|
|
|
322
370
|
const display = val.length > 40 ? val.slice(0, 40) + '…' : val;
|
|
323
371
|
warnings.push(`⚠ "${display}" duplicated ${formatNumber(count)} times`);
|
|
324
372
|
}
|
|
325
|
-
|
|
373
|
+
// Skipped on a light load: `findLargestObject` ranks by retainedSize, which
|
|
374
|
+
// is uncomputed there — every candidate would read 0 and the "retains N% of
|
|
375
|
+
// heap" line would be fiction.
|
|
376
|
+
const largest = light ? null : findLargestObject(snapshot);
|
|
326
377
|
if (largest && totalSelfSize > 0) {
|
|
327
378
|
const pct = ((largest.size / totalSelfSize) * 100).toFixed(0);
|
|
328
379
|
if (largest.size >= totalSelfSize * 0.3) {
|
|
@@ -336,11 +387,32 @@ function quickDiagnosis(snapshot, totalSelfSize) {
|
|
|
336
387
|
for (const [name, count] of anomalous) {
|
|
337
388
|
warnings.push(`⚠ ${formatNumber(count)}× \`${name}\` instances`);
|
|
338
389
|
}
|
|
339
|
-
if (warnings.length > 0) {
|
|
340
|
-
warnings.push('', 'Run `memlab_quick_diagnosis` for actionable triage or `memlab_auto_investigate` for deep analysis.');
|
|
341
|
-
}
|
|
342
390
|
return warnings;
|
|
343
391
|
}
|
|
392
|
+
/**
|
|
393
|
+
* What to call next, indexed by the QUESTION rather than by tool.
|
|
394
|
+
*
|
|
395
|
+
* The old trailer always pointed at `quick_diagnosis` / `auto_investigate`
|
|
396
|
+
* regardless of why the snapshot was loaded, which is right for "I don't know
|
|
397
|
+
* what I'm looking at" and wrong for every other case. Measured effect of the
|
|
398
|
+
* generic version: over one long investigation 28 of the server's tools were
|
|
399
|
+
* used and the rest never were, including several that answer a stated question
|
|
400
|
+
* in a single call. Naming the question is what makes the right tool findable
|
|
401
|
+
* at the only moment the caller is guaranteed to be reading.
|
|
402
|
+
*/
|
|
403
|
+
function nextStepMenu() {
|
|
404
|
+
return [
|
|
405
|
+
'**What do you want to know?**',
|
|
406
|
+
'- _What is this heap made of?_ → `memlab_app_heap` (application data vs bundle source vs V8 machinery — run it before trusting any percentage)',
|
|
407
|
+
'- _Anything obviously wrong?_ → `memlab_quick_diagnosis`, then `memlab_auto_investigate` for the deep dive',
|
|
408
|
+
'- _Is X a leak?_ → `memlab_hypothesis`',
|
|
409
|
+
'- _Who owns the growth?_ → `memlab_explain_delta` (needs a baseline loaded with `keep_previous:true`)',
|
|
410
|
+
'- _What grew across the ladder?_ → `memlab_leak_report` / `memlab_sequence_analysis`',
|
|
411
|
+
'- _What does this object pin?_ → `memlab_dominator_chain`, then `memlab_dominator_attribution`',
|
|
412
|
+
'- _Is it real or an artifact?_ → `memlab_dev_artifacts` (and `memlab_explain_delta` for JIT warmup, which it does not count)',
|
|
413
|
+
'- _Something else_ → `memlab_tools` lists every tool grouped by the question it answers',
|
|
414
|
+
];
|
|
415
|
+
}
|
|
344
416
|
export function registerLoadSnapshot(server) {
|
|
345
417
|
server.tool('memlab_load_snapshot', 'Load and parse a .heapsnapshot file. This builds indexes, computes the dominator tree, and calculates retained sizes. Returns a quick diagnosis highlighting potential issues. Accepts a local absolute path, a manifold:// URL, or a bare snapshot filename (resolved against the nest_server_nodejs_heap_snapshots bucket and fetched automatically). Multiple snapshots can be kept resident — pass keep_previous:true to load several for diffing/comparison; switch between them with memlab_snapshots. Cost note: the load working set is several× the file size and the dominator pass runs uninterruptibly, so large/deep snapshots are memory- and time-heavy — run the server with NODE_OPTIONS="--max-old-space-size=8192", load one snapshot at a time (omit keep_previous) on large heaps, and prefer memlab_sequence_analysis (transient per-snapshot loads) when you only need trend/growth across a ladder. A node/edge-count ceiling (max_nodes/max_edges) refuses pathologically large loads before they wedge the server; the ceiling auto-scales from the server\'s configured --max-old-space-size (an 8 GB server admits the ~1.1 GB / ~14M-node / ~55M-edge Nest tier by default), and force:true self-sizes from the header counts so a one-off oversized load needs no manual max_nodes/max_edges.', {
|
|
346
418
|
file_path: z
|
|
@@ -378,8 +450,24 @@ export function registerLoadSnapshot(server) {
|
|
|
378
450
|
force: z
|
|
379
451
|
.boolean()
|
|
380
452
|
.optional()
|
|
381
|
-
.describe('Bypass the node/edge-count ceiling and attempt the load anyway (default false). force:true is self-sizing — it uses the header-peeked counts, so you do NOT also need to pass max_nodes/max_edges. The load cannot be interrupted once started, so only use this when you accept a potentially very long load or possible OOM.'),
|
|
382
|
-
|
|
453
|
+
.describe('Bypass the node/edge-count ceiling AND the memory-headroom guard, and attempt the load anyway (default false). force:true is self-sizing — it uses the header-peeked counts, so you do NOT also need to pass max_nodes/max_edges. The load cannot be interrupted once started, so only use this when you accept a potentially very long load or possible OOM.'),
|
|
454
|
+
force_reload: z
|
|
455
|
+
.boolean()
|
|
456
|
+
.optional()
|
|
457
|
+
.default(false)
|
|
458
|
+
.describe('Re-parse even if this exact file is already resident in this server process (default false: an already-loaded file is switched to instead, reported as `cache: hit`). Only needed if the file changed on disk since it was loaded.'),
|
|
459
|
+
light: z
|
|
460
|
+
.boolean()
|
|
461
|
+
.optional()
|
|
462
|
+
.default(false)
|
|
463
|
+
.describe('Count-only load: parse the graph but SKIP the dominator / retained-size / shortest-path pass (default false). Roughly halves load time (measured on a 380 MB / 4.19M-node capture: 18s → 8s) for questions that only need counts, names, types, self sizes or string values. The saving is time, not memory — the parsed graph is the bulk of the working set. A light snapshot REFUSES any tool that needs retained sizes, dominators or retainer paths (memlab_retainer_trace, memlab_detached_dom, memlab_largest_objects, memlab_dominator_*, …) instead of silently reporting zeros; reload without light to run those. Light-safe: memlab_class_histogram (include_retained_size:false), memlab_search_nodes, memlab_search_strings, memlab_string_patterns, memlab_snapshot_summary, memlab_snapshot_header, memlab_snapshots.'),
|
|
464
|
+
}, async ({ file_path, alias, keep_previous, quiet, suppress_suggestions, max_file_size_mb, max_nodes, max_edges, force, force_reload, light, }, extra) => {
|
|
465
|
+
// 6 phases: resolve → guards → parse → (dominators | skipped) → index →
|
|
466
|
+
// diagnose. Reported at boundaries only; see progress.ts for why there is
|
|
467
|
+
// nothing finer-grained available.
|
|
468
|
+
const progress = makeProgressReporter(extra, 'load_snapshot');
|
|
469
|
+
const PHASES = 6;
|
|
470
|
+
const startedAt = Date.now();
|
|
383
471
|
try {
|
|
384
472
|
if (quiet != null || suppress_suggestions != null) {
|
|
385
473
|
setSessionConfig({
|
|
@@ -390,6 +478,7 @@ export function registerLoadSnapshot(server) {
|
|
|
390
478
|
});
|
|
391
479
|
}
|
|
392
480
|
const previousMeta = getSnapshotMetadata();
|
|
481
|
+
progress.phase(1, PHASES, `resolving ${file_path} (a manifold:// or bare filename is fetched here)`);
|
|
393
482
|
let resolved;
|
|
394
483
|
let fetchedFrom = null;
|
|
395
484
|
try {
|
|
@@ -403,6 +492,43 @@ export function registerLoadSnapshot(server) {
|
|
|
403
492
|
`Ensure the 'manifold' CLI is installed and you have access to the bucket, ` +
|
|
404
493
|
`or pass a local absolute path instead.`));
|
|
405
494
|
}
|
|
495
|
+
// In-process snapshot cache. Re-parsing a file this process already
|
|
496
|
+
// holds is the single most expensive avoidable thing the server does —
|
|
497
|
+
// a measured 18.8s for a 380 MB capture — and a long investigation
|
|
498
|
+
// re-loads the same file repeatedly. A hit switches to the resident
|
|
499
|
+
// graph instead. It must be indistinguishable from a real load apart
|
|
500
|
+
// from speed, so replace-mode (`keep_previous:false`) still unloads the
|
|
501
|
+
// others, exactly as a re-parse would have.
|
|
502
|
+
//
|
|
503
|
+
// A resident LIGHT snapshot does NOT satisfy a full request: it has no
|
|
504
|
+
// dominator tree, so serving it would silently answer a retention
|
|
505
|
+
// question with zeros. The reverse is fine — a full graph satisfies a
|
|
506
|
+
// light request.
|
|
507
|
+
if (!force_reload) {
|
|
508
|
+
const resident = findResidentByPath(resolved);
|
|
509
|
+
if (resident != null && (!resident.light || light)) {
|
|
510
|
+
if (keep_previous) {
|
|
511
|
+
setCurrentSnapshot(resident.handle);
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
retainOnlySnapshot(resident.handle);
|
|
515
|
+
}
|
|
516
|
+
const headroomHitMB = getHeapHeadroomMB();
|
|
517
|
+
return textResult([
|
|
518
|
+
`cache: **hit** — ${resident.fileName} was already parsed in this server process; switched to it in ${Date.now() - startedAt} ms instead of re-parsing [handle: ${resident.handle}]`,
|
|
519
|
+
`${formatNumber(resident.nodeCount)} nodes, ${formatNumber(resident.edgeCount)} edges, ${formatBytes(resident.totalSize)} heap size` +
|
|
520
|
+
(resident.light
|
|
521
|
+
? ' (LIGHT — no dominator tree / retained sizes)'
|
|
522
|
+
: ''),
|
|
523
|
+
headroomHitMB != null
|
|
524
|
+
? `Memory: ~${formatNumber(headroomHitMB)} MB of the ~${formatNumber(getOldSpaceLimitMB())} MB old-space limit free (unchanged — nothing was parsed).`
|
|
525
|
+
: '',
|
|
526
|
+
'_Pass `force_reload: true` if the file changed on disk since it was loaded._',
|
|
527
|
+
]
|
|
528
|
+
.filter(Boolean)
|
|
529
|
+
.join('\n'));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
406
532
|
if (!fs.existsSync(resolved)) {
|
|
407
533
|
return errorResult(new Error(`File not found: ${resolved}` +
|
|
408
534
|
(file_path.includes('/')
|
|
@@ -491,8 +617,8 @@ export function registerLoadSnapshot(server) {
|
|
|
491
617
|
const countStr = counts
|
|
492
618
|
? `${formatNumber(counts.nodeCount)} nodes / ${formatNumber(counts.edgeCount)} edges, `
|
|
493
619
|
: '';
|
|
494
|
-
process.stderr.write(`Loading ${formatBytes(fileStat.size)} snapshot (${countStr}parsing → dominators → retained sizes). ` +
|
|
495
|
-
`This runs uninterruptibly and can take minutes on a large/deep heap; if it appears to hang it is the dominator pass, not a crash.\n`);
|
|
620
|
+
process.stderr.write(`Loading ${formatBytes(fileStat.size)} snapshot (${countStr}parsing${light ? '' : ' → dominators → retained sizes'}). ` +
|
|
621
|
+
`This runs uninterruptibly and can take minutes on a large/deep heap; if it appears to hang it is the ${light ? 'parse' : 'dominator pass'}, not a crash.\n`);
|
|
496
622
|
}
|
|
497
623
|
// Free any resident snapshot BEFORE parsing the next so peak memory is
|
|
498
624
|
// one heap, not two — the double-residency that tips a large load into
|
|
@@ -501,9 +627,61 @@ export function registerLoadSnapshot(server) {
|
|
|
501
627
|
if (!keep_previous) {
|
|
502
628
|
clearAllSnapshots();
|
|
503
629
|
}
|
|
630
|
+
// Memory-headroom guard. The count ceiling above bounds the graph in the
|
|
631
|
+
// ABSTRACT; this bounds it against what this process actually has left,
|
|
632
|
+
// which is a different question once other snapshots are resident. When
|
|
633
|
+
// the projection does not fit, evict cold snapshots (never the current
|
|
634
|
+
// one) before refusing — an OOM here kills every resident snapshot and
|
|
635
|
+
// the whole session's state, so shedding the coldest is strictly better.
|
|
636
|
+
const evicted = [];
|
|
637
|
+
let headroomMB = getHeapHeadroomMB();
|
|
638
|
+
let estimateMB = null;
|
|
639
|
+
if (counts != null && headroomMB != null) {
|
|
640
|
+
estimateMB = estimateLoadHeapMB(counts, light);
|
|
641
|
+
while (estimateMB > (headroomMB ?? 0) * HEADROOM_SAFETY_FRACTION &&
|
|
642
|
+
!force) {
|
|
643
|
+
const victim = evictLeastRecentlyUsed();
|
|
644
|
+
if (victim == null)
|
|
645
|
+
break;
|
|
646
|
+
evicted.push(victim.handle);
|
|
647
|
+
headroomMB = getHeapHeadroomMB();
|
|
648
|
+
}
|
|
649
|
+
if (!force &&
|
|
650
|
+
estimateMB > (headroomMB ?? 0) * HEADROOM_SAFETY_FRACTION) {
|
|
651
|
+
const heapMB = getOldSpaceLimitMB();
|
|
652
|
+
return errorResult(new Error(`Not enough memory headroom to load this snapshot: it projects to ~${formatNumber(estimateMB)} MB ` +
|
|
653
|
+
`(${formatNumber(counts.nodeCount)} nodes × ${LOAD_BYTES_PER_NODE} B + ${formatNumber(counts.edgeCount)} edges × ${LOAD_BYTES_PER_EDGE} B), ` +
|
|
654
|
+
`over the ~${formatNumber(Math.round((headroomMB ?? 0) * HEADROOM_SAFETY_FRACTION))} MB budget for one load ` +
|
|
655
|
+
`(${Math.round(HEADROOM_SAFETY_FRACTION * 100)}% of the ~${formatNumber(headroomMB ?? 0)} MB free of this server's ~${formatNumber(heapMB)} MB old-space limit; the rest is reserved because the estimate is approximate and the parse allocates in bursts)` +
|
|
656
|
+
(evicted.length > 0
|
|
657
|
+
? ` (after unloading ${evicted.length} cold snapshot(s): ${evicted.join(', ')})`
|
|
658
|
+
: '') +
|
|
659
|
+
`.\n\nAn out-of-memory crash here would take every resident snapshot and all session state with it, so the load is refused instead.\n\nOptions:\n` +
|
|
660
|
+
`1. Unload snapshots you no longer need: memlab_snapshots({action: "unload", handle: "…"}).\n` +
|
|
661
|
+
`2. Load light — skips the dominator/retained-size pass: memlab_load_snapshot({file_path: "${file_path}", light: true}).\n` +
|
|
662
|
+
`3. Use a smaller/earlier snapshot, or memlab_sequence_analysis for trend questions (it holds one graph at a time).\n` +
|
|
663
|
+
`4. Restart the server with more memory: NODE_OPTIONS="--max-old-space-size=8192".\n` +
|
|
664
|
+
`5. Override this projection with force:true (accepts the OOM risk).`));
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
progress.phase(2, PHASES, `guards passed${estimateMB != null ? ` (projected ~${formatNumber(estimateMB)} MB, ~${formatNumber(headroomMB ?? 0)} MB free)` : ''}${evicted.length > 0 ? `, evicted ${evicted.join(', ')}` : ''}`);
|
|
668
|
+
progress.phase(3, PHASES, `parsing ${formatBytes(fileStat.size)} — this phase is synchronous and emits no further updates until it completes`);
|
|
504
669
|
let snapshot;
|
|
505
670
|
try {
|
|
506
|
-
|
|
671
|
+
if (light) {
|
|
672
|
+
// Parse only. `preparePathFinder` (shortest paths + dominators +
|
|
673
|
+
// retained sizes + Fiber marking) is the half being skipped, and it
|
|
674
|
+
// is the expensive half.
|
|
675
|
+
snapshot = await memlabUtils.getSnapshotFromFile(resolved, {
|
|
676
|
+
buildNodeIdIndex: true,
|
|
677
|
+
verbose: false,
|
|
678
|
+
});
|
|
679
|
+
progress.phase(4, PHASES, 'parsed; dominator/retained-size pass SKIPPED (light mode)');
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
snapshot = await getFullHeapFromFile(resolved);
|
|
683
|
+
progress.phase(4, PHASES, 'parsed; dominator tree and retained sizes computed');
|
|
684
|
+
}
|
|
507
685
|
}
|
|
508
686
|
catch (loadErr) {
|
|
509
687
|
const msg = loadErr instanceof Error ? loadErr.message : String(loadErr);
|
|
@@ -525,6 +703,7 @@ export function registerLoadSnapshot(server) {
|
|
|
525
703
|
}
|
|
526
704
|
throw loadErr;
|
|
527
705
|
}
|
|
706
|
+
progress.phase(5, PHASES, 'indexing nodes and edges');
|
|
528
707
|
let nodeCount = 0;
|
|
529
708
|
let edgeCount = 0;
|
|
530
709
|
let totalSize = 0;
|
|
@@ -543,6 +722,7 @@ export function registerLoadSnapshot(server) {
|
|
|
543
722
|
edgeCount,
|
|
544
723
|
totalSize,
|
|
545
724
|
env,
|
|
725
|
+
light,
|
|
546
726
|
}, { alias, replace: !keep_previous });
|
|
547
727
|
const envLabel = env === 'browser'
|
|
548
728
|
? 'Browser'
|
|
@@ -556,7 +736,21 @@ export function registerLoadSnapshot(server) {
|
|
|
556
736
|
if (fetchedFrom) {
|
|
557
737
|
lines.push(`Fetched from Manifold: ${fetchedFrom}`);
|
|
558
738
|
}
|
|
739
|
+
if (evicted.length > 0) {
|
|
740
|
+
lines.push(`⚠ Unloaded ${evicted.length} cold snapshot(s) to make room: ${evicted.join(', ')}`);
|
|
741
|
+
}
|
|
559
742
|
lines.push(`Loaded ${fileName} (${formatBytes(fileStat.size)} on disk): ${formatNumber(nodeCount)} nodes, ${formatNumber(edgeCount)} edges, ${formatBytes(totalSize)} heap size (${envLabel} snapshot) [handle: ${meta.handle}]`);
|
|
743
|
+
if (light) {
|
|
744
|
+
lines.push('⚡ **LIGHT mode** — dominator tree, retained sizes and retainer paths were NOT computed. Counts, names, types, self sizes and string values are available; tools needing retention data will refuse rather than report zeros. Reload without `light` for those.');
|
|
745
|
+
}
|
|
746
|
+
// Report headroom on every load, not just when the guard fires: the
|
|
747
|
+
// number a caller needs in order to decide whether to keep_previous the
|
|
748
|
+
// NEXT one is what is left after this load, and nothing else surfaces it.
|
|
749
|
+
const headroomAfterMB = getHeapHeadroomMB();
|
|
750
|
+
if (headroomAfterMB != null) {
|
|
751
|
+
const limitMB = getOldSpaceLimitMB();
|
|
752
|
+
lines.push(`Memory: ~${formatNumber(headroomAfterMB)} MB of the ~${formatNumber(limitMB)} MB old-space limit still free after this load.`);
|
|
753
|
+
}
|
|
560
754
|
const captured = extractCaptureTime(fileName);
|
|
561
755
|
if (captured) {
|
|
562
756
|
const iso = captured.toISOString().slice(0, 10);
|
|
@@ -574,10 +768,14 @@ export function registerLoadSnapshot(server) {
|
|
|
574
768
|
.join(', ')}. Switch with memlab_snapshots.`);
|
|
575
769
|
}
|
|
576
770
|
}
|
|
577
|
-
|
|
771
|
+
progress.phase(6, PHASES, 'running quick diagnosis');
|
|
772
|
+
const warnings = quickDiagnosis(snapshot, totalSize, light);
|
|
578
773
|
if (warnings.length > 0) {
|
|
579
774
|
lines.push('', ...warnings);
|
|
580
775
|
}
|
|
776
|
+
if (!suggestionsSuppressed()) {
|
|
777
|
+
lines.push('', ...nextStepMenu());
|
|
778
|
+
}
|
|
581
779
|
return textResult(lines.join('\n'));
|
|
582
780
|
}
|
|
583
781
|
catch (err) {
|