@elliemae/encw-heap-doctor 26.2.1

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.
Files changed (68) hide show
  1. package/README.md +371 -0
  2. package/dist/cjs/analysis/leakDetector.js +195 -0
  3. package/dist/cjs/analysis/patternMatcher.js +198 -0
  4. package/dist/cjs/analysis/retainerTracer.js +376 -0
  5. package/dist/cjs/analysis/snapshotComparer.js +107 -0
  6. package/dist/cjs/analysis/suggestionGenerator.js +167 -0
  7. package/dist/cjs/core/snapshot.js +211 -0
  8. package/dist/cjs/core/snapshotParser.js +211 -0
  9. package/dist/cjs/errors/domainError.js +29 -0
  10. package/dist/cjs/errors/index.js +28 -0
  11. package/dist/cjs/errors/parseError.js +27 -0
  12. package/dist/cjs/errors/scenarioError.js +27 -0
  13. package/dist/cjs/heapDoctor.js +271 -0
  14. package/dist/cjs/index.js +32 -0
  15. package/dist/cjs/package.json +7 -0
  16. package/dist/cjs/prompts/promptGenerator.js +132 -0
  17. package/dist/cjs/reporting/markdownReportGenerator.js +241 -0
  18. package/dist/cjs/scenario/playwrightScenarioRunner.js +118 -0
  19. package/dist/cjs/types/index.js +30 -0
  20. package/dist/cjs/types/leak.js +16 -0
  21. package/dist/cjs/types/report.js +16 -0
  22. package/dist/cjs/types/scenario.js +16 -0
  23. package/dist/cjs/utils/formatUtils.js +54 -0
  24. package/dist/esm/analysis/leakDetector.js +175 -0
  25. package/dist/esm/analysis/patternMatcher.js +178 -0
  26. package/dist/esm/analysis/retainerTracer.js +356 -0
  27. package/dist/esm/analysis/snapshotComparer.js +87 -0
  28. package/dist/esm/analysis/suggestionGenerator.js +147 -0
  29. package/dist/esm/core/snapshot.js +191 -0
  30. package/dist/esm/core/snapshotParser.js +191 -0
  31. package/dist/esm/errors/domainError.js +9 -0
  32. package/dist/esm/errors/index.js +8 -0
  33. package/dist/esm/errors/parseError.js +7 -0
  34. package/dist/esm/errors/scenarioError.js +7 -0
  35. package/dist/esm/heapDoctor.js +241 -0
  36. package/dist/esm/index.js +12 -0
  37. package/dist/esm/package.json +7 -0
  38. package/dist/esm/prompts/promptGenerator.js +112 -0
  39. package/dist/esm/reporting/markdownReportGenerator.js +221 -0
  40. package/dist/esm/scenario/playwrightScenarioRunner.js +88 -0
  41. package/dist/esm/types/index.js +10 -0
  42. package/dist/esm/types/leak.js +0 -0
  43. package/dist/esm/types/report.js +0 -0
  44. package/dist/esm/types/scenario.js +0 -0
  45. package/dist/esm/utils/formatUtils.js +34 -0
  46. package/dist/types/lib/analysis/leakDetector.d.ts +23 -0
  47. package/dist/types/lib/analysis/patternMatcher.d.ts +14 -0
  48. package/dist/types/lib/analysis/retainerTracer.d.ts +48 -0
  49. package/dist/types/lib/analysis/snapshotComparer.d.ts +28 -0
  50. package/dist/types/lib/analysis/suggestionGenerator.d.ts +16 -0
  51. package/dist/types/lib/core/snapshot.d.ts +111 -0
  52. package/dist/types/lib/core/snapshotParser.d.ts +42 -0
  53. package/dist/types/lib/errors/domainError.d.ts +8 -0
  54. package/dist/types/lib/errors/index.d.ts +3 -0
  55. package/dist/types/lib/errors/parseError.d.ts +5 -0
  56. package/dist/types/lib/errors/scenarioError.d.ts +5 -0
  57. package/dist/types/lib/heapDoctor.d.ts +60 -0
  58. package/dist/types/lib/index.d.ts +10 -0
  59. package/dist/types/lib/prompts/promptGenerator.d.ts +19 -0
  60. package/dist/types/lib/reporting/markdownReportGenerator.d.ts +26 -0
  61. package/dist/types/lib/scenario/playwrightScenarioRunner.d.ts +29 -0
  62. package/dist/types/lib/types/index.d.ts +35 -0
  63. package/dist/types/lib/types/leak.d.ts +68 -0
  64. package/dist/types/lib/types/report.d.ts +55 -0
  65. package/dist/types/lib/types/scenario.d.ts +28 -0
  66. package/dist/types/lib/utils/formatUtils.d.ts +20 -0
  67. package/dist/types/tsconfig.tsbuildinfo +1 -0
  68. package/package.json +83 -0
