@elliemae/encw-heap-doctor
v26.3.2
Published
CLI tool to analyze Chrome heap snapshots and find memory leak root causes
Readme
heap-doctor
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.
Key Features
- Parses multi-GB heap snapshots using off-heap typed arrays (handles files up to 8 GB)
- Detects common leak types — detached DOM nodes, large closures, stale cache references
- Traces retainer chains to the exact JS reference preventing garbage collection
- Pattern matching for known leak causes (GTM data layer, AngularJS scope/watchers, React fiber trees, event listeners, timers, observers)
- Snapshot comparison — diff two snapshots to find growth in leaked objects
- Scenario runner — automate browser flows with Playwright to capture and compare snapshots
- Fix priority ranking across leak groups sharing the same root cause
- 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.) - Markdown reports as the sole output format — clean, shareable, version-controllable
Architecture
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.
flowchart TD
subgraph publicAPI [HeapDoctor Public API]
analyse["analyse(filePath)"]
compare["compare(before, after)"]
runScenario["runScenario(scenario)"]
end
subgraph core [Core]
SnapshotParser["SnapshotParser"]
Snapshot["Snapshot"]
end
subgraph analysis [Analysis]
LeakDetector["LeakDetector"]
RetainerTracer["RetainerTracer"]
PatternMatcher["PatternMatcher"]
SuggestionGenerator["SuggestionGenerator"]
SnapshotComparer["SnapshotComparer"]
end
subgraph scenario [Scenario]
PlaywrightRunner["PlaywrightScenarioRunner"]
Playwright["Playwright Page"]
end
subgraph prompts [AI Prompt]
PromptGenerator["generateAiPrompt()"]
Consumer["Your LLM (Claude / GPT-4 / Gemini)"]
end
subgraph reporting [Reporting]
MarkdownGen["MarkdownReportGenerator"]
end
analyse --> SnapshotParser
SnapshotParser --> Snapshot
Snapshot --> LeakDetector
LeakDetector --> RetainerTracer
RetainerTracer --> PatternMatcher
PatternMatcher --> SuggestionGenerator
SuggestionGenerator --> MarkdownGen
SuggestionGenerator --> PromptGenerator
PromptGenerator --> Consumer
compare --> SnapshotParser
compare --> SnapshotComparer
SnapshotComparer --> LeakDetector
runScenario --> PlaywrightRunner
PlaywrightRunner --> Playwright
PlaywrightRunner --> compareAnalysis Pipeline
| Stage | Class | What it does |
| ------------- | -------------------------- | ------------------------------------------------------------------------------------------- |
| Parse | SnapshotParser | Reads .heapsnapshot as raw Buffer into off-heap typed arrays for multi-GB files |
| Detect | LeakDetector | Finds detached DOM, large closures, large arrays; groups by tag, ranks by retained size |
| Trace | RetainerTracer | Walks retainer edges via priority BFS to build JS-level chains to GC roots |
| Match | PatternMatcher | Matches chains against known leak patterns (GTM, AngularJS, React, event listeners, timers) |
| Suggest | SuggestionGenerator | Generates severity-ranked fix suggestions from patterns and chain context |
| Compare | SnapshotComparer | Diffs two snapshots: new/removed nodes, retained size delta, new leak groups |
| Scenario | PlaywrightScenarioRunner | Runs a Playwright browser scenario, captures before/after heap snapshots via CDP |
| Report | MarkdownReportGenerator | Renders structured results as a markdown report |
| AI Prompt | generateAiPrompt() | Serializes all findings into a self-contained prompt for any LLM |
File Structure
lib/
index.ts Re-exports HeapDoctor and public types
heapDoctor.ts Public HeapDoctor facade class
types/
index.ts Result<T>, branded IDs, common types
report.ts AnalysisReport, ComparisonReport, ScenarioReport
leak.ts LeakGroup, RetainerChain, Suggestion, etc.
scenario.ts HeapDoctorScenario (uses Playwright Page)
errors/
domainError.ts Base DomainError class
parseError.ts Snapshot parsing errors
scenarioError.ts Scenario execution errors
core/
snapshot.ts Snapshot class (typed V8 heap representation)
snapshotParser.ts SnapshotParser class (Buffer-based parser)
analysis/
leakDetector.ts LeakDetector (detached DOM, closures, arrays)
retainerTracer.ts RetainerTracer (BFS retainer path walking)
patternMatcher.ts PatternMatcher (known leak pattern detection)
suggestionGenerator.ts SuggestionGenerator (fix generation)
snapshotComparer.ts SnapshotComparer (diff two snapshots)
scenario/
playwrightScenarioRunner.ts PlaywrightScenarioRunner (CDP snapshot capture)
prompts/
promptGenerator.ts generateAiPrompt() (LLM-ready prompt builder)
reporting/
markdownReportGenerator.ts MarkdownReportGenerator (markdown output)
utils/
formatUtils.ts FormatUtils (formatBytes, truncate, escapeHtml)
bin/
heap-doctor.ts CLI entry point (composition root)Installation
npm install @elliemae/encw-heap-doctorFor scenario support (automated browser flows):
npm install playwrightProgrammatic API
HeapDoctor is designed as a library-first API. All methods return Result<T> for type-safe error handling.
1. Single Snapshot Analysis
Analyse a .heapsnapshot file for memory leaks:
import { HeapDoctor } from '@elliemae/encw-heap-doctor';
const doctor = new HeapDoctor({ topN: 5 });
const result = await doctor.analyse('app.heapsnapshot');
if (result.ok) {
console.log(result.value.markdown); // full markdown report
console.log(result.value.leakResults); // structured leak data
console.log(result.value.fixPriority); // ranked fix priorities
} else {
console.error(result.error.code, result.error.message);
}2. Snapshot Comparison
Compare two snapshots to find what grew between them:
import { HeapDoctor } from '@elliemae/encw-heap-doctor';
const doctor = new HeapDoctor();
const result = await doctor.compare(
'before.heapsnapshot',
'after.heapsnapshot',
);
if (result.ok) {
const { delta } = result.value;
console.log(`New nodes: +${delta.newNodeCount}`);
console.log(`Removed: -${delta.removedNodeCount}`);
console.log(`Size delta: ${delta.retainedSizeDelta} bytes`);
for (const group of delta.newLeakGroups) {
console.log(`New leak: ${group.label} x${group.count}`);
}
// Full markdown comparison report
console.log(result.value.markdown);
}3. Automated Scenario (Playwright)
Define a browser scenario and let HeapDoctor capture + compare snapshots automatically:
import { HeapDoctor } from '@elliemae/encw-heap-doctor';
import type { HeapDoctorScenario } from '@elliemae/encw-heap-doctor';
const scenario: HeapDoctorScenario = {
url() {
return 'https://myapp.com';
},
async setup(page) {
await page.goto('https://myapp.com/login');
await page.getByPlaceholder('Email').fill('[email protected]');
await page.getByPlaceholder('Password').fill('password');
await page.getByRole('button', { name: 'Sign In' }).click();
await page.waitForURL('**/dashboard/**');
},
async action(page) {
await page.getByRole('button', { name: 'Open Modal' }).click();
await page.waitForSelector('.modal-content');
},
async back(page) {
await page.getByRole('button', { name: 'Close' }).click();
await page.waitForSelector('.modal-content', { state: 'hidden' });
},
repeat() {
return 3; // repeat action->back cycle 3 times
},
};
const doctor = new HeapDoctor();
const result = await doctor.runScenario(scenario);
if (result.ok) {
console.log(`Snapshots: ${result.value.snapshotPaths.join(', ')}`);
console.log(result.value.markdown);
}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.
4. AI Prompt Generation
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.
import { HeapDoctor, generateAiPrompt } from '@elliemae/encw-heap-doctor';
const doctor = new HeapDoctor({ topN: 5 });
const result = await doctor.analyse('app.heapsnapshot');
if (result.ok) {
const prompt = generateAiPrompt(result.value);
// Option A: print to stdout and copy-paste into any chat UI
console.log(prompt);
// Option B: write to a file and attach to your LLM of choice
writeFileSync('heap-doctor-prompt.txt', prompt);
// Option C: pass directly to your own AI integration
const aiResponse = await myLlmClient.complete(prompt);
}Works identically for comparison reports:
const result = await doctor.compare(
'before.heapsnapshot',
'after.heapsnapshot',
);
if (result.ok) {
const prompt = generateAiPrompt(result.value);
// prompt includes the delta summary (new nodes, size change) as context
}The generated prompt includes:
- Full context header instructing the LLM to act as a memory leak specialist
- Per-leak sections: label, count, retained size, matched pattern, root cause, retainer chains, edge paths, existing suggestions
- Structured output format requesting
LEAK #Nsections with specific action items
CLI Usage
# Build first
npm run build
# Analyse a single snapshot
heap-doctor analyse app.heapsnapshot
# Compare two snapshots
heap-doctor compare before.heapsnapshot after.heapsnapshot
# Run a scenario file
heap-doctor scenario my-scenario.ts
# Write report to file instead of stdout
heap-doctor analyse app.heapsnapshot -o report.mdCLI Options
| Flag | Description | Default |
| --------------------- | ----------------------------------------------- | ------- |
| -n, --top <number> | Number of top leak groups to report | 5 |
| -o, --output <path> | Write markdown report to file (default: stdout) | stdout |
| -V, --version | Print the version number | -- |
| -h, --help | Show help | -- |
Subcommands
| Command | Description |
| -------------------------- | -------------------------------------------------------- |
| analyse <file> | Analyse a single heap snapshot |
| compare <before> <after> | Compare two heap snapshots |
| scenario <scenario-file> | Run a Playwright scenario and compare captured snapshots |
Headed Mode (Scenario)
To watch the browser during scenario execution:
HEADLESS=false npx tsx demo/scenario-run.tsExample Report Output
Below is a single leak section from a generated markdown report:
MEMORY LEAK DETECTED
Node
Detached <span>
Retained Memory 1370.16 MB
Retaining Function
gtmDataLayer in Array
Leak Pattern Google Tag Manager Data Layer
Retainer Chain
(GC roots)
└ (Eternal handles)
└ 307
└ DOMStringMap()
└ context
└ extension
└ gtmDataLayer in Array
└ [42] in Object
└ gtm.element in <button id="header-app-switcher" ...>
└ __reactFiber$kau10t4ajn9 in nd
└ return in nd
└ memoizedState in Object
└ <span class="em-ds-popover__arrow" ...>Suggested Fix
- Configure GTM to not capture element references in click/form triggers
- Periodically trim stale entries:
window.dataLayer = window.dataLayer.slice(-50) - Use CSS selectors or element IDs in GTM triggers instead of element references
- Add cleanup that nullifies
gtm.elementon stale dataLayer entries
License
MIT
