Skip to content

Commit 1db1017

Browse files
committed
feat: add heapsnapshot comparison script
1 parent c2d91ae commit 1db1017

2 files changed

Lines changed: 102 additions & 0 deletions

File tree

skills/memory-leak-debugging/SKILL.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,13 @@ Once you have generated `.heapsnapshot` files using `take_memory_snapshot`, use
3636
When you have found a leak trace (e.g., via `memlab` output), you must identify the root cause in the code.
3737

3838
- 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+
```bash
46+
node compare_snapshots.js <baseline.heapsnapshot> <target.heapsnapshot>
47+
```
48+
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: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
const fs = require('fs');
2+
3+
function parseSnapshot(filePath) {
4+
console.log(`Loading ${filePath}...`);
5+
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
6+
const strings = data.strings;
7+
const nodes = data.nodes;
8+
const nodeFields = data.snapshot.meta.node_fields;
9+
const nodeFieldCount = nodeFields.length;
10+
11+
const typeOffset = nodeFields.indexOf('type');
12+
const nameOffset = nodeFields.indexOf('name');
13+
const sizeOffset = nodeFields.indexOf('self_size');
14+
15+
const nodeTypes = data.snapshot.meta.node_types[typeOffset];
16+
17+
const counts = {};
18+
const sizes = {};
19+
20+
for (let i = 0; i < nodes.length; i += nodeFieldCount) {
21+
const typeIdx = nodes[i + typeOffset];
22+
const typeName = nodeTypes[typeIdx];
23+
const nameIdx = nodes[i + nameOffset];
24+
const name = typeof nameIdx === 'number' ? strings[nameIdx] : nameIdx;
25+
const size = nodes[i + sizeOffset];
26+
27+
// Ignore native primitives/arrays that clutter the output unless specifically looking for them
28+
if (typeName === 'string' || typeName === 'number' || typeName === 'array') continue;
29+
30+
const key = `${typeName}::${name}`;
31+
counts[key] = (counts[key] || 0) + 1;
32+
sizes[key] = (sizes[key] || 0) + size;
33+
}
34+
return { counts, sizes };
35+
}
36+
37+
const [,, file1, file2] = process.argv;
38+
if (!file1 || !file2) {
39+
console.error('Usage: node compare_snapshots.js <baseline.heapsnapshot> <target.heapsnapshot>');
40+
process.exit(1);
41+
}
42+
43+
try {
44+
const snap1 = parseSnapshot(file1);
45+
const snap2 = parseSnapshot(file2);
46+
47+
const diffs = [];
48+
for (const key in snap2.counts) {
49+
const count1 = snap1.counts[key] || 0;
50+
const count2 = snap2.counts[key];
51+
const size1 = snap1.sizes[key] || 0;
52+
const size2 = snap2.sizes[key];
53+
54+
if (count2 > count1) {
55+
diffs.push({
56+
key,
57+
countDiff: count2 - count1,
58+
sizeDiff: size2 - size1
59+
});
60+
}
61+
}
62+
63+
diffs.sort((a, b) => b.sizeDiff - a.sizeDiff);
64+
65+
console.log('\n--- Top 10 growing objects by size ---');
66+
diffs.slice(0, 10).forEach(d => {
67+
console.log(`${d.key}: +${d.countDiff} objects, +${d.sizeDiff} bytes`);
68+
});
69+
70+
// Look for common leak indicators
71+
const commonLeaks = diffs.filter(d =>
72+
d.key.toLowerCase().includes('detached') ||
73+
d.key.toLowerCase().includes('html') ||
74+
d.key.toLowerCase().includes('eventlistener') ||
75+
d.key.toLowerCase().includes('context') ||
76+
d.key.toLowerCase().includes('closure')
77+
);
78+
79+
commonLeaks.sort((a, b) => b.countDiff - a.countDiff);
80+
81+
console.log('\n--- Top 3 most common types of memory leaks found ---');
82+
if (commonLeaks.length === 0) {
83+
console.log('No common DOM or Closure leaks detected.');
84+
} else {
85+
commonLeaks.slice(0, 3).forEach(d => {
86+
console.log(`${d.key}: +${d.countDiff} objects, +${d.sizeDiff} bytes`);
87+
});
88+
}
89+
} catch (error) {
90+
console.error('Error parsing snapshots. They might be too large for JSON.parse or invalid.');
91+
console.error(error.message);
92+
}

0 commit comments

Comments
 (0)