package/README.md ADDED
@@ -0,0 +1,371 @@
1
+ # heap-doctor
2
+
3
+ Analyzes Chrome V8 heap snapshots to help developers find and fix memory leaks in web applications. It parses `.heapsnapshot` files, groups leaked objects by type, walks retainer paths to identify the root JS reference holding objects in memory, matches known leak patterns, and generates prioritized fix suggestions — plus a ready-to-use AI prompt you can send to any LLM of your choice.
4
+
5
+ ## Key Features
6
+
7
+ - **Parses multi-GB heap snapshots** using off-heap typed arrays (handles files up to 8 GB)
8
+ - **Detects common leak types** — detached DOM nodes, large closures, stale cache references
9
+ - **Traces retainer chains** to the exact JS reference preventing garbage collection
10
+ - **Pattern matching** for known leak causes (GTM data layer, AngularJS scope/watchers, React fiber trees, event listeners, timers, observers)
11
+ - **Snapshot comparison** — diff two snapshots to find growth in leaked objects
12
+ - **Scenario runner** — automate browser flows with Playwright to capture and compare snapshots
13
+ - **Fix priority ranking** across leak groups sharing the same root cause
14
+ - **AI prompt generation** — `generateAiPrompt()` produces a self-contained prompt from the analysis report that you can send to any LLM (Claude, GPT-4, Gemini, etc.)
15
+ - **Markdown reports** as the sole output format — clean, shareable, version-controllable
16
+
17
+ ## Architecture
18
+
19
+ HeapDoctor exposes a single public class that composes all internal services. Three methods cover every use case: analyse a single snapshot, compare two snapshots, or run an automated browser scenario.
20
+
21
+ ```mermaid
22
+ flowchart TD
23
+ subgraph publicAPI [HeapDoctor Public API]
24
+ analyse["analyse(filePath)"]
25
+ compare["compare(before, after)"]
26
+ runScenario["runScenario(scenario)"]
27
+ end
28
+
29
+ subgraph core [Core]
30
+ SnapshotParser["SnapshotParser"]
31
+ Snapshot["Snapshot"]
32
+ end
33
+
34
+ subgraph analysis [Analysis]
35
+ LeakDetector["LeakDetector"]
36
+ RetainerTracer["RetainerTracer"]
37
+ PatternMatcher["PatternMatcher"]
38
+ SuggestionGenerator["SuggestionGenerator"]
39
+ SnapshotComparer["SnapshotComparer"]
40
+ end
41
+
42
+ subgraph scenario [Scenario]
43
+ PlaywrightRunner["PlaywrightScenarioRunner"]
44
+ Playwright["Playwright Page"]
45
+ end
46
+
47
+ subgraph prompts [AI Prompt]
48
+ PromptGenerator["generateAiPrompt()"]
49
+ Consumer["Your LLM (Claude / GPT-4 / Gemini)"]
50
+ end
51
+
52
+ subgraph reporting [Reporting]
53
+ MarkdownGen["MarkdownReportGenerator"]
54
+ end
55
+
56
+ analyse --> SnapshotParser
57
+ SnapshotParser --> Snapshot
58
+ Snapshot --> LeakDetector
59
+ LeakDetector --> RetainerTracer
60
+ RetainerTracer --> PatternMatcher
61
+ PatternMatcher --> SuggestionGenerator
62
+ SuggestionGenerator --> MarkdownGen
63
+ SuggestionGenerator --> PromptGenerator
64
+ PromptGenerator --> Consumer
65
+
66
+ compare --> SnapshotParser
67
+ compare --> SnapshotComparer
68
+ SnapshotComparer --> LeakDetector
69
+
70
+ runScenario --> PlaywrightRunner
71
+ PlaywrightRunner --> Playwright
72
+ PlaywrightRunner --> compare
73
+ ```
74
+
75
+ ### Analysis Pipeline
76
+
77
+ | Stage | Class | What it does |
78
+ | ------------- | -------------------------- | ------------------------------------------------------------------------------------------- |
79
+ | **Parse** | `SnapshotParser` | Reads `.heapsnapshot` as raw Buffer into off-heap typed arrays for multi-GB files |
80
+ | **Detect** | `LeakDetector` | Finds detached DOM, large closures, large arrays; groups by tag, ranks by retained size |
81
+ | **Trace** | `RetainerTracer` | Walks retainer edges via priority BFS to build JS-level chains to GC roots |
82
+ | **Match** | `PatternMatcher` | Matches chains against known leak patterns (GTM, AngularJS, React, event listeners, timers) |
83
+ | **Suggest** | `SuggestionGenerator` | Generates severity-ranked fix suggestions from patterns and chain context |
84
+ | **Compare** | `SnapshotComparer` | Diffs two snapshots: new/removed nodes, retained size delta, new leak groups |
85
+ | **Scenario** | `PlaywrightScenarioRunner` | Runs a Playwright browser scenario, captures before/after heap snapshots via CDP |
86
+ | **Report** | `MarkdownReportGenerator` | Renders structured results as a markdown report |
87
+ | **AI Prompt** | `generateAiPrompt()` | Serializes all findings into a self-contained prompt for any LLM |
88
+
89
+ ### File Structure
90
+
91
+ ```
92
+ lib/
93
+ index.ts Re-exports HeapDoctor and public types
94
+ heapDoctor.ts Public HeapDoctor facade class
95
+ types/
96
+ index.ts Result<T>, branded IDs, common types
97
+ report.ts AnalysisReport, ComparisonReport, ScenarioReport
98
+ leak.ts LeakGroup, RetainerChain, Suggestion, etc.
99
+ scenario.ts HeapDoctorScenario (uses Playwright Page)
100
+ errors/
101
+ domainError.ts Base DomainError class
102
+ parseError.ts Snapshot parsing errors
103
+ scenarioError.ts Scenario execution errors
104
+ core/
105
+ snapshot.ts Snapshot class (typed V8 heap representation)
106
+ snapshotParser.ts SnapshotParser class (Buffer-based parser)
107
+ analysis/
108
+ leakDetector.ts LeakDetector (detached DOM, closures, arrays)
109
+ retainerTracer.ts RetainerTracer (BFS retainer path walking)
110
+ patternMatcher.ts PatternMatcher (known leak pattern detection)
111
+ suggestionGenerator.ts SuggestionGenerator (fix generation)
112
+ snapshotComparer.ts SnapshotComparer (diff two snapshots)
113
+ scenario/
114
+ playwrightScenarioRunner.ts PlaywrightScenarioRunner (CDP snapshot capture)
115
+ prompts/
116
+ promptGenerator.ts generateAiPrompt() (LLM-ready prompt builder)
117
+ reporting/
118
+ markdownReportGenerator.ts MarkdownReportGenerator (markdown output)
119
+ utils/
120
+ formatUtils.ts FormatUtils (formatBytes, truncate, escapeHtml)
121
+ bin/
122
+ heap-doctor.ts CLI entry point (composition root)
123
+ ```
124
+
125
+ ## Installation
126
+
127
+ ```bash
128
+ npm install @elliemae/encw-heap-doctor
129
+ ```
130
+
131
+ For scenario support (automated browser flows):
132
+
133
+ ```bash
134
+ npm install playwright
135
+ ```
136
+
137
+ ## Programmatic API
138
+
139
+ HeapDoctor is designed as a library-first API. All methods return `Result<T>` for type-safe error handling.
140
+
141
+ ### 1. Single Snapshot Analysis
142
+
143
+ Analyse a `.heapsnapshot` file for memory leaks:
144
+
145
+ ```typescript
146
+ import { HeapDoctor } from '@elliemae/encw-heap-doctor';
147
+
148
+ const doctor = new HeapDoctor({ topN: 5 });
149
+ const result = await doctor.analyse('app.heapsnapshot');
150
+
151
+ if (result.ok) {
152
+ console.log(result.value.markdown); // full markdown report
153
+ console.log(result.value.leakResults); // structured leak data
154
+ console.log(result.value.fixPriority); // ranked fix priorities
155
+ } else {
156
+ console.error(result.error.code, result.error.message);
157
+ }
158
+ ```
159
+
160
+ ### 2. Snapshot Comparison
161
+
162
+ Compare two snapshots to find what grew between them:
163
+
164
+ ```typescript
165
+ import { HeapDoctor } from '@elliemae/encw-heap-doctor';
166
+
167
+ const doctor = new HeapDoctor();
168
+ const result = await doctor.compare(
169
+ 'before.heapsnapshot',
170
+ 'after.heapsnapshot',
171
+ );
172
+
173
+ if (result.ok) {
174
+ const { delta } = result.value;
175
+ console.log(`New nodes: +${delta.newNodeCount}`);
176
+ console.log(`Removed: -${delta.removedNodeCount}`);
177
+ console.log(`Size delta: ${delta.retainedSizeDelta} bytes`);
178
+
179
+ for (const group of delta.newLeakGroups) {
180
+ console.log(`New leak: ${group.label} x${group.count}`);
181
+ }
182
+
183
+ // Full markdown comparison report
184
+ console.log(result.value.markdown);
185
+ }
186
+ ```
187
+
188
+ ### 3. Automated Scenario (Playwright)
189
+
190
+ Define a browser scenario and let HeapDoctor capture + compare snapshots automatically:
191
+
192
+ ```typescript
193
+ import { HeapDoctor } from '@elliemae/encw-heap-doctor';
194
+ import type { HeapDoctorScenario } from '@elliemae/encw-heap-doctor';
195
+
196
+ const scenario: HeapDoctorScenario = {
197
+ url() {
198
+ return 'https://myapp.com';
199
+ },
200
+
201
+ async setup(page) {
202
+ await page.goto('https://myapp.com/login');
203
+ await page.getByPlaceholder('Email').fill('user@example.com');
204
+ await page.getByPlaceholder('Password').fill('password');
205
+ await page.getByRole('button', { name: 'Sign In' }).click();
206
+ await page.waitForURL('**/dashboard/**');
207
+ },
208
+
209
+ async action(page) {
210
+ await page.getByRole('button', { name: 'Open Modal' }).click();
211
+ await page.waitForSelector('.modal-content');
212
+ },
213
+
214
+ async back(page) {
215
+ await page.getByRole('button', { name: 'Close' }).click();
216
+ await page.waitForSelector('.modal-content', { state: 'hidden' });
217
+ },
218
+
219
+ repeat() {
220
+ return 3; // repeat action->back cycle 3 times
221
+ },
222
+ };
223
+
224
+ const doctor = new HeapDoctor();
225
+ const result = await doctor.runScenario(scenario);
226
+
227
+ if (result.ok) {
228
+ console.log(`Snapshots: ${result.value.snapshotPaths.join(', ')}`);
229
+ console.log(result.value.markdown);
230
+ }
231
+ ```
232
+
233
+ The runner captures a "before" snapshot after `setup()`, executes `action()` -> `back()` for `repeat()` cycles, then captures an "after" snapshot. Both are compared and a full report is generated.
234
+
235
+ ### 4. AI Prompt Generation
236
+
237
+ `generateAiPrompt()` converts any analysis or comparison report into a self-contained prompt string. Paste it into Claude, GPT-4, Gemini, or any LLM — no API keys or registry access required on heap-doctor's side.
238
+
239
+ ```typescript
240
+ import { HeapDoctor, generateAiPrompt } from '@elliemae/encw-heap-doctor';
241
+
242
+ const doctor = new HeapDoctor({ topN: 5 });
243
+ const result = await doctor.analyse('app.heapsnapshot');
244
+
245
+ if (result.ok) {
246
+ const prompt = generateAiPrompt(result.value);
247
+
248
+ // Option A: print to stdout and copy-paste into any chat UI
249
+ console.log(prompt);
250
+
251
+ // Option B: write to a file and attach to your LLM of choice
252
+ writeFileSync('heap-doctor-prompt.txt', prompt);
253
+
254
+ // Option C: pass directly to your own AI integration
255
+ const aiResponse = await myLlmClient.complete(prompt);
256
+ }
257
+ ```
258
+
259
+ Works identically for comparison reports:
260
+
261
+ ```typescript
262
+ const result = await doctor.compare(
263
+ 'before.heapsnapshot',
264
+ 'after.heapsnapshot',
265
+ );
266
+ if (result.ok) {
267
+ const prompt = generateAiPrompt(result.value);
268
+ // prompt includes the delta summary (new nodes, size change) as context
269
+ }
270
+ ```
271
+
272
+ The generated prompt includes:
273
+
274
+ - Full context header instructing the LLM to act as a memory leak specialist
275
+ - Per-leak sections: label, count, retained size, matched pattern, root cause, retainer chains, edge paths, existing suggestions
276
+ - Structured output format requesting `LEAK #N` sections with specific action items
277
+
278
+ ## CLI Usage
279
+
280
+ ```bash
281
+ # Build first
282
+ npm run build
283
+
284
+ # Analyse a single snapshot
285
+ heap-doctor analyse app.heapsnapshot
286
+
287
+ # Compare two snapshots
288
+ heap-doctor compare before.heapsnapshot after.heapsnapshot
289
+
290
+ # Run a scenario file
291
+ heap-doctor scenario my-scenario.ts
292
+
293
+ # Write report to file instead of stdout
294
+ heap-doctor analyse app.heapsnapshot -o report.md
295
+ ```
296
+
297
+ ### CLI Options
298
+
299
+ | Flag | Description | Default |
300
+ | --------------------- | ----------------------------------------------- | ------- |
301
+ | `-n, --top <number>` | Number of top leak groups to report | `5` |
302
+ | `-o, --output <path>` | Write markdown report to file (default: stdout) | stdout |
303
+ | `-V, --version` | Print the version number | -- |
304
+ | `-h, --help` | Show help | -- |
305
+
306
+ ### Subcommands
307
+
308
+ | Command | Description |
309
+ | -------------------------- | -------------------------------------------------------- |
310
+ | `analyse <file>` | Analyse a single heap snapshot |
311
+ | `compare <before> <after>` | Compare two heap snapshots |
312
+ | `scenario <scenario-file>` | Run a Playwright scenario and compare captured snapshots |
313
+
314
+ ## Headed Mode (Scenario)
315
+
316
+ To watch the browser during scenario execution:
317
+
318
+ ```bash
319
+ HEADLESS=false npx tsx demo/scenario-run.ts
320
+ ```
321
+
322
+ ## Example Report Output
323
+
324
+ Below is a single leak section from a generated markdown report:
325
+
326
+ ---
327
+
328
+ ### MEMORY LEAK DETECTED
329
+
330
+ **Node**
331
+ `Detached <span>`
332
+
333
+ **Retained Memory**
334
+ 1370.16 MB
335
+
336
+ **Retaining Function**
337
+ `gtmDataLayer in Array`
338
+
339
+ **Leak Pattern**
340
+ Google Tag Manager Data Layer
341
+
342
+ **Retainer Chain**
343
+
344
+ ```text
345
+ (GC roots)
346
+ └ (Eternal handles)
347
+ └ 307
348
+ └ DOMStringMap()
349
+ └ context
350
+ └ extension
351
+ └ gtmDataLayer in Array
352
+ └ [42] in Object
353
+ └ gtm.element in <button id="header-app-switcher" ...>
354
+ └ __reactFiber$kau10t4ajn9 in nd
355
+ └ return in nd
356
+ └ memoizedState in Object
357
+ └ <span class="em-ds-popover__arrow" ...>
358
+ ```
359
+
360
+ **Suggested Fix**
361
+
362
+ - Configure GTM to not capture element references in click/form triggers
363
+ - Periodically trim stale entries: `window.dataLayer = window.dataLayer.slice(-50)`
364
+ - Use CSS selectors or element IDs in GTM triggers instead of element references
365
+ - Add cleanup that nullifies `gtm.element` on stale dataLayer entries
366
+
367
+ ---
368
+
369
+ ## License
370
+
371
+ MIT
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var leakDetector_exports = {};
20
+ __export(leakDetector_exports, {
21
+ LeakDetector: () => LeakDetector
22
+ });
23
+ module.exports = __toCommonJS(leakDetector_exports);
24
+ const DOM_TAG_RE = /^<(\w+)/;
25
+ const DETACHED_TAG_RE = /^Detached <(\w+)/;
26
+ const MAX_SINGLE_BFS = 2e5;
27
+ const FOLLOW_TYPES = /* @__PURE__ */ new Set(["native", "hidden"]);
28
+ class LeakDetector {
29
+ /**
30
+ * @param snapshot - Parsed heap snapshot to scan.
31
+ * @param topN - Maximum number of leak groups to return.
32
+ * @returns Leak groups sorted by retained size, limited to `topN`.
33
+ */
34
+ // eslint-disable-next-line max-statements, complexity
35
+ detect(snapshot, topN = 5) {
36
+ const nc = snapshot.nodeCount;
37
+ const explicitlyDetached = [];
38
+ const domElements = [];
39
+ for (let i = 0; i < nc; i += 1) {
40
+ const ni = i;
41
+ const name = snapshot.nodeName(ni);
42
+ const type = snapshot.nodeType(ni);
43
+ const det = snapshot.nodeDetachedness(ni);
44
+ if (name.startsWith("Detached <") || det > 0 && type === "native") {
45
+ explicitlyDetached.push(ni);
46
+ continue;
47
+ }
48
+ if (type === "native" && DOM_TAG_RE.test(name)) {
49
+ domElements.push(ni);
50
+ }
51
+ }
52
+ const attachedNodes = LeakDetector.buildAttachedSet(snapshot);
53
+ const detachedDomNodes = [...explicitlyDetached];
54
+ domElements.forEach((ni) => {
55
+ if (!attachedNodes.has(ni)) {
56
+ detachedDomNodes.push(ni);
57
+ }
58
+ });
59
+ const tagGroups = /* @__PURE__ */ new Map();
60
+ detachedDomNodes.forEach((ni) => {
61
+ const name = snapshot.nodeName(ni);
62
+ const tag = LeakDetector.extractTag(name) ?? "unknown";
63
+ if (!tagGroups.has(tag)) {
64
+ tagGroups.set(tag, {
65
+ reason: "detached-dom",
66
+ label: `Detached <${tag}>`,
67
+ tag,
68
+ nodeIndices: [],
69
+ count: 0,
70
+ totalSelfSize: 0,
71
+ topElements: [],
72
+ representativeIndex: ni,
73
+ retainedSize: 0
74
+ });
75
+ }
76
+ const group = tagGroups.get(tag);
77
+ group.nodeIndices.push(ni);
78
+ group.count += 1;
79
+ group.totalSelfSize += snapshot.nodeSelfSize(ni);
80
+ });
81
+ const visited = new Uint8Array(nc);
82
+ const domGroups = [...tagGroups.values()];
83
+ domGroups.forEach((group) => {
84
+ LeakDetector.rankGroupElements(group, snapshot, visited);
85
+ });
86
+ domGroups.sort((a, b) => b.retainedSize - a.retainedSize);
87
+ return domGroups.slice(0, topN);
88
+ }
89
+ static extractTag(name) {
90
+ const m = name.match(DETACHED_TAG_RE) ?? name.match(DOM_TAG_RE);
91
+ return m ? m[1] : null;
92
+ }
93
+ /**
94
+ * BFS from HTMLDocument nodes through DOM tree edges to find "attached" nodes.
95
+ * @param snapshot
96
+ */
97
+ // eslint-disable-next-line max-statements, complexity
98
+ static buildAttachedSet(snapshot) {
99
+ const nc = snapshot.nodeCount;
100
+ const attached = /* @__PURE__ */ new Set();
101
+ const docNodes = [];
102
+ for (let i = 0; i < nc; i += 1) {
103
+ const ni = i;
104
+ const name = snapshot.nodeName(ni);
105
+ if (name === "HTMLDocument" || name === "Document") {
106
+ docNodes.push(ni);
107
+ }
108
+ }
109
+ if (docNodes.length === 0) return attached;
110
+ const visited = new Uint8Array(nc);
111
+ const queue = [];
112
+ docNodes.forEach((ni) => {
113
+ if (!visited[ni]) {
114
+ visited[ni] = 1;
115
+ queue.push(ni);
116
+ attached.add(ni);
117
+ }
118
+ });
119
+ let head = 0;
120
+ while (head < queue.length) {
121
+ const ni = queue[head];
122
+ head += 1;
123
+ const nodeType = snapshot.nodeType(ni);
124
+ if (!FOLLOW_TYPES.has(nodeType)) continue;
125
+ for (const ei of snapshot.nodeEdges(ni)) {
126
+ const to = snapshot.edgeToNode(ei);
127
+ if (visited[to]) continue;
128
+ const toType = snapshot.nodeType(to);
129
+ const toName = snapshot.nodeName(to);
130
+ if (FOLLOW_TYPES.has(toType)) {
131
+ visited[to] = 1;
132
+ queue.push(to);
133
+ if (toType === "native" && DOM_TAG_RE.test(toName)) {
134
+ attached.add(to);
135
+ }
136
+ }
137
+ }
138
+ }
139
+ return attached;
140
+ }
141
+ static rankGroupElements(group, snapshot, visited) {
142
+ const { nodeIndices } = group;
143
+ const ranked = nodeIndices.map((ni) => ({
144
+ nodeIndex: ni,
145
+ edgeCount: snapshot.nodeRawEdgeCount(ni),
146
+ name: snapshot.nodeName(ni),
147
+ selfSize: snapshot.nodeSelfSize(ni),
148
+ retainedSize: 0
149
+ }));
150
+ ranked.sort((a, b) => (b.edgeCount ?? 0) - (a.edgeCount ?? 0));
151
+ const sampleSize = Math.min(ranked.length, 30);
152
+ const sampled = ranked.slice(0, sampleSize);
153
+ sampled.forEach((elem) => {
154
+ elem.retainedSize = LeakDetector.estimateSingleRetainedSize(
155
+ elem.nodeIndex,
156
+ snapshot,
157
+ visited
158
+ );
159
+ });
160
+ sampled.sort((a, b) => b.retainedSize - a.retainedSize);
161
+ group.topElements = sampled.slice(0, 5);
162
+ if (sampled.length > 0) {
163
+ group.representativeIndex = sampled[0].nodeIndex;
164
+ group.retainedSize = sampled.reduce((sum, e) => sum + e.retainedSize, 0);
165
+ } else {
166
+ group.representativeIndex = nodeIndices[0];
167
+ group.retainedSize = group.totalSelfSize;
168
+ }
169
+ }
170
+ static estimateSingleRetainedSize(startNi, snapshot, visited) {
171
+ const toClean = [];
172
+ const queue = [startNi];
173
+ visited[startNi] = 1;
174
+ toClean.push(startNi);
175
+ let total = snapshot.nodeSelfSize(startNi);
176
+ let visits = 0;
177
+ let head = 0;
178
+ while (head < queue.length && visits < MAX_SINGLE_BFS) {
179
+ const ni = queue[head];
180
+ head += 1;
181
+ visits += 1;
182
+ for (const ei of snapshot.nodeEdges(ni)) {
183
+ const to = snapshot.edgeToNode(ei);
184
+ if (!visited[to]) {
185
+ visited[to] = 1;
186
+ toClean.push(to);
187
+ total += snapshot.nodeSelfSize(to);
188
+ queue.push(to);
189
+ }
190
+ }
191
+ }
192
+ for (const idx of toClean) visited[idx] = 0;
193
+ return total;
194
+ }
195
+ }