@memlab/mcp-server 2.1.0 → 2.3.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 +88 -3
- package/dist/analysis-budget.d.ts +71 -0
- package/dist/analysis-budget.d.ts.map +1 -0
- package/dist/analysis-budget.js +118 -0
- package/dist/analysis-budget.js.map +1 -0
- package/dist/guardrail.d.ts +24 -0
- package/dist/guardrail.d.ts.map +1 -0
- package/dist/guardrail.js +65 -0
- package/dist/guardrail.js.map +1 -0
- package/dist/heap-state.d.ts +46 -1
- package/dist/heap-state.d.ts.map +1 -1
- package/dist/heap-state.js +142 -10
- package/dist/heap-state.js.map +1 -1
- package/dist/index.js +36 -1
- package/dist/index.js.map +1 -1
- package/dist/tools/array-group-by.d.ts +12 -0
- package/dist/tools/array-group-by.d.ts.map +1 -0
- package/dist/tools/array-group-by.js +191 -0
- package/dist/tools/array-group-by.js.map +1 -0
- package/dist/tools/auto-investigate.d.ts.map +1 -1
- package/dist/tools/auto-investigate.js +999 -37
- package/dist/tools/auto-investigate.js.map +1 -1
- package/dist/tools/cache-analysis.d.ts.map +1 -1
- package/dist/tools/cache-analysis.js +438 -21
- package/dist/tools/cache-analysis.js.map +1 -1
- package/dist/tools/check-health.d.ts.map +1 -1
- package/dist/tools/check-health.js +261 -1
- package/dist/tools/check-health.js.map +1 -1
- package/dist/tools/class-histogram.d.ts.map +1 -1
- package/dist/tools/class-histogram.js +142 -33
- package/dist/tools/class-histogram.js.map +1 -1
- package/dist/tools/closure-inspection.d.ts.map +1 -1
- package/dist/tools/closure-inspection.js +238 -25
- package/dist/tools/closure-inspection.js.map +1 -1
- package/dist/tools/detached-dom.d.ts.map +1 -1
- package/dist/tools/detached-dom.js +25 -3
- package/dist/tools/detached-dom.js.map +1 -1
- package/dist/tools/dev-artifacts.d.ts +33 -0
- package/dist/tools/dev-artifacts.d.ts.map +1 -0
- package/dist/tools/dev-artifacts.js +165 -0
- package/dist/tools/dev-artifacts.js.map +1 -0
- package/dist/tools/diff-snapshots.d.ts.map +1 -1
- package/dist/tools/diff-snapshots.js +113 -18
- package/dist/tools/diff-snapshots.js.map +1 -1
- package/dist/tools/duplicated-strings.d.ts.map +1 -1
- package/dist/tools/duplicated-strings.js +74 -12
- package/dist/tools/duplicated-strings.js.map +1 -1
- package/dist/tools/eval.d.ts.map +1 -1
- package/dist/tools/eval.js +189 -10
- 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 +235 -13
- package/dist/tools/event-listener-leaks.js.map +1 -1
- package/dist/tools/event-registry.d.ts +12 -0
- package/dist/tools/event-registry.d.ts.map +1 -0
- package/dist/tools/event-registry.js +183 -0
- package/dist/tools/event-registry.js.map +1 -0
- package/dist/tools/find-by-property.d.ts.map +1 -1
- package/dist/tools/find-by-property.js +113 -31
- package/dist/tools/find-by-property.js.map +1 -1
- package/dist/tools/find-by-shape.d.ts +12 -0
- package/dist/tools/find-by-shape.d.ts.map +1 -0
- package/dist/tools/find-by-shape.js +178 -0
- package/dist/tools/find-by-shape.js.map +1 -0
- package/dist/tools/find-nodes-by-class.d.ts.map +1 -1
- package/dist/tools/find-nodes-by-class.js +50 -6
- package/dist/tools/find-nodes-by-class.js.map +1 -1
- package/dist/tools/for-each.d.ts.map +1 -1
- package/dist/tools/for-each.js +1 -1
- package/dist/tools/for-each.js.map +1 -1
- package/dist/tools/get-node.d.ts.map +1 -1
- package/dist/tools/get-node.js +6 -0
- package/dist/tools/get-node.js.map +1 -1
- package/dist/tools/get-string.d.ts +12 -0
- package/dist/tools/get-string.d.ts.map +1 -0
- package/dist/tools/get-string.js +223 -0
- package/dist/tools/get-string.js.map +1 -0
- package/dist/tools/get-value.d.ts +12 -0
- package/dist/tools/get-value.d.ts.map +1 -0
- package/dist/tools/get-value.js +152 -0
- package/dist/tools/get-value.js.map +1 -0
- package/dist/tools/global-variables.d.ts.map +1 -1
- package/dist/tools/global-variables.js +54 -34
- package/dist/tools/global-variables.js.map +1 -1
- package/dist/tools/growth-signals.d.ts +12 -0
- package/dist/tools/growth-signals.d.ts.map +1 -0
- package/dist/tools/growth-signals.js +211 -0
- package/dist/tools/growth-signals.js.map +1 -0
- package/dist/tools/intern-opportunities.d.ts +12 -0
- package/dist/tools/intern-opportunities.d.ts.map +1 -0
- package/dist/tools/intern-opportunities.js +289 -0
- package/dist/tools/intern-opportunities.js.map +1 -0
- package/dist/tools/load-snapshot.d.ts +10 -0
- package/dist/tools/load-snapshot.d.ts.map +1 -1
- package/dist/tools/load-snapshot.js +152 -15
- package/dist/tools/load-snapshot.js.map +1 -1
- package/dist/tools/object-cost-breakdown.d.ts +12 -0
- package/dist/tools/object-cost-breakdown.d.ts.map +1 -0
- package/dist/tools/object-cost-breakdown.js +439 -0
- package/dist/tools/object-cost-breakdown.js.map +1 -0
- package/dist/tools/object-shape.d.ts.map +1 -1
- package/dist/tools/object-shape.js +95 -2
- package/dist/tools/object-shape.js.map +1 -1
- package/dist/tools/pinch-points.d.ts.map +1 -1
- package/dist/tools/pinch-points.js +9 -9
- package/dist/tools/pinch-points.js.map +1 -1
- package/dist/tools/property-distribution.d.ts +12 -0
- package/dist/tools/property-distribution.d.ts.map +1 -0
- package/dist/tools/property-distribution.js +168 -0
- package/dist/tools/property-distribution.js.map +1 -0
- package/dist/tools/quick-diagnosis.d.ts.map +1 -1
- package/dist/tools/quick-diagnosis.js +56 -19
- package/dist/tools/quick-diagnosis.js.map +1 -1
- package/dist/tools/retainer-summary.d.ts.map +1 -1
- package/dist/tools/retainer-summary.js +90 -5
- 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 +74 -22
- package/dist/tools/retainer-trace.js.map +1 -1
- package/dist/tools/search-nodes.d.ts.map +1 -1
- package/dist/tools/search-nodes.js +10 -3
- package/dist/tools/search-nodes.js.map +1 -1
- package/dist/tools/search-strings.d.ts +12 -0
- package/dist/tools/search-strings.d.ts.map +1 -0
- package/dist/tools/search-strings.js +182 -0
- package/dist/tools/search-strings.js.map +1 -0
- package/dist/tools/sequence-analysis.d.ts +12 -0
- package/dist/tools/sequence-analysis.d.ts.map +1 -0
- package/dist/tools/sequence-analysis.js +234 -0
- package/dist/tools/sequence-analysis.js.map +1 -0
- package/dist/tools/server-status.d.ts +12 -0
- package/dist/tools/server-status.d.ts.map +1 -0
- package/dist/tools/server-status.js +41 -0
- package/dist/tools/server-status.js.map +1 -0
- package/dist/tools/shape-histogram.d.ts.map +1 -1
- package/dist/tools/shape-histogram.js +61 -16
- 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 +16 -11
- package/dist/tools/sliced-strings.js.map +1 -1
- package/dist/tools/snapshot-summary.d.ts.map +1 -1
- package/dist/tools/snapshot-summary.js +55 -13
- package/dist/tools/snapshot-summary.js.map +1 -1
- package/dist/tools/snapshots.d.ts +12 -0
- package/dist/tools/snapshots.d.ts.map +1 -0
- package/dist/tools/snapshots.js +93 -0
- package/dist/tools/snapshots.js.map +1 -0
- package/dist/tools/stale-collections.d.ts.map +1 -1
- package/dist/tools/stale-collections.js +248 -7
- package/dist/tools/stale-collections.js.map +1 -1
- package/dist/tools/trace-dominators.d.ts.map +1 -1
- package/dist/tools/trace-dominators.js +28 -1
- package/dist/tools/trace-dominators.js.map +1 -1
- package/dist/tools/weakmap-entries.d.ts +12 -0
- package/dist/tools/weakmap-entries.d.ts.map +1 -0
- package/dist/tools/weakmap-entries.js +156 -0
- package/dist/tools/weakmap-entries.js.map +1 -0
- package/dist/utils.d.ts +81 -2
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +303 -35
- package/dist/utils.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that
|
|
|
6
6
|
|
|
7
7
|
The MCP server loads and analyzes large heap snapshots in memory, which can exceed Node.js's default heap limit. You need to configure `--max-old-space-size=8192` (or higher) to avoid out-of-memory crashes. The configuration examples below include this setting.
|
|
8
8
|
|
|
9
|
+
**Snapshot-size ceiling.** The server holds the full parsed graph (nodes, edges, dominator tree, indexes) resident, so its own RSS is roughly **3–5× the on-disk snapshot size**. With the recommended `--max-old-space-size=8192`, snapshots up to ~850 MB on disk have been analyzed reliably; `memlab_load_snapshot` refuses files above `max_file_size_mb` (default 900) to avoid OOM crashes that would lose all state. For larger heaps, raise both `--max-old-space-size` and `max_file_size_mb`. When loading multiple snapshots with `keep_previous: true`, each resident snapshot adds its full graph to RSS — unload ones you're done with via `memlab_snapshots`.
|
|
10
|
+
|
|
9
11
|
## Quick Start
|
|
10
12
|
|
|
11
13
|
### Option 1: Global install
|
|
@@ -126,11 +128,79 @@ console.log(`Heap snapshot written to ${snapshot}`);
|
|
|
126
128
|
|
|
127
129
|
### `memlab_load_snapshot`
|
|
128
130
|
|
|
129
|
-
Load and parse a `.heapsnapshot` file. Builds indexes, computes the dominator tree, and calculates retained sizes.
|
|
131
|
+
Load and parse a `.heapsnapshot` file. Builds indexes, computes the dominator tree, and calculates retained sizes. `file_path` may be a local absolute path, a `manifold://bucket/key` URL, or a bare snapshot filename (resolved against the `nest_server_nodejs_heap_snapshots` bucket and fetched via `manifold get`). Pass `keep_previous: true` to keep earlier snapshots resident for diffing/comparison (each gets a handle; manage with `memlab_snapshots`). `quiet` / `suppress_suggestions` set session-wide output controls to trim repeated boilerplate.
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
Input: { file_path: "snap.heapsnapshot" | "/abs/path" | "manifold://bucket/key",
|
|
135
|
+
alias?: "before", keep_previous?: false, quiet?: false,
|
|
136
|
+
suppress_suggestions?: false, max_file_size_mb?: 900 }
|
|
137
|
+
Output: { status, file_path, node_count, edge_count, total_size, handle }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### `memlab_snapshots`
|
|
141
|
+
|
|
142
|
+
Manage the multi-snapshot session and session output controls.
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
Input: { action?: "list"|"switch"|"unload", handle?: "before",
|
|
146
|
+
quiet?: bool, suppress_suggestions?: bool }
|
|
147
|
+
Output: resident snapshots (active one marked), or switch/unload result
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### `memlab_property_distribution`
|
|
151
|
+
|
|
152
|
+
For a class/shape and a property, report value cardinality plus the top-K most frequent values. The key tool for diagnosing cardinality explosions (OTel metric attributes, cache keys, per-record fields).
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
Input: { property: "http.route", class_name?: "Object", shape?: ["a","b"],
|
|
156
|
+
top_k?: 15, min_count?: 1 }
|
|
157
|
+
Output: { scanned, distinct_values, top_values: [{ value, count, pct }] }
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### `memlab_growth_signals`
|
|
161
|
+
|
|
162
|
+
Single-snapshot heuristic that flags likely unbounded growth: Maps/Sets keyed by timestamps or sequential integers, and large ever-growing Arrays. Confirm with a later snapshot + `memlab_diff_snapshots`.
|
|
130
163
|
|
|
131
164
|
```
|
|
132
|
-
Input: {
|
|
133
|
-
Output:
|
|
165
|
+
Input: { limit?: 15, min_entries?: 200, min_retained_size?: 262144 }
|
|
166
|
+
Output: candidates with kind, entry count, retained size, sample keys
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### `memlab_sequence_analysis`
|
|
170
|
+
|
|
171
|
+
Trend analysis across an ordered sequence of 3+ snapshots. Loads each transiently (does not change the active snapshot), reports each class's count at every step, and labels "↑ every step" (leak signal) vs "grew net (noisy)". Lists classes new since baseline.
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
Input: { paths: ["a","b","c"], limit?: 25, min_growth_count?: 50,
|
|
175
|
+
monotonic_only?: false, max_file_size_mb?: 900 }
|
|
176
|
+
Output: per-step heap totals + growing classes with per-step counts and verdict
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### `memlab_dev_artifacts`
|
|
180
|
+
|
|
181
|
+
Browser snapshots: classify large retainers as production vs. dev-only (retained solely via `__REACT_DEVTOOLS_GLOBAL_HOOK__`, `__REDUX_DEVTOOLS_EXTENSION__`, `window.Debug`, …) and total the bytes to exclude from leak headlines. `memlab_detached_dom` also reports the dev-only share inline.
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
Input: { limit?: 25, min_retained_size?: 524288, only_dev?: false }
|
|
185
|
+
Output: dev-only byte total + per-object classification (production | dev-only via <global>)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### `memlab_event_registry`
|
|
189
|
+
|
|
190
|
+
Detector for per-model event registries (Backbone/observer): objects mapping event names to arrays of `{callback, context}`. Reports top event names by listener count, listeners-per-host distribution, and a structural-vs-leak verdict.
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
Input: { min_events?: 2, limit?: 20, timeout_ms?: 45000 }
|
|
194
|
+
Output: registry stats + top events + verdict (structural O(hosts) vs re-subscription leak)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### `memlab_server_status`
|
|
198
|
+
|
|
199
|
+
Cheap liveness/health check — returns instantly with process RSS, uptime, and resident snapshots. Use to confirm the server is responsive (vs. stuck behind a heavy scan) and to watch RSS against the snapshot-size ceiling.
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
Input: {}
|
|
203
|
+
Output: status, uptime, RSS, resident snapshots
|
|
134
204
|
```
|
|
135
205
|
|
|
136
206
|
### `memlab_snapshot_summary`
|
|
@@ -305,6 +375,21 @@ Structured map/filter/reduce over all heap nodes with code predicates.
|
|
|
305
375
|
Input: { filter_code: "node.type === 'closure'", map_code?: "...", reduce_code?: "...", limit?: 100 }
|
|
306
376
|
```
|
|
307
377
|
|
|
378
|
+
## Investigation Skill for AI Assistants
|
|
379
|
+
|
|
380
|
+
The [MCP Investigation Skill](./MCP_SKILL.md) provides a structured
|
|
381
|
+
methodology for AI coding assistants to systematically investigate memory
|
|
382
|
+
issues using the MCP tools. It covers:
|
|
383
|
+
|
|
384
|
+
- **Step-by-step triage** — load, diagnose, and prioritize findings
|
|
385
|
+
- **Investigation paths** — guided workflows for string waste, DOM leaks,
|
|
386
|
+
object accumulation, closure leaks, global bloat, listener accumulation, and
|
|
387
|
+
repeated errors, each with triggers, tool sequences, and common fixes
|
|
388
|
+
- **Tool selection reference** — a quick-reference table mapping investigation
|
|
389
|
+
goals to the right MCP tool
|
|
390
|
+
- **Token efficiency tips** — how to minimize token usage with compact modes
|
|
391
|
+
and parallel tool calls
|
|
392
|
+
|
|
308
393
|
## Example Workflow
|
|
309
394
|
|
|
310
395
|
A typical memory investigation:
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* @format
|
|
8
|
+
* @oncall memory_lab
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Wall-clock guardrail for heap analysis.
|
|
12
|
+
*
|
|
13
|
+
* Node.js is single-threaded, so a timer cannot interrupt a synchronous
|
|
14
|
+
* full-heap loop mid-iteration — the guardrail has to be COOPERATIVE: hot loops
|
|
15
|
+
* call `tick()` (cheap, clock checked only every few thousand iterations) and a
|
|
16
|
+
* {@link ScanTimeoutError} is thrown once the budget is exceeded. The per-call
|
|
17
|
+
* wrapper (see guardrail.ts) arms a process-global "active" budget around every
|
|
18
|
+
* tool invocation, and the shared `snapshot.nodes`/`edges.forEach` funnels feed
|
|
19
|
+
* it via {@link tickAnalysis}, so any tool that does a full-heap scan is bounded
|
|
20
|
+
* without per-tool wiring.
|
|
21
|
+
*
|
|
22
|
+
* This module imports nothing so it can be shared by utils, heap-state and the
|
|
23
|
+
* guardrail wrapper without creating an import cycle.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Thrown by a budget when a heap walk exceeds its wall-clock limit. Callers
|
|
27
|
+
* either catch it locally to return partial results, or let it bubble to the
|
|
28
|
+
* per-call guardrail which turns it into a clean "analysis stopped" result.
|
|
29
|
+
*/
|
|
30
|
+
export declare class ScanTimeoutError extends Error {
|
|
31
|
+
iterations: number;
|
|
32
|
+
timeoutMs: number;
|
|
33
|
+
constructor(iterations: number, timeoutMs: number);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Wall-clock budget. Call `tick()` once per iteration; it cheaply checks
|
|
37
|
+
* elapsed time every few thousand iterations and throws {@link ScanTimeoutError}
|
|
38
|
+
* when the budget is exceeded.
|
|
39
|
+
*/
|
|
40
|
+
export declare function makeScanBudget(timeoutMs: number): {
|
|
41
|
+
tick: () => void;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Resolve the analysis timeout in ms: an explicit per-call override wins, then
|
|
45
|
+
* the MEMLAB_ANALYSIS_TIMEOUT_MS env var, then the 90s default. A value <= 0
|
|
46
|
+
* disables the guardrail (returns 0 → never trips).
|
|
47
|
+
*/
|
|
48
|
+
export declare function getAnalysisTimeoutMs(override?: number | null): number;
|
|
49
|
+
/** Arm the global budget for one tool call. `timeoutMs <= 0` disables it. */
|
|
50
|
+
export declare function beginAnalysisBudget(timeoutMs: number): void;
|
|
51
|
+
/** Disarm the global budget at the end of a tool call. */
|
|
52
|
+
export declare function endAnalysisBudget(): void;
|
|
53
|
+
/**
|
|
54
|
+
* Cooperative checkpoint for high-iteration loops (e.g. full-heap scans). Cheap
|
|
55
|
+
* no-op when no budget is armed; the clock is only sampled every few thousand
|
|
56
|
+
* calls, so this is meant for loops that run millions of times. For loops with
|
|
57
|
+
* few iterations but expensive bodies (e.g. per-class dominator aggregation),
|
|
58
|
+
* use {@link checkAnalysisDeadline} instead.
|
|
59
|
+
*/
|
|
60
|
+
export declare function tickAnalysis(): void;
|
|
61
|
+
/**
|
|
62
|
+
* Unconditional deadline check (samples the clock every call). Use in loops
|
|
63
|
+
* with a small iteration count but an expensive body, where the sampled
|
|
64
|
+
* {@link tickAnalysis} would not check often enough to be effective.
|
|
65
|
+
*/
|
|
66
|
+
export declare function checkAnalysisDeadline(): void;
|
|
67
|
+
/** Wall-clock ms elapsed in the current tool call (0 if none armed). */
|
|
68
|
+
export declare function activeElapsedMs(): number;
|
|
69
|
+
/** The timeout the active budget was armed with (0 if disabled/none). */
|
|
70
|
+
export declare function activeTimeoutMs_(): number;
|
|
71
|
+
//# sourceMappingURL=analysis-budget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analysis-budget.d.ts","sourceRoot":"","sources":["../src/analysis-budget.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;;;;;;;GAcG;AAEH;;;;GAIG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;IAEhC,UAAU,EAAE,MAAM;IAClB,SAAS,EAAE,MAAM;gBADjB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM;CAK3B;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG;IAAC,IAAI,EAAE,MAAM,IAAI,CAAA;CAAC,CAWpE;AAYD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CASrE;AAED,6EAA6E;AAC7E,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAI3D;AAED,0DAA0D;AAC1D,wBAAgB,iBAAiB,IAAI,IAAI,CAGxC;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,IAAI,IAAI,CAEnC;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAI5C;AAED,wEAAwE;AACxE,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,yEAAyE;AACzE,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* @format
|
|
8
|
+
* @oncall memory_lab
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Wall-clock guardrail for heap analysis.
|
|
12
|
+
*
|
|
13
|
+
* Node.js is single-threaded, so a timer cannot interrupt a synchronous
|
|
14
|
+
* full-heap loop mid-iteration — the guardrail has to be COOPERATIVE: hot loops
|
|
15
|
+
* call `tick()` (cheap, clock checked only every few thousand iterations) and a
|
|
16
|
+
* {@link ScanTimeoutError} is thrown once the budget is exceeded. The per-call
|
|
17
|
+
* wrapper (see guardrail.ts) arms a process-global "active" budget around every
|
|
18
|
+
* tool invocation, and the shared `snapshot.nodes`/`edges.forEach` funnels feed
|
|
19
|
+
* it via {@link tickAnalysis}, so any tool that does a full-heap scan is bounded
|
|
20
|
+
* without per-tool wiring.
|
|
21
|
+
*
|
|
22
|
+
* This module imports nothing so it can be shared by utils, heap-state and the
|
|
23
|
+
* guardrail wrapper without creating an import cycle.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Thrown by a budget when a heap walk exceeds its wall-clock limit. Callers
|
|
27
|
+
* either catch it locally to return partial results, or let it bubble to the
|
|
28
|
+
* per-call guardrail which turns it into a clean "analysis stopped" result.
|
|
29
|
+
*/
|
|
30
|
+
export class ScanTimeoutError extends Error {
|
|
31
|
+
iterations;
|
|
32
|
+
timeoutMs;
|
|
33
|
+
constructor(iterations, timeoutMs) {
|
|
34
|
+
super(`Scan exceeded its ${timeoutMs}ms budget after ~${iterations} nodes`);
|
|
35
|
+
this.iterations = iterations;
|
|
36
|
+
this.timeoutMs = timeoutMs;
|
|
37
|
+
this.name = 'ScanTimeoutError';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Wall-clock budget. Call `tick()` once per iteration; it cheaply checks
|
|
42
|
+
* elapsed time every few thousand iterations and throws {@link ScanTimeoutError}
|
|
43
|
+
* when the budget is exceeded.
|
|
44
|
+
*/
|
|
45
|
+
export function makeScanBudget(timeoutMs) {
|
|
46
|
+
const start = Date.now();
|
|
47
|
+
let i = 0;
|
|
48
|
+
return {
|
|
49
|
+
tick() {
|
|
50
|
+
i++;
|
|
51
|
+
if ((i & 0x3fff) === 0 && Date.now() - start > timeoutMs) {
|
|
52
|
+
throw new ScanTimeoutError(i, timeoutMs);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Process-global "active" budget for the current tool call.
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
const DEFAULT_ANALYSIS_TIMEOUT_MS = 90_000;
|
|
61
|
+
let activeBudget = null;
|
|
62
|
+
let activeStart = 0;
|
|
63
|
+
let activeTimeoutMs = 0;
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the analysis timeout in ms: an explicit per-call override wins, then
|
|
66
|
+
* the MEMLAB_ANALYSIS_TIMEOUT_MS env var, then the 90s default. A value <= 0
|
|
67
|
+
* disables the guardrail (returns 0 → never trips).
|
|
68
|
+
*/
|
|
69
|
+
export function getAnalysisTimeoutMs(override) {
|
|
70
|
+
if (override != null && Number.isFinite(override)) {
|
|
71
|
+
return override > 0 ? override : 0;
|
|
72
|
+
}
|
|
73
|
+
const env = Number(process.env.MEMLAB_ANALYSIS_TIMEOUT_MS);
|
|
74
|
+
if (Number.isFinite(env)) {
|
|
75
|
+
return env > 0 ? env : 0;
|
|
76
|
+
}
|
|
77
|
+
return DEFAULT_ANALYSIS_TIMEOUT_MS;
|
|
78
|
+
}
|
|
79
|
+
/** Arm the global budget for one tool call. `timeoutMs <= 0` disables it. */
|
|
80
|
+
export function beginAnalysisBudget(timeoutMs) {
|
|
81
|
+
activeStart = Date.now();
|
|
82
|
+
activeTimeoutMs = timeoutMs;
|
|
83
|
+
activeBudget = timeoutMs > 0 ? makeScanBudget(timeoutMs) : null;
|
|
84
|
+
}
|
|
85
|
+
/** Disarm the global budget at the end of a tool call. */
|
|
86
|
+
export function endAnalysisBudget() {
|
|
87
|
+
activeBudget = null;
|
|
88
|
+
activeTimeoutMs = 0;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Cooperative checkpoint for high-iteration loops (e.g. full-heap scans). Cheap
|
|
92
|
+
* no-op when no budget is armed; the clock is only sampled every few thousand
|
|
93
|
+
* calls, so this is meant for loops that run millions of times. For loops with
|
|
94
|
+
* few iterations but expensive bodies (e.g. per-class dominator aggregation),
|
|
95
|
+
* use {@link checkAnalysisDeadline} instead.
|
|
96
|
+
*/
|
|
97
|
+
export function tickAnalysis() {
|
|
98
|
+
activeBudget?.tick();
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Unconditional deadline check (samples the clock every call). Use in loops
|
|
102
|
+
* with a small iteration count but an expensive body, where the sampled
|
|
103
|
+
* {@link tickAnalysis} would not check often enough to be effective.
|
|
104
|
+
*/
|
|
105
|
+
export function checkAnalysisDeadline() {
|
|
106
|
+
if (activeTimeoutMs > 0 && Date.now() - activeStart > activeTimeoutMs) {
|
|
107
|
+
throw new ScanTimeoutError(-1, activeTimeoutMs);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Wall-clock ms elapsed in the current tool call (0 if none armed). */
|
|
111
|
+
export function activeElapsedMs() {
|
|
112
|
+
return activeStart ? Date.now() - activeStart : 0;
|
|
113
|
+
}
|
|
114
|
+
/** The timeout the active budget was armed with (0 if disabled/none). */
|
|
115
|
+
export function activeTimeoutMs_() {
|
|
116
|
+
return activeTimeoutMs;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=analysis-budget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analysis-budget.js","sourceRoot":"","sources":["../src/analysis-budget.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* @format
|
|
8
|
+
* @oncall memory_lab
|
|
9
|
+
*/
|
|
10
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
11
|
+
/**
|
|
12
|
+
* Install a process-wide wall-clock guardrail around every tool call.
|
|
13
|
+
*
|
|
14
|
+
* Monkey-patches `server.tool` so each registered handler is wrapped: a budget
|
|
15
|
+
* is armed for the call (per-call `timeout_ms` arg > MEMLAB_ANALYSIS_TIMEOUT_MS
|
|
16
|
+
* env > 90s default), the heap-iteration funnels (see heap-state.ts) feed it,
|
|
17
|
+
* and if the budget trips the handler's loop throws a {@link ScanTimeoutError}
|
|
18
|
+
* which we convert into a clean "analysis stopped" result instead of letting a
|
|
19
|
+
* runaway scan wedge / OOM the server.
|
|
20
|
+
*
|
|
21
|
+
* Must be called BEFORE the tools are registered so they register wrapped.
|
|
22
|
+
*/
|
|
23
|
+
export declare function installAnalysisGuardrail(server: McpServer): void;
|
|
24
|
+
//# sourceMappingURL=guardrail.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guardrail.d.ts","sourceRoot":"","sources":["../src/guardrail.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,yCAAyC,CAAC;AAYvE;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA2ChE"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* @format
|
|
8
|
+
* @oncall memory_lab
|
|
9
|
+
*/
|
|
10
|
+
import { beginAnalysisBudget, endAnalysisBudget, getAnalysisTimeoutMs, activeElapsedMs, ScanTimeoutError, } from './analysis-budget.js';
|
|
11
|
+
import { toolResult } from './utils.js';
|
|
12
|
+
/**
|
|
13
|
+
* Install a process-wide wall-clock guardrail around every tool call.
|
|
14
|
+
*
|
|
15
|
+
* Monkey-patches `server.tool` so each registered handler is wrapped: a budget
|
|
16
|
+
* is armed for the call (per-call `timeout_ms` arg > MEMLAB_ANALYSIS_TIMEOUT_MS
|
|
17
|
+
* env > 90s default), the heap-iteration funnels (see heap-state.ts) feed it,
|
|
18
|
+
* and if the budget trips the handler's loop throws a {@link ScanTimeoutError}
|
|
19
|
+
* which we convert into a clean "analysis stopped" result instead of letting a
|
|
20
|
+
* runaway scan wedge / OOM the server.
|
|
21
|
+
*
|
|
22
|
+
* Must be called BEFORE the tools are registered so they register wrapped.
|
|
23
|
+
*/
|
|
24
|
+
export function installAnalysisGuardrail(server) {
|
|
25
|
+
const origTool = server.tool.bind(server);
|
|
26
|
+
server.tool = (...toolArgs) => {
|
|
27
|
+
const name = String(toolArgs[0]);
|
|
28
|
+
const lastIdx = toolArgs.length - 1;
|
|
29
|
+
const handler = toolArgs[lastIdx];
|
|
30
|
+
if (typeof handler === 'function') {
|
|
31
|
+
const inner = handler;
|
|
32
|
+
toolArgs[lastIdx] = async (...hArgs) => {
|
|
33
|
+
// The SDK calls handler(args, extra) for schema tools, handler(extra)
|
|
34
|
+
// otherwise. Read an optional per-call timeout_ms off the params.
|
|
35
|
+
const params = hArgs[0];
|
|
36
|
+
const override = params && typeof params === 'object' && 'timeout_ms' in params
|
|
37
|
+
? params.timeout_ms
|
|
38
|
+
: undefined;
|
|
39
|
+
const timeoutMs = getAnalysisTimeoutMs(typeof override === 'number' ? override : undefined);
|
|
40
|
+
beginAnalysisBudget(timeoutMs);
|
|
41
|
+
try {
|
|
42
|
+
return await inner(...hArgs);
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
if (e instanceof ScanTimeoutError) {
|
|
46
|
+
const ran = Math.round(activeElapsedMs() / 1000);
|
|
47
|
+
const limit = Math.round(timeoutMs / 1000);
|
|
48
|
+
return toolResult(`⚠ Analysis stopped: \`${name}\` exceeded the ${limit}s guardrail (ran ~${ran}s) and was halted to keep the MCP server responsive. No complete result was produced.\n\n` +
|
|
49
|
+
`This usually means the heap is very large. Options:\n` +
|
|
50
|
+
`- Narrow the query with filters (min_count / min_self_size / min_retained_size / a specific name or type).\n` +
|
|
51
|
+
`- For memlab_class_histogram, pass include_retained_size:false for an instant count + self-size histogram.\n` +
|
|
52
|
+
`- Prefer targeted node-id tools (memlab_object_shape, memlab_trace_dominators, memlab_retainer_trace) or memlab_auto_investigate over whole-heap scans.\n` +
|
|
53
|
+
`- Raise the limit for this call where supported (timeout_ms) or globally via the MEMLAB_ANALYSIS_TIMEOUT_MS env var (current default ${limit}s; set 0 to disable).`);
|
|
54
|
+
}
|
|
55
|
+
throw e;
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
endAnalysisBudget();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return origTool(...toolArgs);
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=guardrail.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guardrail.js","sourceRoot":"","sources":["../src/guardrail.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,oBAAoB,EACpB,eAAe,EACf,gBAAgB,GACjB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAC,UAAU,EAAC,MAAM,YAAY,CAAC;AAItC;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,wBAAwB,CAAC,MAAiB;IACxD,MAAM,QAAQ,GAAI,MAAM,CAAC,IAAc,CAAC,IAAI,CAAC,MAAM,CAAU,CAAC;IAC7D,MAAmC,CAAC,IAAI,GAAG,CAAC,GAAG,QAAmB,EAAE,EAAE;QACrE,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QACjC,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;QACpC,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;QAClC,IAAI,OAAO,OAAO,KAAK,UAAU,EAAE,CAAC;YAClC,MAAM,KAAK,GAAG,OAAgB,CAAC;YAC/B,QAAQ,CAAC,OAAO,CAAC,GAAG,KAAK,EAAE,GAAG,KAAgB,EAAE,EAAE;gBAChD,sEAAsE;gBACtE,kEAAkE;gBAClE,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;gBACxB,MAAM,QAAQ,GACZ,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,YAAY,IAAI,MAAM;oBAC5D,CAAC,CAAE,MAAiC,CAAC,UAAU;oBAC/C,CAAC,CAAC,SAAS,CAAC;gBAChB,MAAM,SAAS,GAAG,oBAAoB,CACpC,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CACpD,CAAC;gBACF,mBAAmB,CAAC,SAAS,CAAC,CAAC;gBAC/B,IAAI,CAAC;oBACH,OAAO,MAAM,KAAK,CAAC,GAAG,KAAK,CAAC,CAAC;gBAC/B,CAAC;gBAAC,OAAO,CAAC,EAAE,CAAC;oBACX,IAAI,CAAC,YAAY,gBAAgB,EAAE,CAAC;wBAClC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,eAAe,EAAE,GAAG,IAAI,CAAC,CAAC;wBACjD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;wBAC3C,OAAO,UAAU,CACf,yBAAyB,IAAI,mBAAmB,KAAK,qBAAqB,GAAG,2FAA2F;4BACtK,uDAAuD;4BACvD,8GAA8G;4BAC9G,8GAA8G;4BAC9G,2JAA2J;4BAC3J,wIAAwI,KAAK,uBAAuB,CACvK,CAAC;oBACJ,CAAC;oBACD,MAAM,CAAC,CAAC;gBACV,CAAC;wBAAS,CAAC;oBACT,iBAAiB,EAAE,CAAC;gBACtB,CAAC;YACH,CAAC,CAAC;QACJ,CAAC;QACD,OAAO,QAAQ,CAAC,GAAG,QAAQ,CAAC,CAAC;IAC/B,CAAC,CAAC;AACJ,CAAC"}
|
package/dist/heap-state.d.ts
CHANGED
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
import type { IHeapSnapshot } from '@memlab/core';
|
|
11
11
|
export type SnapshotEnv = 'browser' | 'node' | 'unknown';
|
|
12
12
|
export interface SnapshotMetadata {
|
|
13
|
+
/**
|
|
14
|
+
* Stable, human-friendly handle for this snapshot within the session.
|
|
15
|
+
* Node ids are only valid relative to the snapshot they were read from;
|
|
16
|
+
* the handle identifies which loaded snapshot a given id belongs to.
|
|
17
|
+
*/
|
|
18
|
+
handle: string;
|
|
13
19
|
filePath: string;
|
|
14
20
|
fileName: string;
|
|
15
21
|
nodeCount: number;
|
|
@@ -17,9 +23,48 @@ export interface SnapshotMetadata {
|
|
|
17
23
|
totalSize: number;
|
|
18
24
|
env: SnapshotEnv;
|
|
19
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Session-level output controls to trim repeated boilerplate tokens.
|
|
28
|
+
* - `quietHeader`: when true, the per-call snapshot header is printed only
|
|
29
|
+
* once per loaded snapshot instead of on every tool result.
|
|
30
|
+
* - `suppressSuggestions`: when true, tools omit their "Suggested next steps"
|
|
31
|
+
* trailers.
|
|
32
|
+
*/
|
|
33
|
+
export interface SessionConfig {
|
|
34
|
+
quietHeader: boolean;
|
|
35
|
+
suppressSuggestions: boolean;
|
|
36
|
+
}
|
|
37
|
+
export declare function getSessionConfig(): SessionConfig;
|
|
38
|
+
export declare function setSessionConfig(patch: Partial<SessionConfig>): SessionConfig;
|
|
39
|
+
/**
|
|
40
|
+
* Returns true exactly once per loaded snapshot when `quietHeader` is on,
|
|
41
|
+
* and always when it is off. Tools call this from `toolResult` to decide
|
|
42
|
+
* whether to prepend the snapshot header.
|
|
43
|
+
*/
|
|
44
|
+
export declare function shouldEmitHeader(): boolean;
|
|
20
45
|
export declare function getSnapshot(): IHeapSnapshot;
|
|
21
46
|
export declare function getFilePath(): string | null;
|
|
22
47
|
export declare function getSnapshotEnv(): SnapshotEnv;
|
|
23
48
|
export declare function getSnapshotMetadata(): SnapshotMetadata | null;
|
|
24
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Register a snapshot and make it the current one.
|
|
51
|
+
*
|
|
52
|
+
* @param snapshot parsed heap snapshot
|
|
53
|
+
* @param filePath absolute path the snapshot was loaded from
|
|
54
|
+
* @param metadata derived stats (handle is assigned here if not supplied)
|
|
55
|
+
* @param opts.alias preferred handle; falls back to the file name
|
|
56
|
+
* @param opts.replace when true (default), unload all other snapshots first,
|
|
57
|
+
* preserving the single-resident memory profile. When false, keep
|
|
58
|
+
* previously-loaded snapshots resident for diffing/comparison.
|
|
59
|
+
*/
|
|
60
|
+
export declare function setSnapshot(snapshot: IHeapSnapshot, filePath: string, metadata: Omit<SnapshotMetadata, 'filePath' | 'handle'>, opts?: {
|
|
61
|
+
alias?: string;
|
|
62
|
+
replace?: boolean;
|
|
63
|
+
}): SnapshotMetadata;
|
|
64
|
+
export declare function listSnapshots(): SnapshotMetadata[];
|
|
65
|
+
export declare function getCurrentHandle(): string | null;
|
|
66
|
+
export declare function getSnapshotByHandle(handle: string): IHeapSnapshot | null;
|
|
67
|
+
export declare function getMetadataByHandle(handle: string): SnapshotMetadata | null;
|
|
68
|
+
export declare function setCurrentSnapshot(handle: string): boolean;
|
|
69
|
+
export declare function removeSnapshot(handle: string): boolean;
|
|
25
70
|
//# sourceMappingURL=heap-state.d.ts.map
|
package/dist/heap-state.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"heap-state.d.ts","sourceRoot":"","sources":["../src/heap-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"heap-state.d.ts","sourceRoot":"","sources":["../src/heap-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,cAAc,CAAC;AAGhD,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;AAgCzD,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,WAAW,CAAC;CAClB;AAaD;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,OAAO,CAAC;IACrB,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAWD,wBAAgB,gBAAgB,IAAI,aAAa,CAEhD;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,aAAa,CAM7E;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAK1C;AAgBD,wBAAgB,WAAW,IAAI,aAAa,CAO3C;AAED,wBAAgB,WAAW,IAAI,MAAM,GAAG,IAAI,CAG3C;AAED,wBAAgB,cAAc,IAAI,WAAW,CAG5C;AAED,wBAAgB,mBAAmB,IAAI,gBAAgB,GAAG,IAAI,CAG7D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CACzB,QAAQ,EAAE,aAAa,EACvB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,IAAI,CAAC,gBAAgB,EAAE,UAAU,GAAG,QAAQ,CAAC,EACvD,IAAI,GAAE;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAM,GAC7C,gBAAgB,CAWlB;AAED,wBAAgB,aAAa,IAAI,gBAAgB,EAAE,CAElD;AAED,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAEhD;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAExE;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAE3E;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAK1D;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAOtD"}
|
package/dist/heap-state.js
CHANGED
|
@@ -7,25 +7,157 @@
|
|
|
7
7
|
* @format
|
|
8
8
|
* @oncall memory_lab
|
|
9
9
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
import { tickAnalysis } from './analysis-budget.js';
|
|
11
|
+
// Wrap the snapshot's full-collection iterators so every heap walk feeds the
|
|
12
|
+
// active wall-clock guardrail (see analysis-budget.ts) without each tool having
|
|
13
|
+
// to wire it in. `snapshot.nodes`/`snapshot.edges` are stable objects assigned
|
|
14
|
+
// once at parse time, so patching their `forEach` once is durable; we mark the
|
|
15
|
+
// collection to stay idempotent across the many getSnapshot() calls per session.
|
|
16
|
+
const TICK_PATCHED = '__memlabTickPatched';
|
|
17
|
+
function patchForEachMethods(collection, methods) {
|
|
18
|
+
if (!collection || typeof collection !== 'object')
|
|
19
|
+
return;
|
|
20
|
+
const c = collection;
|
|
21
|
+
if (c[TICK_PATCHED])
|
|
22
|
+
return;
|
|
23
|
+
for (const m of methods) {
|
|
24
|
+
const fn = c[m];
|
|
25
|
+
if (typeof fn !== 'function')
|
|
26
|
+
continue;
|
|
27
|
+
const orig = fn.bind(c);
|
|
28
|
+
c[m] = (cb) => orig((...args) => {
|
|
29
|
+
tickAnalysis();
|
|
30
|
+
return cb(...args);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
Object.defineProperty(c, TICK_PATCHED, { value: true, enumerable: false });
|
|
34
|
+
}
|
|
35
|
+
function instrumentSnapshot(snapshot) {
|
|
36
|
+
const s = snapshot;
|
|
37
|
+
patchForEachMethods(s.nodes, ['forEach', 'forEachTraceable']);
|
|
38
|
+
patchForEachMethods(s.edges, ['forEach']);
|
|
39
|
+
}
|
|
40
|
+
// Multiple snapshots can be resident at once (for diffing and before/after
|
|
41
|
+
// comparison). The "current" handle is the default target for tools that
|
|
42
|
+
// don't take an explicit snapshot argument.
|
|
43
|
+
const loaded = new Map();
|
|
44
|
+
let currentHandle = null;
|
|
45
|
+
const sessionConfig = {
|
|
46
|
+
quietHeader: false,
|
|
47
|
+
suppressSuggestions: false,
|
|
48
|
+
};
|
|
49
|
+
// Tracks whether the header has been emitted since the current snapshot was
|
|
50
|
+
// loaded/activated, so `quietHeader` can print it exactly once.
|
|
51
|
+
let headerEmitted = false;
|
|
52
|
+
export function getSessionConfig() {
|
|
53
|
+
return sessionConfig;
|
|
54
|
+
}
|
|
55
|
+
export function setSessionConfig(patch) {
|
|
56
|
+
if (patch.quietHeader != null)
|
|
57
|
+
sessionConfig.quietHeader = patch.quietHeader;
|
|
58
|
+
if (patch.suppressSuggestions != null) {
|
|
59
|
+
sessionConfig.suppressSuggestions = patch.suppressSuggestions;
|
|
60
|
+
}
|
|
61
|
+
return sessionConfig;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Returns true exactly once per loaded snapshot when `quietHeader` is on,
|
|
65
|
+
* and always when it is off. Tools call this from `toolResult` to decide
|
|
66
|
+
* whether to prepend the snapshot header.
|
|
67
|
+
*/
|
|
68
|
+
export function shouldEmitHeader() {
|
|
69
|
+
if (!sessionConfig.quietHeader)
|
|
70
|
+
return true;
|
|
71
|
+
if (headerEmitted)
|
|
72
|
+
return false;
|
|
73
|
+
headerEmitted = true;
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
function uniqueHandle(base) {
|
|
77
|
+
// Sanitize to a short slug, then disambiguate against existing handles.
|
|
78
|
+
const slug = base
|
|
79
|
+
.replace(/\.heapsnapshot$/i, '')
|
|
80
|
+
.replace(/[^A-Za-z0-9._-]+/g, '_')
|
|
81
|
+
.replace(/^_+|_+$/g, '')
|
|
82
|
+
.slice(0, 40) || 'snapshot';
|
|
83
|
+
if (!loaded.has(slug))
|
|
84
|
+
return slug;
|
|
85
|
+
let i = 2;
|
|
86
|
+
while (loaded.has(`${slug}#${i}`))
|
|
87
|
+
i++;
|
|
88
|
+
return `${slug}#${i}`;
|
|
89
|
+
}
|
|
12
90
|
export function getSnapshot() {
|
|
13
|
-
|
|
91
|
+
const entry = currentHandle ? loaded.get(currentHandle) : undefined;
|
|
92
|
+
if (!entry) {
|
|
14
93
|
throw new Error('No heap snapshot loaded. Use memlab_load_snapshot first.');
|
|
15
94
|
}
|
|
16
|
-
|
|
95
|
+
instrumentSnapshot(entry.snapshot);
|
|
96
|
+
return entry.snapshot;
|
|
17
97
|
}
|
|
18
98
|
export function getFilePath() {
|
|
19
|
-
|
|
99
|
+
if (!currentHandle)
|
|
100
|
+
return null;
|
|
101
|
+
return loaded.get(currentHandle)?.metadata.filePath ?? null;
|
|
20
102
|
}
|
|
21
103
|
export function getSnapshotEnv() {
|
|
22
|
-
|
|
104
|
+
if (!currentHandle)
|
|
105
|
+
return 'unknown';
|
|
106
|
+
return loaded.get(currentHandle)?.metadata.env ?? 'unknown';
|
|
23
107
|
}
|
|
24
108
|
export function getSnapshotMetadata() {
|
|
25
|
-
|
|
109
|
+
if (!currentHandle)
|
|
110
|
+
return null;
|
|
111
|
+
return loaded.get(currentHandle)?.metadata ?? null;
|
|
26
112
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Register a snapshot and make it the current one.
|
|
115
|
+
*
|
|
116
|
+
* @param snapshot parsed heap snapshot
|
|
117
|
+
* @param filePath absolute path the snapshot was loaded from
|
|
118
|
+
* @param metadata derived stats (handle is assigned here if not supplied)
|
|
119
|
+
* @param opts.alias preferred handle; falls back to the file name
|
|
120
|
+
* @param opts.replace when true (default), unload all other snapshots first,
|
|
121
|
+
* preserving the single-resident memory profile. When false, keep
|
|
122
|
+
* previously-loaded snapshots resident for diffing/comparison.
|
|
123
|
+
*/
|
|
124
|
+
export function setSnapshot(snapshot, filePath, metadata, opts = {}) {
|
|
125
|
+
const replace = opts.replace ?? true;
|
|
126
|
+
if (replace) {
|
|
127
|
+
loaded.clear();
|
|
128
|
+
}
|
|
129
|
+
const handle = uniqueHandle(opts.alias || metadata.fileName);
|
|
130
|
+
const full = { handle, filePath, ...metadata };
|
|
131
|
+
loaded.set(handle, { snapshot, metadata: full });
|
|
132
|
+
currentHandle = handle;
|
|
133
|
+
headerEmitted = false;
|
|
134
|
+
return full;
|
|
135
|
+
}
|
|
136
|
+
export function listSnapshots() {
|
|
137
|
+
return [...loaded.values()].map(l => l.metadata);
|
|
138
|
+
}
|
|
139
|
+
export function getCurrentHandle() {
|
|
140
|
+
return currentHandle;
|
|
141
|
+
}
|
|
142
|
+
export function getSnapshotByHandle(handle) {
|
|
143
|
+
return loaded.get(handle)?.snapshot ?? null;
|
|
144
|
+
}
|
|
145
|
+
export function getMetadataByHandle(handle) {
|
|
146
|
+
return loaded.get(handle)?.metadata ?? null;
|
|
147
|
+
}
|
|
148
|
+
export function setCurrentSnapshot(handle) {
|
|
149
|
+
if (!loaded.has(handle))
|
|
150
|
+
return false;
|
|
151
|
+
currentHandle = handle;
|
|
152
|
+
headerEmitted = false;
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
export function removeSnapshot(handle) {
|
|
156
|
+
const existed = loaded.delete(handle);
|
|
157
|
+
if (existed && currentHandle === handle) {
|
|
158
|
+
currentHandle = loaded.size > 0 ? [...loaded.keys()][0] : null;
|
|
159
|
+
headerEmitted = false;
|
|
160
|
+
}
|
|
161
|
+
return existed;
|
|
30
162
|
}
|
|
31
163
|
//# sourceMappingURL=heap-state.js.map
|