chromedevtools--chrome-devtools-mcp
d19a2350f9
This PR add a new skill for detecting and removing memory leaks using take_memory_snapshot tool. Skill was based on authoritative material from own sources: * https://developer.chrome.com/docs/devtools/memory-problems * https://web.dev/articles/effectivemanagement as well as examples of memory leaks from public domain. It can be tested with https://github.com/devnook/mem-leak-example. Example prompt * A page running at localhost:3000 has memory issues when you click from home to contact tab and back. Can you fix it? The skill prefers using comparison tools like memlab instead of custom evaluate_script snippets to avoid directly reading of .heapsnpashot files to save context space and tokens. Fixes #1056 --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Yusheng <113574546+yshngg@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Alex Rudenko <OrKoN@users.noreply.github.com> Co-authored-by: Mathias Bynens <mathias@qiwi.be>
110 行
3.0 KiB
JavaScript
110 行
3.0 KiB
JavaScript
/**
|
|
* @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);
|
|
}
|