@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,221 @@
|
|
|
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 fs from 'fs';
|
|
11
|
+
import vm from 'vm';
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import memlabHeapAnalysis from '@memlab/heap-analysis';
|
|
14
|
+
const { getFullHeapFromFile } = memlabHeapAnalysis;
|
|
15
|
+
import { resolveSnapshotPath } from './load-snapshot.js';
|
|
16
|
+
import { resolveLadderPaths } from './ladder.js';
|
|
17
|
+
import { formatBytes, formatNumber, markdownTable, errorResult, toolResult, } from '../utils.js';
|
|
18
|
+
// Property names used to fingerprint a shape, matching the other shape tools.
|
|
19
|
+
const SHAPE_PROP_CAP = 12;
|
|
20
|
+
// Shapes tracked per rung. Once this many distinct shapes are seen, tracking
|
|
21
|
+
// STOPS and the tail is uncounted (not folded into an "(other)" bucket) — the
|
|
22
|
+
// report says so explicitly. A heap with a million distinct shapes must not turn
|
|
23
|
+
// this into a memory problem of its own.
|
|
24
|
+
const MAX_SHAPES = 2000;
|
|
25
|
+
function shapeKey(node) {
|
|
26
|
+
if (node.edge_count > 1024)
|
|
27
|
+
return `${node.name} (too wide)`;
|
|
28
|
+
const props = [];
|
|
29
|
+
node.forEachReference(edge => {
|
|
30
|
+
if (edge.type === 'property') {
|
|
31
|
+
props.push(String(edge.name_or_index));
|
|
32
|
+
if (props.length >= SHAPE_PROP_CAP)
|
|
33
|
+
return { stop: true };
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
props.sort();
|
|
37
|
+
return `${node.name} {${props.join(',')}}`;
|
|
38
|
+
}
|
|
39
|
+
function trendOf(counts) {
|
|
40
|
+
let up = true;
|
|
41
|
+
for (let i = 1; i < counts.length; i++) {
|
|
42
|
+
if (counts[i] <= counts[i - 1])
|
|
43
|
+
up = false;
|
|
44
|
+
}
|
|
45
|
+
const net = counts[counts.length - 1] - counts[0];
|
|
46
|
+
if (up && net > 0)
|
|
47
|
+
return '↑ every step';
|
|
48
|
+
if (net > 0)
|
|
49
|
+
return 'grew net';
|
|
50
|
+
if (net < 0)
|
|
51
|
+
return 'shrank';
|
|
52
|
+
return 'flat';
|
|
53
|
+
}
|
|
54
|
+
export function registerHypothesis(server) {
|
|
55
|
+
server.tool('memlab_hypothesis', 'Test one hypothesis against EVERY rung of a snapshot ladder in a single call: supply a JavaScript predicate over heap nodes and get its match count, total self size, and trend per rung. ' +
|
|
56
|
+
'This is the "is my theory true across the whole ladder?" tool. Without it, confirming a specific theory — "the growth is Maps with a `_pending` field", "it is closures capturing `chatId`" — meant loading each rung separately and re-running a query by hand, at minutes per rung, which is why theories tended to be checked against one snapshot and generalized. ' +
|
|
57
|
+
'Set group_by_shape to break matches down by property shape per rung instead of a single count, which answers "WHICH variant of this class is the one accumulating?" — the shape sweep that otherwise required a separate pass. Snapshots are loaded one at a time and released, so a long ladder is memory-safe.', {
|
|
58
|
+
paths: z
|
|
59
|
+
.array(z.string())
|
|
60
|
+
.min(1)
|
|
61
|
+
.describe('Ordered snapshot paths (oldest first). Local paths, manifold:// URLs, bare filenames, or a single ["ladder:<name>"] reference to a ladder saved with memlab_ladder.'),
|
|
62
|
+
predicate: z
|
|
63
|
+
.string()
|
|
64
|
+
.describe('JavaScript expression over `node` returning a boolean, e.g. `node.name === "Map" && node.retainedSize > 10000`, or `node.type === "closure" && node.name.includes("chat")`. Evaluated in a sandbox with no I/O; only the node is exposed. Compiled once and applied per node, so keep it cheap — avoid walking `node.references` for every node in a multi-million-node heap.'),
|
|
65
|
+
group_by_shape: z
|
|
66
|
+
.boolean()
|
|
67
|
+
.optional()
|
|
68
|
+
.default(false)
|
|
69
|
+
.describe('Break matches down by property-shape fingerprint per rung (default false: one count per rung). Use to find which variant of a class is accumulating.'),
|
|
70
|
+
limit: z
|
|
71
|
+
.number()
|
|
72
|
+
.optional()
|
|
73
|
+
.default(15)
|
|
74
|
+
.describe('Maximum shape rows to report when group_by_shape is set.'),
|
|
75
|
+
max_file_size_mb: z
|
|
76
|
+
.number()
|
|
77
|
+
.optional()
|
|
78
|
+
.describe('Per-file size ceiling in MB; see memlab_load_snapshot.'),
|
|
79
|
+
}, async ({ paths, predicate, group_by_shape, limit, max_file_size_mb }) => {
|
|
80
|
+
try {
|
|
81
|
+
let test;
|
|
82
|
+
try {
|
|
83
|
+
// Compiled ONCE, in a context with nothing in it: the predicate gets
|
|
84
|
+
// the node it is called with and no ambient capability.
|
|
85
|
+
test = vm.runInNewContext(`(function(node){ return !!(${predicate}); })`, Object.create(null), { timeout: 1000 });
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
return errorResult(`Could not compile predicate: ${e instanceof Error ? e.message : String(e)}. It must be a JavaScript EXPRESSION over \`node\` (no statements, no return).`);
|
|
89
|
+
}
|
|
90
|
+
const rungs = [];
|
|
91
|
+
const { paths: rungPaths } = resolveLadderPaths(paths);
|
|
92
|
+
for (const p of rungPaths) {
|
|
93
|
+
const { localPath, fetchedFrom } = resolveSnapshotPath(p);
|
|
94
|
+
if (!fs.existsSync(localPath)) {
|
|
95
|
+
return errorResult(`File not found: ${localPath}`);
|
|
96
|
+
}
|
|
97
|
+
const sizeMB = fs.statSync(localPath).size / (1024 * 1024);
|
|
98
|
+
if (max_file_size_mb != null && sizeMB > max_file_size_mb) {
|
|
99
|
+
return errorResult(`${p} is ${sizeMB.toFixed(0)} MB, over the ${max_file_size_mb} MB limit.`);
|
|
100
|
+
}
|
|
101
|
+
const snapshot = await getFullHeapFromFile(localPath);
|
|
102
|
+
let matched = 0;
|
|
103
|
+
let selfBytes = 0;
|
|
104
|
+
let scanned = 0;
|
|
105
|
+
let predicateError = null;
|
|
106
|
+
// memlab's nodes.forEach has no early exit, so a predicate that throws
|
|
107
|
+
// on the first node would otherwise still walk every remaining node in
|
|
108
|
+
// a multi-million-node heap before the error is reported. Throwing a
|
|
109
|
+
// sentinel out of the callback actually stops the walk.
|
|
110
|
+
class AbortWalk extends Error {
|
|
111
|
+
}
|
|
112
|
+
const shapes = new Map();
|
|
113
|
+
try {
|
|
114
|
+
snapshot.nodes.forEach(node => {
|
|
115
|
+
scanned++;
|
|
116
|
+
let hit = false;
|
|
117
|
+
try {
|
|
118
|
+
hit = test(node);
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
// Report the predicate's own failure rather than a silent zero:
|
|
122
|
+
// a predicate that throws on the first node would otherwise look
|
|
123
|
+
// exactly like a hypothesis that is simply false.
|
|
124
|
+
predicateError = e instanceof Error ? e.message : String(e);
|
|
125
|
+
throw new AbortWalk();
|
|
126
|
+
}
|
|
127
|
+
if (!hit)
|
|
128
|
+
return;
|
|
129
|
+
matched++;
|
|
130
|
+
selfBytes += node.self_size;
|
|
131
|
+
if (group_by_shape && shapes.size < MAX_SHAPES) {
|
|
132
|
+
const k = shapeKey(node);
|
|
133
|
+
shapes.set(k, (shapes.get(k) ?? 0) + 1);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
if (!(e instanceof AbortWalk))
|
|
139
|
+
throw e;
|
|
140
|
+
}
|
|
141
|
+
if (predicateError != null) {
|
|
142
|
+
return errorResult(`Predicate threw while scanning ${p}: ${predicateError}. Guard against missing fields — it is applied to every node in the heap, not just the ones you have in mind.`);
|
|
143
|
+
}
|
|
144
|
+
rungs.push({
|
|
145
|
+
label: fetchedFrom ?? p.replace(/^.*\//, ''),
|
|
146
|
+
matched,
|
|
147
|
+
selfBytes,
|
|
148
|
+
scanned,
|
|
149
|
+
shapes,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const counts = rungs.map(r => r.matched);
|
|
153
|
+
const net = counts[counts.length - 1] - counts[0];
|
|
154
|
+
const lines = [
|
|
155
|
+
`## Hypothesis across ${rungs.length} snapshot(s)`,
|
|
156
|
+
'',
|
|
157
|
+
`\`${predicate}\``,
|
|
158
|
+
'',
|
|
159
|
+
markdownTable(['#', 'Snapshot', 'Matches', 'Self size', 'Scanned'], rungs.map((r, i) => [
|
|
160
|
+
String(i + 1),
|
|
161
|
+
r.label.length > 38 ? '…' + r.label.slice(-37) : r.label,
|
|
162
|
+
formatNumber(r.matched),
|
|
163
|
+
formatBytes(r.selfBytes),
|
|
164
|
+
formatNumber(r.scanned),
|
|
165
|
+
]), new Set([2, 3, 4])),
|
|
166
|
+
'',
|
|
167
|
+
];
|
|
168
|
+
if (rungs.length > 1) {
|
|
169
|
+
lines.push(`**Verdict: ${trendOf(counts)}** — ${net >= 0 ? '+' : '−'}${formatNumber(Math.abs(net))} matches across the ladder (${formatNumber(counts[0])} → ${formatNumber(counts[counts.length - 1])}).`);
|
|
170
|
+
if (rungs.length === 2) {
|
|
171
|
+
lines.push('', '> ⚠️ With 2 rungs "↑ every step" is the same statement as "grew overall" and cannot separate a trend from a single GC-band sample. Add a third rung.');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
lines.push('_Single snapshot: this is a count, not a trend. Pass an ordered ladder to test whether the hypothesis holds over time._');
|
|
176
|
+
}
|
|
177
|
+
if (counts.every(c => c === 0)) {
|
|
178
|
+
lines.push('', '_Zero matches in every rung. The predicate compiled and ran against every node, so this is a real negative — the hypothesis is false as written, or the property/class is named differently than assumed (check with `memlab_class_histogram`)._');
|
|
179
|
+
}
|
|
180
|
+
if (group_by_shape) {
|
|
181
|
+
const allShapes = new Set();
|
|
182
|
+
for (const r of rungs)
|
|
183
|
+
for (const k of r.shapes.keys())
|
|
184
|
+
allShapes.add(k);
|
|
185
|
+
const shapeRows = [...allShapes]
|
|
186
|
+
.map(k => ({
|
|
187
|
+
shape: k,
|
|
188
|
+
per: rungs.map(r => r.shapes.get(k) ?? 0),
|
|
189
|
+
}))
|
|
190
|
+
.map(x => ({
|
|
191
|
+
...x,
|
|
192
|
+
net: x.per[x.per.length - 1] - x.per[0],
|
|
193
|
+
}))
|
|
194
|
+
.sort((a, b) => b.net - a.net ||
|
|
195
|
+
b.per[b.per.length - 1] - a.per[a.per.length - 1])
|
|
196
|
+
.slice(0, limit);
|
|
197
|
+
if (shapeRows.length > 0) {
|
|
198
|
+
lines.push('', '### Matches by shape', '', markdownTable([
|
|
199
|
+
'Shape',
|
|
200
|
+
...rungs.map((_, i) => `#${i + 1}`),
|
|
201
|
+
'Δ',
|
|
202
|
+
...(rungs.length > 1 ? ['Trend'] : []),
|
|
203
|
+
], shapeRows.map(r => [
|
|
204
|
+
r.shape.length > 56 ? r.shape.slice(0, 53) + '…}' : r.shape,
|
|
205
|
+
...r.per.map(c => formatNumber(c)),
|
|
206
|
+
`${r.net >= 0 ? '+' : '−'}${formatNumber(Math.abs(r.net))}`,
|
|
207
|
+
...(rungs.length > 1 ? [trendOf(r.per)] : []),
|
|
208
|
+
]), new Set(rungs.map((_, i) => i + 1).concat([rungs.length + 1]))));
|
|
209
|
+
if (rungs.some(r => r.shapes.size >= MAX_SHAPES)) {
|
|
210
|
+
lines.push('', `_⚠ Shape tracking capped at ${formatNumber(MAX_SHAPES)} distinct shapes per rung; the tail is not counted. Narrow the predicate._`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return toolResult(lines.join('\n'));
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
return errorResult(err);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=hypothesis.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hypothesis.js","sourceRoot":"","sources":["../../src/tools/hypothesis.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"intern-opportunities.d.ts","sourceRoot":"","sources":["../../src/tools/intern-opportunities.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"intern-opportunities.d.ts","sourceRoot":"","sources":["../../src/tools/intern-opportunities.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,yCAAyC,CAAC;AAmPvE,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAq8BnE"}
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
import { z } from 'zod';
|
|
11
11
|
import { getSnapshot, getSnapshotMetadata, getSessionConfig, } from '../heap-state.js';
|
|
12
12
|
import { formatBytes, formatNumber, markdownTable, errorResult, toolResult, } from '../utils.js';
|
|
13
|
+
import { collectDevRoots, computeReachableWithoutDevRoots, } from './dev-artifacts.js';
|
|
14
|
+
import { buildStringIndex } from '../string-index.js';
|
|
15
|
+
import { classifyNonProductionString, isHarnessToolManifestShape, } from '../artifact-classes.js';
|
|
13
16
|
// V8 splits a single logical object's storage across several internal backing
|
|
14
17
|
// structures: a `system / PropertyArray` (named props once the object grows past
|
|
15
18
|
// the inline slot count), `(object properties)` / `(object elements)` arrays, and
|
|
@@ -150,7 +153,7 @@ function isFrameworkOwned(propertyName, parentShape, parentShapeProps) {
|
|
|
150
153
|
export function registerInternOpportunities(server) {
|
|
151
154
|
server.tool('memlab_intern_opportunities', 'Identify string interning opportunities by grouping duplicated strings by the property name and parent object shape that holds them. Shows total savings per (property × shape) combination — the key metric for deciding where to add a string interning pool. Also surfaces ARRAY-ELEMENT / columnar duplication (strings held as elements of a rowsAsArray / string[][] result buffer — a common Nest mysql2/Drizzle shape) as first-class groups keyed by column index and array-owner shape, folded into the within-load headline; these are marked with a filled square and fixed by interning at the array-construction/parse site. The within-load figure is split at the canonical 128-char intern cap into cappable (<=128 chars, what the recommended fix reclaims) vs over-cap (longer strings the cap skips), so the headline matches what a compliant fix actually reclaims. Replaces the manual workflow of: duplicated_strings → retainer_summary → codebase grep. ' +
|
|
152
155
|
'Retention-aware: flags groups whose duplicated instances are ALSO held by another structure (e.g. a raw array/matrix) as "co-retained" — interning the property there reclaims ~0, so the savings are reported separately and you must dedupe the shared source instead. ' +
|
|
153
|
-
'The headline "within-load capturable" figure also excludes framework/infra-owned strings (HTTP headers, cookies, auth tokens, Next.js URL/cache context) and low-ROI groups (high-cardinality + long strings, where the intern pool costs more than it saves), reporting each in its own bucket; and it flags concatenated-string (rope) buildup, which is accumulation rather than duplication and cannot be interned. ' +
|
|
156
|
+
'The headline "within-load capturable" figure also excludes framework/infra-owned strings (HTTP headers, cookies, auth tokens, Next.js URL/cache context), measurement-harness content (the CDP/devtools bridge injected to drive the session — an injected bundle can be the single largest "opportunity" in a driven snapshot while reclaiming nothing in production), and low-ROI groups (high-cardinality + long strings, where the intern pool costs more than it saves), reporting each in its own bucket; and it flags concatenated-string (rope) buildup, which is accumulation rather than duplication and cannot be interned. ' +
|
|
154
157
|
'⚠ Full-heap scan (builds string-duplication groups) — slow and memory-heavy on very large heaps (millions of nodes); raise min_copies / min_savings to bound it.', {
|
|
155
158
|
limit: z
|
|
156
159
|
.number()
|
|
@@ -177,45 +180,35 @@ export function registerInternOpportunities(server) {
|
|
|
177
180
|
const snapshot = getSnapshot();
|
|
178
181
|
const meta = getSnapshotMetadata();
|
|
179
182
|
const totalSize = meta?.totalSize ?? 0;
|
|
180
|
-
// Step 1:
|
|
181
|
-
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
entry.count++;
|
|
206
|
-
entry.totalSize += node.retainedSize;
|
|
207
|
-
if (entry.exampleIds.length < 20) {
|
|
208
|
-
entry.exampleIds.push(node.id);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
stringMap.set(value, {
|
|
213
|
-
count: 1,
|
|
214
|
-
totalSize: node.retainedSize,
|
|
215
|
-
exampleIds: [node.id],
|
|
216
|
-
});
|
|
217
|
-
}
|
|
183
|
+
// Step 1: per-value string duplication index.
|
|
184
|
+
//
|
|
185
|
+
// Shared with memlab_duplicated_strings and cached per snapshot, so the
|
|
186
|
+
// two tools (routinely run back to back) walk the string nodes once
|
|
187
|
+
// between them rather than once each. The dev-root reachability pass is
|
|
188
|
+
// requested here because only this tool needs it: strings retained ONLY
|
|
189
|
+
// through a dev/automation root — overwhelmingly the attached
|
|
190
|
+
// inspector's console holding dev-build log records — cannot be
|
|
191
|
+
// reclaimed by an intern pool in production, and counting them inflates
|
|
192
|
+
// the headline saving.
|
|
193
|
+
//
|
|
194
|
+
// `(concatenated string)` (cons/rope) nodes are counted by the same
|
|
195
|
+
// index: a heap dominated by them is string ACCUMULATION (repeated `+=`
|
|
196
|
+
// / join into a long-lived buffer), NOT value duplication, and
|
|
197
|
+
// interning cannot help. Flagging the pattern beats reporting a tiny
|
|
198
|
+
// interning win against it. (Feedback round 7 §4.)
|
|
199
|
+
const devRoots = collectDevRoots(snapshot);
|
|
200
|
+
const productionReachable = devRoots.byId.size > 0
|
|
201
|
+
? computeReachableWithoutDevRoots(snapshot, devRoots)
|
|
202
|
+
: null;
|
|
203
|
+
const isDevOnlyNode = (node) => productionReachable != null &&
|
|
204
|
+
productionReachable[node.nodeIndex] === 0;
|
|
205
|
+
const stringIndex = buildStringIndex(snapshot, {
|
|
206
|
+
withDevOnly: true,
|
|
207
|
+
isDevOnlyNode,
|
|
218
208
|
});
|
|
209
|
+
const stringMap = stringIndex.byValue;
|
|
210
|
+
const concatStringCount = stringIndex.concatStringCount;
|
|
211
|
+
const concatStringSize = stringIndex.concatStringSize;
|
|
219
212
|
// Step 2: For duplicated strings, sample referrers to get property × shape
|
|
220
213
|
const groupMap = new Map();
|
|
221
214
|
for (const [value, stats] of stringMap) {
|
|
@@ -383,6 +376,8 @@ export function registerInternOpportunities(server) {
|
|
|
383
376
|
const scaleFactor = stats.count / samplesProcessed;
|
|
384
377
|
const trueCount = Math.round(dist.sampledCount * scaleFactor);
|
|
385
378
|
const trueSize = stats.totalSize * (dist.sampledCount / samplesProcessed);
|
|
379
|
+
const devOnlyShare = stats.count > 0 ? stats.devOnlyCount / stats.count : 0;
|
|
380
|
+
const trueDevOnly = trueCount * devOnlyShare;
|
|
386
381
|
const existing = groupMap.get(groupKey);
|
|
387
382
|
if (existing) {
|
|
388
383
|
const strEntry = existing.strings.get(value);
|
|
@@ -398,6 +393,7 @@ export function registerInternOpportunities(server) {
|
|
|
398
393
|
}
|
|
399
394
|
existing.totalCopies += trueCount;
|
|
400
395
|
existing.totalSize += trueSize;
|
|
396
|
+
existing.devOnlyCopies += trueDevOnly;
|
|
401
397
|
existing.groupSamples += dist.sampledCount;
|
|
402
398
|
existing.coRetainedSamples += dist.coRetainedCount;
|
|
403
399
|
if (!existing.coRetainedVia && dist.coRetainedVia) {
|
|
@@ -419,6 +415,7 @@ export function registerInternOpportunities(server) {
|
|
|
419
415
|
strings: new Map([[value, { count: trueCount, size: trueSize }]]),
|
|
420
416
|
totalCopies: trueCount,
|
|
421
417
|
totalSize: trueSize,
|
|
418
|
+
devOnlyCopies: trueDevOnly,
|
|
422
419
|
exampleParentId: dist.parentId,
|
|
423
420
|
groupSamples: dist.sampledCount,
|
|
424
421
|
coRetainedSamples: dist.coRetainedCount,
|
|
@@ -436,6 +433,12 @@ export function registerInternOpportunities(server) {
|
|
|
436
433
|
let savingsIfInterned = 0;
|
|
437
434
|
let savingsCappable = 0;
|
|
438
435
|
let savingsOverCap = 0;
|
|
436
|
+
// Savings attributable to harness-injected string CONTENT. Compared
|
|
437
|
+
// against savingsIfInterned below rather than tripping on the first
|
|
438
|
+
// match, so one stray bridge string in an otherwise-app group does
|
|
439
|
+
// not relabel the whole group.
|
|
440
|
+
let savingsHarness = 0;
|
|
441
|
+
let harnessWhat;
|
|
439
442
|
const topStrings = [];
|
|
440
443
|
for (const [value, strStats] of g.strings) {
|
|
441
444
|
if (strStats.count > 1) {
|
|
@@ -448,6 +451,16 @@ export function registerInternOpportunities(server) {
|
|
|
448
451
|
savingsCappable += s;
|
|
449
452
|
else
|
|
450
453
|
savingsOverCap += s;
|
|
454
|
+
const nonProd = classifyNonProductionString(value);
|
|
455
|
+
if (nonProd != null) {
|
|
456
|
+
savingsHarness += s;
|
|
457
|
+
if (!harnessWhat) {
|
|
458
|
+
harnessWhat =
|
|
459
|
+
nonProd.kind === 'dev-build'
|
|
460
|
+
? `${nonProd.what} — DEV build only, absent in production`
|
|
461
|
+
: nonProd.what;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
451
464
|
}
|
|
452
465
|
topStrings.push({
|
|
453
466
|
value,
|
|
@@ -462,6 +475,16 @@ export function registerInternOpportunities(server) {
|
|
|
462
475
|
// another structure — interning the property alone reclaims ~0.
|
|
463
476
|
const coRetained = g.coRetainedSamples > 0 &&
|
|
464
477
|
g.coRetainedSamples >= g.groupSamples * 0.5;
|
|
478
|
+
// Harness by CONTENT (majority of the group's savings sits in
|
|
479
|
+
// bridge-injected strings) or by SHAPE (the parent is one of the
|
|
480
|
+
// bridge's tool-manifest entries, whose long `.description` strings a
|
|
481
|
+
// per-load pool would happily "save"). The shape test is what catches
|
|
482
|
+
// the manifest — its descriptions are ordinary English and no content
|
|
483
|
+
// signature can distinguish them from app copy.
|
|
484
|
+
const harnessShape = isHarnessToolManifestShape(g.parentShapeProps);
|
|
485
|
+
const harness = harnessShape ||
|
|
486
|
+
(savingsIfInterned > 0 &&
|
|
487
|
+
savingsHarness >= savingsIfInterned * 0.5);
|
|
465
488
|
groups.push({
|
|
466
489
|
propertyName: g.propertyName,
|
|
467
490
|
parentShape: g.parentShapeKey,
|
|
@@ -479,6 +502,15 @@ export function registerInternOpportunities(server) {
|
|
|
479
502
|
coRetainedVia: coRetained ? g.coRetainedVia : undefined,
|
|
480
503
|
lowRoi: isLowRoiGroup(g.strings.size, g.totalCopies, g.totalSize),
|
|
481
504
|
frameworkOwned: isFrameworkOwned(g.propertyName, g.parentShapeKey, g.parentShapeProps),
|
|
505
|
+
// Majority-dev-only rather than any-dev-only: a group can legitimately
|
|
506
|
+
// mix a few console-held copies with real app data, and only a group
|
|
507
|
+
// that is essentially all inspector-held should leave the headline.
|
|
508
|
+
devOnly: g.totalCopies > 0 && g.devOnlyCopies / g.totalCopies >= 0.8,
|
|
509
|
+
harness,
|
|
510
|
+
harnessWhat: harness
|
|
511
|
+
? (harnessWhat ??
|
|
512
|
+
'automation/devtools bridge tool manifest (.description strings)')
|
|
513
|
+
: undefined,
|
|
482
514
|
});
|
|
483
515
|
}
|
|
484
516
|
groups.sort((a, b) => b.savingsIfInterned - a.savingsIfInterned);
|
|
@@ -563,6 +595,15 @@ export function registerInternOpportunities(server) {
|
|
|
563
595
|
// is retention-then-ROI; every non-`within` bucket is excluded from the
|
|
564
596
|
// within-load headline regardless of which one a group lands in.
|
|
565
597
|
const bucketOf = (g) => {
|
|
598
|
+
// Checked before everything else: harness content is not the
|
|
599
|
+
// application's memory at all — it is the rig injected to observe it —
|
|
600
|
+
// so no other classification of it is meaningful.
|
|
601
|
+
if (g.harness)
|
|
602
|
+
return 'harness';
|
|
603
|
+
// Retention through a dev root means interning reclaims nothing in
|
|
604
|
+
// production, whatever the property is named.
|
|
605
|
+
if (g.devOnly)
|
|
606
|
+
return 'devOnly';
|
|
566
607
|
if (g.frameworkOwned)
|
|
567
608
|
return 'framework';
|
|
568
609
|
if (g.coRetained)
|
|
@@ -579,7 +620,9 @@ export function registerInternOpportunities(server) {
|
|
|
579
620
|
const coRetainedSavings = sumBucket('coRetained');
|
|
580
621
|
const crossLoadSavings = sumBucket('crossLoad');
|
|
581
622
|
const frameworkSavings = sumBucket('framework');
|
|
623
|
+
const devOnlySavings = sumBucket('devOnly');
|
|
582
624
|
const lowRoiSavings = sumBucket('lowRoi');
|
|
625
|
+
const harnessSavings = sumBucket('harness');
|
|
583
626
|
const withinLoadSavings = sumBucket('within');
|
|
584
627
|
// Within the capturable bucket, split by the canonical 128-char cap so the
|
|
585
628
|
// headline reports what a COMPLIANT fix reclaims, not the raw total that
|
|
@@ -600,7 +643,9 @@ export function registerInternOpportunities(server) {
|
|
|
600
643
|
crossLoadSavings +
|
|
601
644
|
coRetainedSavings +
|
|
602
645
|
frameworkSavings +
|
|
603
|
-
|
|
646
|
+
devOnlySavings +
|
|
647
|
+
lowRoiSavings +
|
|
648
|
+
harnessSavings;
|
|
604
649
|
const pctOf = (n) => totalSize > 0
|
|
605
650
|
? ` (${((n / totalSize) * 100).toFixed(1)}% of heap)`
|
|
606
651
|
: '';
|
|
@@ -642,7 +687,7 @@ export function registerInternOpportunities(server) {
|
|
|
642
687
|
// produce a malformed label, so only prefix `.` for named properties.
|
|
643
688
|
const label = g.arrayElement ? g.propertyName : `.${g.propertyName}`;
|
|
644
689
|
return [
|
|
645
|
-
`${label}${g.arrayElement ? ' ▦' : ''}${g.coRetained ? ' ⚠' : ''}${g.crossLoad ? ' ⤫' : ''}${g.frameworkOwned ? ' ▤' : ''}${g.lowRoi ? ' ▽' : ''}`,
|
|
690
|
+
`${label}${g.arrayElement ? ' ▦' : ''}${g.coRetained ? ' ⚠' : ''}${g.crossLoad ? ' ⤫' : ''}${g.devOnly ? ' 🛠' : ''}${g.harness ? ' 🧪' : ''}${g.frameworkOwned ? ' ▤' : ''}${g.lowRoi ? ' ▽' : ''}`,
|
|
646
691
|
shape,
|
|
647
692
|
formatNumber(g.uniqueStrings),
|
|
648
693
|
formatNumber(g.totalCopies),
|
|
@@ -707,18 +752,21 @@ export function registerInternOpportunities(server) {
|
|
|
707
752
|
coRetainedSavings >= crossLoadSavings &&
|
|
708
753
|
coRetainedSavings >= frameworkSavings &&
|
|
709
754
|
coRetainedSavings >= lowRoiSavings &&
|
|
755
|
+
coRetainedSavings >= harnessSavings &&
|
|
710
756
|
coRetainedSavings > 0) {
|
|
711
757
|
verdict = `Verdict: mostly **co-retained** (${formatBytes(coRetainedSavings)}) — interning the property won't help; dedupe at the shared source.`;
|
|
712
758
|
}
|
|
713
759
|
else if (crossLoadSavings >= withinLoadSavings &&
|
|
714
760
|
crossLoadSavings >= frameworkSavings &&
|
|
715
761
|
crossLoadSavings >= lowRoiSavings &&
|
|
762
|
+
crossLoadSavings >= harnessSavings &&
|
|
716
763
|
crossLoadSavings > 0) {
|
|
717
764
|
verdict = `Verdict: mostly **cross-load** (${formatBytes(crossLoadSavings)}) — a per-request pool won't collapse it; needs a shared/module-scope pool or a retention fix.`;
|
|
718
765
|
}
|
|
719
766
|
else if (withinLoadSavings > 0 &&
|
|
720
767
|
withinLoadSavings >= frameworkSavings &&
|
|
721
|
-
withinLoadSavings >= lowRoiSavings
|
|
768
|
+
withinLoadSavings >= lowRoiSavings &&
|
|
769
|
+
withinLoadSavings >= harnessSavings) {
|
|
722
770
|
// Lead with the ≤128-char cappable figure — the amount the canonical
|
|
723
771
|
// intern fix actually reclaims — not the raw within-load total that also
|
|
724
772
|
// counts over-cap strings the fix skips (feedback §3).
|
|
@@ -730,11 +778,15 @@ export function registerInternOpportunities(server) {
|
|
|
730
778
|
: '';
|
|
731
779
|
verdict = `Verdict: **${formatBytes(withinCappable)} cappable** by the canonical ≤${CANONICAL_CAP_CHARS}-char per-load/per-request intern pool at the parse boundary${capNote}.${columnarNote}`;
|
|
732
780
|
}
|
|
733
|
-
else if (frameworkSavings + lowRoiSavings > 0) {
|
|
734
|
-
// Framework/low-ROI dominate (each ≥ the within-load figure).
|
|
735
|
-
// them so a small capturable remainder isn't mistaken for the
|
|
736
|
-
// but still name that remainder when nonzero so it isn't
|
|
781
|
+
else if (frameworkSavings + lowRoiSavings + harnessSavings > 0) {
|
|
782
|
+
// Framework/low-ROI/harness dominate (each ≥ the within-load figure).
|
|
783
|
+
// Lead with them so a small capturable remainder isn't mistaken for the
|
|
784
|
+
// headline, but still name that remainder when nonzero so it isn't
|
|
785
|
+
// hidden.
|
|
737
786
|
const parts = [];
|
|
787
|
+
if (harnessSavings > 0) {
|
|
788
|
+
parts.push('measurement-harness content (the injected automation/devtools bridge — not application memory)');
|
|
789
|
+
}
|
|
738
790
|
if (frameworkSavings > 0) {
|
|
739
791
|
parts.push('framework/infra-owned (headers/cookies/tokens)');
|
|
740
792
|
}
|
|
@@ -777,6 +829,17 @@ export function registerInternOpportunities(server) {
|
|
|
777
829
|
if (frameworkSavings > 0) {
|
|
778
830
|
headerLines.push(`- **▤ Framework/infra-owned (HTTP headers, cookies, auth tokens, Next.js URL/cache context — not app data; out of scope for an app-level intern pool): ${formatBytes(frameworkSavings)}${pctOf(frameworkSavings)}**`);
|
|
779
831
|
}
|
|
832
|
+
if (devOnlySavings > 0) {
|
|
833
|
+
headerLines.push(`- **🛠 Dev/automation-retained (held only via the attached inspector's console or another dev root — reclaims NOTHING in production; do not chase): ${formatBytes(devOnlySavings)}${pctOf(devOnlySavings)}**`);
|
|
834
|
+
}
|
|
835
|
+
if (harnessSavings > 0) {
|
|
836
|
+
const whats = [
|
|
837
|
+
...new Set(shown
|
|
838
|
+
.filter(g => g.harness && g.harnessWhat)
|
|
839
|
+
.map(g => g.harnessWhat)),
|
|
840
|
+
];
|
|
841
|
+
headerLines.push(`- **🧪 Measurement-harness content (${whats.join('; ')}) — the CDP/devtools bridge injected into the page to drive this session, NOT application memory; interning it reclaims nothing in production: ${formatBytes(harnessSavings)}${pctOf(harnessSavings)}**`);
|
|
842
|
+
}
|
|
780
843
|
if (lowRoiSavings > 0) {
|
|
781
844
|
headerLines.push(`- **▽ Low-ROI (high-cardinality, few repeats + long strings — a per-load pool retains a large unique set for a small collapse; usually skip): ${formatBytes(lowRoiSavings)}${pctOf(lowRoiSavings)}**`);
|
|
782
845
|
}
|
|
@@ -794,9 +857,11 @@ export function registerInternOpportunities(server) {
|
|
|
794
857
|
if (coRetainedGroups.length > 0 ||
|
|
795
858
|
crossLoadSavings > 0 ||
|
|
796
859
|
frameworkSavings > 0 ||
|
|
860
|
+
devOnlySavings > 0 ||
|
|
861
|
+
harnessSavings > 0 ||
|
|
797
862
|
lowRoiSavings > 0 ||
|
|
798
863
|
hasArrayElement) {
|
|
799
|
-
lines.push("▦ = array element (columnar / rowsAsArray — intern at the parse site); ⚠ = co-retained (interning won't reclaim); ⤫ = cross-load (high Dup ×, needs shared pool); ▤ = framework/infra-owned (not app data); ▽ = low-ROI (high-cardinality + long; usually skip).", '');
|
|
864
|
+
lines.push("▦ = array element (columnar / rowsAsArray — intern at the parse site); ⚠ = co-retained (interning won't reclaim); ⤫ = cross-load (high Dup ×, needs shared pool); ▤ = framework/infra-owned (not app data); 🛠 = dev/automation-retained (inspector console — reclaims nothing in production); 🧪 = measurement-harness content (injected automation/devtools bridge — not app memory); ▽ = low-ROI (high-cardinality + long; usually skip).", '');
|
|
800
865
|
}
|
|
801
866
|
// Triage mode: stop after the headline split + ranked table. Drops the
|
|
802
867
|
// per-group string lists, partial-interning detail, and the fix recipe
|