@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,57 @@
|
|
|
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 type { IHeapNode } from '@memlab/core';
|
|
11
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
12
|
+
/**
|
|
13
|
+
* A population selector: which nodes the comparison is about.
|
|
14
|
+
*
|
|
15
|
+
* `detached` is the motivating case — see the tool description — but the same
|
|
16
|
+
* question ("is this the SAME set, or a set of the same size?") applies to any
|
|
17
|
+
* population a hunt is tracking.
|
|
18
|
+
*/
|
|
19
|
+
export type Population = {
|
|
20
|
+
kind: 'detached';
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'class';
|
|
23
|
+
name: string;
|
|
24
|
+
} | {
|
|
25
|
+
kind: 'shape';
|
|
26
|
+
props: string[];
|
|
27
|
+
};
|
|
28
|
+
export declare function parsePopulation(spec: string): Population;
|
|
29
|
+
/**
|
|
30
|
+
* Membership test for a population selector. Shared with
|
|
31
|
+
* `memlab_dominator_attribution` so the two tools cannot drift on what, for
|
|
32
|
+
* example, "detached" means — a difference there would make their numbers
|
|
33
|
+
* silently incomparable.
|
|
34
|
+
*/
|
|
35
|
+
export declare function makeMembershipTest(pop: Population): (node: IHeapNode) => boolean;
|
|
36
|
+
interface Bucket {
|
|
37
|
+
count: number;
|
|
38
|
+
selfSize: number;
|
|
39
|
+
}
|
|
40
|
+
interface Census {
|
|
41
|
+
buckets: Map<string, Bucket>;
|
|
42
|
+
total: number;
|
|
43
|
+
totalSelf: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The verdict this tool exists to produce. Equal totals have three distinct
|
|
47
|
+
* causes and they call for opposite conclusions, so the headline states which
|
|
48
|
+
* one holds rather than leaving the reader to infer it from the table.
|
|
49
|
+
*/
|
|
50
|
+
export declare function summarizeComposition(a: Census, b: Census): {
|
|
51
|
+
headline: string;
|
|
52
|
+
added: number;
|
|
53
|
+
removed: number;
|
|
54
|
+
};
|
|
55
|
+
export declare function registerPopulationDiff(server: McpServer): void;
|
|
56
|
+
export {};
|
|
57
|
+
//# sourceMappingURL=population-diff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"population-diff.d.ts","sourceRoot":"","sources":["../../src/tools/population-diff.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAgB,MAAM,cAAc,CAAC;AAC3D,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,yCAAyC,CAAC;AAmBvE;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAClB;IAAC,IAAI,EAAE,UAAU,CAAA;CAAC,GAClB;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,GAC7B;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAC,CAAC;AAErC,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAyBxD;AAOD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,UAAU,GACd,CAAC,IAAI,EAAE,SAAS,KAAK,OAAO,CAyB9B;AA8ED,UAAU,MAAM;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,MAAM;IACd,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AA0BD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,GACR;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,CA6BpD;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAiN9D"}
|
|
@@ -0,0 +1,338 @@
|
|
|
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 { getCurrentHandle, getMetadataByHandle, getSnapshotByHandle, listSnapshots, } from '../heap-state.js';
|
|
12
|
+
import { errorResult, formatBytes, formatNumber, isNodeWorthInspecting, markdownTable, toolResult, truncateNodeName, } from '../utils.js';
|
|
13
|
+
import { normalizeClassName } from './sequence-analysis.js';
|
|
14
|
+
export function parsePopulation(spec) {
|
|
15
|
+
const trimmed = spec.trim();
|
|
16
|
+
if (trimmed === 'detached')
|
|
17
|
+
return { kind: 'detached' };
|
|
18
|
+
if (trimmed.startsWith('class:')) {
|
|
19
|
+
const name = trimmed.slice('class:'.length).trim();
|
|
20
|
+
if (name.length === 0) {
|
|
21
|
+
throw new Error('Empty class name in population "class:".');
|
|
22
|
+
}
|
|
23
|
+
return { kind: 'class', name };
|
|
24
|
+
}
|
|
25
|
+
if (trimmed.startsWith('shape:')) {
|
|
26
|
+
const props = trimmed
|
|
27
|
+
.slice('shape:'.length)
|
|
28
|
+
.replace(/^\[|\]$/g, '')
|
|
29
|
+
.split(',')
|
|
30
|
+
.map(s => s.trim().replace(/^["']|["']$/g, ''))
|
|
31
|
+
.filter(s => s.length > 0);
|
|
32
|
+
if (props.length === 0) {
|
|
33
|
+
throw new Error('Empty property list in population "shape:".');
|
|
34
|
+
}
|
|
35
|
+
return { kind: 'shape', props };
|
|
36
|
+
}
|
|
37
|
+
throw new Error(`Unrecognized population "${spec}". Use "detached", "class:<ClassName>", or "shape:prop1,prop2".`);
|
|
38
|
+
}
|
|
39
|
+
function isDetached(node) {
|
|
40
|
+
if (node.id <= 3)
|
|
41
|
+
return false;
|
|
42
|
+
return node.is_detached || node.name.startsWith('Detached ');
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Membership test for a population selector. Shared with
|
|
46
|
+
* `memlab_dominator_attribution` so the two tools cannot drift on what, for
|
|
47
|
+
* example, "detached" means — a difference there would make their numbers
|
|
48
|
+
* silently incomparable.
|
|
49
|
+
*/
|
|
50
|
+
export function makeMembershipTest(pop) {
|
|
51
|
+
switch (pop.kind) {
|
|
52
|
+
case 'detached':
|
|
53
|
+
return isDetached;
|
|
54
|
+
case 'class': {
|
|
55
|
+
const want = pop.name;
|
|
56
|
+
return (node) => node.id > 3 && node.name === want;
|
|
57
|
+
}
|
|
58
|
+
case 'shape': {
|
|
59
|
+
const want = pop.props;
|
|
60
|
+
return (node) => {
|
|
61
|
+
if (node.id <= 3 || node.type !== 'object')
|
|
62
|
+
return false;
|
|
63
|
+
let hits = 0;
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
for (const edge of node.references) {
|
|
66
|
+
if (edge.type !== 'property')
|
|
67
|
+
continue;
|
|
68
|
+
const name = String(edge.name_or_index);
|
|
69
|
+
if (!want.includes(name) || seen.has(name))
|
|
70
|
+
continue;
|
|
71
|
+
seen.add(name);
|
|
72
|
+
if (++hits === want.length)
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Bound on the upward dominator walk when grouping by owner, mirroring
|
|
81
|
+
// `boundedDominatorRetainedSize`. A truncated walk yields '(no owner found)'
|
|
82
|
+
// rather than a wrong owner.
|
|
83
|
+
const MAX_OWNER_HOPS = 500;
|
|
84
|
+
function domTagKey(name) {
|
|
85
|
+
const detached = name.startsWith('Detached ') ? 'Detached ' : '';
|
|
86
|
+
const bare = detached ? name.slice(9) : name;
|
|
87
|
+
const tag = bare.match(/^<(\w+)/)?.[1];
|
|
88
|
+
return tag ? `${detached}<${tag}>` : normalizeClassName(name);
|
|
89
|
+
}
|
|
90
|
+
function ownerKey(node) {
|
|
91
|
+
let cur = node.dominatorNode ?? null;
|
|
92
|
+
let hops = 0;
|
|
93
|
+
while (cur && hops++ < MAX_OWNER_HOPS) {
|
|
94
|
+
if (cur.id === node.id)
|
|
95
|
+
break;
|
|
96
|
+
if (cur.name !== '(GC roots)' &&
|
|
97
|
+
cur.id > 3 &&
|
|
98
|
+
isNodeWorthInspecting(cur) &&
|
|
99
|
+
!isDetached(cur)) {
|
|
100
|
+
// Deliberately NOT keyed by node id: ids are per-capture, so an id key
|
|
101
|
+
// would make every bucket "added" in the target and "removed" in the
|
|
102
|
+
// baseline. The owner's CLASS is what is comparable across snapshots.
|
|
103
|
+
return normalizeClassName(truncateNodeName(cur.name, cur.type, cur.self_size, 60));
|
|
104
|
+
}
|
|
105
|
+
const next = cur.dominatorNode ?? null;
|
|
106
|
+
if (!next || next.id === cur.id)
|
|
107
|
+
break;
|
|
108
|
+
cur = next;
|
|
109
|
+
}
|
|
110
|
+
return '(no owner — dominated by the GC root)';
|
|
111
|
+
}
|
|
112
|
+
function shapeKey(node) {
|
|
113
|
+
const props = [];
|
|
114
|
+
for (const edge of node.references) {
|
|
115
|
+
if (edge.type !== 'property')
|
|
116
|
+
continue;
|
|
117
|
+
const name = String(edge.name_or_index);
|
|
118
|
+
if (name === '__proto__')
|
|
119
|
+
continue;
|
|
120
|
+
props.push(name);
|
|
121
|
+
}
|
|
122
|
+
if (props.length === 0)
|
|
123
|
+
return '(no properties)';
|
|
124
|
+
props.sort();
|
|
125
|
+
const joined = props.join(',');
|
|
126
|
+
return joined.length > 80 ? joined.slice(0, 77) + '…' : joined;
|
|
127
|
+
}
|
|
128
|
+
function makeKeyFn(groupBy, pop) {
|
|
129
|
+
switch (groupBy) {
|
|
130
|
+
case 'owner':
|
|
131
|
+
return ownerKey;
|
|
132
|
+
case 'type':
|
|
133
|
+
return (node) => node.type;
|
|
134
|
+
case 'shape':
|
|
135
|
+
return shapeKey;
|
|
136
|
+
case 'class':
|
|
137
|
+
return pop.kind === 'detached'
|
|
138
|
+
? (node) => domTagKey(node.name)
|
|
139
|
+
: (node) => normalizeClassName(node.name);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function census(snapshot, isMember, keyOf) {
|
|
143
|
+
const buckets = new Map();
|
|
144
|
+
let total = 0;
|
|
145
|
+
let totalSelf = 0;
|
|
146
|
+
snapshot.nodes.forEach(node => {
|
|
147
|
+
if (!isMember(node))
|
|
148
|
+
return;
|
|
149
|
+
total++;
|
|
150
|
+
totalSelf += node.self_size;
|
|
151
|
+
const key = keyOf(node);
|
|
152
|
+
const b = buckets.get(key);
|
|
153
|
+
if (b) {
|
|
154
|
+
b.count++;
|
|
155
|
+
b.selfSize += node.self_size;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
buckets.set(key, { count: 1, selfSize: node.self_size });
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
return { buckets, total, totalSelf };
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The verdict this tool exists to produce. Equal totals have three distinct
|
|
165
|
+
* causes and they call for opposite conclusions, so the headline states which
|
|
166
|
+
* one holds rather than leaving the reader to infer it from the table.
|
|
167
|
+
*/
|
|
168
|
+
export function summarizeComposition(a, b) {
|
|
169
|
+
let added = 0;
|
|
170
|
+
let removed = 0;
|
|
171
|
+
for (const key of new Set([...a.buckets.keys(), ...b.buckets.keys()])) {
|
|
172
|
+
const d = (b.buckets.get(key)?.count ?? 0) - (a.buckets.get(key)?.count ?? 0);
|
|
173
|
+
if (d > 0)
|
|
174
|
+
added += d;
|
|
175
|
+
else
|
|
176
|
+
removed += -d;
|
|
177
|
+
}
|
|
178
|
+
const netTotal = b.total - a.total;
|
|
179
|
+
if (added === 0 && removed === 0) {
|
|
180
|
+
return {
|
|
181
|
+
headline: `✅ **Identical composition** — every one of the ${formatNumber(a.buckets.size)} bucket(s) has the same count at both ends. The population did not just stay the same SIZE, it stayed the same population.`,
|
|
182
|
+
added,
|
|
183
|
+
removed,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (netTotal === 0) {
|
|
187
|
+
return {
|
|
188
|
+
headline: `⚠ **Equal totals, DIFFERENT composition** — ${formatNumber(added)} member(s) appeared and ${formatNumber(removed)} disappeared, and they cancel out. Do NOT read the equal total as "nothing changed": the round stranded and freed in equal measure, which is exactly what a leak paired with an eviction looks like.`,
|
|
189
|
+
added,
|
|
190
|
+
removed,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
headline: `Population ${netTotal > 0 ? 'grew' : 'shrank'} by **${formatNumber(Math.abs(netTotal))}** (${formatNumber(a.total)} → ${formatNumber(b.total)}); ${formatNumber(added)} appeared, ${formatNumber(removed)} disappeared.`,
|
|
195
|
+
added,
|
|
196
|
+
removed,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
export function registerPopulationDiff(server) {
|
|
200
|
+
server.tool('memlab_population_diff', 'Compare the COMPOSITION of a population between two loaded snapshots, not just its size. Equal totals are not identity: a round that strands N objects and frees N others reports the same count at both ends, and reading that as "nothing leaked" is a wrong verdict a count comparison cannot catch. This breaks the population into buckets (class, dominator-owner class, node type, or object shape), reports both ends per bucket, and states which of the three cases holds — identical composition / equal totals but different composition / net growth. ' +
|
|
201
|
+
'Populations: "detached" (all detached DOM), "class:<ClassName>", or "shape:prop1,prop2" (objects carrying all of those properties). ' +
|
|
202
|
+
'Buckets are keyed by CLASS, never by node id — ids are per-capture, so an id-keyed comparison reports every member as both added and removed. Both snapshots must be resident: load them with keep_previous:true.', {
|
|
203
|
+
baseline_handle: z
|
|
204
|
+
.string()
|
|
205
|
+
.describe('Handle of the earlier snapshot (memlab_snapshots lists them).'),
|
|
206
|
+
target_handle: z
|
|
207
|
+
.string()
|
|
208
|
+
.optional()
|
|
209
|
+
.describe('Handle of the later snapshot. Defaults to the current snapshot.'),
|
|
210
|
+
population: z
|
|
211
|
+
.string()
|
|
212
|
+
.describe('Which nodes to compare: "detached", "class:<ClassName>", or "shape:prop1,prop2".'),
|
|
213
|
+
group_by: z
|
|
214
|
+
.enum(['class', 'owner', 'type', 'shape'])
|
|
215
|
+
.optional()
|
|
216
|
+
.describe('Composition axis. "class" = the member\'s own class (for detached DOM, its element tag); "owner" = the class of its nearest accountable dominator (needs a full, non-light load); "type" = heap node type; "shape" = its sorted property-name set. Defaults to "owner" for a class: population (the members already share a class, so their own name carries no information) and "class" otherwise.'),
|
|
217
|
+
limit: z
|
|
218
|
+
.number()
|
|
219
|
+
.optional()
|
|
220
|
+
.default(25)
|
|
221
|
+
.describe('Maximum buckets to show in the table (default 25).'),
|
|
222
|
+
min_delta: z
|
|
223
|
+
.number()
|
|
224
|
+
.optional()
|
|
225
|
+
.default(0)
|
|
226
|
+
.describe('Hide buckets whose count moved by less than this (default 0: show everything, since an unchanged bucket is itself evidence).'),
|
|
227
|
+
}, async ({ baseline_handle, target_handle, population, group_by, limit, min_delta, }) => {
|
|
228
|
+
try {
|
|
229
|
+
const pop = parsePopulation(population);
|
|
230
|
+
const targetHandle = target_handle ?? getCurrentHandle();
|
|
231
|
+
if (targetHandle == null) {
|
|
232
|
+
return errorResult('No current snapshot; pass target_handle.');
|
|
233
|
+
}
|
|
234
|
+
if (targetHandle === baseline_handle) {
|
|
235
|
+
return errorResult('baseline_handle and target_handle are the same snapshot.');
|
|
236
|
+
}
|
|
237
|
+
const baseline = getSnapshotByHandle(baseline_handle);
|
|
238
|
+
const target = getSnapshotByHandle(targetHandle);
|
|
239
|
+
if (baseline == null || target == null) {
|
|
240
|
+
const available = listSnapshots()
|
|
241
|
+
.map(m => m.handle)
|
|
242
|
+
.join(', ');
|
|
243
|
+
return errorResult(`Both snapshots must be resident. Missing: ${baseline == null ? baseline_handle : targetHandle}. Resident: ${available || '(none)'}. Load with memlab_load_snapshot({file_path, keep_previous: true}).`);
|
|
244
|
+
}
|
|
245
|
+
const groupBy = group_by ?? (pop.kind === 'class' ? 'owner' : 'class');
|
|
246
|
+
if (groupBy === 'owner') {
|
|
247
|
+
for (const h of [baseline_handle, targetHandle]) {
|
|
248
|
+
if (getMetadataByHandle(h)?.light) {
|
|
249
|
+
return errorResult(`Snapshot "${h}" was loaded in LIGHT mode and has no dominator tree, which group_by:"owner" requires. Reload it without light, or group by "class" / "type" / "shape".`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const isMember = makeMembershipTest(pop);
|
|
254
|
+
const keyOf = makeKeyFn(groupBy, pop);
|
|
255
|
+
const a = census(baseline, isMember, keyOf);
|
|
256
|
+
const b = census(target, isMember, keyOf);
|
|
257
|
+
if (a.total === 0 && b.total === 0) {
|
|
258
|
+
return toolResult(`No nodes match population \`${population}\` in either snapshot — nothing to compare. Check the class name with \`memlab_class_histogram\`, or the property names with \`memlab_shape_histogram\`.`);
|
|
259
|
+
}
|
|
260
|
+
const { headline, added, removed } = summarizeComposition(a, b);
|
|
261
|
+
const keys = new Set([...a.buckets.keys(), ...b.buckets.keys()]);
|
|
262
|
+
const rows = [];
|
|
263
|
+
for (const key of keys) {
|
|
264
|
+
const before = a.buckets.get(key);
|
|
265
|
+
const after = b.buckets.get(key);
|
|
266
|
+
const delta = (after?.count ?? 0) - (before?.count ?? 0);
|
|
267
|
+
if (Math.abs(delta) < min_delta)
|
|
268
|
+
continue;
|
|
269
|
+
rows.push({
|
|
270
|
+
key,
|
|
271
|
+
before: before?.count ?? 0,
|
|
272
|
+
after: after?.count ?? 0,
|
|
273
|
+
delta,
|
|
274
|
+
bytesDelta: (after?.selfSize ?? 0) - (before?.selfSize ?? 0),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
// Movers first (largest |Δ|), then the stable buckets by size — the
|
|
278
|
+
// stable ones are the evidence behind an "identical composition"
|
|
279
|
+
// verdict, so they are shown, just not ahead of the changes.
|
|
280
|
+
rows.sort((x, y) => Math.abs(y.delta) - Math.abs(x.delta) || y.after - x.after);
|
|
281
|
+
const shown = rows.slice(0, limit);
|
|
282
|
+
const addedKeys = [...keys].filter(k => !a.buckets.has(k) && b.buckets.has(k));
|
|
283
|
+
const removedKeys = [...keys].filter(k => a.buckets.has(k) && !b.buckets.has(k));
|
|
284
|
+
const axisLabel = groupBy === 'owner'
|
|
285
|
+
? 'Owner (dominator class)'
|
|
286
|
+
: groupBy === 'shape'
|
|
287
|
+
? 'Shape (property names)'
|
|
288
|
+
: groupBy === 'type'
|
|
289
|
+
? 'Node type'
|
|
290
|
+
: 'Class';
|
|
291
|
+
const lines = [
|
|
292
|
+
`## Population diff \`${population}\` by ${groupBy}: "${baseline_handle}" → "${targetHandle}"`,
|
|
293
|
+
'',
|
|
294
|
+
`Members: ${formatNumber(a.total)} (${formatBytes(a.totalSelf)} self) → ${formatNumber(b.total)} (${formatBytes(b.totalSelf)} self), across ${formatNumber(a.buckets.size)} → ${formatNumber(b.buckets.size)} bucket(s).`,
|
|
295
|
+
'',
|
|
296
|
+
headline,
|
|
297
|
+
'',
|
|
298
|
+
markdownTable([axisLabel, 'Before', 'After', 'Δ count', 'Δ self'], shown.map(r => [
|
|
299
|
+
r.key.length > 52 ? r.key.slice(0, 49) + '…' : r.key,
|
|
300
|
+
formatNumber(r.before),
|
|
301
|
+
formatNumber(r.after),
|
|
302
|
+
r.delta === 0
|
|
303
|
+
? '—'
|
|
304
|
+
: `${r.delta > 0 ? '+' : '−'}${formatNumber(Math.abs(r.delta))}`,
|
|
305
|
+
r.bytesDelta === 0
|
|
306
|
+
? '—'
|
|
307
|
+
: `${r.bytesDelta > 0 ? '+' : '−'}${formatBytes(Math.abs(r.bytesDelta))}`,
|
|
308
|
+
]), new Set([1, 2, 3, 4])),
|
|
309
|
+
];
|
|
310
|
+
if (rows.length > shown.length) {
|
|
311
|
+
lines.push('', `_${formatNumber(rows.length - shown.length)} further bucket(s) not shown (raise \`limit\`)._`);
|
|
312
|
+
}
|
|
313
|
+
if (addedKeys.length > 0) {
|
|
314
|
+
lines.push('', `**Buckets present only in the target (${formatNumber(addedKeys.length)}):** ${addedKeys
|
|
315
|
+
.slice(0, 15)
|
|
316
|
+
.map(k => `\`${k}\``)
|
|
317
|
+
.join(', ')}${addedKeys.length > 15 ? ', …' : ''}`);
|
|
318
|
+
}
|
|
319
|
+
if (removedKeys.length > 0) {
|
|
320
|
+
lines.push('', `**Buckets present only in the baseline (${formatNumber(removedKeys.length)}):** ${removedKeys
|
|
321
|
+
.slice(0, 15)
|
|
322
|
+
.map(k => `\`${k}\``)
|
|
323
|
+
.join(', ')}${removedKeys.length > 15 ? ', …' : ''}`);
|
|
324
|
+
}
|
|
325
|
+
if (added > 0 || removed > 0) {
|
|
326
|
+
lines.push('', `_Churn is measured per bucket, so it is a LOWER bound on how many individual objects were replaced: two members swapping within one bucket are invisible here. Node identity cannot be tracked across captures — ids are per-capture — so this is the strongest available statement._`);
|
|
327
|
+
}
|
|
328
|
+
lines.push('', `**Next:** ${groupBy === 'owner'
|
|
329
|
+
? 'Use `memlab_dominator_attribution` on the owners that moved to measure how much each actually pins.'
|
|
330
|
+
: 'Re-run with `group_by: "owner"` to see WHICH object holds the buckets that moved, then `memlab_dominator_attribution` to size each owner.'}`);
|
|
331
|
+
return toolResult(lines.join('\n'));
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
return errorResult(err);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
//# sourceMappingURL=population-diff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"population-diff.js","sourceRoot":"","sources":["../../src/tools/population-diff.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
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 type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
11
|
+
import type { IHeapSnapshot } from '@memlab/core';
|
|
12
|
+
/**
|
|
13
|
+
* Reachability from the synthetic GC roots with one node treated as a sink:
|
|
14
|
+
* `blocked` is marked reached (so it is not reported as unreachable) but its
|
|
15
|
+
* outgoing edges are never followed. Anything left unmarked is reachable ONLY
|
|
16
|
+
* through `blocked`, i.e. it lives inside that node's retained subtree.
|
|
17
|
+
*
|
|
18
|
+
* Mirrors `computeReachableWithoutDevRoots` in dev-artifacts.ts, which does the
|
|
19
|
+
* same walk with the dev/automation roots as sinks.
|
|
20
|
+
*/
|
|
21
|
+
export declare function computeReachableWithoutNode(snapshot: IHeapSnapshot, blockedId: number): Uint8Array;
|
|
22
|
+
export declare function registerRetainerLayers(server: McpServer): void;
|
|
23
|
+
//# sourceMappingURL=retainer-layers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retainer-layers.d.ts","sourceRoot":"","sources":["../../src/tools/retainer-layers.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,yCAAyC,CAAC;AACvE,OAAO,KAAK,EAAuB,aAAa,EAAC,MAAM,cAAc,CAAC;AAatE;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,aAAa,EACvB,SAAS,EAAE,MAAM,GAChB,UAAU,CAmBZ;AAqBD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA6K9D"}
|
|
@@ -0,0 +1,179 @@
|
|
|
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 } from '../heap-state.js';
|
|
12
|
+
import { errorResult, formatBytes, formatNumber, isNodeWorthInspecting, markdownTable, toolResult, truncateNodeName, } from '../utils.js';
|
|
13
|
+
/**
|
|
14
|
+
* Reachability from the synthetic GC roots with one node treated as a sink:
|
|
15
|
+
* `blocked` is marked reached (so it is not reported as unreachable) but its
|
|
16
|
+
* outgoing edges are never followed. Anything left unmarked is reachable ONLY
|
|
17
|
+
* through `blocked`, i.e. it lives inside that node's retained subtree.
|
|
18
|
+
*
|
|
19
|
+
* Mirrors `computeReachableWithoutDevRoots` in dev-artifacts.ts, which does the
|
|
20
|
+
* same walk with the dev/automation roots as sinks.
|
|
21
|
+
*/
|
|
22
|
+
export function computeReachableWithoutNode(snapshot, blockedId) {
|
|
23
|
+
const reached = new Uint8Array(snapshot.nodes.length);
|
|
24
|
+
const stack = [];
|
|
25
|
+
snapshot.nodes.forEach((node) => {
|
|
26
|
+
if (node.type !== 'synthetic' && node.id > 3)
|
|
27
|
+
return;
|
|
28
|
+
if (reached[node.nodeIndex])
|
|
29
|
+
return;
|
|
30
|
+
reached[node.nodeIndex] = 1;
|
|
31
|
+
if (node.id !== blockedId)
|
|
32
|
+
stack.push(node);
|
|
33
|
+
});
|
|
34
|
+
while (stack.length > 0) {
|
|
35
|
+
const node = stack.pop();
|
|
36
|
+
node.forEachReference((edge) => {
|
|
37
|
+
const to = edge.toNode;
|
|
38
|
+
if (reached[to.nodeIndex])
|
|
39
|
+
return;
|
|
40
|
+
reached[to.nodeIndex] = 1;
|
|
41
|
+
if (to.id !== blockedId)
|
|
42
|
+
stack.push(to);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return reached;
|
|
46
|
+
}
|
|
47
|
+
function groupKey(g) {
|
|
48
|
+
return `${g.fromType}\u0000${g.fromName}\u0000${g.edgeType}\u0000${g.edgeName}`;
|
|
49
|
+
}
|
|
50
|
+
export function registerRetainerLayers(server) {
|
|
51
|
+
server.tool('memlab_retainer_layers', 'Counterfactual retainer analysis: answer "if I cut this edge, would the object still be retained, and by what?". ' +
|
|
52
|
+
"Enumerates the INDEPENDENT retaining edges into a node — the incoming edges whose source is still reachable from a GC root when the target itself is treated as a sink — so back-edges from inside the target's own retained subtree are excluded. " +
|
|
53
|
+
'The object becomes unreachable only when ALL independent retainers are cut, so a count above 1 means any single fix leaves it retained by the others. ' +
|
|
54
|
+
'Use this BEFORE writing a fix: memlab_retainer_trace and memlab_detached_dom --group_by retainer each report ONE path/grouping, which is why a stack of caches holding the same object looks like a single retainer and a partial fix reports a misleading win. ' +
|
|
55
|
+
'Complements memlab_dominator_chain (accountability chain upward) and memlab_retainer_trace (one shortest path).', {
|
|
56
|
+
node_id: z
|
|
57
|
+
.number()
|
|
58
|
+
.int()
|
|
59
|
+
.describe('The numeric ID of the heap node to analyze'),
|
|
60
|
+
max_retainers: z
|
|
61
|
+
.number()
|
|
62
|
+
.int()
|
|
63
|
+
.min(1)
|
|
64
|
+
.optional()
|
|
65
|
+
.default(20)
|
|
66
|
+
.describe('Maximum number of distinct retainer groups to list (default 20). Grouping is by (source class, source type, edge name, edge type).'),
|
|
67
|
+
}, async ({ node_id, max_retainers }) => {
|
|
68
|
+
try {
|
|
69
|
+
const snapshot = getSnapshot();
|
|
70
|
+
const target = snapshot.getNodeById(node_id);
|
|
71
|
+
if (!target) {
|
|
72
|
+
return errorResult(`Node with id ${node_id} not found`);
|
|
73
|
+
}
|
|
74
|
+
const reached = computeReachableWithoutNode(snapshot, target.id);
|
|
75
|
+
// Partition incoming edges into independent retainers (source still
|
|
76
|
+
// reachable with the target blocked) and internal back-edges (source
|
|
77
|
+
// only reachable through the target).
|
|
78
|
+
const groups = new Map();
|
|
79
|
+
let independentEdgeCount = 0;
|
|
80
|
+
let backEdgeCount = 0;
|
|
81
|
+
for (const edge of target.referrers) {
|
|
82
|
+
const from = edge.fromNode;
|
|
83
|
+
// A self-edge (V8 emits them for DOM nodes) cannot retain the target,
|
|
84
|
+
// but the sink walk marks the target itself reached so it would
|
|
85
|
+
// otherwise pass the test below. Count it with the back-edges.
|
|
86
|
+
if (from.id === target.id || !reached[from.nodeIndex]) {
|
|
87
|
+
backEdgeCount++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
independentEdgeCount++;
|
|
91
|
+
const g = {
|
|
92
|
+
fromName: from.name,
|
|
93
|
+
fromType: from.type,
|
|
94
|
+
edgeName: String(edge.name_or_index),
|
|
95
|
+
edgeType: edge.type,
|
|
96
|
+
};
|
|
97
|
+
const key = groupKey(g);
|
|
98
|
+
const existing = groups.get(key);
|
|
99
|
+
if (existing) {
|
|
100
|
+
existing.count++;
|
|
101
|
+
if (from.retainedSize > existing.maxRetained) {
|
|
102
|
+
existing.maxRetained = from.retainedSize;
|
|
103
|
+
existing.exampleFromId = from.id;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
groups.set(key, {
|
|
108
|
+
...g,
|
|
109
|
+
count: 1,
|
|
110
|
+
exampleFromId: from.id,
|
|
111
|
+
maxRetained: from.retainedSize,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const sorted = [...groups.values()].sort((a, b) => b.maxRetained - a.maxRetained || b.count - a.count);
|
|
116
|
+
const shown = sorted.slice(0, max_retainers);
|
|
117
|
+
const targetLabel = truncateNodeName(target.name, target.type, target.self_size, 60);
|
|
118
|
+
const lines = [
|
|
119
|
+
`## Retainer layers for @${target.id} (${targetLabel})`,
|
|
120
|
+
'',
|
|
121
|
+
`Retained ${formatBytes(target.retainedSize)}. ` +
|
|
122
|
+
`${formatNumber(independentEdgeCount)} independent retaining edge(s) in ${formatNumber(sorted.length)} group(s)` +
|
|
123
|
+
(backEdgeCount > 0
|
|
124
|
+
? `; ${formatNumber(backEdgeCount)} back-edge(s) from inside its own retained subtree excluded.`
|
|
125
|
+
: '.'),
|
|
126
|
+
'',
|
|
127
|
+
];
|
|
128
|
+
if (independentEdgeCount === 0) {
|
|
129
|
+
lines.push(target.type === 'synthetic' || target.id <= 3
|
|
130
|
+
? '**This node is a GC root itself** — it has no external retainer to cut.'
|
|
131
|
+
: "**No independent retainer found.** Nothing outside this object's own subtree references it, so it is already GC-eligible (or reachable only via weak references). It should not be counted toward a leak total — see `memlab_detached_dom --only_with_retainer_path`.");
|
|
132
|
+
return toolResult(lines.join('\n'));
|
|
133
|
+
}
|
|
134
|
+
// The counterfactual verdict. Cutting one independent edge cannot change
|
|
135
|
+
// the reachability of any other independent source, because every such
|
|
136
|
+
// source is reachable on a path that does not pass through the target.
|
|
137
|
+
if (independentEdgeCount === 1) {
|
|
138
|
+
const only = sorted[0];
|
|
139
|
+
lines.push(`**Sole retainer — cutting it frees the object.** \`${only.edgeName}\` (${only.edgeType}) from @${only.exampleFromId} ${only.fromName} (${only.fromType}). ` +
|
|
140
|
+
`Fixing this one reference reclaims ${formatBytes(target.retainedSize)}.`, '');
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
lines.push(`**${formatNumber(independentEdgeCount)} independent retainers — cutting any ONE leaves the object retained by the other ${formatNumber(independentEdgeCount - 1)}.** ` +
|
|
144
|
+
'A fix that removes a single one of these will show little or no memory win, and a before/after measurement that attributes the whole object to the removed retainer will overstate the result. ' +
|
|
145
|
+
'All of them must be cut to reclaim the object.', '');
|
|
146
|
+
}
|
|
147
|
+
lines.push(markdownTable(['#', 'edges', 'from', 'edge', 'example', 'source retains'], shown.map((g, i) => [
|
|
148
|
+
String(i + 1),
|
|
149
|
+
formatNumber(g.count),
|
|
150
|
+
`${g.fromName} (${g.fromType})`,
|
|
151
|
+
`${g.edgeName} (${g.edgeType})`,
|
|
152
|
+
`@${g.exampleFromId}`,
|
|
153
|
+
formatBytes(g.maxRetained),
|
|
154
|
+
])));
|
|
155
|
+
if (sorted.length > shown.length) {
|
|
156
|
+
lines.push('', `_${formatNumber(sorted.length - shown.length)} more retainer group(s) not shown — raise max_retainers to see them._`);
|
|
157
|
+
}
|
|
158
|
+
// A dominator that is not the GC-roots super-node means every path to
|
|
159
|
+
// the target passes through it, so it is a single choke point even when
|
|
160
|
+
// several independent edges exist.
|
|
161
|
+
const dom = target.dominatorNode;
|
|
162
|
+
if (independentEdgeCount > 1 &&
|
|
163
|
+
dom &&
|
|
164
|
+
dom.id !== target.id &&
|
|
165
|
+
dom.name !== '(GC roots)' &&
|
|
166
|
+
isNodeWorthInspecting(dom)) {
|
|
167
|
+
lines.push('', `**Single choke point:** every path still passes through the immediate dominator @${dom.id} ${truncateNodeName(dom.name, dom.type, dom.self_size, 60)} (retains ${formatBytes(dom.retainedSize)}). ` +
|
|
168
|
+
'Cutting there frees the target in one change instead of cutting each retainer. Use `memlab_dominator_chain` to walk further up.');
|
|
169
|
+
}
|
|
170
|
+
lines.push('', '_Independence test: an incoming edge counts only if its source is still reachable from a GC root when the target is treated as a sink. ' +
|
|
171
|
+
"Edges failing that test originate inside the target's own retained subtree and cannot keep it alive._");
|
|
172
|
+
return toolResult(lines.join('\n'));
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
return errorResult(err);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=retainer-layers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retainer-layers.js","sourceRoot":"","sources":["../../src/tools/retainer-layers.ts"],"names":[],"mappings":"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"}
|
|
@@ -8,5 +8,6 @@
|
|
|
8
8
|
* @oncall memory_lab
|
|
9
9
|
*/
|
|
10
10
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
11
|
+
export declare const COMPACT_GAP_MARKER = "\u22EF";
|
|
11
12
|
export declare function registerRetainerSummary(server: McpServer): void;
|
|
12
13
|
//# sourceMappingURL=retainer-summary.d.ts.map
|