Skip to content

Commit e00d30b

Browse files
authored
Merge branch 'main' into patch-1
2 parents e0a2b6b + b1684c6 commit e00d30b

19 files changed

Lines changed: 495 additions & 231 deletions

.claude-plugin/marketplace.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"name": "chrome-devtools-plugins",
3-
"version": "1.0.0",
3+
"version": "0.20.3",
44
"description": "Bundled plugins for actuating and debugging the Chrome browser.",
5+
"repository": "https://github.com/ChromeDevTools/chrome-devtools-mcp",
56
"owner": {
67
"name": "Chrome DevTools Team",
78
"email": "devtools-dev@chromium.org"

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "chrome-devtools-mcp",
3-
"version": "latest",
3+
"version": "0.20.3",
44
"description": "Reliable automation, in-depth debugging, and performance analysis in Chrome using Chrome DevTools and Puppeteer",
55
"mcpServers": {
66
"chrome-devtools": {

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ allowing them to inspect, debug, and modify any data in the browser or DevTools.
2727
Avoid sharing sensitive or personal information that you don't want to share with
2828
MCP clients.
2929

30+
`chrome-devtools-mcp` officially supports Google Chrome and [Chrome for Testing](https://developer.chrome.com/blog/chrome-for-testing/) only.
31+
Other Chromium-based browser may work, but this is not guaranteed, and you may encounter unexpected behavior. Use at your own discretion.
32+
We are committed to providing fixes and support for the latest version of [Extended Stable Chrome](https://chromiumdash.appspot.com/schedule).
33+
3034
Performance tools may send trace URLs to the Google CrUX API to fetch real-user
3135
experience data. This helps provide a holistic performance picture by
3236
presenting field data alongside lab data. This data is collected by the [Chrome
@@ -53,7 +57,7 @@ Collection is disabled if CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS or CI env vari
5357

5458
- [Node.js](https://nodejs.org/) v20.19 or a newer [latest maintenance LTS](https://github.com/nodejs/Release#release-schedule) version.
5559
- [Chrome](https://www.google.com/chrome/) current stable version or newer.
56-
- [npm](https://www.npmjs.com/).
60+
- [npm](https://www.npmjs.com/)
5761

5862
## Getting started
5963

eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export default defineConfig([
3737
'puppeteer.config.cjs',
3838
'eslint.config.mjs',
3939
'rollup.config.mjs',
40+
'skills/memory-leak-debugging/references/compare_snapshots.js',
4041
],
4142
},
4243
},

package-lock.json

Lines changed: 200 additions & 200 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"devDependencies": {
4848
"@eslint/js": "^9.35.0",
4949
"@google/genai": "^1.37.0",
50-
"@modelcontextprotocol/sdk": "1.27.1",
50+
"@modelcontextprotocol/sdk": "1.28.0",
5151
"@rollup/plugin-commonjs": "^29.0.0",
5252
"@rollup/plugin-json": "^6.1.0",
5353
"@rollup/plugin-node-resolve": "^16.0.3",
@@ -59,17 +59,17 @@
5959
"@types/yargs": "^17.0.33",
6060
"@typescript-eslint/eslint-plugin": "^8.43.0",
6161
"@typescript-eslint/parser": "^8.43.0",
62-
"chrome-devtools-frontend": "1.0.1599001",
63-
"core-js": "3.48.0",
62+
"chrome-devtools-frontend": "1.0.1602348",
63+
"core-js": "3.49.0",
6464
"debug": "4.4.3",
6565
"eslint": "^9.35.0",
6666
"eslint-import-resolver-typescript": "^4.4.4",
6767
"eslint-plugin-import": "^2.32.0",
6868
"globals": "^17.0.0",
6969
"lighthouse": "13.0.3",
7070
"prettier": "^3.6.2",
71-
"puppeteer": "24.39.1",
72-
"rollup": "4.59.0",
71+
"puppeteer": "24.40.0",
72+
"rollup": "4.59.1",
7373
"rollup-plugin-cleanup": "^3.2.1",
7474
"rollup-plugin-license": "^3.6.0",
7575
"sinon": "^21.0.0",

release-please-config.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@
2929
"type": "json",
3030
"path": "server.json",
3131
"jsonpath": "packages[0].version"
32+
},
33+
{
34+
"type": "json",
35+
"path": ".claude-plugin/marketplace.json",
36+
"jsonpath": "version"
37+
},
38+
{
39+
"type": "json",
40+
"path": ".claude-plugin/plugin.json",
41+
"jsonpath": "version"
3242
}
3343
]
3444
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
name: memory-leak-debugging
3+
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.
4+
---
5+
6+
# Memory Leak Debugging
7+
8+
This skill provides expert guidance and workflows for finding, diagnosing, and fixing memory leaks in JavaScript and Node.js applications.
9+
10+
## Core Principles
11+
12+
- **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.
13+
- **Isolate the Leak:** Determine if the leak is in the browser (client-side) or Node.js (server-side).
14+
- **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._
15+
16+
## Workflows
17+
18+
### 1. Capturing Snapshots
19+
20+
When investigating a frontend web application memory leak, utilize the `chrome-devtools-mcp` tools to interact with the application and take snapshots.
21+
22+
- Use tools like `click`, `navigate_page`, `fill`, etc., to manipulate the page into the desired state.
23+
- Revert the page back to the original state after interactions to see if memory is released.
24+
- Repeat the same user interactions 10 times to amplify the leak.
25+
- Use `take_memory_snapshot` to save `.heapsnapshot` files to disk at baseline, target (after actions), and final (after reverting actions) states.
26+
27+
### 2. Using Memlab to Find Leaks (Recommended)
28+
29+
Once you have generated `.heapsnapshot` files using `take_memory_snapshot`, use `memlab` to automatically find memory leaks.
30+
31+
- Read [references/memlab.md](references/memlab.md) for how to use `memlab` to analyze the generated heapsnapshots.
32+
- Do **not** read raw `.heapsnapshot` files using `read_file` or `cat`.
33+
34+
### 3. Identifying Common Leaks
35+
36+
When you have found a leak trace (e.g., via `memlab` output), you must identify the root cause in the code.
37+
38+
- Read [references/common-leaks.md](references/common-leaks.md) for examples of common memory leaks and how to fix them.
39+
40+
### 4. Fallback: Comparing Snapshots Manually
41+
42+
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.
43+
44+
Run the script using Node.js:
45+
46+
```bash
47+
node compare_snapshots.js <baseline.heapsnapshot> <target.heapsnapshot>
48+
```
49+
50+
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.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Common Memory Leaks
2+
3+
When analyzing a retainer trace from `memlab`, look for these common patterns in the codebase:
4+
5+
## 1. Uncleared Event Listeners
6+
7+
Event listeners attached to global objects (like `window` or `document`) or long-living objects prevent garbage collection of the objects referenced in their callbacks.
8+
9+
**Fix:** Always call `removeEventListener` when a component unmounts or the listener is no longer needed.
10+
11+
## 2. Detached DOM Nodes
12+
13+
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.
14+
15+
**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.
16+
17+
## 3. Unintentional Global Variables
18+
19+
Variables declared without `var`, `let`, or `const` (in non-strict mode) or explicitly attached to `window` remain in memory forever.
20+
21+
**Fix:** Use strict mode, properly declare variables, and avoid global state.
22+
23+
## 4. Closures
24+
25+
Closures can unintentionally keep references to large objects in their outer scope.
26+
27+
**Fix:** Nullify large objects when they are no longer needed, or refactor the closure to not capture unnecessary variables.
28+
29+
## 5. Unbounded Caches or Arrays
30+
31+
Data structures used for caching (like objects, Arrays, or Maps) that grow without limits.
32+
33+
**Fix:** Implement caching limits, use LRU caches, or use `WeakMap`/`WeakSet` for data associated with object lifecycles.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import * as fs from 'node:fs';
8+
9+
function parseSnapshot(filePath) {
10+
console.log(`Loading ${filePath}...`);
11+
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
12+
const strings = data.strings;
13+
const nodes = data.nodes;
14+
const nodeFields = data.snapshot.meta.node_fields;
15+
const nodeFieldCount = nodeFields.length;
16+
17+
const typeOffset = nodeFields.indexOf('type');
18+
const nameOffset = nodeFields.indexOf('name');
19+
const sizeOffset = nodeFields.indexOf('self_size');
20+
21+
const nodeTypes = data.snapshot.meta.node_types[typeOffset];
22+
23+
const counts = {};
24+
const sizes = {};
25+
26+
for (let i = 0; i < nodes.length; i += nodeFieldCount) {
27+
const typeIdx = nodes[i + typeOffset];
28+
const typeName = nodeTypes[typeIdx];
29+
const nameIdx = nodes[i + nameOffset];
30+
const name = typeof nameIdx === 'number' ? strings[nameIdx] : nameIdx;
31+
const size = nodes[i + sizeOffset];
32+
33+
// Ignore native primitives/arrays that clutter the output unless specifically looking for them
34+
if (
35+
typeName === 'string' ||
36+
typeName === 'number' ||
37+
typeName === 'array'
38+
) {
39+
continue;
40+
}
41+
42+
const key = `${typeName}::${name}`;
43+
counts[key] = (counts[key] || 0) + 1;
44+
sizes[key] = (sizes[key] || 0) + size;
45+
}
46+
return {counts, sizes};
47+
}
48+
49+
const [, , file1, file2] = process.argv;
50+
if (!file1 || !file2) {
51+
console.error(
52+
'Usage: node compare_snapshots.js <baseline.heapsnapshot> <target.heapsnapshot>',
53+
);
54+
process.exit(1);
55+
}
56+
57+
try {
58+
const snap1 = parseSnapshot(file1);
59+
const snap2 = parseSnapshot(file2);
60+
61+
const diffs = [];
62+
for (const key in snap2.counts) {
63+
const count1 = snap1.counts[key] || 0;
64+
const count2 = snap2.counts[key];
65+
const size1 = snap1.sizes[key] || 0;
66+
const size2 = snap2.sizes[key];
67+
68+
if (count2 > count1) {
69+
diffs.push({
70+
key,
71+
countDiff: count2 - count1,
72+
sizeDiff: size2 - size1,
73+
});
74+
}
75+
}
76+
77+
diffs.sort((a, b) => b.sizeDiff - a.sizeDiff);
78+
79+
console.log('\n--- Top 10 growing objects by size ---');
80+
diffs.slice(0, 10).forEach(d => {
81+
console.log(`${d.key}: +${d.countDiff} objects, +${d.sizeDiff} bytes`);
82+
});
83+
84+
// Look for common leak indicators
85+
const commonLeaks = diffs.filter(
86+
d =>
87+
d.key.toLowerCase().includes('detached') ||
88+
d.key.toLowerCase().includes('html') ||
89+
d.key.toLowerCase().includes('eventlistener') ||
90+
d.key.toLowerCase().includes('context') ||
91+
d.key.toLowerCase().includes('closure'),
92+
);
93+
94+
commonLeaks.sort((a, b) => b.countDiff - a.countDiff);
95+
96+
console.log('\n--- Top 3 most common types of memory leaks found ---');
97+
if (commonLeaks.length === 0) {
98+
console.log('No common DOM or Closure leaks detected.');
99+
} else {
100+
commonLeaks.slice(0, 3).forEach(d => {
101+
console.log(`${d.key}: +${d.countDiff} objects, +${d.sizeDiff} bytes`);
102+
});
103+
}
104+
} catch (error) {
105+
console.error(
106+
'Error parsing snapshots. They might be too large for JSON.parse or invalid.',
107+
);
108+
console.error(error.message);
109+
}

0 commit comments

Comments
 (0)