analyze-heap-snapshots.cjs•4.75 kB
const fs = require('fs');
const path = require('path');
// Simple heap snapshot analyzer
// V8 heap snapshot format reference: https://github.com/v8/v8/blob/master/src/profiler/heap-snapshot-generator.cc
function analyzeHeapSnapshot(filename) {
console.log(`\nAnalyzing ${path.basename(filename)}...`);
const content = fs.readFileSync(filename, 'utf8');
const snapshot = JSON.parse(content);
// Get snapshot metadata
const meta = snapshot.snapshot.meta;
console.log(`Node types: ${meta.node_types.length}`);
console.log(`Edge types: ${meta.edge_types.length}`);
console.log(`Total nodes: ${snapshot.nodes.length / meta.node_fields.length}`);
// Parse nodes
const nodeFieldCount = meta.node_fields.length;
const nodes = [];
for (let i = 0; i < snapshot.nodes.length; i += nodeFieldCount) {
const node = {
type: meta.node_types[0][snapshot.nodes[i]],
name: snapshot.strings[snapshot.nodes[i + 1]],
id: snapshot.nodes[i + 2],
self_size: snapshot.nodes[i + 3],
edge_count: snapshot.nodes[i + 4],
trace_node_id: snapshot.nodes[i + 5]
};
nodes.push(node);
}
// Group by type and calculate sizes
const typeStats = {};
nodes.forEach(node => {
if (!typeStats[node.type]) {
typeStats[node.type] = { count: 0, size: 0, examples: [] };
}
typeStats[node.type].count++;
typeStats[node.type].size += node.self_size;
// Keep top 5 largest examples
if (typeStats[node.type].examples.length < 5 ||
node.self_size > typeStats[node.type].examples[4].self_size) {
typeStats[node.type].examples.push(node);
typeStats[node.type].examples.sort((a, b) => b.self_size - a.self_size);
if (typeStats[node.type].examples.length > 5) {
typeStats[node.type].examples.pop();
}
}
});
// Sort by total size
const sortedTypes = Object.entries(typeStats)
.sort((a, b) => b[1].size - a[1].size)
.slice(0, 10);
console.log('\nTop 10 object types by total size:');
sortedTypes.forEach(([type, stats]) => {
console.log(` ${type}: ${(stats.size / 1024 / 1024).toFixed(2)} MB (${stats.count} objects)`);
if (stats.examples.length > 0 && type !== 'string') {
console.log(` Examples:`);
stats.examples.slice(0, 3).forEach(ex => {
console.log(` - ${ex.name || '(anonymous)'} (${(ex.self_size / 1024).toFixed(2)} KB)`);
});
}
});
// Find large individual objects
const largeObjects = nodes
.filter(n => n.self_size > 100000) // > 100KB
.sort((a, b) => b.self_size - a.self_size)
.slice(0, 10);
if (largeObjects.length > 0) {
console.log('\nLargest individual objects:');
largeObjects.forEach(obj => {
console.log(` ${obj.type} "${obj.name || '(anonymous)'}": ${(obj.self_size / 1024).toFixed(2)} KB`);
});
}
// Total snapshot size
const totalSize = nodes.reduce((sum, node) => sum + node.self_size, 0);
console.log(`\nTotal heap size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`);
return { totalSize, typeStats, largeObjects };
}
// Compare snapshots
function compareSnapshots(snapshot1, snapshot2) {
console.log('\n=== Snapshot Comparison ===');
const sizeDiff = snapshot2.totalSize - snapshot1.totalSize;
console.log(`Size difference: ${(sizeDiff / 1024 / 1024).toFixed(2)} MB`);
// Compare type stats
console.log('\nType growth:');
Object.keys(snapshot2.typeStats).forEach(type => {
const stats1 = snapshot1.typeStats[type] || { size: 0, count: 0 };
const stats2 = snapshot2.typeStats[type];
const sizeDiff = stats2.size - stats1.size;
const countDiff = stats2.count - stats1.count;
if (sizeDiff > 1024 * 1024) { // Only show > 1MB differences
console.log(` ${type}: +${(sizeDiff / 1024 / 1024).toFixed(2)} MB (+${countDiff} objects)`);
}
});
}
// Main
const snapshotDir = path.join(__dirname, 'heap-snapshots');
const files = fs.readdirSync(snapshotDir)
.filter(f => f.endsWith('.heapsnapshot'))
.sort();
if (files.length === 0) {
console.log('No heap snapshots found in', snapshotDir);
process.exit(1);
}
const snapshots = [];
files.forEach(file => {
const result = analyzeHeapSnapshot(path.join(snapshotDir, file));
snapshots.push(result);
});
// Compare first and last
if (snapshots.length >= 2) {
compareSnapshots(snapshots[0], snapshots[snapshots.length - 1]);
}