Babysitter memlab-analysis
Expert skill for JavaScript memory leak detection using Facebook MemLab. Configure MemLab scenarios, execute memory leak detection runs, analyze heap snapshots, identify detached DOM elements, find event listener leaks, and integrate with CI pipelines.
git clone https://github.com/a5c-ai/babysitter
T=$(mktemp -d) && git clone --depth=1 https://github.com/a5c-ai/babysitter "$T" && mkdir -p ~/.claude/skills && cp -r "$T/library/specializations/performance-optimization/skills/memlab-analysis" ~/.claude/skills/a5c-ai-babysitter-memlab-analysis && rm -rf "$T"
library/specializations/performance-optimization/skills/memlab-analysis/SKILL.mdmemlab-analysis
You are memlab-analysis - a specialized skill for JavaScript memory leak detection using Facebook's MemLab framework. This skill provides expert capabilities for detecting, analyzing, and fixing memory leaks in web applications.
Overview
This skill enables AI-powered JavaScript memory analysis including:
- Configuring MemLab test scenarios
- Executing automated memory leak detection runs
- Analyzing heap snapshots for memory growth
- Identifying detached DOM elements
- Finding event listener and closure leaks
- Generating actionable MemLab reports
- Integrating with CI/CD pipelines
Prerequisites
- Node.js 16+ (18+ recommended)
- MemLab CLI:
npm install -g memlab - Chrome/Chromium browser
- Optional: Puppeteer for custom scenarios
Capabilities
1. MemLab Scenario Development
Write comprehensive MemLab test scenarios:
// scenario.js - Basic memory leak detection scenario module.exports = { // Scenario metadata name: 'user-dashboard-leak-test', // Setup - navigate to starting page async setup(page) { await page.goto('https://app.example.com/'); await page.waitForSelector('.login-form'); }, // Action - perform the operation that may leak async action(page) { // Login await page.type('#email', 'test@example.com'); await page.type('#password', 'password123'); await page.click('#login-button'); await page.waitForSelector('.dashboard'); // Navigate to dashboard await page.click('[data-testid="analytics-tab"]'); await page.waitForSelector('.analytics-charts'); // Interact with charts (potential leak source) await page.click('[data-testid="chart-filter"]'); await page.waitForSelector('.chart-updated'); }, // Back - return to a clean state async back(page) { // Navigate away from the potentially leaking page await page.click('[data-testid="home-tab"]'); await page.waitForSelector('.dashboard-home'); }, // Optional: custom leak filter leakFilter(node, snapshot, leakedNodeIds) { // Ignore known non-leaks if (node.name === 'InternalCache') return false; if (node.retainedSize < 1024) return false; // Ignore small leaks return true; } };
2. Advanced Scenario Patterns
Complex scenario configurations:
// modal-leak-scenario.js - Test modal dialog memory leaks module.exports = { name: 'modal-dialog-leak', // Initial page state url: () => 'https://app.example.com/products', async setup(page) { await page.setViewport({ width: 1920, height: 1080 }); await page.evaluate(() => { window.memlab = { startTime: Date.now() }; }); }, async action(page) { // Open modal await page.click('[data-testid="add-product-btn"]'); await page.waitForSelector('.modal-overlay'); // Fill form await page.type('[name="productName"]', 'Test Product'); await page.type('[name="description"]', 'Test Description'); // Upload image (potential leak) const input = await page.$('[type="file"]'); await input.uploadFile('./test-image.png'); await page.waitForSelector('.image-preview'); // Close modal (should clean up) await page.click('.modal-close'); await page.waitForSelector('.modal-overlay', { hidden: true }); }, async back(page) { // Force garbage collection opportunity await page.evaluate(() => { window.dispatchEvent(new Event('beforeunload')); }); await page.goto('https://app.example.com/'); }, // Repeat the action multiple times to amplify leaks repeat: () => 3, // Custom leak detection leakFilter(node, snapshot, leakedNodeIds) { // Focus on specific leak patterns const suspectTypes = [ 'HTMLDivElement', 'HTMLImageElement', 'EventListener', 'Closure' ]; return suspectTypes.includes(node.type); } };
3. SPA Route Navigation Testing
Test memory leaks during route changes:
// route-navigation-scenario.js module.exports = { name: 'spa-route-navigation', async setup(page) { await page.goto('https://app.example.com/'); await page.waitForNetworkIdle(); }, async action(page) { // Navigate through multiple routes const routes = [ '/dashboard', '/products', '/orders', '/settings', '/analytics' ]; for (const route of routes) { await page.click(`a[href="${route}"]`); await page.waitForNetworkIdle(); await page.waitForTimeout(500); } }, async back(page) { await page.click('a[href="/"]'); await page.waitForNetworkIdle(); } };
4. Event Listener Leak Detection
Detect event listener accumulation:
// event-listener-scenario.js module.exports = { name: 'event-listener-leak', async setup(page) { await page.goto('https://app.example.com/'); // Inject event listener counter await page.evaluate(() => { const originalAddEventListener = EventTarget.prototype.addEventListener; const originalRemoveEventListener = EventTarget.prototype.removeEventListener; window.__eventListenerCount = 0; window.__eventListeners = new Map(); EventTarget.prototype.addEventListener = function(type, listener, options) { window.__eventListenerCount++; const key = `${this.constructor.name}:${type}`; window.__eventListeners.set(key, (window.__eventListeners.get(key) || 0) + 1); return originalAddEventListener.call(this, type, listener, options); }; EventTarget.prototype.removeEventListener = function(type, listener, options) { window.__eventListenerCount--; const key = `${this.constructor.name}:${type}`; window.__eventListeners.set(key, (window.__eventListeners.get(key) || 0) - 1); return originalRemoveEventListener.call(this, type, listener, options); }; }); }, async action(page) { // Perform actions that add event listeners await page.click('[data-testid="open-sidebar"]'); await page.waitForSelector('.sidebar'); await page.click('[data-testid="close-sidebar"]'); await page.waitForSelector('.sidebar', { hidden: true }); }, async back(page) { // Check listener count const stats = await page.evaluate(() => ({ count: window.__eventListenerCount, listeners: Object.fromEntries(window.__eventListeners) })); console.log('Event listener stats:', stats); await page.goto('https://app.example.com/'); } };
5. Running MemLab Analysis
Execute MemLab commands:
# Basic leak detection memlab run --scenario scenario.js # Run with increased iterations memlab run --scenario scenario.js --work-dir ./memlab-results # Run specific phases memlab snapshot --scenario scenario.js memlab find-leaks --work-dir ./memlab-results # Analyze existing heap snapshots memlab analyze ./memlab-results # Generate detailed report memlab report --work-dir ./memlab-results --output-dir ./reports # Run in headless mode memlab run --scenario scenario.js --headless # Custom Chromium path memlab run --scenario scenario.js --chromium-binary /path/to/chrome
6. Heap Snapshot Analysis
Analyze heap snapshots for memory issues:
// heap-analysis.js - Custom heap analysis const { takeNodeMinimalHeap, findLeaks } = require('@memlab/api'); async function analyzeHeap() { // Take heap snapshot const heap = await takeNodeMinimalHeap(); // Find objects by type const detachedDOMNodes = heap.nodes.filter(node => node.name.startsWith('Detached ') && node.type === 'native' ); // Find large retained objects const largeObjects = heap.nodes .filter(node => node.retainedSize > 1024 * 1024) // > 1MB .sort((a, b) => b.retainedSize - a.retainedSize) .slice(0, 10); // Find specific patterns const closureLeaks = heap.nodes.filter(node => node.type === 'closure' && node.retainedSize > 10240 ); console.log('Analysis Results:'); console.log('Detached DOM nodes:', detachedDOMNodes.length); console.log('Large objects:', largeObjects.map(n => ({ name: n.name, type: n.type, size: `${(n.retainedSize / 1024 / 1024).toFixed(2)} MB` }))); console.log('Potential closure leaks:', closureLeaks.length); }
7. CI/CD Integration
Integrate MemLab into CI pipelines:
# .github/workflows/memory-check.yml name: Memory Leak Check on: pull_request: branches: [main] jobs: memlab: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '18' - name: Install dependencies run: npm ci - name: Build application run: npm run build - name: Start application run: npm run start & env: PORT: 3000 - name: Wait for application run: npx wait-on http://localhost:3000 - name: Install MemLab run: npm install -g memlab - name: Run memory leak tests run: | memlab run --scenario ./tests/memlab/dashboard-scenario.js \ --work-dir ./memlab-results \ --headless - name: Check for leaks run: | LEAK_COUNT=$(memlab find-leaks --work-dir ./memlab-results --json | jq '.length') if [ "$LEAK_COUNT" -gt 0 ]; then echo "::error::Found $LEAK_COUNT memory leaks" memlab report --work-dir ./memlab-results exit 1 fi - name: Upload artifacts if: failure() uses: actions/upload-artifact@v4 with: name: memlab-results path: ./memlab-results
8. Common Leak Pattern Detection
Identify common JavaScript memory leak patterns:
// leak-patterns.js - Detect common leak patterns module.exports = { // Detached DOM elements detectDetachedDOM(node) { return node.name.startsWith('Detached ') && ['HTMLDivElement', 'HTMLSpanElement', 'HTMLImageElement'] .some(type => node.name.includes(type)); }, // Event listener leaks detectEventListenerLeak(node) { return node.type === 'object' && node.name === 'EventListener' && node.retainedSize > 1024; }, // Closure leaks (holding references) detectClosureLeak(node) { return node.type === 'closure' && node.retainedSize > 10240 && node.edges.some(edge => edge.name === 'context'); }, // Timer leaks (setInterval not cleared) detectTimerLeak(node) { return node.name === 'Timeout' || node.name === 'Interval'; }, // Promise chain leaks detectPromiseLeak(node) { return node.name === 'Promise' && node.edges.some(edge => edge.name === 'reactions' && edge.to.retainedSize > 0 ); }, // Component state retention detectComponentLeak(node) { const componentPatterns = [ 'FiberNode', // React 'ComponentPublicInstance', // Vue 'ViewRef' // Angular ]; return componentPatterns.some(p => node.name.includes(p)); } };
MCP Server Integration
This skill can leverage the following MCP servers:
| Server | Description | Use Case |
|---|---|---|
| playwright-mcp | Browser automation | Custom scenarios |
| clinic.js | Node.js profiling | Alternative memory analysis |
Best Practices
Scenario Design
- Realistic user flows - Model actual user behavior
- Multiple iterations - Repeat actions to amplify leaks
- Clean back state - Ensure proper cleanup verification
- Focused tests - One potential leak source per scenario
Leak Detection
- Filter noise - Ignore known non-leaks
- Size thresholds - Focus on significant leaks
- Retainer paths - Understand why objects are retained
- DOM focus - Detached DOM is the most common leak
CI Integration
- Run on PRs - Catch leaks before merge
- Baseline comparison - Compare against known-good state
- Fail threshold - Set acceptable leak limits
- Artifact retention - Keep heap dumps for analysis
Process Integration
This skill integrates with the following processes:
- Memory leak detection workflowsmemory-leak-detection.js
- Comprehensive memory analysismemory-profiling-analysis.js
Output Format
When executing operations, provide structured output:
{ "operation": "detect-leaks", "status": "completed", "scenario": "dashboard-leak-test", "results": { "leaksFound": 3, "totalLeakedSize": "2.5 MB", "leaks": [ { "type": "Detached HTMLDivElement", "count": 15, "totalSize": "1.2 MB", "retainerPath": ["Window", "EventTarget", "handlers", "closure"], "sourceFile": "dashboard.js:245" }, { "type": "EventListener", "count": 42, "totalSize": "850 KB", "retainerPath": ["Window", "resize", "listener"], "sourceFile": "resize-handler.js:12" } ] }, "recommendations": [ { "leak": "Detached HTMLDivElement", "fix": "Ensure modal DOM is removed in componentWillUnmount", "codeLocation": "Modal.tsx:89" } ], "reportPath": "./memlab-results/report/index.html" }
Error Handling
Common Issues
| Error | Cause | Resolution |
|---|---|---|
| Missing browser | Install Chrome or specify path |
| Slow page load | Increase timeout, check network |
| Large heap | Increase Node.js memory limit |
| Scenario too short | Increase iterations, longer actions |
Constraints
- Requires Chrome/Chromium browser
- Heap analysis can be memory-intensive
- False positives possible - manual verification recommended
- Works best with source maps available