-
Notifications
You must be signed in to change notification settings - Fork 2.3k
feat: add a skill for detecting memory leaks using take_memory_snapshot tool #1162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
5e961a6
feat: initial memory debugging skill
devnook 8062d47
feat: update memory debugging skill
devnook c83424b
docs: fix typo (#1155)
yshngg 13fb291
chore(deps-dev): bump puppeteer from 24.38.0 to 24.39.0 in the bundle…
dependabot[bot] 3d1961c
chore(deps-dev): bump the dev-dependencies group with 4 updates (#1157)
dependabot[bot] 33faa45
feat: experimental `chrome-devtools` CLI (#1100)
OrKoN 3cad505
chore: lint
devnook c2d91ae
chore: merge
devnook 1db1017
feat: add heapsnapshot comparison script
devnook 5915ff1
feat: Update skill recommendations
devnook 96b2ecc
chore: eslint errors
devnook File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| --- | ||
| name: memory-leak-debugging | ||
| description: Diagnoses and resolves memory leaks in JavaScript/Node.js applications. Use when a user reports high memory usage, OOM errors, or wants to analyze heapsnapshots or run memory leak detection tools like memlab. | ||
| --- | ||
|
|
||
| # Memory Leak Debugging | ||
|
|
||
| This skill provides expert guidance and workflows for finding, diagnosing, and fixing memory leaks in JavaScript and Node.js applications. | ||
|
|
||
| ## Core Principles | ||
|
|
||
| - **Prefer `memlab`:** Do NOT attempt to read raw `.heapsnapshot` files directly, as they are extremely large and will consume too many tokens. Always recommend and use `memlab` to process snapshots and identify leak traces. | ||
| - **Isolate the Leak:** Determine if the leak is in the browser (client-side) or Node.js (server-side). | ||
| - **Common Culprits:** Look for detached DOM nodes, unhandled closures, global variables, event listeners not being removed, and caches growing unbounded. _Note: Detached DOM nodes are sometimes intentional caches; always ask the user before nulling them._ | ||
|
|
||
| ## Workflows | ||
|
|
||
| ### 1. Capturing Snapshots | ||
|
|
||
| When investigating a frontend web application memory leak, utilize the `chrome-devtools-mcp` tools to interact with the application and take snapshots. | ||
|
|
||
| - Use tools like `click`, `navigate_page`, `fill`, etc., to manipulate the page into the desired state. | ||
| - Revert the page back to the original state after interactions to see if memory is released. | ||
| - Repeat the same user interactions 10 times to amplify the leak. | ||
| - Use `take_memory_snapshot` to save `.heapsnapshot` files to disk at baseline, target (after actions), and final (after reverting actions) states. | ||
|
|
||
| ### 2. Using Memlab to Find Leaks (Recommended) | ||
|
|
||
| Once you have generated `.heapsnapshot` files using `take_memory_snapshot`, use `memlab` to automatically find memory leaks. | ||
|
|
||
| - Read [references/memlab.md](references/memlab.md) for how to use `memlab` to analyze the generated heapsnapshots. | ||
|
devnook marked this conversation as resolved.
|
||
| - Do **not** read raw `.heapsnapshot` files using `read_file` or `cat`. | ||
|
|
||
| ### 3. Identifying Common Leaks | ||
|
|
||
| When you have found a leak trace (e.g., via `memlab` output), you must identify the root cause in the code. | ||
|
|
||
| - Read [references/common-leaks.md](references/common-leaks.md) for examples of common memory leaks and how to fix them. | ||
|
|
||
| ### 4. Fallback: Comparing Snapshots Manually | ||
|
|
||
| If `memlab` is not available, you MUST use the fallback script in the references directory to compare two `.heapsnapshot` files and identify the top growing objects and common leak types. | ||
|
|
||
| Run the script using Node.js: | ||
|
|
||
| ```bash | ||
| node compare_snapshots.js <baseline.heapsnapshot> <target.heapsnapshot> | ||
| ``` | ||
|
|
||
| The script will analyze and output the top growing objects by size and highlight the 3 most common types of memory leaks (e.g., Detached DOM nodes, closures, Contexts) if they are present. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| # Common Memory Leaks | ||
|
|
||
| When analyzing a retainer trace from `memlab`, look for these common patterns in the codebase: | ||
|
devnook marked this conversation as resolved.
|
||
|
|
||
| ## 1. Uncleared Event Listeners | ||
|
|
||
| Event listeners attached to global objects (like `window` or `document`) or long-living objects prevent garbage collection of the objects referenced in their callbacks. | ||
|
|
||
| **Fix:** Always call `removeEventListener` when a component unmounts or the listener is no longer needed. | ||
|
|
||
| ## 2. Detached DOM Nodes | ||
|
|
||
| A DOM node is removed from the document tree but is still referenced by a JavaScript variable. While detachedness is a good signal for a memory leak, it's not always a bug. For example, websites sometimes intentionally cache detached navigation trees. | ||
|
|
||
| **Fix:** Signal the detached nodes to the user first. **Ask the user first** before nulling the references or changing the code, as the detached nodes might be part of an intentional cache. If confirmed as a leak, ensure variables holding DOM references are set to `null` when the node is removed, or limit their scope. | ||
|
|
||
| ## 3. Unintentional Global Variables | ||
|
|
||
| Variables declared without `var`, `let`, or `const` (in non-strict mode) or explicitly attached to `window` remain in memory forever. | ||
|
|
||
| **Fix:** Use strict mode, properly declare variables, and avoid global state. | ||
|
|
||
| ## 4. Closures | ||
|
|
||
| Closures can unintentionally keep references to large objects in their outer scope. | ||
|
|
||
| **Fix:** Nullify large objects when they are no longer needed, or refactor the closure to not capture unnecessary variables. | ||
|
|
||
| ## 5. Unbounded Caches or Arrays | ||
|
|
||
| Data structures used for caching (like objects, Arrays, or Maps) that grow without limits. | ||
|
|
||
| **Fix:** Implement caching limits, use LRU caches, or use `WeakMap`/`WeakSet` for data associated with object lifecycles. | ||
109 changes: 109 additions & 0 deletions
109
skills/memory-leak-debugging/references/compare_snapshots.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| /** | ||
| * @license | ||
| * Copyright 2025 Google LLC | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import * as fs from 'node:fs'; | ||
|
|
||
| function parseSnapshot(filePath) { | ||
| console.log(`Loading ${filePath}...`); | ||
| const data = JSON.parse(fs.readFileSync(filePath, 'utf8')); | ||
| const strings = data.strings; | ||
| const nodes = data.nodes; | ||
| const nodeFields = data.snapshot.meta.node_fields; | ||
| const nodeFieldCount = nodeFields.length; | ||
|
|
||
| const typeOffset = nodeFields.indexOf('type'); | ||
| const nameOffset = nodeFields.indexOf('name'); | ||
| const sizeOffset = nodeFields.indexOf('self_size'); | ||
|
|
||
| const nodeTypes = data.snapshot.meta.node_types[typeOffset]; | ||
|
|
||
| const counts = {}; | ||
| const sizes = {}; | ||
|
|
||
| for (let i = 0; i < nodes.length; i += nodeFieldCount) { | ||
| const typeIdx = nodes[i + typeOffset]; | ||
| const typeName = nodeTypes[typeIdx]; | ||
| const nameIdx = nodes[i + nameOffset]; | ||
| const name = typeof nameIdx === 'number' ? strings[nameIdx] : nameIdx; | ||
| const size = nodes[i + sizeOffset]; | ||
|
|
||
| // Ignore native primitives/arrays that clutter the output unless specifically looking for them | ||
| if ( | ||
| typeName === 'string' || | ||
| typeName === 'number' || | ||
| typeName === 'array' | ||
| ) { | ||
| continue; | ||
| } | ||
|
|
||
| const key = `${typeName}::${name}`; | ||
| counts[key] = (counts[key] || 0) + 1; | ||
| sizes[key] = (sizes[key] || 0) + size; | ||
| } | ||
| return {counts, sizes}; | ||
| } | ||
|
|
||
| const [, , file1, file2] = process.argv; | ||
| if (!file1 || !file2) { | ||
| console.error( | ||
| 'Usage: node compare_snapshots.js <baseline.heapsnapshot> <target.heapsnapshot>', | ||
| ); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| try { | ||
| const snap1 = parseSnapshot(file1); | ||
| const snap2 = parseSnapshot(file2); | ||
|
|
||
| const diffs = []; | ||
| for (const key in snap2.counts) { | ||
| const count1 = snap1.counts[key] || 0; | ||
| const count2 = snap2.counts[key]; | ||
| const size1 = snap1.sizes[key] || 0; | ||
| const size2 = snap2.sizes[key]; | ||
|
|
||
| if (count2 > count1) { | ||
| diffs.push({ | ||
| key, | ||
| countDiff: count2 - count1, | ||
| sizeDiff: size2 - size1, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| diffs.sort((a, b) => b.sizeDiff - a.sizeDiff); | ||
|
|
||
| console.log('\n--- Top 10 growing objects by size ---'); | ||
| diffs.slice(0, 10).forEach(d => { | ||
| console.log(`${d.key}: +${d.countDiff} objects, +${d.sizeDiff} bytes`); | ||
| }); | ||
|
|
||
| // Look for common leak indicators | ||
| const commonLeaks = diffs.filter( | ||
| d => | ||
| d.key.toLowerCase().includes('detached') || | ||
| d.key.toLowerCase().includes('html') || | ||
| d.key.toLowerCase().includes('eventlistener') || | ||
| d.key.toLowerCase().includes('context') || | ||
| d.key.toLowerCase().includes('closure'), | ||
| ); | ||
|
|
||
| commonLeaks.sort((a, b) => b.countDiff - a.countDiff); | ||
|
|
||
| console.log('\n--- Top 3 most common types of memory leaks found ---'); | ||
| if (commonLeaks.length === 0) { | ||
| console.log('No common DOM or Closure leaks detected.'); | ||
| } else { | ||
| commonLeaks.slice(0, 3).forEach(d => { | ||
| console.log(`${d.key}: +${d.countDiff} objects, +${d.sizeDiff} bytes`); | ||
| }); | ||
| } | ||
| } catch (error) { | ||
| console.error( | ||
| 'Error parsing snapshots. They might be too large for JSON.parse or invalid.', | ||
| ); | ||
| console.error(error.message); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| # Using Memlab | ||
|
|
||
| [Memlab](https://facebook.github.io/memlab/) is an E2E testing and analysis framework for finding JavaScript memory leaks. | ||
|
|
||
| ## Important Rule | ||
|
|
||
| **NEVER read raw `.heapsnapshot` files directly.** They are too large and will exceed context limits. Always use `memlab` commands to analyze them. | ||
|
|
||
| ## Analyzing Snapshots | ||
|
|
||
| You can use the `take_memory_snapshot` tool provided by the `chrome-devtools-mcp` extension to generate heap snapshots during an investigation. To find leaks, you generally need 3 snapshots: | ||
|
|
||
| 1. **Baseline:** Before the suspect action. | ||
| 2. **Target:** After the suspect action. | ||
| 3. **Final:** After reverting the suspect action (e.g., closing a modal, navigating away). | ||
|
|
||
| Once you have these 3 snapshots saved to disk, you can use `memlab` to find leaks: | ||
|
|
||
| ```bash | ||
| npx memlab find-leaks --baseline <path-to-baseline> --target <path-to-target> --final <path-to-final> | ||
| ``` | ||
|
|
||
| You can also parse a single snapshot to find the largest objects or explore it individually: | ||
|
|
||
| ```bash | ||
| npx memlab analyze snapshot --snapshot <path-to-snapshot> | ||
| ``` | ||
|
|
||
| Memlab will output the retainer traces for identified leaks. Use these traces to guide your search in the codebase. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.