@memlab/mcp-server 2.18.1 → 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 +30 -16
- package/bin/.eslintrc.js +26 -0
- package/bin/memlab-cli.js +313 -0
- 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/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/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/guardrail.d.ts.map +1 -1
- package/dist/guardrail.js +13 -0
- package/dist/guardrail.js.map +1 -1
- 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 +109 -2
- package/dist/heap-state.d.ts.map +1 -1
- package/dist/heap-state.js +194 -3
- package/dist/heap-state.js.map +1 -1
- package/dist/index.js +102 -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/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/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/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 +55 -0
- package/dist/string-index.d.ts.map +1 -0
- package/dist/string-index.js +99 -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 +404 -0
- package/dist/tools/analyze-run.js.map +1 -0
- 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 +30 -0
- package/dist/tools/app-heap.d.ts.map +1 -0
- package/dist/tools/app-heap.js +106 -0
- package/dist/tools/app-heap.js.map +1 -0
- 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 +12 -0
- package/dist/tools/batch.d.ts.map +1 -0
- package/dist/tools/batch.js +196 -0
- package/dist/tools/batch.js.map +1 -0
- 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 +240 -14
- 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/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-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 +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 +5 -0
- package/dist/tools/detached-dom.d.ts.map +1 -1
- package/dist/tools/detached-dom.js +368 -22
- 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 +685 -24
- 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/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 +49 -40
- 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 +79 -0
- package/dist/tools/eval.d.ts.map +1 -1
- package/dist/tools/eval.js +2523 -197
- 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 +12 -0
- package/dist/tools/explain-delta.d.ts.map +1 -0
- package/dist/tools/explain-delta.js +221 -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 +203 -0
- package/dist/tools/finding-index.d.ts.map +1 -0
- package/dist/tools/finding-index.js +566 -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/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/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 +280 -0
- package/dist/tools/hypothesis.js.map +1 -0
- 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 +116 -50
- 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 +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 +35 -0
- package/dist/tools/leak-report.d.ts.map +1 -0
- package/dist/tools/leak-report.js +398 -0
- package/dist/tools/leak-report.js.map +1 -0
- package/dist/tools/load-snapshot.d.ts +29 -0
- package/dist/tools/load-snapshot.d.ts.map +1 -1
- package/dist/tools/load-snapshot.js +294 -23
- 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/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/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/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-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 +18 -0
- package/dist/tools/retainer-summary.d.ts.map +1 -1
- package/dist/tools/retainer-summary.js +210 -51
- 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 +14 -2
- 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/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 +53 -0
- package/dist/tools/sequence-analysis.d.ts.map +1 -1
- package/dist/tools/sequence-analysis.js +313 -89
- 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/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 +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 +7 -5
- 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 +492 -0
- package/dist/tools/tools-index.js.map +1 -0
- 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 +23 -0
- package/dist/tools/verify-fix.d.ts.map +1 -0
- package/dist/tools/verify-fix.js +219 -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/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 +238 -3
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +507 -15
- package/dist/utils.js.map +1 -1
- package/package.json +7 -6
package/dist/utils.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @format
|
|
8
8
|
* @oncall memory_lab
|
|
9
9
|
*/
|
|
10
|
-
import { getSnapshotMetadata, getSessionConfig, shouldEmitHeader, } from './heap-state.js';
|
|
10
|
+
import { getSnapshotMetadata, getSessionConfig, isLightSnapshot, shouldEmitHeader, } from './heap-state.js';
|
|
11
11
|
// ScanTimeoutError / makeScanBudget moved to analysis-budget.ts (shared, no
|
|
12
12
|
// import cycle). Re-exported here so existing `from '../utils.js'` imports in
|
|
13
13
|
// the tools keep working unchanged.
|
|
@@ -59,6 +59,36 @@ export function truncateDomToTag(name) {
|
|
|
59
59
|
}
|
|
60
60
|
return `${detached}<${tag}>`;
|
|
61
61
|
}
|
|
62
|
+
// Marker left in place of elided C++ template arguments. Distinctive enough to
|
|
63
|
+
// detect when rendering a legend, and short enough to be worth the swap.
|
|
64
|
+
export const BLINK_TEMPLATE_ELISION = '<…>';
|
|
65
|
+
// A Blink/cppgc type name carries its whole template instantiation, and one hop
|
|
66
|
+
// of a retainer trace can be 400+ tokens of
|
|
67
|
+
// `blink::HeapHashTableBacking<blink::HashTable<cppgc::internal::BasicMember<…>>>`.
|
|
68
|
+
// The information a reader acts on is the OUTER type; the arguments are noise
|
|
69
|
+
// that crowds out the rest of the trace. Collapse everything from the first `<`
|
|
70
|
+
// to the matching final `>` into one marker, keeping any suffix after it.
|
|
71
|
+
//
|
|
72
|
+
// Deliberately conservative: only applied to `::`-qualified C++ names (so a JS
|
|
73
|
+
// class that happens to contain `<` is untouched), and only when the name is
|
|
74
|
+
// long enough for the elision to be worth it — abbreviating
|
|
75
|
+
// `blink::Foo<Bar>` into something barely shorter just costs information.
|
|
76
|
+
const CPP_TEMPLATE_MIN_LEN = 48;
|
|
77
|
+
export function abbreviateBlinkTypeName(name) {
|
|
78
|
+
if (name.length < CPP_TEMPLATE_MIN_LEN)
|
|
79
|
+
return name;
|
|
80
|
+
if (!name.includes('::') || !name.includes('<'))
|
|
81
|
+
return name;
|
|
82
|
+
const open = name.indexOf('<');
|
|
83
|
+
const close = name.lastIndexOf('>');
|
|
84
|
+
if (close <= open)
|
|
85
|
+
return name;
|
|
86
|
+
const head = name.slice(0, open);
|
|
87
|
+
const tail = name.slice(close + 1);
|
|
88
|
+
const abbreviated = `${head}${BLINK_TEMPLATE_ELISION}${tail}`;
|
|
89
|
+
// Never let the "abbreviation" be longer than what it replaced.
|
|
90
|
+
return abbreviated.length < name.length ? abbreviated : name;
|
|
91
|
+
}
|
|
62
92
|
export function truncateNodeName(name, type, selfSize, maxLen = 150, aggressiveDom = false) {
|
|
63
93
|
if (type === 'string' ||
|
|
64
94
|
type === 'concatenated string' ||
|
|
@@ -83,9 +113,10 @@ export function truncateNodeName(name, type, selfSize, maxLen = 150, aggressiveD
|
|
|
83
113
|
}
|
|
84
114
|
return name;
|
|
85
115
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
116
|
+
const abbreviated = abbreviateBlinkTypeName(name);
|
|
117
|
+
if (abbreviated.length <= maxLen)
|
|
118
|
+
return abbreviated;
|
|
119
|
+
return `${abbreviated.slice(0, maxLen)}…`;
|
|
89
120
|
}
|
|
90
121
|
export function serializeNodeSummary(node) {
|
|
91
122
|
return {
|
|
@@ -153,11 +184,21 @@ const NODE_NAME_BLOCK_LIST = new Set([
|
|
|
153
184
|
'(External strings)',
|
|
154
185
|
'(Builtins)',
|
|
155
186
|
]);
|
|
156
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Whether a node is worth showing in a default "interesting objects" listing.
|
|
189
|
+
*
|
|
190
|
+
* `NODE_TYPE_BLOCK_LIST` exists to keep noise out of the DEFAULT view — it is not
|
|
191
|
+
* meant to make those types unreachable. When a caller explicitly asks for one
|
|
192
|
+
* of them (e.g. `largest_objects({node_type: 'array'})`), honour the request:
|
|
193
|
+
* otherwise the filter silently returns zero rows for `array`, `native`, `code`,
|
|
194
|
+
* `synthetic` and `hidden`, which reads as "there are none" on a heap that
|
|
195
|
+
* plainly contains 100k+ of them.
|
|
196
|
+
*/
|
|
197
|
+
export function isNodeWorthInspecting(node, opts) {
|
|
157
198
|
if (node.id <= 3) {
|
|
158
199
|
return false;
|
|
159
200
|
}
|
|
160
|
-
if (NODE_TYPE_BLOCK_LIST.has(node.type)) {
|
|
201
|
+
if (!opts?.allowBlockedTypes && NODE_TYPE_BLOCK_LIST.has(node.type)) {
|
|
161
202
|
return false;
|
|
162
203
|
}
|
|
163
204
|
if (NODE_NAME_BLOCK_LIST.has(node.name)) {
|
|
@@ -228,11 +269,27 @@ export function looksLikeFailurePayload(value) {
|
|
|
228
269
|
/"error"\s*:\s*"(?!null)[^"]+/i.test(head));
|
|
229
270
|
}
|
|
230
271
|
export function filterLargestObjects(snapshot, filter, limit) {
|
|
272
|
+
return filterLargestObjectsCounted(snapshot, filter, limit).nodes;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* As `filterLargestObjects`, but also reports how many nodes MATCHED.
|
|
276
|
+
*
|
|
277
|
+
* The population size is not a nicety. A conclusion drawn from the top 5 of a
|
|
278
|
+
* matching population reads identically whether that population was 5 or 23,918,
|
|
279
|
+
* and the difference decides whether the conclusion is sound: one report stated
|
|
280
|
+
* "all 5 sampled instances share the same retainer pattern — likely a single
|
|
281
|
+
* root cause" where a full-population trace found 1,468 distinct paths, the
|
|
282
|
+
* largest covering 2.2%. The sample fraction has to reach the caller, so the
|
|
283
|
+
* walk that already visits every node returns it.
|
|
284
|
+
*/
|
|
285
|
+
export function filterLargestObjectsCounted(snapshot, filter, limit) {
|
|
231
286
|
let result = [];
|
|
287
|
+
let matched = 0;
|
|
232
288
|
snapshot.nodes.forEach(node => {
|
|
233
289
|
if (!filter(node)) {
|
|
234
290
|
return;
|
|
235
291
|
}
|
|
292
|
+
matched++;
|
|
236
293
|
const size = node.retainedSize;
|
|
237
294
|
let i;
|
|
238
295
|
for (i = result.length - 1; i >= 0; --i) {
|
|
@@ -246,7 +303,7 @@ export function filterLargestObjects(snapshot, filter, limit) {
|
|
|
246
303
|
}
|
|
247
304
|
result = result.slice(0, limit);
|
|
248
305
|
});
|
|
249
|
-
return result;
|
|
306
|
+
return { nodes: result, matched };
|
|
250
307
|
}
|
|
251
308
|
/**
|
|
252
309
|
* Collapse heap nodes that are merely different VIEWS of the same dominator
|
|
@@ -413,6 +470,159 @@ export function boundedDominatorRetainedSize(ids, snapshot, maxWalk = 500) {
|
|
|
413
470
|
}
|
|
414
471
|
return { retained, exact };
|
|
415
472
|
}
|
|
473
|
+
/**
|
|
474
|
+
* Collect the object-valued slots of a Map/Set/WeakMap backing store, sorted by
|
|
475
|
+
* their true FixedArray slot index.
|
|
476
|
+
*
|
|
477
|
+
* V8 stores entries in an OrderedHashMap/OrderedHashSet reachable via the
|
|
478
|
+
* `table` (or `backing_store`) edge. Browser (Chromium) snapshots type the
|
|
479
|
+
* key/value slot edges as `internal` with a numeric name (the array index);
|
|
480
|
+
* Node snapshots type them `element`. The header counts and hash-chain links are
|
|
481
|
+
* inline SMIs, which emit NO edge at all — so the slot indices are NOT
|
|
482
|
+
* contiguous, and the GAPS are meaningful (they mark SMI-valued or deleted
|
|
483
|
+
* slots). Returning the slots WITH their indices lets callers pair by index
|
|
484
|
+
* instead of by position, which is what makes the pairing correct when some
|
|
485
|
+
* values are SMIs (see `enumerateMapEntries`).
|
|
486
|
+
*/
|
|
487
|
+
export function collectBackingSlots(node) {
|
|
488
|
+
const out = [];
|
|
489
|
+
for (const edge of node.references) {
|
|
490
|
+
const eName = String(edge.name_or_index);
|
|
491
|
+
if (eName !== 'table' && eName !== 'backing_store')
|
|
492
|
+
continue;
|
|
493
|
+
const backing = edge.toNode;
|
|
494
|
+
for (const te of backing.references) {
|
|
495
|
+
// Skip the backing store's hidden-class pointer and any hidden edges; the
|
|
496
|
+
// remaining element/internal edges are the real key/value slots.
|
|
497
|
+
if (te.type === 'hidden' || String(te.name_or_index) === 'map')
|
|
498
|
+
continue;
|
|
499
|
+
const idx = Number(te.name_or_index);
|
|
500
|
+
if (!Number.isFinite(idx))
|
|
501
|
+
continue;
|
|
502
|
+
const target = te.toNode;
|
|
503
|
+
// `the_hole` marks a deleted/empty backing-store slot; oddball roots
|
|
504
|
+
// (id<=3) are not real entries. Do NOT skip `undefined`: an explicit
|
|
505
|
+
// `undefined` value/element is a real entry, and dropping it would make it
|
|
506
|
+
// indistinguishable from an inline-primitive gap (mis-reported as absent).
|
|
507
|
+
if (target.id <= 3 || target.name === 'the_hole') {
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
out.push({ idx, node: target });
|
|
511
|
+
}
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
out.sort((a, b) => a.idx - b.idx);
|
|
515
|
+
return out;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Enumerate a Map/WeakMap's (key, value) entries.
|
|
519
|
+
*
|
|
520
|
+
* V8 lays each entry out as consecutive slots `[key, value, chain]` (the chain
|
|
521
|
+
* link is an inline SMI, so it emits no edge). We therefore pair a key with the
|
|
522
|
+
* value in the immediately-following slot INDEX (`key.idx + 1`). When no slot
|
|
523
|
+
* exists at `key.idx + 1`, the value was an inline SMI/primitive and we emit
|
|
524
|
+
* `value: null` — instead of mispairing the key with the NEXT entry's key, which
|
|
525
|
+
* is what naive "consecutive object edges = key,value" pairing does whenever a
|
|
526
|
+
* Map has SMI values (a real correctness bug on browser heaps).
|
|
527
|
+
*/
|
|
528
|
+
export function enumerateMapEntries(node) {
|
|
529
|
+
const slots = collectBackingSlots(node);
|
|
530
|
+
const entries = [];
|
|
531
|
+
let i = 0;
|
|
532
|
+
while (i < slots.length) {
|
|
533
|
+
const key = slots[i];
|
|
534
|
+
const next = slots[i + 1];
|
|
535
|
+
if (next && next.idx === key.idx + 1) {
|
|
536
|
+
entries.push({ key: key.node, value: next.node });
|
|
537
|
+
i += 2;
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
// Value slot absent at key.idx+1 → the value was an inline SMI/primitive.
|
|
541
|
+
entries.push({ key: key.node, value: null });
|
|
542
|
+
i += 1;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return entries;
|
|
546
|
+
}
|
|
547
|
+
/** Enumerate a Set's elements (every occupied backing-store slot). */
|
|
548
|
+
export function enumerateSetElements(node) {
|
|
549
|
+
return collectBackingSlots(node).map(s => s.node);
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Property names that identify an individual record rather than describe its
|
|
553
|
+
* content — always unique per instance, so INCLUDING them in a content
|
|
554
|
+
* signature defeats duplicate detection (every record looks distinct). Excluded
|
|
555
|
+
* by default from `objectContentSignature`. `id`/`__id`/`__ref` are Relay/GraphQL
|
|
556
|
+
* data-id fields; `key`/`clientMutationId` are common per-instance keys.
|
|
557
|
+
*/
|
|
558
|
+
export const IDENTITY_PROPS = new Set([
|
|
559
|
+
'id',
|
|
560
|
+
'__id',
|
|
561
|
+
'__ref',
|
|
562
|
+
'key',
|
|
563
|
+
'clientMutationId',
|
|
564
|
+
]);
|
|
565
|
+
/**
|
|
566
|
+
* A stable, shallow content signature for an object: the sorted set of property
|
|
567
|
+
* names, each annotated with its scalar value (strings capped) or a marker for
|
|
568
|
+
* object/array-valued properties. Two objects with the same signature are
|
|
569
|
+
* structurally identical at one level — the basis for duplicate-record
|
|
570
|
+
* detection (`memlab_duplicate_objects`) and ad-hoc dedup checks in eval.
|
|
571
|
+
*
|
|
572
|
+
* By default per-instance identity fields (`IDENTITY_PROPS`: id/__id/__ref/…) are
|
|
573
|
+
* EXCLUDED so records that differ only by their data-id still collapse together
|
|
574
|
+
* — pass `ignoreProps` to override the set (e.g. `new Set()` to include them).
|
|
575
|
+
*
|
|
576
|
+
* String, boolean, null and object-valued properties are captured (objects
|
|
577
|
+
* generically as `o`, so nested content is not compared).
|
|
578
|
+
*
|
|
579
|
+
* Caveat — numeric values are NOT captured: the V8 heap-snapshot format does not
|
|
580
|
+
* store the actual value of a number field (SMI nodes are `smi number`,
|
|
581
|
+
* heap-numbers are `heap number` — the IEEE-754/int value is not accessible; see
|
|
582
|
+
* `tools/get-value.ts`). A numeric property therefore contributes only a generic
|
|
583
|
+
* `<name>=n` marker, so two objects differing ONLY in a numeric field hash to the
|
|
584
|
+
* same signature and are reported as duplicates. Treat numeric-heavy records
|
|
585
|
+
* accordingly (or compare their values via another tool).
|
|
586
|
+
*/
|
|
587
|
+
export function objectContentSignature(node, opts = {}) {
|
|
588
|
+
const maxLen = opts.maxStringLen ?? 40;
|
|
589
|
+
const ignore = opts.ignoreProps ?? IDENTITY_PROPS;
|
|
590
|
+
const parts = [];
|
|
591
|
+
for (const edge of node.references) {
|
|
592
|
+
// Hidden-class ("map") and other internal edges are already excluded by the
|
|
593
|
+
// `edge.type !== 'property'` guard above, so no explicit `map` name skip is
|
|
594
|
+
// needed (it would only hide a legitimate user property literally named "map").
|
|
595
|
+
if (edge.type !== 'property')
|
|
596
|
+
continue;
|
|
597
|
+
const name = String(edge.name_or_index);
|
|
598
|
+
if (name === '__proto__')
|
|
599
|
+
continue;
|
|
600
|
+
if (ignore.has(name))
|
|
601
|
+
continue;
|
|
602
|
+
const t = edge.toNode;
|
|
603
|
+
if (t.isString) {
|
|
604
|
+
const v = t.toStringNode()?.stringValue ?? '';
|
|
605
|
+
parts.push(`${name}=s:${v.length > maxLen ? v.slice(0, maxLen) : v}`);
|
|
606
|
+
}
|
|
607
|
+
else if (t.type === 'number' || t.name === 'heap number') {
|
|
608
|
+
// Numeric values are NOT recoverable from the snapshot format (SMI /
|
|
609
|
+
// heap-number nodes carry no value — see get-value.ts), so we can only
|
|
610
|
+
// record that the property is a number, not distinguish by value.
|
|
611
|
+
parts.push(`${name}=n`);
|
|
612
|
+
}
|
|
613
|
+
else if (t.name === 'true' ||
|
|
614
|
+
t.name === 'false' ||
|
|
615
|
+
t.name === 'null' ||
|
|
616
|
+
t.name === 'undefined') {
|
|
617
|
+
parts.push(`${name}=${t.name}`);
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
parts.push(`${name}=o`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
parts.sort();
|
|
624
|
+
return parts.join('|');
|
|
625
|
+
}
|
|
416
626
|
/**
|
|
417
627
|
* Best-effort detection of which app a Node snapshot came from, by tallying the
|
|
418
628
|
* `/app(s)/<name>/` segment in bundle paths embedded throughout the heap (string
|
|
@@ -489,6 +699,97 @@ export function instrumentationRetainerNote(steps) {
|
|
|
489
699
|
}
|
|
490
700
|
return null;
|
|
491
701
|
}
|
|
702
|
+
/** Describe a non-property edge in the terms a reader can act on. */
|
|
703
|
+
function describeOwnerRelation(edge) {
|
|
704
|
+
const name = String(edge.name_or_index);
|
|
705
|
+
if (edge.type === 'property' && !/^\d+$/.test(name))
|
|
706
|
+
return null;
|
|
707
|
+
// A captured variable keeps its source name even after minification, which
|
|
708
|
+
// makes it more useful than the anchor it hangs under.
|
|
709
|
+
if (edge.type === 'context')
|
|
710
|
+
return `closure var ${name}`;
|
|
711
|
+
if (/^\d+$/.test(name))
|
|
712
|
+
return 'entry';
|
|
713
|
+
return name;
|
|
714
|
+
}
|
|
715
|
+
export function nearestNamedOwner(node, opts) {
|
|
716
|
+
const maxHops = opts?.maxHops ?? 6;
|
|
717
|
+
let relation = null;
|
|
718
|
+
let current = node;
|
|
719
|
+
for (let hop = 0; hop < maxHops; hop++) {
|
|
720
|
+
// A named property wins over any other referrer at the same hop: it is the
|
|
721
|
+
// only edge that names the object rather than merely containing it.
|
|
722
|
+
let chosen = null;
|
|
723
|
+
for (const edge of current.referrers) {
|
|
724
|
+
const name = String(edge.name_or_index);
|
|
725
|
+
if (edge.type === 'property' && !/^\d+$/.test(name)) {
|
|
726
|
+
chosen = edge;
|
|
727
|
+
break;
|
|
728
|
+
}
|
|
729
|
+
if (!chosen)
|
|
730
|
+
chosen = edge;
|
|
731
|
+
}
|
|
732
|
+
if (!chosen)
|
|
733
|
+
return null;
|
|
734
|
+
// Only the FIRST hop describes how the target itself is held; hops above it
|
|
735
|
+
// are the anchor's own retention and not the caller's concern.
|
|
736
|
+
if (hop === 0)
|
|
737
|
+
relation = describeOwnerRelation(chosen);
|
|
738
|
+
const name = String(chosen.name_or_index);
|
|
739
|
+
if (chosen.type === 'property' && !/^\d+$/.test(name)) {
|
|
740
|
+
const anchor = `${chosen.fromNode.name}.${name}`;
|
|
741
|
+
return {
|
|
742
|
+
anchor,
|
|
743
|
+
relation,
|
|
744
|
+
label: relation == null ? anchor : `${anchor} ▸ ${relation}`,
|
|
745
|
+
ownerNode: chosen.fromNode,
|
|
746
|
+
hops: hop + 1,
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
current = chosen.fromNode;
|
|
750
|
+
}
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* V8 emits a WeakMap entry as one synthetic edge on the backing table:
|
|
755
|
+
*
|
|
756
|
+
* `26 / part of key (Foo @644221) -> value (Bar @644219) pair in WeakMap (table @3212981)`
|
|
757
|
+
*
|
|
758
|
+
* A retainer path that ENDS on such an edge proves nothing. The WeakMap holds
|
|
759
|
+
* the value only for as long as something else holds the KEY, and V8 collects
|
|
760
|
+
* value→key cycles, so "retained by a WeakMap" is not a reason an object is
|
|
761
|
+
* alive — the reason is whatever retains the key. Acting on the path as printed
|
|
762
|
+
* produces a fix for a leak that is not there.
|
|
763
|
+
*/
|
|
764
|
+
const EPHEMERON_EDGE_RE = /part of key \(([^@()]*)@(\d+)\)\s*->\s*value \(([^@()]*)@(\d+)\) pair in WeakMap/;
|
|
765
|
+
/** Parse a WeakMap ephemeron edge name, or `null` if it is an ordinary edge. */
|
|
766
|
+
export function parseEphemeronEdge(edgeName) {
|
|
767
|
+
if (edgeName == null)
|
|
768
|
+
return null;
|
|
769
|
+
const m = EPHEMERON_EDGE_RE.exec(edgeName);
|
|
770
|
+
if (!m)
|
|
771
|
+
return null;
|
|
772
|
+
return {
|
|
773
|
+
keyName: m[1].trim(),
|
|
774
|
+
keyId: Number(m[2]),
|
|
775
|
+
valueName: m[3].trim(),
|
|
776
|
+
valueId: Number(m[4]),
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* The caveat to print when a path traverses an ephemeron edge. `atTarget` marks
|
|
781
|
+
* the case where it is the LAST hop, which is the one that invalidates the
|
|
782
|
+
* conclusion outright rather than merely complicating it.
|
|
783
|
+
*/
|
|
784
|
+
export function ephemeronCaveat(eph, atTarget) {
|
|
785
|
+
return (`⚠ **This path crosses a WeakMap key→value pair${atTarget ? ' as its FINAL hop' : ''}, so it does not explain retention.** ` +
|
|
786
|
+
`A WeakMap holds \`${eph.valueName || 'the value'}\` only while something else holds the KEY ` +
|
|
787
|
+
`\`${eph.keyName || 'key'}\` @${eph.keyId}, and V8 collects value→key cycles — so "retained by a WeakMap" is never the reason an object is alive. ` +
|
|
788
|
+
`Re-trace from the key to find the real retainer: \`memlab_retainer_trace({node_id: ${eph.keyId}})\`.` +
|
|
789
|
+
(atTarget
|
|
790
|
+
? ' Until you do, treat this object as UNEXPLAINED, not as a confirmed leak.'
|
|
791
|
+
: ''));
|
|
792
|
+
}
|
|
492
793
|
export function formatBytes(bytes) {
|
|
493
794
|
if (!Number.isFinite(bytes) || bytes < 0)
|
|
494
795
|
return 'N/A';
|
|
@@ -527,6 +828,89 @@ export function formatNodeInline(id, name, type, selfSize) {
|
|
|
527
828
|
const displayName = selfSize != null ? truncateNodeName(name, type, selfSize, 80) : name;
|
|
528
829
|
return `@${id} ${displayName} (${type})`;
|
|
529
830
|
}
|
|
831
|
+
/**
|
|
832
|
+
* Collapse runs of consecutive same-class hops in a retainer path into a single
|
|
833
|
+
* step tagged with `repeatCount`.
|
|
834
|
+
*
|
|
835
|
+
* Linked structures (a React hook update queue's `pending → .next → .next → …`,
|
|
836
|
+
* intrusive lists, promise chains) produce paths that are hundreds of identical
|
|
837
|
+
* `Object → Object → Object` hops long. Rendering every hop costs thousands of
|
|
838
|
+
* tokens and tells the reader nothing the count doesn't: the interesting part is
|
|
839
|
+
* the chain's head, its length, and where it attaches. This keeps the first hop
|
|
840
|
+
* of each run (so the edge into the run is preserved) and folds the rest.
|
|
841
|
+
*
|
|
842
|
+
* Only runs of at least `minRun` are collapsed, so ordinary short paths render
|
|
843
|
+
* exactly as before.
|
|
844
|
+
*/
|
|
845
|
+
export function collapseRepeatedRuns(steps, minRun = 3) {
|
|
846
|
+
if (steps.length < minRun)
|
|
847
|
+
return steps;
|
|
848
|
+
const key = (s) => `${s.type}::${s.name}`;
|
|
849
|
+
const out = [];
|
|
850
|
+
// The final step is the node the caller asked about. Never fold it into a
|
|
851
|
+
// run, or the trace stops showing the object under investigation and the
|
|
852
|
+
// "← retained object" marker lands on a summarised run instead.
|
|
853
|
+
const foldLimit = steps.length - 1;
|
|
854
|
+
let i = 0;
|
|
855
|
+
while (i < foldLimit) {
|
|
856
|
+
let j = i + 1;
|
|
857
|
+
while (j < foldLimit && key(steps[j]) === key(steps[i]))
|
|
858
|
+
j++;
|
|
859
|
+
const run = j - i;
|
|
860
|
+
if (run >= minRun) {
|
|
861
|
+
// Keep the first hop (carries the edge into the run) and the last hop's
|
|
862
|
+
// identity is the same class, so one step with a count is lossless enough.
|
|
863
|
+
out.push({ ...steps[i], repeatCount: run });
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
for (let k = i; k < j; k++)
|
|
867
|
+
out.push(steps[k]);
|
|
868
|
+
}
|
|
869
|
+
i = j;
|
|
870
|
+
}
|
|
871
|
+
out.push(steps[foldLimit]);
|
|
872
|
+
return out;
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Collapse runs of an identical rendered label, for formatters that have already
|
|
876
|
+
* reduced each hop to a string. Mirrors `collapseRepeatedRuns` for chain-style
|
|
877
|
+
* (`A → B → C`) renderers.
|
|
878
|
+
*/
|
|
879
|
+
export function collapseRepeatedLabels(labels, minRun = 3) {
|
|
880
|
+
if (labels.length < minRun)
|
|
881
|
+
return labels;
|
|
882
|
+
const out = [];
|
|
883
|
+
// Mirror collapseRepeatedRuns: the last label is the target, keep it intact.
|
|
884
|
+
const foldLimit = labels.length - 1;
|
|
885
|
+
let i = 0;
|
|
886
|
+
while (i < foldLimit) {
|
|
887
|
+
let j = i + 1;
|
|
888
|
+
while (j < foldLimit && labels[j] === labels[i])
|
|
889
|
+
j++;
|
|
890
|
+
const run = j - i;
|
|
891
|
+
out.push(run >= minRun ? `${labels[i]} ×${run}` : labels[i]);
|
|
892
|
+
for (let k = i + 1; k < j && run < minRun; k++)
|
|
893
|
+
out.push(labels[k]);
|
|
894
|
+
i = j;
|
|
895
|
+
}
|
|
896
|
+
out.push(labels[foldLimit]);
|
|
897
|
+
return out;
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* One-line note describing what a tool's thresholds excluded from its own
|
|
901
|
+
* output. Printing this everywhere a default threshold exists prevents the
|
|
902
|
+
* failure mode where a tool reports "0" or "none found" and the reader takes it
|
|
903
|
+
* as "there is nothing there", when in fact everything interesting was below the
|
|
904
|
+
* cutoff. (A `dev_artifacts` run reporting `0 B` dev-only — because every
|
|
905
|
+
* console-retained string was under the 512 KB default — nearly landed a
|
|
906
|
+
* measurement artifact in a diff as a production leak.)
|
|
907
|
+
*/
|
|
908
|
+
export function describeSkipped(examined, skipped, thresholdLabel) {
|
|
909
|
+
if (skipped <= 0) {
|
|
910
|
+
return `_Examined all ${formatNumber(examined)} candidates; no ${thresholdLabel} filter excluded anything._`;
|
|
911
|
+
}
|
|
912
|
+
return `_Examined ${formatNumber(examined)} candidates; **${formatNumber(skipped)} were skipped by ${thresholdLabel}** and are NOT reflected in the rows above. Lower it if you expect something smaller._`;
|
|
913
|
+
}
|
|
530
914
|
function formatEdgeLabel(name) {
|
|
531
915
|
const trimmed = name.length > 40 ? `${name.slice(0, 39)}…` : name;
|
|
532
916
|
if (/^\d+$/.test(trimmed))
|
|
@@ -569,7 +953,10 @@ function formatRetainerLadder(steps, showSizes) {
|
|
|
569
953
|
? ` — ${formatBytes(s.retainedSize)}`
|
|
570
954
|
: '';
|
|
571
955
|
const nodeStr = formatNodeInline(s.id, s.name, s.type, s.selfSize);
|
|
572
|
-
|
|
956
|
+
const repeat = s.repeatCount && s.repeatCount > 1
|
|
957
|
+
? ` ×${s.repeatCount} (chain of identical hops)`
|
|
958
|
+
: '';
|
|
959
|
+
lines.push(`${nodeStr}${repeat}${size}${retainerMarker(i, steps.length)}`);
|
|
573
960
|
}
|
|
574
961
|
return lines.join('\n');
|
|
575
962
|
}
|
|
@@ -618,11 +1005,18 @@ export function formatRetainerTree(steps, opts = {}) {
|
|
|
618
1005
|
? ` — ${formatBytes(s.retainedSize)}`
|
|
619
1006
|
: '';
|
|
620
1007
|
const nodeStr = formatNodeInline(s.id, s.name, s.type, s.selfSize);
|
|
621
|
-
|
|
1008
|
+
const repeat = s.repeatCount && s.repeatCount > 1
|
|
1009
|
+
? ` ×${s.repeatCount} (chain of identical hops)`
|
|
1010
|
+
: '';
|
|
1011
|
+
lines.push(`${pad}${connector}${nodeStr}${repeat}${size}${retainerMarker(i, steps.length)}`);
|
|
622
1012
|
}
|
|
623
1013
|
return lines.join('\n');
|
|
624
1014
|
}
|
|
625
1015
|
export function formatNodeSummaryTable(nodes) {
|
|
1016
|
+
// On a light load there is no dominator pass, so every `retainedSize` reads
|
|
1017
|
+
// 0. Printing "0 B" in a Retained Size column is a wrong answer wearing the
|
|
1018
|
+
// costume of a right one; say the number does not exist instead.
|
|
1019
|
+
const light = isLightSnapshot();
|
|
626
1020
|
const headers = ['ID', 'Name', 'Type', 'Self Size', 'Retained Size'];
|
|
627
1021
|
const rightCols = new Set([3, 4]);
|
|
628
1022
|
const rows = nodes.map(n => [
|
|
@@ -630,9 +1024,22 @@ export function formatNodeSummaryTable(nodes) {
|
|
|
630
1024
|
n.name,
|
|
631
1025
|
n.type,
|
|
632
1026
|
formatBytes(n.self_size),
|
|
633
|
-
formatBytes(n.retained_size),
|
|
1027
|
+
light ? 'n/a (light)' : formatBytes(n.retained_size),
|
|
634
1028
|
]);
|
|
635
|
-
return markdownTable(headers, rows, rightCols);
|
|
1029
|
+
return markdownTable(headers, rows, rightCols) + blinkLegend(rows.flat());
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Legend for elided C++ template arguments, emitted only when something in the
|
|
1033
|
+
* rendered output actually carries the marker — an unconditional legend is the
|
|
1034
|
+
* same token tax the abbreviation exists to remove.
|
|
1035
|
+
*/
|
|
1036
|
+
export function blinkLegend(rendered) {
|
|
1037
|
+
for (const cell of rendered) {
|
|
1038
|
+
if (typeof cell === 'string' && cell.includes(BLINK_TEMPLATE_ELISION)) {
|
|
1039
|
+
return `\n\n_\`${BLINK_TEMPLATE_ELISION}\` = C++ template arguments elided; the outer type is what identifies the object. Use \`memlab_get_node\` for the full instantiated name._`;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return '';
|
|
636
1043
|
}
|
|
637
1044
|
export function formatQueryNodesResult(result, offset) {
|
|
638
1045
|
const partial = result.timed_out
|
|
@@ -655,6 +1062,31 @@ export function formatQueryNodesResult(result, offset) {
|
|
|
655
1062
|
}
|
|
656
1063
|
return `Total matching nodes: ${formatNumber(result.total_count)}${partial}`;
|
|
657
1064
|
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Build a name filter from a caller-supplied pattern.
|
|
1067
|
+
*
|
|
1068
|
+
* Interpreted as a case-insensitive regular expression, falling back to a plain
|
|
1069
|
+
* case-insensitive substring test when the pattern is not valid regex — so a
|
|
1070
|
+
* literal like `blink::UndoStep` works without escaping and a real pattern
|
|
1071
|
+
* still works. An empty/absent pattern matches everything.
|
|
1072
|
+
*
|
|
1073
|
+
* Why this exists: answering "is `blink::UndoStep` present?" previously meant
|
|
1074
|
+
* `class_histogram({node_type: "native", limit: 200})` and reading ~4 KB of
|
|
1075
|
+
* table for one row. Filtering at the source is the difference between a
|
|
1076
|
+
* yes/no answer and a page of output.
|
|
1077
|
+
*/
|
|
1078
|
+
export function makeNamePatternTest(pattern) {
|
|
1079
|
+
if (pattern == null || pattern.length === 0)
|
|
1080
|
+
return () => true;
|
|
1081
|
+
try {
|
|
1082
|
+
const re = new RegExp(pattern, 'i');
|
|
1083
|
+
return (name) => re.test(name);
|
|
1084
|
+
}
|
|
1085
|
+
catch {
|
|
1086
|
+
const needle = pattern.toLowerCase();
|
|
1087
|
+
return (name) => name.toLowerCase().includes(needle);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
658
1090
|
export function snapshotHeader() {
|
|
659
1091
|
const meta = getSnapshotMetadata();
|
|
660
1092
|
if (!meta)
|
|
@@ -666,21 +1098,81 @@ export function snapshotHeader() {
|
|
|
666
1098
|
: 'Unknown';
|
|
667
1099
|
return `> Snapshot: ${meta.fileName} (${formatBytes(meta.totalSize)}, ${formatNumber(meta.nodeCount)} nodes, ${envLabel})`;
|
|
668
1100
|
}
|
|
1101
|
+
/**
|
|
1102
|
+
* The header for a tool that read snapshots given as PATHS.
|
|
1103
|
+
*
|
|
1104
|
+
* `snapshotHeader()` can only ever describe the RESIDENT snapshot, because that
|
|
1105
|
+
* is the only one the session knows about. For a path-taking tool that is the
|
|
1106
|
+
* wrong file by construction whenever the caller has something else loaded —
|
|
1107
|
+
* and these tools restore the caller's active snapshot before they build their
|
|
1108
|
+
* result (see `withSnapshotAt`), so by the time the header is rendered the
|
|
1109
|
+
* snapshot it names is never the one that was measured.
|
|
1110
|
+
*
|
|
1111
|
+
* Measured across a 12-round investigation: every path-based call printed a
|
|
1112
|
+
* header naming a capture from an earlier round. A ladder result labelled with
|
|
1113
|
+
* another round's rung is a cross-round misattribution hazard in exactly the
|
|
1114
|
+
* workflow these tools exist for, so name what was actually read.
|
|
1115
|
+
*/
|
|
1116
|
+
export function pathsHeader(labels) {
|
|
1117
|
+
const seen = labels.filter(l => l.length > 0);
|
|
1118
|
+
if (seen.length === 0)
|
|
1119
|
+
return '';
|
|
1120
|
+
if (seen.length === 1)
|
|
1121
|
+
return `> Snapshot: ${seen[0]}`;
|
|
1122
|
+
if (seen.length === 2)
|
|
1123
|
+
return `> Snapshots: ${seen[0]} → ${seen[1]}`;
|
|
1124
|
+
return `> Snapshots: ${seen.length} rungs, ${seen[0]} … ${seen[seen.length - 1]}`;
|
|
1125
|
+
}
|
|
669
1126
|
export function textResult(text) {
|
|
670
1127
|
return { content: [{ type: 'text', text }] };
|
|
671
1128
|
}
|
|
672
|
-
|
|
673
|
-
|
|
1129
|
+
/**
|
|
1130
|
+
* @param headerOverride Pass `pathsHeader([...])` when the tool read snapshots
|
|
1131
|
+
* by PATH rather than operating on the resident one. Passing `null` suppresses
|
|
1132
|
+
* the header entirely. Omitting it keeps the session-header behaviour, which is
|
|
1133
|
+
* correct only for tools that genuinely act on the active snapshot.
|
|
1134
|
+
*/
|
|
1135
|
+
export function toolResult(text, headerOverride) {
|
|
1136
|
+
const header = headerOverride !== undefined
|
|
1137
|
+
? (headerOverride ?? '')
|
|
1138
|
+
: shouldEmitHeader()
|
|
1139
|
+
? snapshotHeader()
|
|
1140
|
+
: '';
|
|
674
1141
|
const body = header ? `${header}\n\n${text}` : text;
|
|
675
1142
|
return { content: [{ type: 'text', text: body }] };
|
|
676
1143
|
}
|
|
1144
|
+
/**
|
|
1145
|
+
* After this many suggestion trailers in one server process, the rest are
|
|
1146
|
+
* suppressed automatically.
|
|
1147
|
+
*
|
|
1148
|
+
* The trailer is genuinely useful the first time and pure cost thereafter: it
|
|
1149
|
+
* is the same text on every call, and a long investigation makes dozens of
|
|
1150
|
+
* calls. `suppressSuggestions` already existed but has to be set deliberately,
|
|
1151
|
+
* which means it is set — if at all — only after the tokens have been spent.
|
|
1152
|
+
* Auto-suppressing keeps the onboarding value and drops the repetition without
|
|
1153
|
+
* anyone having to notice.
|
|
1154
|
+
*/
|
|
1155
|
+
const SUGGESTION_TRAILER_BUDGET = 5;
|
|
1156
|
+
let suggestionTrailersEmitted = 0;
|
|
677
1157
|
/**
|
|
678
1158
|
* Whether tools should omit "Suggested next steps" / "How to fix" trailers.
|
|
679
1159
|
* Honors the session-level `suppressSuggestions` config so callers can trim
|
|
680
|
-
* repeated boilerplate tokens across a long investigation
|
|
1160
|
+
* repeated boilerplate tokens across a long investigation, and self-suppresses
|
|
1161
|
+
* after `SUGGESTION_TRAILER_BUDGET` trailers.
|
|
1162
|
+
*
|
|
1163
|
+
* NOTE: this both reads and ADVANCES the budget, so call it once per decision.
|
|
681
1164
|
*/
|
|
682
1165
|
export function suggestionsSuppressed() {
|
|
683
|
-
|
|
1166
|
+
if (getSessionConfig().suppressSuggestions)
|
|
1167
|
+
return true;
|
|
1168
|
+
if (suggestionTrailersEmitted >= SUGGESTION_TRAILER_BUDGET)
|
|
1169
|
+
return true;
|
|
1170
|
+
suggestionTrailersEmitted++;
|
|
1171
|
+
return false;
|
|
1172
|
+
}
|
|
1173
|
+
/** Test seam; also reset when the session config is changed explicitly. */
|
|
1174
|
+
export function resetSuggestionBudget() {
|
|
1175
|
+
suggestionTrailersEmitted = 0;
|
|
684
1176
|
}
|
|
685
1177
|
export function jsonResult(data) {
|
|
686
1178
|
return textResult(JSON.stringify(data, null, 2));
|