Memory Profiling and Optimization
Memory management is crucial for Node.js performance. Learn to profile, analyze, and optimize memory usage in your applications.
Challenge
Section titled “Challenge”Analyze and optimize memory usage to prevent memory leaks, reduce consumption, and improve application performance and stability.
Explanation
Section titled “Explanation”Memory in Node.js
Section titled “Memory in Node.js”Heap: Dynamic memory allocation Stack: Function calls and local variables Memory Leak: Unreferenced objects not garbage collected Fragmentation: Inefficient memory layout Garbage Collection: Automatic memory reclamation
Profiling Tools
Section titled “Profiling Tools”- Node Inspector: Built-in debugging
- clinic.js: Performance profiling
- 0x: Flame graph generation
- Chrome DevTools: Remote debugging
- heapdump: Heap snapshots
Code Examples
Section titled “Code Examples”1. Memory Usage Monitoring
Section titled “1. Memory Usage Monitoring”function getMemoryUsage() { const mem = process.memoryUsage(); return { rss: Math.round(mem.rss / 1024 / 1024) + 'MB', heapTotal: Math.round(mem.heapTotal / 1024 / 1024) + 'MB', heapUsed: Math.round(mem.heapUsed / 1024 / 1024) + 'MB', external: Math.round(mem.external / 1024 / 1024) + 'MB', arrayBuffers: Math.round(mem.arrayBuffers / 1024 / 1024) + 'MB', };}
// Log memory usage periodicallysetInterval(() => { console.log('Memory usage:', getMemoryUsage());}, 30000);
// Express endpointapp.get('/metrics/memory', (req, res) => { res.json(getMemoryUsage());});2. Detecting Memory Leaks
Section titled “2. Detecting Memory Leaks”class LeakDetector { constructor(threshold = 100 * 1024 * 1024) { // 100MB this.threshold = threshold; this.samples = []; this.maxSamples = 10; }
sample() { const heapUsed = process.memoryUsage().heapUsed; this.samples.push({ timestamp: Date.now(), heapUsed, });
// Keep only recent samples if (this.samples.length > this.maxSamples) { this.samples.shift(); }
this.analyzeGrowth(); }
analyzeGrowth() { if (this.samples.length < 3) return;
const firstSample = this.samples[0]; const lastSample = this.samples[this.samples.length - 1]; const growth = lastSample.heapUsed - firstSample.heapUsed; const timeDiff = lastSample.timestamp - firstSample.timestamp;
// Check for consistent growth if (growth > this.threshold) { console.warn('Potential memory leak detected!', { growth: Math.round(growth / 1024 / 1024) + 'MB', timespan: Math.round(timeDiff / 1000) + 's', growthRate: (growth / timeDiff * 1000).toFixed(2) + 'MB/s', });
// Trigger heap dump this.dumpHeap(); } }
dumpHeap() { const heapdump = require('heapdump'); heapdump.writeSnapshot(`./heap-${Date.now()}.heapsnapshot`); }
start(interval = 60000) { setInterval(() => this.sample(), interval); }}
const leakDetector = new LeakDetector();leakDetector.start(60000); // Check every minute3. Object Pool Pattern
Section titled “3. Object Pool Pattern”class ObjectPool { constructor(factory, resetFn, size = 10) { this.factory = factory; this.resetFn = resetFn; this.available = []; this.in_use = new Set();
// Pre-allocate objects for (let i = 0; i < size; i++) { this.available.push(factory()); } }
acquire() { let obj;
if (this.available.length > 0) { obj = this.available.pop(); } else { obj = this.factory(); }
this.in_use.add(obj); return obj; }
release(obj) { if (!this.in_use.has(obj)) { throw new Error('Object not from this pool'); }
this.resetFn(obj); this.in_use.delete(obj); this.available.push(obj); }
size() { return { available: this.available.length, in_use: this.in_use.size, }; }}
// Usageconst bufferPool = new ObjectPool( () => Buffer.allocUnsafe(1024 * 1024), // 1MB buffers (buf) => buf.fill(0), // Reset buffer 50 // Pool size);
// Using poolconst buffer = bufferPool.acquire();try { // Use buffer buffer.write('data');} finally { bufferPool.release(buffer);}4. Stream Processing for Large Data
Section titled “4. Stream Processing for Large Data”const fs = require('fs');
// BAD: Loading entire file into memoryasync function processFileBad(filePath) { const data = fs.readFileSync(filePath, 'utf-8'); const lines = data.split('\n'); return lines.map(line => line.toUpperCase());}
// GOOD: Streaming processingfunction processFileGood(filePath) { return new Promise((resolve, reject) => { const lines = []; const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
stream.on('data', (chunk) => { const chunkLines = chunk.split('\n'); chunkLines.forEach(line => { if (line) lines.push(line.toUpperCase()); }); });
stream.on('end', () => resolve(lines)); stream.on('error', reject); });}
// BETTER: Process lines without storing allfunction processFileStreaming(filePath) { const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
stream.on('data', (chunk) => { // Process chunk immediately const lines = chunk.split('\n'); lines.forEach(line => { if (line) { // Do something with line without storing console.log(line.toUpperCase()); } }); });
return new Promise((resolve, reject) => { stream.on('end', resolve); stream.on('error', reject); });}5. Weak References and WeakMap
Section titled “5. Weak References and WeakMap”// Regular Map - keeps references aliveconst cache = new Map();
// WeakMap - allows garbage collectionconst weakCache = new WeakMap();
class CacheExample { constructor() { this.strongCache = new Map(); this.weakCache = new WeakMap(); }
setStrong(key, value) { this.strongCache.set(key, value); }
setWeak(key, value) { // key must be an object, won't prevent garbage collection this.weakCache.set(key, value); }
getWeak(key) { return this.weakCache.get(key); }}
// Usageconst cache = new CacheExample();let obj = { id: 1 };
cache.setWeak(obj, 'some data');console.log(cache.getWeak(obj)); // 'some data'
obj = null; // Object can now be garbage collected// WeakMap entry will be automatically removed6. Profiling with clinic.js
Section titled “6. Profiling with clinic.js”# Install clinic.jsnpm install -g clinic
# Doctor profilingclinic doctor -- node server.js
# Flame graphclinic flame -- node server.js
# Bubbleprofclinic bubbleprof -- node server.js7. Heap Snapshot Analysis
Section titled “7. Heap Snapshot Analysis”const v8 = require('v8');const fs = require('fs');
// Trigger heap snapshotfunction createHeapSnapshot() { const timestamp = Date.now(); const snapshotStream = v8.writeHeapSnapshot(`./heap-${timestamp}.heapsnapshot`);
return new Promise((resolve, reject) => { snapshotStream.on('finish', () => { console.log(`Heap snapshot written to heap-${timestamp}.heapsnapshot`); resolve(); }); snapshotStream.on('error', reject); });}
// Endpoint to trigger snapshotapp.post('/admin/heap-snapshot', async (req, res) => { try { await createHeapSnapshot(); res.json({ success: true, message: 'Heap snapshot created' }); } catch (error) { res.status(500).json({ error: error.message }); }});Best Practices
Section titled “Best Practices”-
Monitor Memory
- Use memory profiling tools
- Set up alerts for high usage
- Track memory trends
- Test under load
-
Prevent Leaks
- Remove event listeners
- Clear timers and intervals
- Properly close connections
- Avoid circular references
-
Optimize Usage
- Use streams for large data
- Implement object pooling
- Leverage caching appropriately
- Use WeakMap for caches
-
Garbage Collection
- Understand GC pauses
- Tune GC parameters if needed
- Monitor GC frequency
- Avoid forcing GC
-
Testing
- Load test applications
- Check memory under stress
- Monitor long-running processes
- Use heap snapshots for analysis
Real-World Applications
Section titled “Real-World Applications”- Server Applications: Prevent memory exhaustion
- Microservices: Resource-constrained environments
- Data Processing: Handle large datasets efficiently
- Real-time Systems: Consistent performance
- Long-running Processes: Detect memory leaks early