@memlab/mcp-server 2.3.0 → 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 +18 -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 +96 -2
- package/dist/heap-state.d.ts.map +1 -1
- package/dist/heap-state.js +197 -2
- package/dist/heap-state.js.map +1 -1
- package/dist/index.js +41 -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/auto-investigate.d.ts.map +1 -1
- package/dist/tools/auto-investigate.js +91 -49
- package/dist/tools/auto-investigate.js.map +1 -1
- 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/diff-snapshots.d.ts.map +1 -1
- package/dist/tools/diff-snapshots.js +101 -44
- package/dist/tools/diff-snapshots.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 +787 -30
- 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-references.d.ts.map +1 -1
- package/dist/tools/get-references.js +35 -6
- 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 +52 -6
- 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 +721 -75
- 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 +43 -16
- 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 +78 -0
- package/dist/tools/load-snapshot.d.ts.map +1 -1
- package/dist/tools/load-snapshot.js +519 -29
- 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 +92 -30
- 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/quick-diagnosis.d.ts.map +1 -1
- package/dist/tools/quick-diagnosis.js +12 -4
- package/dist/tools/quick-diagnosis.js.map +1 -1
- 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 +105 -25
- 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 +268 -78
- 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/sliced-strings.d.ts.map +1 -1
- package/dist/tools/sliced-strings.js +22 -4
- package/dist/tools/sliced-strings.js.map +1 -1
- package/dist/tools/snapshot-header.d.ts +28 -0
- package/dist/tools/snapshot-header.d.ts.map +1 -0
- package/dist/tools/snapshot-header.js +111 -0
- package/dist/tools/snapshot-header.js.map +1 -0
- 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 +204 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +478 -8
- package/dist/utils.js.map +1 -1
- package/package.json +5 -4
|
@@ -0,0 +1,140 @@
|
|
|
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 { formatBytes, formatNumber, markdownTable, errorResult, toolResult, } from '../utils.js';
|
|
13
|
+
import { classifyArtifact } from '../artifact-classes.js';
|
|
14
|
+
import { collectDevRoots, computeReachableWithoutDevRoots, } from './dev-artifacts.js';
|
|
15
|
+
/**
|
|
16
|
+
* How much of this heap is actually the APPLICATION?
|
|
17
|
+
*
|
|
18
|
+
* A dev-build heap is mostly not the app. Measured on one WhatsApp Web capture:
|
|
19
|
+
* `system / ExternalStringData` — the bundle's own source text — was 56% of a
|
|
20
|
+
* 311 MB heap, single strings in it retained 84.9 / 22.5 / 17.8 / 10.2 MB, and
|
|
21
|
+
* code-related memory came to roughly two thirds of the total. So "drive to
|
|
22
|
+
* 600 MB" is mostly an instruction to load more bundle, the number is not
|
|
23
|
+
* comparable to production, and a threshold expressed against it is measuring
|
|
24
|
+
* the wrong thing.
|
|
25
|
+
*
|
|
26
|
+
* The split below is deliberately coarse and named, not a single magic number:
|
|
27
|
+
* a reader has to be able to see WHICH bucket dominates, because that is what
|
|
28
|
+
* decides whether the capture is usable at all.
|
|
29
|
+
*/
|
|
30
|
+
const CODE_TYPES = new Set(['code']);
|
|
31
|
+
// Bundle source text and the V8 structures that hold it. These exist because
|
|
32
|
+
// the app was *loaded*, not because it did anything.
|
|
33
|
+
const BUNDLE_NAME_RE = /ExternalStringData|InstructionStream|BytecodeArray|SharedFunctionInfo|ScopeInfo|UncompiledData|FeedbackVector|FeedbackMetadata|FeedbackCell|ClosureFeedbackCellArray|\(constant pool\)|ConstantPool/;
|
|
34
|
+
export function registerAppHeap(server) {
|
|
35
|
+
server.tool('memlab_app_heap', 'Report APP-ATTRIBUTABLE heap: total self size minus bundle/code memory, minus dev-build artifacts, minus measurement-harness content — the part of the heap that is actually the application. ' +
|
|
36
|
+
'On a dev build the raw number is mostly not the app: a measured WhatsApp Web capture had bundle source text (`system / ExternalStringData`) at 56% of a 311 MB heap and code-related memory at roughly two thirds of the total. A leak-hunt threshold expressed against the raw total therefore mostly measures how much bundle has loaded, and is not comparable to production at all. ' +
|
|
37
|
+
'Use this as the number to drive a hunt against and to quote in a report; use memlab_leak_report for whether anything is actually growing.', {
|
|
38
|
+
show_breakdown: z
|
|
39
|
+
.boolean()
|
|
40
|
+
.optional()
|
|
41
|
+
.default(true)
|
|
42
|
+
.describe('Show the per-bucket table (default true). The breakdown is the point — which bucket dominates decides whether the capture is usable.'),
|
|
43
|
+
}, async ({ show_breakdown }) => {
|
|
44
|
+
try {
|
|
45
|
+
const snapshot = getSnapshot();
|
|
46
|
+
const meta = getSnapshotMetadata();
|
|
47
|
+
const devRoots = collectDevRoots(snapshot);
|
|
48
|
+
const reached = devRoots.byId.size > 0
|
|
49
|
+
? computeReachableWithoutDevRoots(snapshot, devRoots)
|
|
50
|
+
: null;
|
|
51
|
+
let total = 0;
|
|
52
|
+
let code = 0;
|
|
53
|
+
let bundle = 0;
|
|
54
|
+
let devOnly = 0;
|
|
55
|
+
let artifact = 0;
|
|
56
|
+
let app = 0;
|
|
57
|
+
let appNodes = 0;
|
|
58
|
+
const isDevOnly = (node) => reached != null && reached[node.nodeIndex] === 0;
|
|
59
|
+
snapshot.nodes.forEach(node => {
|
|
60
|
+
if (node.id <= 3)
|
|
61
|
+
return;
|
|
62
|
+
const size = node.self_size;
|
|
63
|
+
total += size;
|
|
64
|
+
// Precedence matters: a node is counted once, in the most specific
|
|
65
|
+
// bucket that explains why it is NOT app memory.
|
|
66
|
+
if (isDevOnly(node)) {
|
|
67
|
+
devOnly += size;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (CODE_TYPES.has(node.type)) {
|
|
71
|
+
code += size;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (BUNDLE_NAME_RE.test(node.name)) {
|
|
75
|
+
bundle += size;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (classifyArtifact(node.name) != null) {
|
|
79
|
+
artifact += size;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
app += size;
|
|
83
|
+
appNodes++;
|
|
84
|
+
});
|
|
85
|
+
const pct = (n) => total > 0 ? `${((n / total) * 100).toFixed(1)}%` : '—';
|
|
86
|
+
const lines = [
|
|
87
|
+
'## App-attributable heap',
|
|
88
|
+
'',
|
|
89
|
+
`**${formatBytes(app)} of ${formatBytes(total)} (${pct(app)}) is application memory** across ${formatNumber(appNodes)} nodes` +
|
|
90
|
+
(meta ? ` in \`${meta.fileName}\`` : '') +
|
|
91
|
+
'.',
|
|
92
|
+
'',
|
|
93
|
+
];
|
|
94
|
+
if (show_breakdown) {
|
|
95
|
+
lines.push(markdownTable(['Bucket', 'Self size', '% of heap', 'What it is'], [
|
|
96
|
+
[
|
|
97
|
+
'App',
|
|
98
|
+
formatBytes(app),
|
|
99
|
+
pct(app),
|
|
100
|
+
'The number to drive and report',
|
|
101
|
+
],
|
|
102
|
+
[
|
|
103
|
+
'Bundle / source text',
|
|
104
|
+
formatBytes(bundle),
|
|
105
|
+
pct(bundle),
|
|
106
|
+
'Loaded, not allocated by use; absent at this scale in prod',
|
|
107
|
+
],
|
|
108
|
+
[
|
|
109
|
+
'Code / bytecode',
|
|
110
|
+
formatBytes(code),
|
|
111
|
+
pct(code),
|
|
112
|
+
'JIT + compiled code; grows by exercising new paths',
|
|
113
|
+
],
|
|
114
|
+
[
|
|
115
|
+
'Dev/automation-retained',
|
|
116
|
+
formatBytes(devOnly),
|
|
117
|
+
pct(devOnly),
|
|
118
|
+
'Held only via inspector/dev roots; GC-eligible in prod',
|
|
119
|
+
],
|
|
120
|
+
[
|
|
121
|
+
'Known artifacts',
|
|
122
|
+
formatBytes(artifact),
|
|
123
|
+
pct(artifact),
|
|
124
|
+
'CDP/a11y/warmup families',
|
|
125
|
+
],
|
|
126
|
+
], new Set([1, 2])), '');
|
|
127
|
+
}
|
|
128
|
+
const nonApp = total - app;
|
|
129
|
+
if (total > 0 && nonApp / total > 0.5) {
|
|
130
|
+
lines.push(`> ⚠️ **${pct(nonApp)} of this heap is not the application.** A threshold expressed against the raw total is mostly measuring how much bundle and code has loaded. Drive against the app-attributable figure above, and do not compare the raw total to production.`);
|
|
131
|
+
}
|
|
132
|
+
lines.push('', '_Buckets are mutually exclusive and sum to the total; each node lands in the most specific bucket that explains why it is not app memory. Self size, not retained: retained sizes overlap and would not sum._');
|
|
133
|
+
return toolResult(lines.join('\n'));
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
return errorResult(err);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=app-heap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-heap.js","sourceRoot":"","sources":["../../src/tools/app-heap.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auto-investigate.d.ts","sourceRoot":"","sources":["../../src/tools/auto-investigate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"auto-investigate.d.ts","sourceRoot":"","sources":["../../src/tools/auto-investigate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,yCAAyC,CAAC;AA49CvE,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA0tB/D"}
|
|
@@ -11,7 +11,7 @@ import memlabCore from '@memlab/core';
|
|
|
11
11
|
const { utils, NumericSet } = memlabCore;
|
|
12
12
|
import { z } from 'zod';
|
|
13
13
|
import { getSnapshot, getSnapshotMetadata } from '../heap-state.js';
|
|
14
|
-
import { filterLargestObjects, isNodeWorthInspecting, formatBytes, formatNumber, truncateNodeName, instrumentationRetainerNote, errorResult, toolResult, suggestionsSuppressed, } from '../utils.js';
|
|
14
|
+
import { filterLargestObjects, isNodeWorthInspecting, formatBytes, formatNumber, formatRetainerTree, truncateNodeName, instrumentationRetainerNote, errorResult, toolResult, suggestionsSuppressed, } from '../utils.js';
|
|
15
15
|
function classifySeverity(retainedSize, totalSize) {
|
|
16
16
|
if (totalSize === 0)
|
|
17
17
|
return 'LOW';
|
|
@@ -598,22 +598,31 @@ function formatTrace(trace, maxSteps) {
|
|
|
598
598
|
continue;
|
|
599
599
|
collapsed.push(step);
|
|
600
600
|
}
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
601
|
+
const overLimit = collapsed.length > maxSteps;
|
|
602
|
+
const shown = overLimit
|
|
603
|
+
? [...collapsed.slice(0, 3), ...collapsed.slice(-2)]
|
|
604
|
+
: collapsed;
|
|
605
|
+
// Render as a top-down indented tree so the retention direction is
|
|
606
|
+
// unambiguous (GC root on top, each node retained by the one above it).
|
|
607
|
+
// `s.edgeName` is the OUTGOING edge (s -> next); the edge pointing INTO a node
|
|
608
|
+
// is the previous node's edgeName, which is what the renderer surfaces.
|
|
609
|
+
const treeSteps = shown.map((s, i) => ({
|
|
610
|
+
id: s.nodeId,
|
|
611
|
+
name: truncateNodeName(s.name, s.type, s.selfSize, 50),
|
|
612
|
+
type: s.type,
|
|
613
|
+
retainedSize: s.retainedSize,
|
|
614
|
+
// `shown[i - 1].edgeName` is the previous node's OUTGOING edge = the edge
|
|
615
|
+
// INTO this node, but only when the two are actually adjacent. At the
|
|
616
|
+
// truncation boundary (i === 3) `shown[2]`'s outgoing edge points into the
|
|
617
|
+
// elided `collapsed[3]`, not into this tail node, so leave it undefined
|
|
618
|
+
// rather than misattribute an unrelated edge — the gap is already marked by
|
|
619
|
+
// `collapsedBefore`.
|
|
620
|
+
edgeName: i > 0 && !(overLimit && i === 3) ? shown[i - 1].edgeName : undefined,
|
|
621
|
+
// The two tail nodes were sliced off the end, so the hop into the first of
|
|
622
|
+
// them crosses the elided middle — mark the gap.
|
|
623
|
+
collapsedBefore: overLimit && i === 3 ? collapsed.length - 5 : undefined,
|
|
624
|
+
}));
|
|
625
|
+
return formatRetainerTree(treeSteps, { showSizes: true });
|
|
617
626
|
}
|
|
618
627
|
function computeTraceOverlap(a, b) {
|
|
619
628
|
let prefixLen = 0;
|
|
@@ -1265,7 +1274,7 @@ export function registerAutoInvestigate(server) {
|
|
|
1265
1274
|
lines.push(`**Retainer chain:** (same chain as #${traceMatch.matchedIndex + 1}) → [${divergeEdge}] ${divergeName} — ${formatBytes(f.node.retainedSize)}`);
|
|
1266
1275
|
}
|
|
1267
1276
|
else {
|
|
1268
|
-
lines.push(`**Retainer chain
|
|
1277
|
+
lines.push(`**Retainer chain:**\n${formatTrace(f.trace, 8)}`);
|
|
1269
1278
|
}
|
|
1270
1279
|
lines.push('');
|
|
1271
1280
|
lines.push(`**Alternate retainers of the same subtree** (retained sizes overlap — not additive): @${f.node.id} (${formatBytes(f.node.retainedSize)})` +
|
|
@@ -1286,7 +1295,7 @@ export function registerAutoInvestigate(server) {
|
|
|
1286
1295
|
else {
|
|
1287
1296
|
lines.push(`### ${i + 1}. ${sevIcon} [${f.severity}] @${f.node.id} \`${name}\` (${f.node.type}) — ${formatBytes(f.node.retainedSize)}${pct}`);
|
|
1288
1297
|
lines.push('');
|
|
1289
|
-
lines.push(`**Retainer chain
|
|
1298
|
+
lines.push(`**Retainer chain:**\n${formatTrace(f.trace, 8)}`);
|
|
1290
1299
|
}
|
|
1291
1300
|
previousTraces.push(f.trace);
|
|
1292
1301
|
const instrNote = instrumentationRetainerNote(f.trace);
|
|
@@ -1419,8 +1428,16 @@ export function registerAutoInvestigate(server) {
|
|
|
1419
1428
|
const sampleNodes = shapeBuckets.get(s.properties.join(',')) ?? [];
|
|
1420
1429
|
if (sampleNodes.length < 50)
|
|
1421
1430
|
continue;
|
|
1422
|
-
|
|
1431
|
+
const sampleCount = sampleNodes.length;
|
|
1432
|
+
const shapeKey = s.properties.join(',');
|
|
1433
|
+
const propStats = [];
|
|
1423
1434
|
for (const propName of s.properties) {
|
|
1435
|
+
// Skip prototype / internal pseudo-properties — they are not data
|
|
1436
|
+
// columns, so "pre-filter / intern this column" advice is noise
|
|
1437
|
+
// (Feedback round 4 §2).
|
|
1438
|
+
if (propName === '__proto__' || propName.startsWith('<')) {
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1424
1441
|
const values = new Set();
|
|
1425
1442
|
let totalValueSize = 0;
|
|
1426
1443
|
let stringValueCount = 0;
|
|
@@ -1445,39 +1462,64 @@ export function registerAutoInvestigate(server) {
|
|
|
1445
1462
|
}
|
|
1446
1463
|
}
|
|
1447
1464
|
}
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1465
|
+
propStats.push({
|
|
1466
|
+
name: propName,
|
|
1467
|
+
cardinality: values.size,
|
|
1468
|
+
stringValueCount,
|
|
1469
|
+
totalValueSize,
|
|
1470
|
+
allUnique,
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
// Same-object duplication (§1b), copy-factor corrected (round 4 §1a):
|
|
1474
|
+
// derive the factor from the MOST-DISTINCT string-valued field — the
|
|
1475
|
+
// record's true discriminator — NOT from an arbitrary unique-looking
|
|
1476
|
+
// field. The old code did `sampleCount / cardinality` on any
|
|
1477
|
+
// `*_id`-shaped key, so a CONSTANT field (e.g. `gala_project_id`
|
|
1478
|
+
// with 1 value) reported "~200 copies" when the real duplication was
|
|
1479
|
+
// 2×. If even the most-distinct field repeats, the whole record is
|
|
1480
|
+
// duplicated; if the most-distinct field is ~unique, there is no
|
|
1481
|
+
// duplication regardless of how many constant columns exist.
|
|
1482
|
+
const stringStats = propStats.filter(p => p.stringValueCount >= sampleCount * 0.5);
|
|
1483
|
+
let maxCard = 0;
|
|
1484
|
+
let discrim = '';
|
|
1485
|
+
for (const p of stringStats) {
|
|
1486
|
+
if (p.cardinality > maxCard) {
|
|
1487
|
+
maxCard = p.cardinality;
|
|
1488
|
+
discrim = p.name;
|
|
1466
1489
|
}
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1490
|
+
}
|
|
1491
|
+
if (stringStats.length > 0 &&
|
|
1492
|
+
maxCard >= 1 &&
|
|
1493
|
+
maxCard < sampleCount * 0.6 &&
|
|
1494
|
+
sampleCount >= 100 &&
|
|
1495
|
+
!dupShapesSeen.has(shapeKey)) {
|
|
1496
|
+
const copyFactor = Math.round(sampleCount / maxCard);
|
|
1497
|
+
if (copyFactor >= 2) {
|
|
1498
|
+
dupShapesSeen.add(shapeKey);
|
|
1499
|
+
duplicationAlerts.push(`- \`{${s.properties.slice(0, 4).join(', ')}${s.properties.length > 4 ? ', …' : ''}}\` (${formatNumber(s.count)} total instances): even the most-distinct field \`${discrim}\` has only **${formatNumber(maxCard)} distinct value(s)** across ${formatNumber(sampleCount)} sampled — the same records appear to be retained **≈${copyFactor}×** (sample-based estimate; confirm the exact factor with \`memlab_property_distribution\` on \`${discrim}\`, or a content-hash count). Investigate **retention/concurrency** (e.g. many concurrent requests each holding a full copy of this dataset, or the same listing materialized twice), NOT string interning.`);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
// Pass 2: per-property low-cardinality + high-cost-unique notes.
|
|
1503
|
+
for (const p of propStats) {
|
|
1504
|
+
const uniqueKey = looksUniqueKey(p.name);
|
|
1505
|
+
if (!uniqueKey && p.cardinality <= 20 && sampleCount >= 100) {
|
|
1506
|
+
// Genuine low-cardinality column. Only suggest string interning
|
|
1507
|
+
// when the column actually holds strings — interning a numeric
|
|
1508
|
+
// (SMI/heap-number) or boolean column is meaningless
|
|
1509
|
+
// (Feedback round 4 §2).
|
|
1510
|
+
const isStringColumn = p.stringValueCount >= sampleCount * 0.5;
|
|
1511
|
+
columnAlerts.push(isStringColumn
|
|
1512
|
+
? `- Property \`${p.name}\` has only **${p.cardinality} unique value(s)** across ${formatNumber(s.count)} instances — low-cardinality column suitable for pre-filtering at the data source or string interning`
|
|
1513
|
+
: `- Property \`${p.name}\` has only **${p.cardinality} unique value(s)** across ${formatNumber(s.count)} instances — low-cardinality column suitable for pre-filtering at the data source`);
|
|
1472
1514
|
}
|
|
1473
|
-
// High-cost unique: all values unique, string, large average size
|
|
1474
|
-
if (allUnique &&
|
|
1475
|
-
stringValueCount > sampleCount * 0.8 &&
|
|
1476
|
-
cardinality > sampleCount * 0.9) {
|
|
1477
|
-
const avgSize = totalValueSize / stringValueCount;
|
|
1515
|
+
// High-cost unique: all values unique, string, large average size.
|
|
1516
|
+
if (p.allUnique &&
|
|
1517
|
+
p.stringValueCount > sampleCount * 0.8 &&
|
|
1518
|
+
p.cardinality > sampleCount * 0.9) {
|
|
1519
|
+
const avgSize = p.totalValueSize / p.stringValueCount;
|
|
1478
1520
|
if (avgSize > 50 && s.count > 10_000) {
|
|
1479
1521
|
const estimatedWaste = avgSize * s.count;
|
|
1480
|
-
columnAlerts.push(`- Property \`${
|
|
1522
|
+
columnAlerts.push(`- Property \`${p.name}\` has **all unique string values** (avg ${Math.round(avgSize)}B each × ${formatNumber(s.count)} instances = ~${formatBytes(estimatedWaste)}) — verify this field is needed by consumers`);
|
|
1481
1523
|
}
|
|
1482
1524
|
}
|
|
1483
1525
|
}
|