@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
|
@@ -7,9 +7,12 @@
|
|
|
7
7
|
* @format
|
|
8
8
|
* @oncall memory_lab
|
|
9
9
|
*/
|
|
10
|
+
import memlabCore from '@memlab/core';
|
|
11
|
+
const { utils, NumericSet } = memlabCore;
|
|
10
12
|
import { z } from 'zod';
|
|
11
13
|
import { getSnapshot, getSnapshotMetadata } from '../heap-state.js';
|
|
12
|
-
import { formatBytes, markdownTable, truncateNodeName, errorResult, toolResult, } from '../utils.js';
|
|
14
|
+
import { describeSkipped, formatBytes, formatNumber, markdownTable, truncateNodeName, errorResult, toolResult, } from '../utils.js';
|
|
15
|
+
import { isReactRefreshSignatureShape, REACT_REFRESH_REGISTRY_EDGE_NAMES, REACT_REFRESH_SIGNATURE_GATE_PROP, } from '../artifact-classes.js';
|
|
13
16
|
// Globals installed by browser dev tools / extensions. Anything retained ONLY
|
|
14
17
|
// through one of these would be garbage-collected in production — it's a
|
|
15
18
|
// profiling artifact, not a real leak (Feedback round 2 §6).
|
|
@@ -24,7 +27,195 @@ const DEV_GLOBAL_EDGE_NAMES = new Set([
|
|
|
24
27
|
'__RECOIL_DEVTOOLS_EXTENSION__',
|
|
25
28
|
'Debug', // window.Debug — common debugging handle (e.g. WhatsApp Web)
|
|
26
29
|
]);
|
|
30
|
+
// React Fast Refresh (react-refresh) bookkeeping, installed on the global by the
|
|
31
|
+
// DEV-only runtime. `$RefreshSig$` / `$RefreshReg$` close over the refresh
|
|
32
|
+
// runtime's module-scope registries — `allFamiliesByID`, `allSignaturesByType`,
|
|
33
|
+
// and the family/signature maps behind them — which hold every component type
|
|
34
|
+
// and hook signature the page has ever compiled, plus the fibers and DOM those
|
|
35
|
+
// types transitively reach.
|
|
36
|
+
//
|
|
37
|
+
// Neither the globals nor the registries exist in a production build, but the
|
|
38
|
+
// chain roots at the real Window (not at a devtools hook or the inspector), so
|
|
39
|
+
// every earlier check classified the whole family `production`. Measured cost of
|
|
40
|
+
// getting this wrong: on one WA Web round ~93 MB of a 137.7 MB "detached DOM
|
|
41
|
+
// leak" was Fast Refresh retention, and a later round reported a 16,375-entry /
|
|
42
|
+
// 1.2 MB `allFamiliesByID` Map as a production cache-like collection. Both were
|
|
43
|
+
// dev-build-only.
|
|
44
|
+
const REACT_REFRESH_GLOBAL_EDGE_NAMES = new Set([
|
|
45
|
+
'$RefreshSig$',
|
|
46
|
+
'$RefreshReg$',
|
|
47
|
+
]);
|
|
48
|
+
// Detecting Fast Refresh by its GLOBALS alone is not enough, and the gap is
|
|
49
|
+
// large rather than marginal. `$RefreshSig$` / `$RefreshReg$` are thin wrappers;
|
|
50
|
+
// the registries they talk to live in the refresh runtime's module scope and are
|
|
51
|
+
// reachable only through compiled-code constant pools
|
|
52
|
+
// (`system/TrustedWeakFixedArray`, `system/InstructionStream`), so the globals
|
|
53
|
+
// DOMINATE almost nothing and the registries stay classified `production`.
|
|
54
|
+
//
|
|
55
|
+
// Measured on a WhatsApp Web round: dev_artifacts reported
|
|
56
|
+
// `React Fast Refresh registry ($RefreshSig$) x2` and attributed 1.4 KB, while
|
|
57
|
+
// in the same snapshot the `allSignaturesByType` WeakMap's backing table alone
|
|
58
|
+
// was 4,096 KB and `allFamiliesByID` held 16,367 entries / 1.2 MB. The 4 MB was
|
|
59
|
+
// filed as a production optimization opportunity before being withdrawn.
|
|
60
|
+
//
|
|
61
|
+
// So match the registries by CONTENT as well, independent of retainer path:
|
|
62
|
+
//
|
|
63
|
+
// 1. the registry container, by the edge name that binds it in module scope;
|
|
64
|
+
// 2. the per-type signature record react-refresh stores via
|
|
65
|
+
// `setSignature(type, key, forceReset, getCustomHooks)`.
|
|
66
|
+
//
|
|
67
|
+
// Both signatures live in `artifact-classes`, which is where every
|
|
68
|
+
// content-matched non-production family is registered.
|
|
69
|
+
/**
|
|
70
|
+
* True when `node` is a react-refresh signature record. Callers gate on the
|
|
71
|
+
* rare property first; this confirms the rest.
|
|
72
|
+
*/
|
|
73
|
+
function isReactRefreshSignatureRecord(node) {
|
|
74
|
+
const propNames = [];
|
|
75
|
+
for (const edge of node.references) {
|
|
76
|
+
if (edge.type !== 'property')
|
|
77
|
+
continue;
|
|
78
|
+
propNames.push(String(edge.name_or_index));
|
|
79
|
+
}
|
|
80
|
+
return isReactRefreshSignatureShape(propNames);
|
|
81
|
+
}
|
|
82
|
+
// The automation/devtools *bridge* injected into the page by a CDP-driven
|
|
83
|
+
// harness (the browser MCP plugin, a browser-tools extension, Puppeteer helper
|
|
84
|
+
// bundles). This is the harness observing the app, not the app.
|
|
85
|
+
//
|
|
86
|
+
// Two independent signatures, both observed on real hunts:
|
|
87
|
+
// - `TOOL_DEFINITIONS`: the bridge's tool manifest, reached through its
|
|
88
|
+
// listener closure's scope chain. On one run it held 64 `.description`
|
|
89
|
+
// strings that `intern_opportunities` then reported as the single largest
|
|
90
|
+
// interning opportunity in the heap.
|
|
91
|
+
// - bridge entry points by name (`getDevToolBridge`,
|
|
92
|
+
// `BrowserToolsSuspenseInterop`), which show up as multi-MB `production`
|
|
93
|
+
// retainers in `dev_artifacts`' own table.
|
|
94
|
+
//
|
|
95
|
+
// Like `_debugStack` this is found by scanning edges rather than global roots,
|
|
96
|
+
// because the bridge is reached from ordinary app-side listeners.
|
|
97
|
+
const HARNESS_EDGE_NAMES = new Set(['TOOL_DEFINITIONS']);
|
|
98
|
+
const HARNESS_NODE_NAME_RE = /getDevToolBridge|BrowserToolsSuspenseInterop|__BROWSER_TOOLS_|__PUPPETEER_/;
|
|
27
99
|
const DEV_NODE_NAME_RE = /__REACT_DEVTOOLS|DEVTOOLS_GLOBAL_HOOK|ReactDevTools/;
|
|
100
|
+
// Dev-tools BRIDGES are not reached through a window global, so none of the
|
|
101
|
+
// checks above see them: the bridge module sits in the app's own module
|
|
102
|
+
// registry and the objects it accumulates (stanza logs, route history, command
|
|
103
|
+
// registries) root at the real Window. A dev-build bridge that records one
|
|
104
|
+
// entry per network message accumulates for as long as the tab is open — on one
|
|
105
|
+
// measured session 15,582 records / 3.05 MB, still growing while idle — and was
|
|
106
|
+
// reported as production memory.
|
|
107
|
+
const DEV_BRIDGE_NODE_NAME_RE = /DevToolsBridge|DevToolBridge|__DEVTOOLS_BRIDGE__/i;
|
|
108
|
+
/**
|
|
109
|
+
* Deployment-specific artifact roots, so a repo can teach this tool about a
|
|
110
|
+
* dev-only global or module the OSS package cannot know about, instead of the
|
|
111
|
+
* family being silently counted as production memory.
|
|
112
|
+
*
|
|
113
|
+
* MEMLAB_DEV_ARTIFACT_GLOBALS=myDebugHook,__MY_DEVTOOLS__
|
|
114
|
+
* MEMLAB_DEV_ARTIFACT_NODE_PATTERN=MyAppDebugBridge|MyProfiler
|
|
115
|
+
*/
|
|
116
|
+
function extraDevGlobalNames() {
|
|
117
|
+
const raw = process.env.MEMLAB_DEV_ARTIFACT_GLOBALS;
|
|
118
|
+
if (raw == null || raw.trim() === '')
|
|
119
|
+
return new Set();
|
|
120
|
+
return new Set(raw
|
|
121
|
+
.split(',')
|
|
122
|
+
.map(n => n.trim())
|
|
123
|
+
.filter(n => n.length > 0));
|
|
124
|
+
}
|
|
125
|
+
function extraDevNodePattern() {
|
|
126
|
+
const raw = process.env.MEMLAB_DEV_ARTIFACT_NODE_PATTERN;
|
|
127
|
+
if (raw == null || raw.trim() === '')
|
|
128
|
+
return null;
|
|
129
|
+
try {
|
|
130
|
+
return new RegExp(raw, 'i');
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// A malformed pattern must not take the whole tool down; it is reported by
|
|
134
|
+
// the explain output instead.
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Blink accessibility caches. Under CDP-driven automation the a11y tree is
|
|
139
|
+
// materialized (and browser_take_snapshot inflates it further), so this native
|
|
140
|
+
// cache balloons and co-retains detached DOM — automation-inflated retention
|
|
141
|
+
// that is not present at that scale in a normal user session, so it should not
|
|
142
|
+
// be counted toward a production leak total (Feedback: WA Web hunts §A).
|
|
143
|
+
const AX_NODE_NAME_RE = /AXObjectCache|AXNodeObject|AXDirtyObject|AXComputedObject|blink::AX/;
|
|
144
|
+
// CDP / DevTools inspector retention. When DevTools — or a CDP-driven
|
|
145
|
+
// automation session (Puppeteer / the browser MCP) — is attached, any object
|
|
146
|
+
// passed to console.log (or a dev-build devConsole) is held by the inspector
|
|
147
|
+
// via edges on the "(Global handles)" synthetic node named ".../ DevTools
|
|
148
|
+
// console". Such an object, and any subtree it pins (e.g. a logged
|
|
149
|
+
// `{node}` keeping a detached DOM tree alive), would be garbage-collected in a
|
|
150
|
+
// real user session with DevTools closed, so retention ONLY through them is a
|
|
151
|
+
// measurement artifact, not a production leak. `dev_artifacts` previously
|
|
152
|
+
// missed this because it only knew about dev globals and the a11y cache, which
|
|
153
|
+
// caused console-retained detached DOM to be mis-reported as production-real
|
|
154
|
+
// (Feedback round 3 §A).
|
|
155
|
+
const GLOBAL_HANDLES_NODE_NAME = '(Global handles)';
|
|
156
|
+
const CONSOLE_HANDLE_EDGE_RE = /DevTools console/i;
|
|
157
|
+
// React DEV-build owner stacks. In a development build React attaches
|
|
158
|
+
// `_debugStack` (a captured `Error`) to fibers. That Error's `ErrorStackData`
|
|
159
|
+
// holds the captured stack-frame array, which holds whatever closures were on
|
|
160
|
+
// the stack — commonly the `batchedUpdates` closure whose scope still refers to
|
|
161
|
+
// the handler's `nativeEvent`, which in turn pins the DOM subtree that was being
|
|
162
|
+
// unmounted. The result is a detached subtree retained entirely by DEV-only
|
|
163
|
+
// bookkeeping.
|
|
164
|
+
//
|
|
165
|
+
// This family is invisible to the checks above because the chain is rooted at a
|
|
166
|
+
// LIVE DOM element's fiber, not at a dev global or the inspector — so it was
|
|
167
|
+
// classified `production` and read as a genuine unmount leak. It is production-
|
|
168
|
+
// safe (`_debugStack` does not exist in a production React build) and, because
|
|
169
|
+
// it hangs off whatever handler was running, it fires on ANY synthetic-click
|
|
170
|
+
// hammer that unmounts a subtree.
|
|
171
|
+
//
|
|
172
|
+
// Marking the Error itself as a dev root is enough: the existing dev-only
|
|
173
|
+
// reachability pass then attributes the whole ErrorStackData -> frames ->
|
|
174
|
+
// closure -> nativeEvent -> detached-subtree chain to it.
|
|
175
|
+
const REACT_DEBUG_STACK_EDGE_NAMES = new Set(['_debugStack', '_debugTask']);
|
|
176
|
+
const CATEGORY_LABEL = {
|
|
177
|
+
console: 'DevTools console (CDP inspector)',
|
|
178
|
+
a11y: 'a11y / CDP automation cache',
|
|
179
|
+
devGlobal: 'dev/extension global',
|
|
180
|
+
reactDebugStack: 'React DEV owner stack (_debugStack)',
|
|
181
|
+
reactFastRefresh: 'React Fast Refresh registry ($RefreshSig$)',
|
|
182
|
+
harness: 'automation/devtools bridge (test harness)',
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* One-line summary of the dev/automation roots present, as counts per family.
|
|
186
|
+
*
|
|
187
|
+
* The full list was printed on every call and is unbounded: each Blink a11y
|
|
188
|
+
* object and each bridge export is its own root, so a measured WhatsApp Web run
|
|
189
|
+
* spent ~1,500 tokens per call on a single comma-separated line of names, and an
|
|
190
|
+
* Ads Manager run repeated an entire multi-sentence React warning inside it.
|
|
191
|
+
* Nothing downstream keyed on the individual names — the reader needs to know
|
|
192
|
+
* WHICH FAMILIES are present, which is what the categories already encode.
|
|
193
|
+
* `show_roots: true` restores the full list for the rare case of chasing one
|
|
194
|
+
* specific root.
|
|
195
|
+
*/
|
|
196
|
+
export function summarizeDevRoots(devRoots, showAll = false) {
|
|
197
|
+
if (devRoots.byId.size === 0)
|
|
198
|
+
return 'none';
|
|
199
|
+
if (showAll) {
|
|
200
|
+
return [...new Set(devRoots.byId.values())].join(', ');
|
|
201
|
+
}
|
|
202
|
+
const byCat = new Map();
|
|
203
|
+
let uncategorized = 0;
|
|
204
|
+
for (const id of devRoots.byId.keys()) {
|
|
205
|
+
const cat = devRoots.categoryById.get(id);
|
|
206
|
+
if (cat == null) {
|
|
207
|
+
uncategorized++;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
byCat.set(cat, (byCat.get(cat) ?? 0) + 1);
|
|
211
|
+
}
|
|
212
|
+
const parts = [...byCat.entries()]
|
|
213
|
+
.sort((a, b) => b[1] - a[1])
|
|
214
|
+
.map(([cat, n]) => `${CATEGORY_LABEL[cat]} ×${n}`);
|
|
215
|
+
if (uncategorized > 0)
|
|
216
|
+
parts.push(`other ×${uncategorized}`);
|
|
217
|
+
return `${devRoots.byId.size} root(s) — ${parts.join(', ')}`;
|
|
218
|
+
}
|
|
28
219
|
/**
|
|
29
220
|
* Find the dev/extension "root" objects: the targets of dev-global edges on
|
|
30
221
|
* the Window/global object, plus any node whose own name marks it as a
|
|
@@ -32,36 +223,332 @@ const DEV_NODE_NAME_RE = /__REACT_DEVTOOLS|DEVTOOLS_GLOBAL_HOOK|ReactDevTools/;
|
|
|
32
223
|
*/
|
|
33
224
|
export function collectDevRoots(snapshot) {
|
|
34
225
|
const byId = new Map();
|
|
226
|
+
const categoryById = new Map();
|
|
227
|
+
const extraGlobals = extraDevGlobalNames();
|
|
228
|
+
const extraNodeRe = extraDevNodePattern();
|
|
35
229
|
snapshot.nodes.forEach(node => {
|
|
36
230
|
if (node.id <= 3)
|
|
37
231
|
return;
|
|
232
|
+
// Bridges and deployment-specific dev modules are matched by NODE name:
|
|
233
|
+
// they are not hung off a window global, so the edge-name checks below
|
|
234
|
+
// never see them.
|
|
235
|
+
if (DEV_BRIDGE_NODE_NAME_RE.test(node.name) ||
|
|
236
|
+
(extraNodeRe != null && extraNodeRe.test(node.name))) {
|
|
237
|
+
byId.set(node.id, `${node.name} (dev-tools bridge; absent in production)`);
|
|
238
|
+
categoryById.set(node.id, 'devGlobal');
|
|
239
|
+
}
|
|
38
240
|
const isGlobal = node.name.startsWith('Window ') ||
|
|
39
241
|
node.name === 'global' ||
|
|
40
242
|
node.name === 'globalThis';
|
|
41
243
|
if (isGlobal) {
|
|
42
244
|
for (const edge of node.references) {
|
|
43
245
|
const eName = String(edge.name_or_index);
|
|
44
|
-
if (DEV_GLOBAL_EDGE_NAMES.has(eName)
|
|
246
|
+
if ((DEV_GLOBAL_EDGE_NAMES.has(eName) || extraGlobals.has(eName)) &&
|
|
247
|
+
edge.toNode.id > 3) {
|
|
45
248
|
byId.set(edge.toNode.id, eName);
|
|
249
|
+
categoryById.set(edge.toNode.id, 'devGlobal');
|
|
250
|
+
}
|
|
251
|
+
if (REACT_REFRESH_GLOBAL_EDGE_NAMES.has(eName) && edge.toNode.id > 3) {
|
|
252
|
+
byId.set(edge.toNode.id, `${eName} (React Fast Refresh; absent in production builds)`);
|
|
253
|
+
categoryById.set(edge.toNode.id, 'reactFastRefresh');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Objects held only by the attached inspector's console (CDP global
|
|
258
|
+
// handles named ".../ DevTools console"). Mark just those targets — NOT the
|
|
259
|
+
// whole (Global handles) node, which also holds legitimate native/global
|
|
260
|
+
// handles for real app objects.
|
|
261
|
+
if (node.name === GLOBAL_HANDLES_NODE_NAME) {
|
|
262
|
+
for (const edge of node.references) {
|
|
263
|
+
if (CONSOLE_HANDLE_EDGE_RE.test(String(edge.name_or_index)) &&
|
|
264
|
+
edge.toNode.id > 3) {
|
|
265
|
+
byId.set(edge.toNode.id, 'DevTools console (CDP inspector-retained; GC-eligible with DevTools closed)');
|
|
266
|
+
categoryById.set(edge.toNode.id, 'console');
|
|
46
267
|
}
|
|
47
268
|
}
|
|
48
269
|
}
|
|
270
|
+
// React DEV owner stacks hang off fibers, which are reachable from LIVE
|
|
271
|
+
// DOM nodes — so they are found by scanning edges, not global roots. The
|
|
272
|
+
// automation bridge is reached the same way, from app-side listeners.
|
|
273
|
+
for (const edge of node.references) {
|
|
274
|
+
const eName = String(edge.name_or_index);
|
|
275
|
+
if (REACT_DEBUG_STACK_EDGE_NAMES.has(eName) && edge.toNode.id > 3) {
|
|
276
|
+
byId.set(edge.toNode.id, 'React DEV owner stack (_debugStack; absent in production builds)');
|
|
277
|
+
categoryById.set(edge.toNode.id, 'reactDebugStack');
|
|
278
|
+
}
|
|
279
|
+
if (HARNESS_EDGE_NAMES.has(eName) && edge.toNode.id > 3) {
|
|
280
|
+
byId.set(edge.toNode.id, `${eName} (automation/devtools bridge injected by the harness)`);
|
|
281
|
+
categoryById.set(edge.toNode.id, 'harness');
|
|
282
|
+
}
|
|
283
|
+
// Fast Refresh registries bound in the refresh runtime's module scope.
|
|
284
|
+
// Matched by edge name rather than by retainer path, because the path
|
|
285
|
+
// runs through compiled-code constant pools that no dev global dominates.
|
|
286
|
+
if (REACT_REFRESH_REGISTRY_EDGE_NAMES.has(eName) && edge.toNode.id > 3) {
|
|
287
|
+
byId.set(edge.toNode.id, `${eName} (React Fast Refresh registry; absent in production builds)`);
|
|
288
|
+
categoryById.set(edge.toNode.id, 'reactFastRefresh');
|
|
289
|
+
}
|
|
290
|
+
// The signature record itself, so entries survive being read out of a
|
|
291
|
+
// WeakMap whose table is not itself dominated by the registry edge.
|
|
292
|
+
if (eName === REACT_REFRESH_SIGNATURE_GATE_PROP &&
|
|
293
|
+
node.id > 3 &&
|
|
294
|
+
isReactRefreshSignatureRecord(node)) {
|
|
295
|
+
byId.set(node.id, 'react-refresh signature record ({forceReset, ownKey, fullKey, getCustomHooks}; absent in production builds)');
|
|
296
|
+
categoryById.set(node.id, 'reactFastRefresh');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
49
299
|
if (DEV_NODE_NAME_RE.test(node.name)) {
|
|
50
300
|
byId.set(node.id, node.name);
|
|
301
|
+
categoryById.set(node.id, 'devGlobal');
|
|
302
|
+
}
|
|
303
|
+
if (AX_NODE_NAME_RE.test(node.name)) {
|
|
304
|
+
byId.set(node.id, `${node.name} (a11y/CDP automation cache)`);
|
|
305
|
+
categoryById.set(node.id, 'a11y');
|
|
306
|
+
}
|
|
307
|
+
if (HARNESS_NODE_NAME_RE.test(node.name)) {
|
|
308
|
+
byId.set(node.id, `${node.name} (automation/devtools bridge)`);
|
|
309
|
+
categoryById.set(node.id, 'harness');
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
return { byId, categoryById };
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Size each Fast Refresh registry container together with its hash-table
|
|
316
|
+
* backing store, and report the table's OCCUPANCY.
|
|
317
|
+
*
|
|
318
|
+
* Occupancy is the part that is not obvious and not otherwise reported. V8 never
|
|
319
|
+
* shrinks an `EphemeronHashTable`, so a WeakMap that ever peaked large keeps
|
|
320
|
+
* that capacity for the lifetime of the isolate. Measured across one sweep, the
|
|
321
|
+
* same `allSignaturesByType` table read:
|
|
322
|
+
*
|
|
323
|
+
* 2,097,172 B / 262,144 slots / 2,989 live entries = 1.14% occupancy
|
|
324
|
+
* 8,388,628 B / 1,048,576 slots / 7,564 live entries = 0.72% occupancy
|
|
325
|
+
*
|
|
326
|
+
* i.e. it quadrupled while the live entry count only 2.5x'd. Printing bytes
|
|
327
|
+
* alone invites "the registry grew"; printing occupancy says "the table is
|
|
328
|
+
* stale capacity", which is the true statement and a different fix.
|
|
329
|
+
*
|
|
330
|
+
* A hash table's slot count is derived from the backing store's byte size
|
|
331
|
+
* (8 bytes per slot on 64-bit V8) rather than from `edge_count`, because
|
|
332
|
+
* `edge_count` counts only the non-hole entries — which is the numerator, not
|
|
333
|
+
* the denominator.
|
|
334
|
+
*/
|
|
335
|
+
function describeReactRefreshRegistries(snapshot, devRoots) {
|
|
336
|
+
const out = [];
|
|
337
|
+
const seen = new Set();
|
|
338
|
+
snapshot.nodes.forEach((node) => {
|
|
339
|
+
if (node.id <= 3)
|
|
340
|
+
return;
|
|
341
|
+
for (const edge of node.references) {
|
|
342
|
+
const eName = String(edge.name_or_index);
|
|
343
|
+
if (!REACT_REFRESH_REGISTRY_EDGE_NAMES.has(eName))
|
|
344
|
+
continue;
|
|
345
|
+
const container = edge.toNode;
|
|
346
|
+
if (container == null || container.id <= 3 || seen.has(container.id)) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
seen.add(container.id);
|
|
350
|
+
// The hash table hanging off the Map/Set/WeakMap.
|
|
351
|
+
let tableBytes = 0;
|
|
352
|
+
let liveSlots = 0;
|
|
353
|
+
for (const e2 of container.references) {
|
|
354
|
+
const t = e2.toNode;
|
|
355
|
+
if (t == null || t.type !== 'array')
|
|
356
|
+
continue;
|
|
357
|
+
if (t.self_size > tableBytes) {
|
|
358
|
+
tableBytes = t.self_size;
|
|
359
|
+
liveSlots = t.edge_count;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const capacity = tableBytes > 0 ? Math.round(tableBytes / 8) : 0;
|
|
363
|
+
const occupancy = capacity > 0 ? ((liveSlots / capacity) * 100).toFixed(2) : null;
|
|
364
|
+
out.push(`\`${eName}\` (${container.name}) — table ${formatBytes(tableBytes)}` +
|
|
365
|
+
(capacity > 0
|
|
366
|
+
? `, ~${formatNumber(capacity)} slots, ${formatNumber(liveSlots)} live` +
|
|
367
|
+
(occupancy != null ? ` (**${occupancy}% occupancy**)` : '')
|
|
368
|
+
: '') +
|
|
369
|
+
(occupancy != null && Number(occupancy) < 25
|
|
370
|
+
? ' — mostly STALE CAPACITY: V8 never shrinks an EphemeronHashTable, so this is a high-water mark, not live data'
|
|
371
|
+
: ''));
|
|
51
372
|
}
|
|
52
373
|
});
|
|
53
|
-
|
|
374
|
+
if (out.length > 0 && devRoots.byId.size > 0) {
|
|
375
|
+
out.push('_These are dev-build only. If an `(unnamed array)` is one of the largest growers in a `memlab_leak_report` on this capture, check these byte figures before attributing it to the app._');
|
|
376
|
+
}
|
|
377
|
+
return out;
|
|
54
378
|
}
|
|
55
379
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
380
|
+
* Reachability from GC roots with every dev/automation root treated as a sink
|
|
381
|
+
* (its outgoing edges are not followed). Returns a bitmap indexed by
|
|
382
|
+
* `node.nodeIndex`: 1 = still reachable from a real GC root without passing
|
|
383
|
+
* through a dev root (production-reachable), 0 = reachable ONLY through one or
|
|
384
|
+
* more dev roots (a dev/automation-only artifact).
|
|
385
|
+
*
|
|
386
|
+
* This is stricter and more complete than a dominator walk: when an object is
|
|
387
|
+
* co-retained by SEVERAL different dev roots (e.g. the DevTools console AND the
|
|
388
|
+
* a11y cache both point at the same detached-DOM subtree), no single dev root
|
|
389
|
+
* dominates it, so a dominator walk misses it — but it is still dev-only because
|
|
390
|
+
* every path to it passes through some dev root. This pass catches that case.
|
|
61
391
|
*/
|
|
62
|
-
export function
|
|
392
|
+
export function computeReachableWithoutDevRoots(snapshot, devRoots) {
|
|
393
|
+
const { byId } = devRoots;
|
|
394
|
+
const reached = new Uint8Array(snapshot.nodes.length);
|
|
395
|
+
const stack = [];
|
|
396
|
+
// Seed from the synthetic GC roots (the "(GC roots)" super-root and its
|
|
397
|
+
// synthetic children such as "(Global handles)"): all real objects are
|
|
398
|
+
// reachable from these. A dev root that is itself synthetic is seeded but not
|
|
399
|
+
// expanded.
|
|
400
|
+
snapshot.nodes.forEach(node => {
|
|
401
|
+
if (node.type !== 'synthetic' && node.id > 3)
|
|
402
|
+
return;
|
|
403
|
+
if (reached[node.nodeIndex])
|
|
404
|
+
return;
|
|
405
|
+
reached[node.nodeIndex] = 1;
|
|
406
|
+
if (!byId.has(node.id))
|
|
407
|
+
stack.push(node);
|
|
408
|
+
});
|
|
409
|
+
while (stack.length > 0) {
|
|
410
|
+
const node = stack.pop();
|
|
411
|
+
node.forEachReference((edge) => {
|
|
412
|
+
const to = edge.toNode;
|
|
413
|
+
if (reached[to.nodeIndex])
|
|
414
|
+
return;
|
|
415
|
+
reached[to.nodeIndex] = 1;
|
|
416
|
+
// Mark the dev root reached (it is held by a real global handle) but do
|
|
417
|
+
// NOT follow its edges — anything reachable only through it is dev-only.
|
|
418
|
+
if (!byId.has(to.id))
|
|
419
|
+
stack.push(to);
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
return reached;
|
|
423
|
+
}
|
|
424
|
+
const CATEGORY_BIT = {
|
|
425
|
+
console: 1,
|
|
426
|
+
a11y: 2,
|
|
427
|
+
devGlobal: 4,
|
|
428
|
+
reactDebugStack: 8,
|
|
429
|
+
reactFastRefresh: 16,
|
|
430
|
+
harness: 32,
|
|
431
|
+
};
|
|
432
|
+
/**
|
|
433
|
+
* Whole-heap total of everything retained ONLY via dev/automation roots, plus a
|
|
434
|
+
* per-source breakdown.
|
|
435
|
+
*
|
|
436
|
+
* This is deliberately independent of any display threshold. The previous
|
|
437
|
+
* implementation accumulated the total inside the same loop that filtered
|
|
438
|
+
* candidates by `min_retained_size`, so the headline number only counted
|
|
439
|
+
* objects above the cutoff. With the 512 KB default that made a heap holding
|
|
440
|
+
* thousands of small console-retained objects report **0 B dev-only** — read by
|
|
441
|
+
* a caller as "this is production-real". Whole classes of artifact (per-event
|
|
442
|
+
* log strings, one logged object per interaction) are individually tiny and
|
|
443
|
+
* collectively large, which is exactly the case that matters.
|
|
444
|
+
*
|
|
445
|
+
* Attribution is a single BFS over the dev-only subgraph, seeded from each dev
|
|
446
|
+
* root's dev-only successors and OR-ing a category bit as it goes, so a subtree
|
|
447
|
+
* co-retained by several artifact families is counted under each.
|
|
448
|
+
*/
|
|
449
|
+
export function summarizeDevOnly(snapshot, devRoots, reached) {
|
|
450
|
+
const { byId, categoryById } = devRoots;
|
|
451
|
+
const mask = new Uint8Array(snapshot.nodes.length);
|
|
452
|
+
const stack = [];
|
|
453
|
+
const devOnlyIds = new NumericSet();
|
|
454
|
+
let nodes = 0;
|
|
455
|
+
// Seed: dev-only successors of each dev root, tagged with that root's family.
|
|
456
|
+
snapshot.nodes.forEach(node => {
|
|
457
|
+
const cat = categoryById.get(node.id);
|
|
458
|
+
if (cat == null || !byId.has(node.id))
|
|
459
|
+
return;
|
|
460
|
+
const bit = CATEGORY_BIT[cat];
|
|
461
|
+
node.forEachReference((edge) => {
|
|
462
|
+
const to = edge.toNode;
|
|
463
|
+
if (reached[to.nodeIndex])
|
|
464
|
+
return; // production-reachable, not an artifact
|
|
465
|
+
if ((mask[to.nodeIndex] & bit) !== 0)
|
|
466
|
+
return;
|
|
467
|
+
mask[to.nodeIndex] |= bit;
|
|
468
|
+
stack.push(to);
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
while (stack.length > 0) {
|
|
472
|
+
const node = stack.pop();
|
|
473
|
+
const bits = mask[node.nodeIndex];
|
|
474
|
+
node.forEachReference((edge) => {
|
|
475
|
+
const to = edge.toNode;
|
|
476
|
+
if (reached[to.nodeIndex])
|
|
477
|
+
return;
|
|
478
|
+
if ((mask[to.nodeIndex] & bits) === bits)
|
|
479
|
+
return; // nothing new to add
|
|
480
|
+
mask[to.nodeIndex] |= bits;
|
|
481
|
+
stack.push(to);
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
const byCategory = new Map();
|
|
485
|
+
snapshot.nodes.forEach(node => {
|
|
486
|
+
if (node.id <= 3)
|
|
487
|
+
return;
|
|
488
|
+
if (reached[node.nodeIndex])
|
|
489
|
+
return;
|
|
490
|
+
nodes++;
|
|
491
|
+
devOnlyIds.add(node.id);
|
|
492
|
+
const bits = mask[node.nodeIndex];
|
|
493
|
+
for (const cat of Object.keys(CATEGORY_BIT)) {
|
|
494
|
+
if ((bits & CATEGORY_BIT[cat]) === 0)
|
|
495
|
+
continue;
|
|
496
|
+
const cur = byCategory.get(cat) ?? { nodes: 0, selfBytes: 0 };
|
|
497
|
+
cur.nodes++;
|
|
498
|
+
cur.selfBytes += node.self_size;
|
|
499
|
+
byCategory.set(cat, cur);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
// Dominator-deduplicated so nested artifacts are not double-counted.
|
|
503
|
+
const retained = nodes === 0
|
|
504
|
+
? 0
|
|
505
|
+
: utils.aggregateDominatorMetrics(devOnlyIds, snapshot, () => true, (node) => node.retainedSize);
|
|
506
|
+
return { nodes, retained, byCategory };
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Find a dev-root name on the node's shortest retainer path, for the "via"
|
|
510
|
+
* label. Best-effort: the shortest path usually runs through the dominant
|
|
511
|
+
* retainer; falls back to a generic label when it does not.
|
|
512
|
+
*/
|
|
513
|
+
function findDevRootVia(node, devRoots) {
|
|
514
|
+
let cur = node;
|
|
515
|
+
const seen = new Set();
|
|
516
|
+
let steps = 0;
|
|
517
|
+
while (cur && cur.hasPathEdge && steps < 1000) {
|
|
518
|
+
const edge = cur.pathEdge;
|
|
519
|
+
if (!edge)
|
|
520
|
+
break;
|
|
521
|
+
const from = edge.fromNode;
|
|
522
|
+
const via = devRoots.byId.get(from.id);
|
|
523
|
+
if (via != null)
|
|
524
|
+
return via;
|
|
525
|
+
if (seen.has(from.id))
|
|
526
|
+
break;
|
|
527
|
+
seen.add(from.id);
|
|
528
|
+
cur = from;
|
|
529
|
+
steps++;
|
|
530
|
+
}
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Classify whether a node is retained ONLY via dev/automation roots. Prefer the
|
|
535
|
+
* reachability bitmap from `computeReachableWithoutDevRoots` (rigorous and
|
|
536
|
+
* complete, including multi-dev-root co-retention). Without a bitmap, fall back
|
|
537
|
+
* to a dominator walk: if a dev root *dominates* the node, every path to it
|
|
538
|
+
* passes through that dev object — conservative, catches only single-dev-root
|
|
539
|
+
* retention.
|
|
540
|
+
*/
|
|
541
|
+
export function classifyDevOnly(node, devRoots, reached, maxWalk = 1000) {
|
|
63
542
|
if (devRoots.byId.size === 0)
|
|
64
543
|
return { devOnly: false, via: null };
|
|
544
|
+
if (reached != null) {
|
|
545
|
+
if (reached[node.nodeIndex])
|
|
546
|
+
return { devOnly: false, via: null };
|
|
547
|
+
return {
|
|
548
|
+
devOnly: true,
|
|
549
|
+
via: findDevRootVia(node, devRoots) ?? 'dev/automation roots',
|
|
550
|
+
};
|
|
551
|
+
}
|
|
65
552
|
let cur = node.dominatorNode ?? null;
|
|
66
553
|
let steps = 0;
|
|
67
554
|
while (cur && steps < maxWalk) {
|
|
@@ -75,13 +562,82 @@ export function classifyDevOnly(node, devRoots, maxWalk = 1000) {
|
|
|
75
562
|
}
|
|
76
563
|
return { devOnly: false, via: null };
|
|
77
564
|
}
|
|
565
|
+
/**
|
|
566
|
+
* The families this tool knows how to look for, and whether any root for each
|
|
567
|
+
* was found. Printed on demand because the failure mode of a hardcoded family
|
|
568
|
+
* list is silence: a family nobody thought of contributes 0 bytes and reads
|
|
569
|
+
* exactly like "there are no artifacts in this snapshot".
|
|
570
|
+
*/
|
|
571
|
+
function explainCoverage(devRoots) {
|
|
572
|
+
const seen = new Map();
|
|
573
|
+
for (const cat of devRoots.categoryById.values()) {
|
|
574
|
+
seen.set(cat, (seen.get(cat) ?? 0) + 1);
|
|
575
|
+
}
|
|
576
|
+
const families = [
|
|
577
|
+
[
|
|
578
|
+
'devGlobal',
|
|
579
|
+
'Dev/extension globals + dev-tools bridges',
|
|
580
|
+
[...DEV_GLOBAL_EDGE_NAMES].join(', ') +
|
|
581
|
+
`; node names matching ${DEV_BRIDGE_NODE_NAME_RE}`,
|
|
582
|
+
],
|
|
583
|
+
[
|
|
584
|
+
'reactFastRefresh',
|
|
585
|
+
'React Fast Refresh registries',
|
|
586
|
+
[...REACT_REFRESH_GLOBAL_EDGE_NAMES].join(', '),
|
|
587
|
+
],
|
|
588
|
+
[
|
|
589
|
+
'a11y',
|
|
590
|
+
'Blink accessibility caches (CDP-inflated)',
|
|
591
|
+
String(AX_NODE_NAME_RE),
|
|
592
|
+
],
|
|
593
|
+
[
|
|
594
|
+
'console',
|
|
595
|
+
'Inspector console retention',
|
|
596
|
+
`${GLOBAL_HANDLES_NODE_NAME} edges matching ${CONSOLE_HANDLE_EDGE_RE}`,
|
|
597
|
+
],
|
|
598
|
+
[
|
|
599
|
+
'harness',
|
|
600
|
+
'Automation/test-harness bundle',
|
|
601
|
+
[...HARNESS_EDGE_NAMES].join(', ') + `; ${HARNESS_NODE_NAME_RE}`,
|
|
602
|
+
],
|
|
603
|
+
[
|
|
604
|
+
'reactDebugStack',
|
|
605
|
+
'React DEV owner stacks',
|
|
606
|
+
[...REACT_DEBUG_STACK_EDGE_NAMES].join(', '),
|
|
607
|
+
],
|
|
608
|
+
];
|
|
609
|
+
const rows = families.map(([cat, name, rule]) => [
|
|
610
|
+
name,
|
|
611
|
+
seen.has(cat) ? `${seen.get(cat)} root(s)` : 'none found',
|
|
612
|
+
rule.length > 90 ? rule.slice(0, 87) + '…' : rule,
|
|
613
|
+
]);
|
|
614
|
+
const extraGlobals = [...extraDevGlobalNames()];
|
|
615
|
+
const extraPattern = process.env.MEMLAB_DEV_ARTIFACT_NODE_PATTERN;
|
|
616
|
+
const lines = [
|
|
617
|
+
'### Families checked',
|
|
618
|
+
'',
|
|
619
|
+
markdownTable(['Family', 'Found here', 'Match rule'], rows),
|
|
620
|
+
'',
|
|
621
|
+
extraGlobals.length > 0 || extraPattern
|
|
622
|
+
? `Deployment additions in effect: ${extraGlobals.length > 0 ? `globals [${extraGlobals.join(', ')}]` : ''}${extraGlobals.length > 0 && extraPattern ? '; ' : ''}${extraPattern ? `node pattern /${extraPattern}/` : ''}.`
|
|
623
|
+
: '_No deployment additions configured. If a family below reads "none found" but you know the surface has one, teach it: `MEMLAB_DEV_ARTIFACT_GLOBALS=name1,name2` for a window global, `MEMLAB_DEV_ARTIFACT_NODE_PATTERN=regex` for a module/bridge reached through the app\'s own registry._',
|
|
624
|
+
'',
|
|
625
|
+
'_"none found" means no ROOT for that family exists in this capture — it does not mean the family was ruled out for the objects you are looking at._',
|
|
626
|
+
];
|
|
627
|
+
return lines.join('\n');
|
|
628
|
+
}
|
|
78
629
|
export function registerDevArtifacts(server) {
|
|
79
|
-
server.tool('memlab_dev_artifacts', 'Classify large retainers as production-relevant vs. dev-only (browser snapshots). Flags any object retained ONLY through a dev/extension global (__REACT_DEVTOOLS_GLOBAL_HOOK__, __REDUX_DEVTOOLS_EXTENSION__, window.Debug, …) —
|
|
630
|
+
server.tool('memlab_dev_artifacts', 'Classify large retainers as production-relevant vs. dev/automation-only (browser snapshots). Flags any object retained ONLY through a dev/extension global (__REACT_DEVTOOLS_GLOBAL_HOOK__, __REDUX_DEVTOOLS_EXTENSION__, window.Debug, …), through a Blink accessibility cache (AXObjectCacheImpl/AXNodeObject/AXDirtyObject) inflated by CDP-driven automation building the a11y tree, OR through the attached inspector\'s console — objects passed to console.log / a dev-build devConsole are held by DevTools/CDP via "(Global handles) → … / DevTools console" edges (and can pin a whole detached DOM subtree, e.g. a logged {node} ref), which are GC\'d once DevTools/automation detaches. All three are measurement artifacts that should not be counted as production leaks. Reports total bytes attributable to them so "241 MB leak!" headlines that are really DevTools / a11y-cache / console retention get caught.', {
|
|
80
631
|
limit: z
|
|
81
632
|
.number()
|
|
82
633
|
.optional()
|
|
83
634
|
.default(25)
|
|
84
635
|
.describe('Maximum number of objects to classify (default 25).'),
|
|
636
|
+
explain: z
|
|
637
|
+
.boolean()
|
|
638
|
+
.optional()
|
|
639
|
+
.default(false)
|
|
640
|
+
.describe('List every artifact family this tool checks and whether roots for it were found in this snapshot (default false). A family that is missing from the built-in set produces a silent zero, which reads as "no artifacts here" — this makes the coverage visible so a miss can be recognised and, if needed, taught with MEMLAB_DEV_ARTIFACT_GLOBALS / MEMLAB_DEV_ARTIFACT_NODE_PATTERN.'),
|
|
85
641
|
min_retained_size: z
|
|
86
642
|
.number()
|
|
87
643
|
.optional()
|
|
@@ -92,30 +648,83 @@ export function registerDevArtifacts(server) {
|
|
|
92
648
|
.optional()
|
|
93
649
|
.default(false)
|
|
94
650
|
.describe('Show only the dev-only artifacts (default false: show both, dev-only flagged).'),
|
|
95
|
-
|
|
651
|
+
show_roots: z
|
|
652
|
+
.boolean()
|
|
653
|
+
.optional()
|
|
654
|
+
.default(false)
|
|
655
|
+
.describe('List every dev/automation root by name instead of summarizing them as counts per family (default false). The full list is unbounded — one root per a11y object and per bridge export — and cost ~1,500 tokens per call on a measured run. Use it only when chasing one specific root.'),
|
|
656
|
+
summary_only: z
|
|
657
|
+
.boolean()
|
|
658
|
+
.optional()
|
|
659
|
+
.default(true)
|
|
660
|
+
.describe('Headline totals and the per-source breakdown ONLY (DEFAULT TRUE). This is the largest single output of any tool here and the ranked table is re-read for a number that is already in the headline; pass false for the per-object classification table and the below-threshold aggregation'),
|
|
661
|
+
}, async ({ limit, min_retained_size, only_dev, show_roots, explain, summary_only, }) => {
|
|
96
662
|
try {
|
|
97
663
|
const snapshot = getSnapshot();
|
|
98
664
|
const meta = getSnapshotMetadata();
|
|
99
665
|
const totalSize = meta?.totalSize ?? 0;
|
|
100
666
|
const devRoots = collectDevRoots(snapshot);
|
|
101
667
|
if (devRoots.byId.size === 0) {
|
|
102
|
-
return toolResult('No dev/extension globals (__REACT_DEVTOOLS_GLOBAL_HOOK__, __REDUX_DEVTOOLS_EXTENSION__, window.Debug, …) found in this snapshot. ' +
|
|
103
|
-
'Either this is a production/clean capture, or none were installed — large retainers here are NOT dev artifacts.'
|
|
668
|
+
return toolResult('No dev/extension globals (__REACT_DEVTOOLS_GLOBAL_HOOK__, __REDUX_DEVTOOLS_EXTENSION__, window.Debug, …) or accessibility caches (AXObjectCacheImpl, …) found in this snapshot. ' +
|
|
669
|
+
'Either this is a production/clean capture, or none were installed — large retainers here are NOT dev/automation artifacts.' +
|
|
670
|
+
(explain ? '\n\n' + explainCoverage(devRoots) : ''));
|
|
104
671
|
}
|
|
672
|
+
// One reachability pass (dev roots as sinks) drives every
|
|
673
|
+
// classification below — catches multi-dev-root co-retention that a
|
|
674
|
+
// per-node dominator walk misses.
|
|
675
|
+
const reached = computeReachableWithoutDevRoots(snapshot, devRoots);
|
|
105
676
|
const cands = [];
|
|
106
|
-
|
|
677
|
+
// Whole-heap total, independent of `min_retained_size` (see
|
|
678
|
+
// summarizeDevOnly): the headline number must never be filtered by the
|
|
679
|
+
// display threshold.
|
|
680
|
+
const totals = summarizeDevOnly(snapshot, devRoots, reached);
|
|
681
|
+
let examined = 0;
|
|
682
|
+
let skippedBySize = 0;
|
|
683
|
+
// Dev-only objects that fall UNDER the display threshold, aggregated by
|
|
684
|
+
// class. The headline total already counts them, but the table did not
|
|
685
|
+
// show them at any size, so the common shape of an automation artifact —
|
|
686
|
+
// thousands of ~10 KB console-retained records, none individually near
|
|
687
|
+
// the 512 KB default — was invisible: a reader saw "N MB is dev-only",
|
|
688
|
+
// scanned an empty-looking table, and concluded there was nothing to
|
|
689
|
+
// discount. Grouping restores them without lowering the threshold.
|
|
690
|
+
const belowByClass = new Map();
|
|
691
|
+
let belowDevOnlyCount = 0;
|
|
692
|
+
let belowDevOnlyBytes = 0;
|
|
107
693
|
snapshot.nodes.forEach(node => {
|
|
108
694
|
if (node.id <= 3)
|
|
109
695
|
return;
|
|
110
|
-
if (node.retainedSize < min_retained_size)
|
|
111
|
-
return;
|
|
112
696
|
if (node.type !== 'object' &&
|
|
113
697
|
node.type !== 'closure' &&
|
|
114
698
|
node.type !== 'array')
|
|
115
699
|
return;
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
|
|
700
|
+
examined++;
|
|
701
|
+
if (node.retainedSize < min_retained_size) {
|
|
702
|
+
skippedBySize++;
|
|
703
|
+
// Test reachability directly rather than via classifyDevOnly: the
|
|
704
|
+
// latter also resolves the "via" label by walking retainers, which
|
|
705
|
+
// is far too expensive to pay per node on the below-threshold tail.
|
|
706
|
+
if (reached[node.nodeIndex] === 0) {
|
|
707
|
+
belowDevOnlyCount++;
|
|
708
|
+
// SELF size, not retained: retained sizes overlap wherever these
|
|
709
|
+
// objects nest, so summing them across a class reports more than
|
|
710
|
+
// exists. Self size is additive, and matches the units the
|
|
711
|
+
// "By source" breakdown above already uses.
|
|
712
|
+
belowDevOnlyBytes += node.self_size;
|
|
713
|
+
const key = node.name.length > 0
|
|
714
|
+
? truncateNodeName(node.name, node.type, node.self_size, 40)
|
|
715
|
+
: `(unnamed ${node.type})`;
|
|
716
|
+
const e = belowByClass.get(key);
|
|
717
|
+
if (e) {
|
|
718
|
+
e.count++;
|
|
719
|
+
e.bytes += node.self_size;
|
|
720
|
+
}
|
|
721
|
+
else {
|
|
722
|
+
belowByClass.set(key, { count: 1, bytes: node.self_size });
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
const { devOnly, via } = classifyDevOnly(node, devRoots, reached);
|
|
119
728
|
if (only_dev && !devOnly)
|
|
120
729
|
return;
|
|
121
730
|
// keep top-N by retained size
|
|
@@ -129,16 +738,48 @@ export function registerDevArtifacts(server) {
|
|
|
129
738
|
if (cands.length > limit)
|
|
130
739
|
cands.length = limit;
|
|
131
740
|
});
|
|
741
|
+
const breakdown = [...totals.byCategory.entries()]
|
|
742
|
+
.sort((a, b) => b[1].selfBytes - a[1].selfBytes)
|
|
743
|
+
.map(([cat, v]) => `${CATEGORY_LABEL[cat]} ${formatBytes(v.selfBytes)} self across ${formatNumber(v.nodes)} objects`);
|
|
132
744
|
const lines = [
|
|
133
745
|
'## Dev-only artifact classification',
|
|
134
746
|
'',
|
|
135
|
-
`Dev/
|
|
136
|
-
`Total retained held ONLY via dev artifacts: **${formatBytes(
|
|
747
|
+
`Dev/automation roots present: ${summarizeDevRoots(devRoots, show_roots)}`,
|
|
748
|
+
`Total retained held ONLY via dev/automation artifacts: **${formatBytes(totals.retained)}** across **${formatNumber(totals.nodes)} objects**` +
|
|
137
749
|
(totalSize > 0
|
|
138
|
-
? ` (${Math.min(100, (
|
|
750
|
+
? ` (${Math.min(100, (totals.retained / totalSize) * 100).toFixed(1)}% of heap — exclude from production leak totals)`
|
|
139
751
|
: ''),
|
|
140
|
-
'',
|
|
141
752
|
];
|
|
753
|
+
if (breakdown.length > 0) {
|
|
754
|
+
lines.push(`By source: ${breakdown.join(' · ')}`);
|
|
755
|
+
}
|
|
756
|
+
// Name the Fast Refresh registries and their BACKING STORES explicitly.
|
|
757
|
+
// The category rollup above already counts these bytes, but it counts
|
|
758
|
+
// them as a number, and the thing an operator actually needs is the
|
|
759
|
+
// sentence "the biggest `(unnamed array)` in your leak report is this".
|
|
760
|
+
// Without it the top grower gets hand-resolved every single round: it
|
|
761
|
+
// presents as an anonymous multi-MB array, `leak_report` can only say
|
|
762
|
+
// "50-70% dev-only" because the CLASS mixes Fast Refresh tables with
|
|
763
|
+
// real ones, and the operator ends up walking to the WeakMap by hand to
|
|
764
|
+
// find `{forceReset, ownKey, fullKey, getCustomHooks}` again.
|
|
765
|
+
const refreshTables = describeReactRefreshRegistries(snapshot, devRoots);
|
|
766
|
+
if (refreshTables.length > 0) {
|
|
767
|
+
lines.push('', '**React Fast Refresh registries (dev-only) — including backing stores:**', ...refreshTables.map(t => `- ${t}`));
|
|
768
|
+
}
|
|
769
|
+
lines.push('_This total covers the whole heap and is NOT limited by `min_retained_size` — high-count/low-size artifacts (per-event log strings, one logged object per interaction) are included._', '', "> ⚠️ **This is not the whole artifact bill.** This tool measures RETENTION-BY-A-DEV-ROOT (dev/extension globals, the inspector console, a11y caches, Fast Refresh, the automation bridge). It does NOT count **V8 JIT warmup** — `system/Code`, `InstructionStream`, `BytecodeArray`, `ProtectedFixedArray` — which is a separate and often larger bucket, and which grows simply because a hunt exercises new code paths. On one measured round this tool reported 4.8 MB / 1.5% dev-only while JIT warmup accounted for +5.7 MB of that round's 10.8 MB of growth. For the growth-side view of both, run `memlab_explain_delta` with `include_artifacts: true` against the baseline rung.", '');
|
|
770
|
+
if (summary_only) {
|
|
771
|
+
// Say what was withheld and how to get it. A tool that quietly prints
|
|
772
|
+
// less reads as a tool that found less.
|
|
773
|
+
lines.push(`_summary_only: the ${formatNumber(cands.length)}-row classification table` +
|
|
774
|
+
(belowDevOnlyCount > 0
|
|
775
|
+
? ` and the below-threshold aggregation (${formatNumber(belowDevOnlyCount)} objects)`
|
|
776
|
+
: '') +
|
|
777
|
+
' are withheld. Re-run with `summary_only: false` for them._');
|
|
778
|
+
if (explain) {
|
|
779
|
+
lines.push('', explainCoverage(devRoots));
|
|
780
|
+
}
|
|
781
|
+
return toolResult(lines.join('\n'));
|
|
782
|
+
}
|
|
142
783
|
if (cands.length > 0) {
|
|
143
784
|
const headers = ['ID', 'Name', 'Type', 'Retained', 'Classification'];
|
|
144
785
|
const rightCols = new Set([3]);
|
|
@@ -154,7 +795,27 @@ export function registerDevArtifacts(server) {
|
|
|
154
795
|
else {
|
|
155
796
|
lines.push('No objects matched the size threshold.');
|
|
156
797
|
}
|
|
157
|
-
lines.push('',
|
|
798
|
+
lines.push('', describeSkipped(examined, skippedBySize, `min_retained_size (${formatBytes(min_retained_size)})`));
|
|
799
|
+
// Below-threshold dev-only artifacts, grouped so a large aggregate made
|
|
800
|
+
// of small objects is visible in the table rather than only in the
|
|
801
|
+
// headline total.
|
|
802
|
+
if (belowDevOnlyCount > 0) {
|
|
803
|
+
const top = [...belowByClass.entries()]
|
|
804
|
+
.sort((a, b) => b[1].bytes - a[1].bytes)
|
|
805
|
+
.slice(0, 8);
|
|
806
|
+
lines.push('', `### Below \`min_retained_size\` — dev-only, aggregated by class`, '', `**${formatNumber(belowDevOnlyCount)} dev-only object(s) totalling ${formatBytes(belowDevOnlyBytes)} of self size** are individually under the ${formatBytes(min_retained_size)} threshold, so none appears above. This is the usual shape of an automation artifact: many small records, no single large one.`, '', markdownTable(['Class', 'Count', 'Self (sum)'], top.map(([name, v]) => [
|
|
807
|
+
name,
|
|
808
|
+
formatNumber(v.count),
|
|
809
|
+
formatBytes(v.bytes),
|
|
810
|
+
]), new Set([1, 2])));
|
|
811
|
+
if (belowByClass.size > top.length) {
|
|
812
|
+
lines.push('', `_… +${formatNumber(belowByClass.size - top.length)} more class(es). Lower \`min_retained_size\` to list individual objects._`);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
lines.push('', '_"dev-only" = the object\'s dominator chain passes through a dev/extension global, a Blink a11y cache, or an inspector "DevTools console" global handle, so every retainer path goes through it. Dev-global-retained objects would be GC\'d in production; a11y-cache and DevTools-console retention are automation/inspector-inflated (the a11y tree is materialized by CDP; console-logged objects are held by the attached inspector) and not present in a normal user session with DevTools closed. Either way, discount from production leak totals. Verify with `memlab_retainer_trace`._');
|
|
816
|
+
if (explain) {
|
|
817
|
+
lines.push('', explainCoverage(devRoots));
|
|
818
|
+
}
|
|
158
819
|
return toolResult(lines.join('\n'));
|
|
159
820
|
}
|
|
160
821
|
catch (err) {
|