@memlab/mcp-server 2.52.0 → 2.86.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 +22 -2
- package/dist/anonymized-snapshot.d.ts +75 -0
- package/dist/anonymized-snapshot.d.ts.map +1 -0
- package/dist/anonymized-snapshot.js +101 -0
- package/dist/anonymized-snapshot.js.map +1 -0
- package/dist/eval-hints.d.ts +41 -0
- package/dist/eval-hints.d.ts.map +1 -0
- package/dist/eval-hints.js +115 -0
- package/dist/eval-hints.js.map +1 -0
- package/dist/heap-budget.d.ts +32 -0
- package/dist/heap-budget.d.ts.map +1 -0
- package/dist/heap-budget.js +90 -0
- package/dist/heap-budget.js.map +1 -0
- package/dist/heap-shapes.d.ts +148 -0
- package/dist/heap-shapes.d.ts.map +1 -0
- package/dist/heap-shapes.js +193 -0
- package/dist/heap-shapes.js.map +1 -0
- package/dist/heap-state.d.ts +20 -0
- package/dist/heap-state.d.ts.map +1 -1
- package/dist/heap-state.js +8 -1
- package/dist/heap-state.js.map +1 -1
- package/dist/index.js +64 -1
- package/dist/index.js.map +1 -1
- package/dist/react-shapes.d.ts +42 -0
- package/dist/react-shapes.d.ts.map +1 -0
- package/dist/react-shapes.js +149 -0
- package/dist/react-shapes.js.map +1 -0
- package/dist/server-version.d.ts +19 -0
- package/dist/server-version.d.ts.map +1 -0
- package/dist/server-version.js +19 -0
- package/dist/server-version.js.map +1 -0
- package/dist/snapshot-borrow.d.ts +67 -0
- package/dist/snapshot-borrow.d.ts.map +1 -0
- package/dist/snapshot-borrow.js +113 -0
- package/dist/snapshot-borrow.js.map +1 -0
- package/dist/string-index.d.ts.map +1 -1
- package/dist/string-index.js +8 -0
- package/dist/string-index.js.map +1 -1
- package/dist/tools/analyze-run.d.ts.map +1 -1
- package/dist/tools/analyze-run.js +79 -18
- package/dist/tools/analyze-run.js.map +1 -1
- package/dist/tools/anonymize-snapshot.d.ts +12 -0
- package/dist/tools/anonymize-snapshot.d.ts.map +1 -0
- package/dist/tools/anonymize-snapshot.js +159 -0
- package/dist/tools/anonymize-snapshot.js.map +1 -0
- package/dist/tools/app-config.d.ts +12 -0
- package/dist/tools/app-config.d.ts.map +1 -0
- package/dist/tools/app-config.js +217 -0
- package/dist/tools/app-config.js.map +1 -0
- package/dist/tools/app-heap.d.ts +18 -0
- package/dist/tools/app-heap.d.ts.map +1 -1
- package/dist/tools/app-heap.js +12 -46
- package/dist/tools/app-heap.js.map +1 -1
- package/dist/tools/artifact-budget.d.ts +12 -0
- package/dist/tools/artifact-budget.d.ts.map +1 -0
- package/dist/tools/artifact-budget.js +219 -0
- package/dist/tools/artifact-budget.js.map +1 -0
- package/dist/tools/async-census.d.ts +12 -0
- package/dist/tools/async-census.d.ts.map +1 -0
- package/dist/tools/async-census.js +243 -0
- package/dist/tools/async-census.js.map +1 -0
- package/dist/tools/auto-investigate.d.ts.map +1 -1
- package/dist/tools/auto-investigate.js +54 -5
- package/dist/tools/auto-investigate.js.map +1 -1
- package/dist/tools/batch.d.ts.map +1 -1
- package/dist/tools/batch.js +33 -5
- package/dist/tools/batch.js.map +1 -1
- package/dist/tools/cache-analysis.d.ts +15 -0
- package/dist/tools/cache-analysis.d.ts.map +1 -1
- package/dist/tools/cache-analysis.js +85 -10
- package/dist/tools/cache-analysis.js.map +1 -1
- package/dist/tools/census-diff.d.ts +55 -0
- package/dist/tools/census-diff.d.ts.map +1 -0
- package/dist/tools/census-diff.js +228 -0
- package/dist/tools/census-diff.js.map +1 -0
- package/dist/tools/chain-walk.d.ts +12 -0
- package/dist/tools/chain-walk.d.ts.map +1 -0
- package/dist/tools/chain-walk.js +184 -0
- package/dist/tools/chain-walk.js.map +1 -0
- package/dist/tools/collection-diff.d.ts +44 -0
- package/dist/tools/collection-diff.d.ts.map +1 -0
- package/dist/tools/collection-diff.js +343 -0
- package/dist/tools/collection-diff.js.map +1 -0
- package/dist/tools/collection-trend.d.ts.map +1 -1
- package/dist/tools/collection-trend.js +2 -2
- package/dist/tools/collection-trend.js.map +1 -1
- package/dist/tools/detached-dom.d.ts +3 -0
- package/dist/tools/detached-dom.d.ts.map +1 -1
- package/dist/tools/detached-dom.js +78 -3
- package/dist/tools/detached-dom.js.map +1 -1
- package/dist/tools/dev-artifacts.d.ts.map +1 -1
- package/dist/tools/dev-artifacts.js +221 -3
- 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 +18 -2
- package/dist/tools/diff-snapshots.js.map +1 -1
- package/dist/tools/dom-audit.d.ts +37 -0
- package/dist/tools/dom-audit.d.ts.map +1 -0
- package/dist/tools/dom-audit.js +247 -0
- package/dist/tools/dom-audit.js.map +1 -0
- package/dist/tools/duplicated-strings.d.ts.map +1 -1
- package/dist/tools/duplicated-strings.js +3 -2
- package/dist/tools/duplicated-strings.js.map +1 -1
- package/dist/tools/eval-across.d.ts +12 -0
- package/dist/tools/eval-across.d.ts.map +1 -0
- package/dist/tools/eval-across.js +135 -0
- package/dist/tools/eval-across.js.map +1 -0
- package/dist/tools/eval.d.ts +78 -0
- package/dist/tools/eval.d.ts.map +1 -1
- package/dist/tools/eval.js +2270 -701
- package/dist/tools/eval.js.map +1 -1
- package/dist/tools/event-listener-leaks.d.ts.map +1 -1
- package/dist/tools/event-listener-leaks.js +42 -17
- package/dist/tools/event-listener-leaks.js.map +1 -1
- package/dist/tools/event-registry.d.ts.map +1 -1
- package/dist/tools/event-registry.js +42 -8
- package/dist/tools/event-registry.js.map +1 -1
- package/dist/tools/explain-delta.d.ts.map +1 -1
- package/dist/tools/explain-delta.js +61 -10
- package/dist/tools/explain-delta.js.map +1 -1
- package/dist/tools/finding-index.d.ts +181 -0
- package/dist/tools/finding-index.d.ts.map +1 -1
- package/dist/tools/finding-index.js +343 -24
- package/dist/tools/finding-index.js.map +1 -1
- package/dist/tools/get-value.d.ts +16 -0
- package/dist/tools/get-value.d.ts.map +1 -1
- package/dist/tools/get-value.js +96 -2
- package/dist/tools/get-value.js.map +1 -1
- package/dist/tools/hypothesis.d.ts.map +1 -1
- package/dist/tools/hypothesis.js +117 -58
- package/dist/tools/hypothesis.js.map +1 -1
- package/dist/tools/id-space-audit.d.ts +12 -0
- package/dist/tools/id-space-audit.d.ts.map +1 -0
- package/dist/tools/id-space-audit.js +262 -0
- package/dist/tools/id-space-audit.js.map +1 -0
- package/dist/tools/identify.d.ts +12 -0
- package/dist/tools/identify.d.ts.map +1 -0
- package/dist/tools/identify.js +205 -0
- package/dist/tools/identify.js.map +1 -0
- package/dist/tools/identity-diff.d.ts +12 -0
- package/dist/tools/identity-diff.d.ts.map +1 -0
- package/dist/tools/identity-diff.js +188 -0
- package/dist/tools/identity-diff.js.map +1 -0
- package/dist/tools/intern-opportunities.d.ts.map +1 -1
- package/dist/tools/intern-opportunities.js +4 -3
- package/dist/tools/intern-opportunities.js.map +1 -1
- package/dist/tools/ladder-probe.d.ts +65 -0
- package/dist/tools/ladder-probe.d.ts.map +1 -0
- package/dist/tools/ladder-probe.js +445 -0
- package/dist/tools/ladder-probe.js.map +1 -0
- package/dist/tools/ladder.d.ts.map +1 -1
- package/dist/tools/ladder.js +5 -5
- package/dist/tools/ladder.js.map +1 -1
- package/dist/tools/leak-report.d.ts +23 -0
- package/dist/tools/leak-report.d.ts.map +1 -1
- package/dist/tools/leak-report.js +153 -11
- package/dist/tools/leak-report.js.map +1 -1
- package/dist/tools/load-snapshot.d.ts +11 -0
- package/dist/tools/load-snapshot.d.ts.map +1 -1
- package/dist/tools/load-snapshot.js +83 -10
- package/dist/tools/load-snapshot.js.map +1 -1
- package/dist/tools/metric.d.ts +13 -0
- package/dist/tools/metric.d.ts.map +1 -0
- package/dist/tools/metric.js +185 -0
- package/dist/tools/metric.js.map +1 -0
- package/dist/tools/module-attribution.d.ts +36 -0
- package/dist/tools/module-attribution.d.ts.map +1 -0
- package/dist/tools/module-attribution.js +235 -0
- package/dist/tools/module-attribution.js.map +1 -0
- package/dist/tools/next-measurement.d.ts +26 -0
- package/dist/tools/next-measurement.d.ts.map +1 -0
- package/dist/tools/next-measurement.js +149 -0
- package/dist/tools/next-measurement.js.map +1 -0
- package/dist/tools/object-cost-breakdown.d.ts.map +1 -1
- package/dist/tools/object-cost-breakdown.js +64 -0
- package/dist/tools/object-cost-breakdown.js.map +1 -1
- package/dist/tools/population-vs-owners.d.ts +12 -0
- package/dist/tools/population-vs-owners.d.ts.map +1 -0
- package/dist/tools/population-vs-owners.js +183 -0
- package/dist/tools/population-vs-owners.js.map +1 -0
- package/dist/tools/property-names.d.ts +29 -0
- package/dist/tools/property-names.d.ts.map +1 -0
- package/dist/tools/property-names.js +131 -0
- package/dist/tools/property-names.js.map +1 -0
- package/dist/tools/quick-diagnosis.d.ts.map +1 -1
- package/dist/tools/quick-diagnosis.js +7 -1
- package/dist/tools/quick-diagnosis.js.map +1 -1
- package/dist/tools/react-owners.d.ts +12 -0
- package/dist/tools/react-owners.d.ts.map +1 -0
- package/dist/tools/react-owners.js +167 -0
- package/dist/tools/react-owners.js.map +1 -0
- package/dist/tools/react-update-queues.d.ts +12 -0
- package/dist/tools/react-update-queues.d.ts.map +1 -0
- package/dist/tools/react-update-queues.js +173 -0
- package/dist/tools/react-update-queues.js.map +1 -0
- package/dist/tools/replicate.d.ts +49 -0
- package/dist/tools/replicate.d.ts.map +1 -0
- package/dist/tools/replicate.js +241 -0
- package/dist/tools/replicate.js.map +1 -0
- package/dist/tools/retainer-diff.d.ts +12 -0
- package/dist/tools/retainer-diff.d.ts.map +1 -0
- package/dist/tools/retainer-diff.js +168 -0
- package/dist/tools/retainer-diff.js.map +1 -0
- package/dist/tools/retainer-summary.d.ts +17 -0
- package/dist/tools/retainer-summary.d.ts.map +1 -1
- package/dist/tools/retainer-summary.js +85 -22
- 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 +11 -1
- package/dist/tools/retainer-trace.js.map +1 -1
- package/dist/tools/round-audit.d.ts +66 -0
- package/dist/tools/round-audit.d.ts.map +1 -0
- package/dist/tools/round-audit.js +258 -0
- package/dist/tools/round-audit.js.map +1 -0
- package/dist/tools/round-digest.d.ts +12 -0
- package/dist/tools/round-digest.d.ts.map +1 -0
- package/dist/tools/round-digest.js +190 -0
- package/dist/tools/round-digest.js.map +1 -0
- package/dist/tools/script-census.d.ts +12 -0
- package/dist/tools/script-census.d.ts.map +1 -0
- package/dist/tools/script-census.js +182 -0
- package/dist/tools/script-census.js.map +1 -0
- package/dist/tools/sequence-analysis.d.ts +4 -0
- package/dist/tools/sequence-analysis.d.ts.map +1 -1
- package/dist/tools/sequence-analysis.js +53 -9
- package/dist/tools/sequence-analysis.js.map +1 -1
- package/dist/tools/server-status.d.ts.map +1 -1
- package/dist/tools/server-status.js +4 -1
- package/dist/tools/server-status.js.map +1 -1
- package/dist/tools/settle-check.d.ts +12 -0
- package/dist/tools/settle-check.d.ts.map +1 -0
- package/dist/tools/settle-check.js +188 -0
- package/dist/tools/settle-check.js.map +1 -0
- package/dist/tools/sliced-strings.d.ts.map +1 -1
- package/dist/tools/sliced-strings.js +2 -1
- package/dist/tools/sliced-strings.js.map +1 -1
- package/dist/tools/snapshots.d.ts.map +1 -1
- package/dist/tools/snapshots.js +24 -6
- package/dist/tools/snapshots.js.map +1 -1
- package/dist/tools/sparse-elements.d.ts +12 -0
- package/dist/tools/sparse-elements.d.ts.map +1 -0
- package/dist/tools/sparse-elements.js +215 -0
- package/dist/tools/sparse-elements.js.map +1 -0
- package/dist/tools/stale-collections.d.ts +17 -0
- package/dist/tools/stale-collections.d.ts.map +1 -1
- package/dist/tools/stale-collections.js +28 -0
- package/dist/tools/stale-collections.js.map +1 -1
- package/dist/tools/string-patterns.d.ts.map +1 -1
- package/dist/tools/string-patterns.js +2 -1
- package/dist/tools/string-patterns.js.map +1 -1
- package/dist/tools/tools-index.d.ts.map +1 -1
- package/dist/tools/tools-index.js +155 -5
- package/dist/tools/tools-index.js.map +1 -1
- package/dist/tools/trace-all.d.ts +42 -0
- package/dist/tools/trace-all.d.ts.map +1 -0
- package/dist/tools/trace-all.js +263 -0
- package/dist/tools/trace-all.js.map +1 -0
- package/dist/tools/unit-cost.d.ts +12 -0
- package/dist/tools/unit-cost.d.ts.map +1 -0
- package/dist/tools/unit-cost.js +219 -0
- package/dist/tools/unit-cost.js.map +1 -0
- package/dist/tools/verify-fix.d.ts.map +1 -1
- package/dist/tools/verify-fix.js +11 -3
- package/dist/tools/verify-fix.js.map +1 -1
- package/dist/tools/weakref-census.d.ts +15 -0
- package/dist/tools/weakref-census.d.ts.map +1 -0
- package/dist/tools/weakref-census.js +147 -0
- package/dist/tools/weakref-census.js.map +1 -0
- package/dist/tools/what-if.d.ts +12 -0
- package/dist/tools/what-if.d.ts.map +1 -0
- package/dist/tools/what-if.js +285 -0
- package/dist/tools/what-if.js.map +1 -0
- package/dist/utils.d.ts +92 -2
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +172 -5
- package/dist/utils.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,263 @@
|
|
|
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 memlabCore from '@memlab/core';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { getSnapshot, getSnapshotMetadata } from '../heap-state.js';
|
|
13
|
+
import { formatTraceChain, getRetainerTrace, traceToKey, } from './retainer-summary.js';
|
|
14
|
+
import { boundedDominatorRetainedSize, errorResult, formatBytes, formatNumber, markdownTable, toolResult, } from '../utils.js';
|
|
15
|
+
const { NumericSet } = memlabCore;
|
|
16
|
+
// Tracing is a pointer walk per node, so the whole population is affordable at
|
|
17
|
+
// heap scale — but not without a bound, because a pathological graph turns it
|
|
18
|
+
// into the load-time dominator pass all over again.
|
|
19
|
+
const DEFAULT_MAX_TRACE = 200000;
|
|
20
|
+
const MAX_IDS_PER_CLUSTER = 50000;
|
|
21
|
+
/**
|
|
22
|
+
* Render a cluster's chain. When a path was truncated for grouping, its last
|
|
23
|
+
* step still carries the edge leading to the next hop, and that edge IS part
|
|
24
|
+
* of the grouping key — so suppressing it (which is correct for a complete
|
|
25
|
+
* path, whose last node is the target) renders two distinct clusters
|
|
26
|
+
* byte-identically. Show it, with a marker for the hops beyond.
|
|
27
|
+
*/
|
|
28
|
+
export function renderClusterChain(steps) {
|
|
29
|
+
const base = formatTraceChain(steps);
|
|
30
|
+
const last = steps[steps.length - 1];
|
|
31
|
+
return last?.edgeName != null ? `${base} --${last.edgeName}--> ⋯` : base;
|
|
32
|
+
}
|
|
33
|
+
export function selectPopulation(snapshot, sel) {
|
|
34
|
+
const out = [];
|
|
35
|
+
if (sel.nodeIds != null && sel.nodeIds.length > 0) {
|
|
36
|
+
for (const id of sel.nodeIds) {
|
|
37
|
+
const n = snapshot.getNodeById(id);
|
|
38
|
+
if (n != null)
|
|
39
|
+
out.push(n);
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
const want = sel.shape != null ? new Set(sel.shape) : null;
|
|
44
|
+
snapshot.nodes.forEach(node => {
|
|
45
|
+
if (node.id <= 3)
|
|
46
|
+
return;
|
|
47
|
+
if (sel.className != null) {
|
|
48
|
+
if (node.name !== sel.className)
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
else if (want != null) {
|
|
52
|
+
const remaining = new Set(want);
|
|
53
|
+
for (const e of node.references) {
|
|
54
|
+
if (e.type !== 'property')
|
|
55
|
+
continue;
|
|
56
|
+
remaining.delete(String(e.name_or_index));
|
|
57
|
+
if (remaining.size === 0)
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
if (remaining.size > 0)
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
out.push(node);
|
|
64
|
+
});
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
export function clusterByRetainerPath(nodes, frameworkFilter, maxTrace,
|
|
68
|
+
// Hops from the GC root to key on. Paths are root-first, so truncating keeps
|
|
69
|
+
// the owner side and collapses per-instance leaves — the difference between
|
|
70
|
+
// "one owner, 40,000 objects" and 26,000 one-object paths that differ only
|
|
71
|
+
// in which array index they sit at.
|
|
72
|
+
maxDepth) {
|
|
73
|
+
const byKey = new Map();
|
|
74
|
+
let traced = 0;
|
|
75
|
+
let noPath = 0;
|
|
76
|
+
let truncated = false;
|
|
77
|
+
for (const node of nodes) {
|
|
78
|
+
if (traced + noPath >= maxTrace) {
|
|
79
|
+
truncated = true;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
const steps = getRetainerTrace(node);
|
|
83
|
+
if (steps == null) {
|
|
84
|
+
noPath++;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
traced++;
|
|
88
|
+
const keySteps = maxDepth != null && maxDepth > 0 && maxDepth < steps.length
|
|
89
|
+
? steps.slice(0, maxDepth)
|
|
90
|
+
: steps;
|
|
91
|
+
const key = traceToKey(keySteps, frameworkFilter);
|
|
92
|
+
const existing = byKey.get(key);
|
|
93
|
+
if (existing != null) {
|
|
94
|
+
existing.count++;
|
|
95
|
+
existing.selfSize += node.self_size;
|
|
96
|
+
if (existing.exampleIds.length < 3)
|
|
97
|
+
existing.exampleIds.push(node.id);
|
|
98
|
+
if (existing.ids.length < MAX_IDS_PER_CLUSTER)
|
|
99
|
+
existing.ids.push(node.id);
|
|
100
|
+
else
|
|
101
|
+
existing.idsTruncated = true;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
byKey.set(key, {
|
|
105
|
+
key,
|
|
106
|
+
steps: keySteps,
|
|
107
|
+
count: 1,
|
|
108
|
+
selfSize: node.self_size,
|
|
109
|
+
exampleIds: [node.id],
|
|
110
|
+
ids: [node.id],
|
|
111
|
+
idsTruncated: false,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const clusters = [...byKey.values()].sort((a, b) => b.count - a.count);
|
|
116
|
+
return { clusters, traced, noPath, truncated };
|
|
117
|
+
}
|
|
118
|
+
export function registerTraceAll(server) {
|
|
119
|
+
server.tool('memlab_trace_all', 'Retainer-trace an ENTIRE population and cluster the paths server-side, instead of sampling a handful and hoping they are representative.\n\n' +
|
|
120
|
+
'`memlab_retainer_summary` samples ~10 instances and stops early once they agree, which is the right default for a quick read but is structurally unable to find a minority path: a cluster holding 3% of the objects and 60% of the bytes is invisible to it, and that cluster is very often the leak. This traces every member (bounded, and it says so when the bound is hit), groups by structural path signature, and reports each cluster with its share of the population — so the output cost is the number of DISTINCT paths, not the size of the population.\n\n' +
|
|
121
|
+
'Select by class, shape or explicit ids. Use it when a population is large enough that "are they all retained the same way?" is the question, and when a sampled answer has already been used to justify a conclusion.', {
|
|
122
|
+
class_name: z
|
|
123
|
+
.string()
|
|
124
|
+
.optional()
|
|
125
|
+
.describe('Trace every instance of this class (exact name match).'),
|
|
126
|
+
shape: z
|
|
127
|
+
.array(z.string())
|
|
128
|
+
.optional()
|
|
129
|
+
.describe('Trace every object carrying ALL of these properties — the usable selector on a minified heap.'),
|
|
130
|
+
node_ids: z
|
|
131
|
+
.array(z.number())
|
|
132
|
+
.optional()
|
|
133
|
+
.describe('Trace exactly these node ids.'),
|
|
134
|
+
framework_filter: z
|
|
135
|
+
.boolean()
|
|
136
|
+
.optional()
|
|
137
|
+
.default(true)
|
|
138
|
+
.describe('Collapse V8/framework internals (system / Context, PromiseReaction, …) out of the grouping key so structurally identical application paths cluster together (default true).'),
|
|
139
|
+
limit: z
|
|
140
|
+
.number()
|
|
141
|
+
.optional()
|
|
142
|
+
.default(8)
|
|
143
|
+
.describe('Clusters to render in full (default 8).'),
|
|
144
|
+
exact_bytes: z
|
|
145
|
+
.boolean()
|
|
146
|
+
.optional()
|
|
147
|
+
.default(true)
|
|
148
|
+
.describe('Compute dominator-deduped retained bytes for each rendered cluster (default true). Turn off on very large populations if the walk is slow; self size is always reported.'),
|
|
149
|
+
max_depth: z
|
|
150
|
+
.number()
|
|
151
|
+
.optional()
|
|
152
|
+
.describe('Group on the first N hops from the GC ROOT instead of the whole path. Defaults to the whole path, which is exact but fragments when the leaf hop is per-instance — a per-chat property edge or a per-event listener array gives nearly one path per object. Try 5-7 to see owners rather than instances.'),
|
|
153
|
+
max_trace: z
|
|
154
|
+
.number()
|
|
155
|
+
.optional()
|
|
156
|
+
.default(DEFAULT_MAX_TRACE)
|
|
157
|
+
.describe(`Safety bound on how many nodes to trace (default ${formatNumber(DEFAULT_MAX_TRACE)}). Truncation is always reported — a partial sweep is never presented as a complete one.`),
|
|
158
|
+
}, async ({ class_name, shape, node_ids, framework_filter, limit, exact_bytes, max_depth, max_trace, }) => {
|
|
159
|
+
try {
|
|
160
|
+
const snapshot = getSnapshot();
|
|
161
|
+
const selectors = [
|
|
162
|
+
class_name != null,
|
|
163
|
+
shape != null && shape.length > 0,
|
|
164
|
+
node_ids != null && node_ids.length > 0,
|
|
165
|
+
].filter(Boolean).length;
|
|
166
|
+
if (selectors !== 1) {
|
|
167
|
+
return errorResult(new Error('Pass exactly one of class_name / shape / node_ids — combining them would make the reported population ambiguous.'));
|
|
168
|
+
}
|
|
169
|
+
const nodes = selectPopulation(snapshot, {
|
|
170
|
+
nodeIds: node_ids,
|
|
171
|
+
className: class_name,
|
|
172
|
+
shape,
|
|
173
|
+
});
|
|
174
|
+
if (nodes.length === 0) {
|
|
175
|
+
return toolResult('Nothing matched. Check the class name with `memlab_class_histogram` or the shape with `memlab_shape_histogram`.');
|
|
176
|
+
}
|
|
177
|
+
const { clusters, traced, noPath, truncated } = clusterByRetainerPath(nodes, framework_filter, max_trace, max_depth);
|
|
178
|
+
if (clusters.length === 0) {
|
|
179
|
+
return toolResult(`All ${formatNumber(nodes.length)} matched objects have no retainer path — they are unreachable and awaiting collection, not retained. Nothing to trace.`);
|
|
180
|
+
}
|
|
181
|
+
const label = class_name != null
|
|
182
|
+
? `class \`${class_name}\``
|
|
183
|
+
: shape != null
|
|
184
|
+
? `shape \`{${shape.join(', ')}}\``
|
|
185
|
+
: `${formatNumber(nodes.length)} explicit id(s)`;
|
|
186
|
+
const meta = getSnapshotMetadata();
|
|
187
|
+
const rendered = clusters.slice(0, limit);
|
|
188
|
+
const bytesByKey = new Map();
|
|
189
|
+
if (exact_bytes) {
|
|
190
|
+
for (const c of rendered) {
|
|
191
|
+
// Only the rendered clusters get the dominator walk: it is the
|
|
192
|
+
// expensive part, and a cluster nobody sees does not need a number
|
|
193
|
+
// attached to it. Member ids were collected during the single
|
|
194
|
+
// clustering pass, so this does not re-walk the population.
|
|
195
|
+
const bytes = boundedDominatorRetainedSize(new NumericSet(c.ids), snapshot);
|
|
196
|
+
bytesByKey.set(c.key, {
|
|
197
|
+
retained: bytes.retained,
|
|
198
|
+
exact: bytes.exact && !c.idsTruncated,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const totalTraced = traced;
|
|
203
|
+
const rows = rendered.map((c, i) => {
|
|
204
|
+
const bytes = bytesByKey.get(c.key);
|
|
205
|
+
return [
|
|
206
|
+
String(i + 1),
|
|
207
|
+
formatNumber(c.count),
|
|
208
|
+
`${((c.count / totalTraced) * 100).toFixed(1)}%`,
|
|
209
|
+
formatBytes(c.selfSize),
|
|
210
|
+
bytes != null
|
|
211
|
+
? `${formatBytes(bytes.retained)}${bytes.exact ? '' : ' (ub)'}`
|
|
212
|
+
: '—',
|
|
213
|
+
`@${c.exampleIds[0]}`,
|
|
214
|
+
];
|
|
215
|
+
});
|
|
216
|
+
const lines = [
|
|
217
|
+
`## Retainer paths for ${label}`,
|
|
218
|
+
'',
|
|
219
|
+
`**${formatNumber(traced)} of ${formatNumber(nodes.length)} traced** into **${formatNumber(clusters.length)} distinct path${clusters.length === 1 ? '' : 's'}**` +
|
|
220
|
+
`${noPath > 0 ? `; ${formatNumber(noPath)} had no retainer path (unreachable, awaiting collection)` : ''}` +
|
|
221
|
+
`${truncated ? `. ⚠ Stopped at the ${formatNumber(max_trace)}-node bound, so this is a partial sweep — raise max_trace for the full population` : ''}.`,
|
|
222
|
+
'',
|
|
223
|
+
markdownTable(['#', 'Objects', 'Share', 'Self size', 'Retained', 'Example'], rows, new Set([1, 2, 3, 4])),
|
|
224
|
+
];
|
|
225
|
+
// A path count close to the population count means the signature is
|
|
226
|
+
// keying on something per-instance, and every cluster is a single
|
|
227
|
+
// object wearing a path. That reads as "no dominant owner" when the
|
|
228
|
+
// truth may be one owner and a per-instance leaf hop.
|
|
229
|
+
if (max_depth == null && clusters.length > totalTraced * 0.5) {
|
|
230
|
+
lines.push('', `⚠ ${formatNumber(clusters.length)} paths for ${formatNumber(totalTraced)} objects — the full-depth signature is keying on a per-instance hop (a per-chat property edge, a per-event listener array), so almost every object is its own "path". Re-run with \`max_depth: 5\` (or 6-7) to group on the owner side of the chain instead. Shares below are computed over this fragmented grouping and should not be read as "no dominant owner".`);
|
|
231
|
+
}
|
|
232
|
+
if (clusters.length > rendered.length) {
|
|
233
|
+
const rest = clusters.slice(rendered.length);
|
|
234
|
+
const restCount = rest.reduce((a, c) => a + c.count, 0);
|
|
235
|
+
lines.push('', `_${formatNumber(clusters.length - rendered.length)} further path(s) hold ${formatNumber(restCount)} object(s) (${((restCount / totalTraced) * 100).toFixed(1)}%); raise \`limit\` to render them._`);
|
|
236
|
+
}
|
|
237
|
+
// The whole point of a full sweep is that it can see what sampling
|
|
238
|
+
// cannot, so say when it did.
|
|
239
|
+
const minority = clusters.filter(c => c.count / totalTraced < 0.1 && c.count > 0);
|
|
240
|
+
if (clusters.length > 1) {
|
|
241
|
+
const dominant = clusters[0];
|
|
242
|
+
const dominantShare = (dominant.count / totalTraced) * 100;
|
|
243
|
+
lines.push('', dominantShare >= 95
|
|
244
|
+
? `**One path dominates** (${dominantShare.toFixed(1)}%): a single retention mechanism, and a fix at that owner addresses effectively the whole population.`
|
|
245
|
+
: `**Retention is split across ${formatNumber(clusters.length)} paths**, the largest holding ${dominantShare.toFixed(1)}%. A fix aimed at one owner leaves the rest — size it against the cluster, not the class total.`);
|
|
246
|
+
if (minority.length > 0) {
|
|
247
|
+
const minCount = minority.reduce((a, c) => a + c.count, 0);
|
|
248
|
+
lines.push('', `⚠ ${formatNumber(minority.length)} path(s) below 10% share, holding ${formatNumber(minCount)} object(s) in total. A 10-instance sample would very likely have missed every one of them; check whether any is the interesting one before concluding the population is homogeneous.`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
rendered.forEach((c, i) => {
|
|
252
|
+
const bytes = bytesByKey.get(c.key);
|
|
253
|
+
lines.push('', `### Path ${i + 1} — ${formatNumber(c.count)} object(s), ${((c.count / totalTraced) * 100).toFixed(1)}%${bytes != null ? `, ${formatBytes(bytes.retained)} retained` : ''}`, '', renderClusterChain(c.steps), '', `Examples: ${c.exampleIds.map(id => `@${id}`).join(', ')}`);
|
|
254
|
+
});
|
|
255
|
+
lines.push('', `_Paths are grouped by structural signature${framework_filter ? ' with framework internals collapsed' : ''}; per-instance node ids in V8 internal names are normalized out, so two rows are never byte-identical. Retained bytes are dominator-deduped within a cluster and do not add up across clusters when one cluster dominates another._`, '', `Next: \`memlab_what_if\` to size a fix at one owner, \`memlab_identify\` to name the population, \`memlab_retainer_diff\` to see whether these paths are the ones that grew.${meta != null ? '' : ''}`);
|
|
256
|
+
return toolResult(lines.join('\n'));
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
return errorResult(err);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
//# sourceMappingURL=trace-all.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace-all.js","sourceRoot":"","sources":["../../src/tools/trace-all.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
export declare function registerUnitCost(server: McpServer): void;
|
|
12
|
+
//# sourceMappingURL=unit-cost.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unit-cost.d.ts","sourceRoot":"","sources":["../../src/tools/unit-cost.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,yCAAyC,CAAC;AA6FvE,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAmNxD"}
|
|
@@ -0,0 +1,219 @@
|
|
|
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 memlabCore from '@memlab/core';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { getCurrentHandle, getMetadataByHandle, getSnapshot, getSnapshotByHandle, } from '../heap-state.js';
|
|
13
|
+
import { boundedDominatorRetainedSize, errorResult, formatBytes, formatNumber, markdownTable, toolResult, } from '../utils.js';
|
|
14
|
+
const { NumericSet } = memlabCore;
|
|
15
|
+
function matchesShape(node, shape) {
|
|
16
|
+
if (shape.length === 0)
|
|
17
|
+
return false;
|
|
18
|
+
const want = new Set(shape);
|
|
19
|
+
for (const e of node.references) {
|
|
20
|
+
if (e.type !== 'property')
|
|
21
|
+
continue;
|
|
22
|
+
want.delete(String(e.name_or_index));
|
|
23
|
+
if (want.size === 0)
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
function measure(snapshot, opts) {
|
|
29
|
+
const ids = [];
|
|
30
|
+
// Sizing a cap needs the per-entry cost of THAT cache, not of a class that
|
|
31
|
+
// happens to include its entries alongside thousands of unrelated objects.
|
|
32
|
+
// The entries of one container are the population the cap actually bounds.
|
|
33
|
+
if (opts.containerId != null) {
|
|
34
|
+
const container = snapshot.getNodeById(opts.containerId);
|
|
35
|
+
if (container == null)
|
|
36
|
+
return { count: 0, retained: 0, exact: true };
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
for (const edge of container.references) {
|
|
39
|
+
if (edge.type !== 'element' && edge.type !== 'property')
|
|
40
|
+
continue;
|
|
41
|
+
const name = String(edge.name_or_index);
|
|
42
|
+
// Structural properties are not entries; counting them inflates the
|
|
43
|
+
// denominator and understates the per-entry cost.
|
|
44
|
+
if (edge.type === 'property' &&
|
|
45
|
+
(name === 'length' ||
|
|
46
|
+
name === 'size' ||
|
|
47
|
+
name === '__proto__' ||
|
|
48
|
+
name.startsWith('_'))) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const target = edge.toNode;
|
|
52
|
+
if (target.id <= 3 || seen.has(target.id))
|
|
53
|
+
continue;
|
|
54
|
+
seen.add(target.id);
|
|
55
|
+
ids.push(target.id);
|
|
56
|
+
}
|
|
57
|
+
if (ids.length === 0)
|
|
58
|
+
return { count: 0, retained: 0, exact: true };
|
|
59
|
+
const { retained, exact } = boundedDominatorRetainedSize(new NumericSet(ids), snapshot);
|
|
60
|
+
return { count: ids.length, retained, exact };
|
|
61
|
+
}
|
|
62
|
+
snapshot.nodes.forEach(node => {
|
|
63
|
+
if (node.id <= 3)
|
|
64
|
+
return;
|
|
65
|
+
if (opts.className != null) {
|
|
66
|
+
if (node.name !== opts.className)
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
else if (opts.shape != null) {
|
|
70
|
+
if (!matchesShape(node, opts.shape))
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
ids.push(node.id);
|
|
74
|
+
});
|
|
75
|
+
if (ids.length === 0)
|
|
76
|
+
return { count: 0, retained: 0, exact: true };
|
|
77
|
+
const { retained, exact } = boundedDominatorRetainedSize(new NumericSet(ids), snapshot);
|
|
78
|
+
return { count: ids.length, retained, exact };
|
|
79
|
+
}
|
|
80
|
+
export function registerUnitCost(server) {
|
|
81
|
+
server.tool('memlab_unit_cost', 'How much memory does ONE of these actually cost? Reports dominator-deduped retained bytes per instance for a class or an object shape, ' +
|
|
82
|
+
'and — given two snapshots — separates the AVERAGE from the MARGINAL cost.\n\n' +
|
|
83
|
+
'That distinction decides fixes. A population whose first instances drag in shared structure has a high average and a low marginal cost, ' +
|
|
84
|
+
'so capping it saves far less than the average implies; a population with a flat per-instance cost scales linearly and a cap saves exactly ' +
|
|
85
|
+
'what the arithmetic says. Sizing an LRU, an undo-stack cap, an eviction policy or a "is this worth interning?" decision all need the ' +
|
|
86
|
+
'marginal number, and computing it by hand means four calls and a subtraction that is easy to get backwards.\n\n' +
|
|
87
|
+
'With one snapshot you get the average and a projection table. With `baseline_handle` you also get the marginal cost between the two rungs.', {
|
|
88
|
+
class_name: z
|
|
89
|
+
.string()
|
|
90
|
+
.optional()
|
|
91
|
+
.describe('Class/constructor name as reported by memlab_class_histogram (exact match). Use this OR shape.'),
|
|
92
|
+
shape: z
|
|
93
|
+
.array(z.string())
|
|
94
|
+
.optional()
|
|
95
|
+
.describe('Property names an instance must ALL carry, e.g. ["callback","context"]. Use for minified heaps where the class name is meaningless. Use this OR class_name.'),
|
|
96
|
+
handle: z
|
|
97
|
+
.string()
|
|
98
|
+
.optional()
|
|
99
|
+
.describe('Snapshot to measure (defaults to the active one).'),
|
|
100
|
+
baseline_handle: z
|
|
101
|
+
.string()
|
|
102
|
+
.optional()
|
|
103
|
+
.describe('Earlier snapshot. When given, the marginal cost is (retained_now − retained_baseline) / (count_now − count_baseline) — what one MORE instance costs, which is the number a cap or eviction policy is sized against.'),
|
|
104
|
+
container_id: z
|
|
105
|
+
.number()
|
|
106
|
+
.optional()
|
|
107
|
+
.describe('Measure the ENTRIES of this container (a Map, Set, Array or plain-object cache from memlab_cache_analysis) instead of a class or shape. This is the population an LRU cap actually bounds, so it is the right denominator when sizing one — a class-wide average mixes in instances the cache does not hold. Structural properties (length, size, _-prefixed) are excluded from the entry count.'),
|
|
108
|
+
project_caps: z
|
|
109
|
+
.array(z.number())
|
|
110
|
+
.optional()
|
|
111
|
+
.describe('Instance counts to project retained size for, e.g. [100, 500, 2000] when choosing a cache cap. Uses the marginal cost when available, otherwise the average (and says which).'),
|
|
112
|
+
}, async ({ class_name, shape, container_id, handle, baseline_handle, project_caps, }) => {
|
|
113
|
+
try {
|
|
114
|
+
if (container_id == null &&
|
|
115
|
+
class_name == null &&
|
|
116
|
+
(shape == null || shape.length === 0)) {
|
|
117
|
+
return errorResult(new Error('Pass class_name, shape or container_id. On a minified heap prefer shape — class names like "t"/"e" match thousands of unrelated objects — and prefer container_id when sizing a cap for one specific cache.'));
|
|
118
|
+
}
|
|
119
|
+
if (class_name != null && shape != null && shape.length > 0) {
|
|
120
|
+
return errorResult(new Error('Pass class_name OR shape, not both.'));
|
|
121
|
+
}
|
|
122
|
+
const target = handle != null ? getSnapshotByHandle(handle) : getSnapshot();
|
|
123
|
+
if (target == null) {
|
|
124
|
+
return errorResult(new Error(`Snapshot "${handle}" is not resident.`));
|
|
125
|
+
}
|
|
126
|
+
const targetLabel = handle ?? getCurrentHandle() ?? '(active)';
|
|
127
|
+
if (container_id != null && baseline_handle != null) {
|
|
128
|
+
return errorResult(new Error('container_id cannot be combined with baseline_handle: node ids are per-capture, so the same id in the baseline is a different object (or nothing) and the marginal cost would be computed from two unrelated populations. Select the container in each snapshot separately, or use a shape.'));
|
|
129
|
+
}
|
|
130
|
+
const now = measure(target, {
|
|
131
|
+
className: class_name,
|
|
132
|
+
shape,
|
|
133
|
+
containerId: container_id,
|
|
134
|
+
});
|
|
135
|
+
if (now.count === 0) {
|
|
136
|
+
return toolResult(`No instances found for ${container_id != null ? `the entries of @${container_id}` : class_name != null ? `class \`${class_name}\`` : `shape {${(shape ?? []).join(', ')}}`} in \`${targetLabel}\`. ` +
|
|
137
|
+
'For a class, check the exact name with memlab_class_histogram; for a shape, with memlab_shape_histogram.');
|
|
138
|
+
}
|
|
139
|
+
const average = now.retained / now.count;
|
|
140
|
+
let marginal = null;
|
|
141
|
+
let base = null;
|
|
142
|
+
if (baseline_handle != null) {
|
|
143
|
+
const baseSnapshot = getSnapshotByHandle(baseline_handle);
|
|
144
|
+
if (baseSnapshot == null) {
|
|
145
|
+
return errorResult(new Error(`Baseline snapshot "${baseline_handle}" is not resident.`));
|
|
146
|
+
}
|
|
147
|
+
base = measure(baseSnapshot, {
|
|
148
|
+
className: class_name,
|
|
149
|
+
shape,
|
|
150
|
+
// Node ids are per-capture, so a container selected by id in one
|
|
151
|
+
// snapshot is a different object (or nothing) in another. Refusing
|
|
152
|
+
// is the only honest option; silently measuring whatever holds
|
|
153
|
+
// that id in the baseline would produce a marginal cost computed
|
|
154
|
+
// from two unrelated populations.
|
|
155
|
+
containerId: undefined,
|
|
156
|
+
});
|
|
157
|
+
const dCount = now.count - base.count;
|
|
158
|
+
const dBytes = now.retained - base.retained;
|
|
159
|
+
if (dCount > 0)
|
|
160
|
+
marginal = dBytes / dCount;
|
|
161
|
+
}
|
|
162
|
+
const label = container_id != null
|
|
163
|
+
? `entries of @${container_id}`
|
|
164
|
+
: class_name != null
|
|
165
|
+
? `class \`${class_name}\``
|
|
166
|
+
: `shape \`{${(shape ?? []).join(', ')}}\``;
|
|
167
|
+
const lines = [
|
|
168
|
+
`## Unit cost — ${label}`,
|
|
169
|
+
'',
|
|
170
|
+
`\`${targetLabel}\`${getMetadataByHandle(targetLabel) ? ` (${getMetadataByHandle(targetLabel)?.fileName})` : ''}: ` +
|
|
171
|
+
`**${formatNumber(now.count)} instances retaining ${formatBytes(now.retained)}**${now.exact ? '' : ' (upper bound — the dominator walk hit its depth cap)'}.`,
|
|
172
|
+
'',
|
|
173
|
+
`- **Average**: ${formatBytes(Math.round(average))} per instance.`,
|
|
174
|
+
];
|
|
175
|
+
if (base != null && marginal != null) {
|
|
176
|
+
lines.push(`- **Marginal**: ${formatBytes(Math.round(marginal))} per instance — measured across ${formatNumber(now.count - base.count)} added instances between \`${baseline_handle}\` (${formatNumber(base.count)} / ${formatBytes(base.retained)}) and \`${targetLabel}\`.`);
|
|
177
|
+
const ratio = average > 0 ? marginal / average : 1;
|
|
178
|
+
if (ratio < 0.6) {
|
|
179
|
+
lines.push('', `⚠ Marginal is ${(ratio * 100).toFixed(0)}% of average: much of what this population retains is **shared structure the first instances pulled in**, not per-instance data. Size any cap or eviction policy on the marginal figure — the average overstates what capping saves.`);
|
|
180
|
+
}
|
|
181
|
+
else if (ratio > 1.4) {
|
|
182
|
+
lines.push('', `⚠ Marginal is ${(ratio * 100).toFixed(0)}% of average: later instances cost MORE than earlier ones (they are retaining progressively more, or a shared structure is growing with them). Check whether the cost per instance is itself unbounded before sizing a cap.`);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
lines.push('', '_Marginal ≈ average: the population scales linearly, so a cap saves what the arithmetic says._');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else if (baseline_handle != null) {
|
|
189
|
+
lines.push('', '_No marginal cost: the population did not grow between the two snapshots, so there is no delta to divide._');
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
lines.push('', '_Average only. Pass `baseline_handle` (an earlier rung) for the marginal cost — the two differ whenever the first instances drag in shared structure, and it is the marginal one that sizes a fix._');
|
|
193
|
+
}
|
|
194
|
+
if (project_caps != null && project_caps.length > 0) {
|
|
195
|
+
const unit = marginal ?? average;
|
|
196
|
+
const usingMarginal = marginal != null;
|
|
197
|
+
lines.push('', `### Projected retained size at a cap (using the ${usingMarginal ? 'marginal' : 'average'} cost)`, '');
|
|
198
|
+
const rows = [...project_caps]
|
|
199
|
+
.sort((a, b) => b - a)
|
|
200
|
+
.map(cap => {
|
|
201
|
+
const projected = Math.round(unit * Math.min(cap, now.count));
|
|
202
|
+
const saved = Math.round(Math.max(0, now.retained - unit * cap));
|
|
203
|
+
return [
|
|
204
|
+
formatNumber(cap),
|
|
205
|
+
formatBytes(projected),
|
|
206
|
+
cap >= now.count ? '—' : formatBytes(saved),
|
|
207
|
+
];
|
|
208
|
+
});
|
|
209
|
+
lines.push(markdownTable(['Cap (instances)', 'Projected retained', 'Saved vs today'], rows, new Set([0, 1, 2])));
|
|
210
|
+
lines.push('', '_A projection, not a measurement: it assumes the per-instance cost holds at the cap and that evicted values are not co-retained elsewhere. Confirm the top candidate with `memlab_what_if` or a capped re-run._');
|
|
211
|
+
}
|
|
212
|
+
return toolResult(lines.join('\n'));
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
return errorResult(err);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=unit-cost.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unit-cost.js","sourceRoot":"","sources":["../../src/tools/unit-cost.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify-fix.d.ts","sourceRoot":"","sources":["../../src/tools/verify-fix.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAY,aAAa,EAAC,MAAM,cAAc,CAAC;AAC3D,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"verify-fix.d.ts","sourceRoot":"","sources":["../../src/tools/verify-fix.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAY,aAAa,EAAC,MAAM,cAAc,CAAC;AAC3D,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,yCAAyC,CAAC;AAiBvE,KAAK,UAAU,GAAG,mBAAmB,GAAG,aAAa,GAAG,eAAe,CAAC;AAOxE;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,aAAa,EACvB,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAChB,MAAM,CAuCR;AA4CD,2DAA2D;AAC3D,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAIrE;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAoKzD"}
|
package/dist/tools/verify-fix.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import fs from 'fs';
|
|
11
11
|
import memlabCore from '@memlab/core';
|
|
12
12
|
import { z } from 'zod';
|
|
13
|
-
import { errorResult, formatNumber, markdownTable, toolResult, } from '../utils.js';
|
|
13
|
+
import { errorResult, formatNumber, markdownTable, pathsHeader, toolResult, } from '../utils.js';
|
|
14
14
|
import { countEntries, parseLocator, resolvePath } from './collection-trend.js';
|
|
15
15
|
import { resolveLadderPaths } from './ladder.js';
|
|
16
16
|
import { resolveMaxFileSizeMB, resolveSnapshotPath } from './load-snapshot.js';
|
|
@@ -110,6 +110,7 @@ export function perCycleRate(values, cycles) {
|
|
|
110
110
|
}
|
|
111
111
|
export function registerVerifyFix(server) {
|
|
112
112
|
server.tool('memlab_verify_fix', 'Decide whether a fix actually worked, by comparing the per-cycle growth RATE of one metric between a before ladder and an after ladder. hunt_runner --ab drives both arms but nothing analyses them, which is why fix write-ups stall at "A/B pending". ' +
|
|
113
|
+
'NO RUNTIME GATE IS REQUIRED: the two arms are two sets of snapshot files passed as `before_paths` and `after_paths`, so a fix that cannot be put behind a flag — a build-only change, a local patch, a reverted commit — is verified exactly the same way. One session concluded this tool was unusable for an ungated fix and skipped A/B entirely; it is not. ' +
|
|
113
114
|
'Metrics: "collection_length" (entries under "<OwnerClass>.<property>"), "class_count" (instances of a class), "pending_chain" (total length of the linked lists hanging off "<OwnerClass>.<property>", following `next` — the React update-queue shape). ' +
|
|
114
115
|
'Compares RATES, not levels: a build difference, a warmer cache or a longer session all shift the absolute level, and only the slope per interaction says whether the thing still accumulates. Each arm therefore needs at least 2 rungs — a single snapshot per arm is refused rather than compared, because that comparison is confounded. ' +
|
|
115
116
|
'Returns both series, both rates, the reduction, and a PASS/FAIL against expected_reduction. Loads rungs transiently in LIGHT mode, one graph at a time.', {
|
|
@@ -146,6 +147,13 @@ export function registerVerifyFix(server) {
|
|
|
146
147
|
try {
|
|
147
148
|
const before = resolveLadderPaths(before_paths).paths;
|
|
148
149
|
const after = resolveLadderPaths(after_paths).paths;
|
|
150
|
+
// Name BOTH arms: this tool never reads the resident snapshot, and a
|
|
151
|
+
// fix-validation result labelled with an unrelated capture is the
|
|
152
|
+
// worst place to leave a reader guessing which build was measured.
|
|
153
|
+
const armsHeader = pathsHeader([
|
|
154
|
+
...before.map(p => `before:${p.replace(/^.*\//, '')}`),
|
|
155
|
+
...after.map(p => `after:${p.replace(/^.*\//, '')}`),
|
|
156
|
+
]);
|
|
149
157
|
for (const [label, arm] of [
|
|
150
158
|
['before_paths', before],
|
|
151
159
|
['after_paths', after],
|
|
@@ -193,7 +201,7 @@ export function registerVerifyFix(server) {
|
|
|
193
201
|
];
|
|
194
202
|
if (beforeRate <= 0) {
|
|
195
203
|
lines.push(`⚠ **Inconclusive — the BEFORE arm does not grow** (rate ${fmtRate(beforeRate)}/cycle). There is nothing for the fix to reduce, so this run cannot validate it. The unfixed arm has to reproduce the leak first: check the metric and locator, drive more cycles, or confirm the arm really is running the unfixed build.`);
|
|
196
|
-
return toolResult(lines.join('\n'));
|
|
204
|
+
return toolResult(lines.join('\n'), armsHeader);
|
|
197
205
|
}
|
|
198
206
|
const reduction = (beforeRate - afterRate) / beforeRate;
|
|
199
207
|
const pass = reduction >= expected_reduction;
|
|
@@ -201,7 +209,7 @@ export function registerVerifyFix(server) {
|
|
|
201
209
|
? `✅ **PASS** — the fixed arm accumulates ${(reduction * 100).toFixed(1)}% less per cycle.${afterRate > 0 ? ` It is not zero (${fmtRate(afterRate)}/cycle remains), so state the residual rather than claiming the leak is gone.` : ' The residual rate is zero.'}`
|
|
202
210
|
: `❌ **FAIL** — ${(reduction * 100).toFixed(1)}% is short of the expected ${(expected_reduction * 100).toFixed(0)}%. The fixed arm still accumulates ${fmtRate(afterRate)} per cycle.`);
|
|
203
211
|
lines.push('', `_Both arms must have been driven with the same interaction and the same ${formatNumber(cycles)} cycles between rungs; nothing in a .heapsnapshot records that, so it is the caller's to guarantee — record it with \`memlab_ladder\`. Two rungs per arm give a two-point slope: add rungs if the before-arm series is not monotonic, since a single noisy pair can produce either verdict._`);
|
|
204
|
-
return toolResult(lines.join('\n'));
|
|
212
|
+
return toolResult(lines.join('\n'), armsHeader);
|
|
205
213
|
}
|
|
206
214
|
catch (err) {
|
|
207
215
|
return errorResult(err);
|