#!/usr/bin/env node /** * Memory Leak Detector for Trigger.dev Webapp * * This script starts the server, performs memory snapshots, executes API requests, * and analyzes memory usage patterns to detect potential memory leaks. * * Usage: node memory-leak-detector.js [options] */ const { spawn, exec: _exec } = require("child_process"); const fs = require("fs"); const path = require("path"); const http = require("http"); const { performance } = require("perf_hooks"); class MemoryLeakDetector { constructor(options = {}) { // Create timestamped directory for this run const runTimestamp = new Date().toISOString().replace(/[:.]/g, "-"); const baseDir = options.baseDir || "./.memory-snapshots"; const runDir = `${baseDir}/${runTimestamp}-${options.label || "memory-leak-detector"}`; this.options = { // Server configuration serverPort: options.serverPort || 3030, serverStartTimeout: options.serverStartTimeout || 30000, // Sentry configuration sentryDsn: options.sentryDsn || "", // Memory snapshot configuration heapSnapshotPath: `${runDir}/snapshots`, adminToken: options.adminToken, // Bearer token for admin API access apiKey: options.apiKey, // Testing configuration warmupRequests: options.warmupRequests || 10, testRequests: options.testRequests || 100, requestDelay: options.requestDelay || 50, // ms between requests requestTimeout: options.requestTimeout || 5000, // API endpoints to test (configurable) apiEndpoints: options.apiEndpoints || ["/api/v1/runs"], postApiEndpoints: options.postApiEndpoints || ["/api/v1/mock"], // Memory analysis thresholds memoryLeakThreshold: options.memoryLeakThreshold || 50, // MB increase heapGrowthThreshold: options.heapGrowthThreshold || 0.2, // 20% growth // Output configuration verbose: options.verbose || false, outputFile: `${runDir}/memory-leak-report.json`, runDir: runDir, runTimestamp: runTimestamp, ...options, }; this.serverProcess = null; this.results = { startTime: new Date().toISOString(), runTimestamp: runTimestamp, runDirectory: runDir, serverInfo: {}, snapshots: [], testPhases: [], // Track each phase of testing totalRequests: { warmup: 0, phase1: 0, phase2: 0, successful: 0, failed: 0, errors: [], }, memoryAnalysis: {}, recommendations: [], }; } log(message, level = "info") { const timestamp = new Date().toISOString(); const prefix = `[${timestamp}] [${level.toUpperCase()}]`; if (level === "error") { console.error(`${prefix} ${message}`); } else if (this.options.verbose || level === "info") { console.log(`${prefix} ${message}`); } } async ensureSnapshotDir() { if (!fs.existsSync(this.options.runDir)) { fs.mkdirSync(this.options.runDir, { recursive: true }); this.log(`Created run directory: ${this.options.runDir}`); } if (!fs.existsSync(this.options.heapSnapshotPath)) { fs.mkdirSync(this.options.heapSnapshotPath, { recursive: true }); this.log(`Created snapshot directory: ${this.options.heapSnapshotPath}`); } } async takeHeapSnapshot(label) { const timestamp = Date.now(); const filename = `heap-${label}-${timestamp}.heapsnapshot`; const filepath = path.join(this.options.heapSnapshotPath, filename); try { let snapshotData; if (this.options.adminToken) { this.log(`Running GC before snapshot...`); await this.runGc(); // Use the admin API endpoint to get actual V8 heap snapshot this.log(`Taking V8 heap snapshot via admin API: ${label}...`); snapshotData = await this.takeV8HeapSnapshot(label, filepath, timestamp); } else { // Fallback to basic memory usage info this.log(`Taking basic memory snapshot: ${label} (no admin token provided)...`); const memUsage = process.memoryUsage(); snapshotData = { label, timestamp, filename: filename.replace(".heapsnapshot", ".json"), filepath: filepath.replace(".heapsnapshot", ".json"), processMemory: memUsage, type: "basic", }; fs.writeFileSync(snapshotData.filepath, JSON.stringify(snapshotData, null, 2)); } this.results.snapshots.push(snapshotData); this.log(`Snapshot completed: ${label}`); return snapshotData; } catch (error) { this.log(`Failed to take heap snapshot: ${error.message}`, "error"); throw error; } } async takeV8HeapSnapshot(label, filepath, timestamp) { return new Promise((resolve, reject) => { const requestOptions = { hostname: "localhost", port: this.options.serverPort, path: "/admin/api/v1/snapshot", method: "GET", timeout: 120000, // Heap snapshots can take a while headers: { Authorization: `Bearer ${this.options.adminToken}`, "User-Agent": "memory-leak-detector/1.0", }, }; const req = http.request(requestOptions, (res) => { if (res.statusCode !== 200) { reject(new Error(`Admin API returned ${res.statusCode}: ${res.statusMessage}`)); return; } const writeStream = fs.createWriteStream(filepath); let downloadedBytes = 0; res.on("data", (chunk) => { downloadedBytes += chunk.length; this.log(`Downloaded ${downloadedBytes} bytes`); writeStream.write(chunk); }); res.on("end", () => { writeStream.end(); const snapshotData = { label, timestamp, filename: path.basename(filepath), filepath, size: downloadedBytes, type: "v8-heapsnapshot", }; this.log(`V8 heap snapshot saved: ${Math.round(downloadedBytes / 1024 / 1024)}MB`); resolve(snapshotData); }); res.on("error", (error) => { writeStream.destroy(); fs.unlink(filepath, () => {}); // Clean up partial file reject(error); }); writeStream.on("error", (error) => { res.destroy(); reject(error); }); }); req.on("error", (error) => { reject(new Error(`Failed to connect to admin API: ${error.message}`)); }); req.on("timeout", () => { req.destroy(); reject(new Error("Admin API request timeout")); }); req.end(); }); } async runGc() { return new Promise((resolve, reject) => { const requestOptions = { hostname: "localhost", port: this.options.serverPort, path: "/admin/api/v1/gc", method: "GET", timeout: 120000, // GC can take a while headers: { Authorization: `Bearer ${this.options.adminToken}`, "User-Agent": "memory-leak-detector/1.0", }, }; const req = http.request(requestOptions, (res) => { if (res.statusCode !== 200) { reject(new Error(`Admin API returned ${res.statusCode}: ${res.statusMessage}`)); return; } res.on("data", (chunk) => { this.log(`GC run completed`); }); res.on("end", () => { resolve(); }); res.on("error", (error) => { reject(error); }); }); req.on("error", (error) => { reject(new Error(`Failed to connect to admin API: ${error.message}`)); }); req.on("timeout", () => { req.destroy(); reject(new Error("Admin API request timeout")); }); req.end(); }); } async startServer() { return new Promise((resolve, reject) => { this.log("Starting server..."); // First, build the project const buildProcess = spawn("npm", ["run", "build"], { cwd: process.cwd(), stdio: this.options.verbose ? "inherit" : "pipe", }); buildProcess.on("close", (code) => { if (code !== 0) { reject(new Error(`Build failed with code ${code}`)); return; } this.log("Build completed, starting server..."); const nodePath = path.resolve(process.cwd(), "../../node_modules/.pnpm/node_modules"); this.log(`Using NODE_PATH: ${nodePath}`); // Start the server with memory inspection flags this.serverProcess = spawn( "node", ["--max-old-space-size=16384", "--expose-gc", "./build/server.js"], { cwd: process.cwd(), stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, NODE_ENV: "production", NODE_PATH: nodePath, PORT: this.options.serverPort, // Disable Sentry to prevent memory leaks from it SENTRY_DSN: this.options.sentryDsn, }, } ); let serverReady = false; const timeout = setTimeout(() => { if (!serverReady) { this.serverProcess.kill(); reject(new Error("Server failed to start within timeout")); } }, this.options.serverStartTimeout); this.serverProcess.stdout.on("data", (data) => { const output = data.toString(); if (this.options.verbose) { console.log("SERVER:", output.trim()); } if (output.includes("server ready") && !serverReady) { serverReady = true; clearTimeout(timeout); this.results.serverInfo = { port: this.options.serverPort, startTime: Date.now(), nodeVersion: process.version, }; this.log(`Server started on port ${this.options.serverPort}`); // Wait a bit more for server to fully initialize setTimeout(() => resolve(), 2000); } }); this.serverProcess.stderr.on("data", (data) => { const error = data.toString(); if (this.options.verbose || error.includes("Error")) { console.error("SERVER ERROR:", error.trim()); } }); this.serverProcess.on("close", (code) => { if (!serverReady && code !== 0) { clearTimeout(timeout); reject(new Error(`Server process exited with code ${code}`)); } }); }); }); } async makeRequest(endpoint, options = {}) { return new Promise((resolve, reject) => { const requestOptions = { hostname: "localhost", port: this.options.serverPort, path: endpoint, method: options.method || "GET", timeout: this.options.requestTimeout, headers: { "User-Agent": "memory-leak-detector/1.0", Accept: "application/json", Authorization: `Bearer ${this.options.apiKey}`, ...options.headers, }, }; const req = http.request(requestOptions, (res) => { let data = ""; res.on("data", (chunk) => { data += chunk; }); res.on("end", () => { resolve({ statusCode: res.statusCode, headers: res.headers, body: data, endpoint: endpoint, }); }); }); req.on("error", (error) => { reject({ error, endpoint }); }); req.on("timeout", () => { req.destroy(); reject({ error: new Error("Request timeout"), endpoint }); }); if (options.body) { req.write(options.body); } req.end(); }); } async performRequestPhase(phaseName, numRequests) { this.log(`Starting ${phaseName} phase with ${numRequests} GET requests...`); const startTime = performance.now(); let successfulRequests = 0; let failedRequests = 0; const errors = []; for (let i = 0; i < numRequests; i++) { const endpoint = this.options.apiEndpoints[i % this.options.apiEndpoints.length]; try { const response = await this.makeRequest(endpoint); if (response.statusCode >= 200 && response.statusCode < 300) { successfulRequests++; } else { failedRequests++; errors.push({ endpoint, statusCode: response.statusCode, error: `HTTP ${response.statusCode}`, phase: phaseName, }); } } catch (error) { failedRequests++; errors.push({ endpoint: error.endpoint, error: error.error?.message || "Unknown error", phase: phaseName, }); } // Add delay between requests if (i < numRequests - 1) { await this.delay(this.options.requestDelay); } // Progress reporting if (this.options.verbose && numRequests > 25 && (i + 1) % 25 === 0) { this.log(`${phaseName}: ${i + 1}/${numRequests} requests completed`); } } this.log(`Continuing with ${phaseName} phase with ${numRequests} POST requests...`); for (let i = 0; i < numRequests; i++) { const endpoint = this.options.postApiEndpoints[i % this.options.postApiEndpoints.length]; try { // Send a LARGE body to try and trigger a memory leak const response = await this.makeRequest(endpoint, { method: "POST", body: JSON.stringify( Array.from({ length: 1000 }, (_, index) => ({ id: index, name: `Mock ${index}`, description: `Mock ${index} description`, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), uuid: crypto.randomUUID(), })) ), }); if (response.statusCode >= 200 && response.statusCode < 300) { successfulRequests++; } else { failedRequests++; errors.push({ endpoint, statusCode: response.statusCode, error: `HTTP ${response.statusCode}`, phase: phaseName, }); } } catch (error) { failedRequests++; errors.push({ endpoint: error.endpoint, error: error.error?.message || "Unknown error", phase: phaseName, }); } // Add delay between requests if (i < numRequests - 1) { await this.delay(this.options.requestDelay); } // Progress reporting if (this.options.verbose && numRequests > 25 && (i + 1) % 25 === 0) { this.log(`${phaseName}: ${i + 1}/${numRequests} requests completed`); } } const endTime = performance.now(); const duration = endTime - startTime; const phaseResults = { phase: phaseName, total: numRequests, successful: successfulRequests, failed: failedRequests, errors: errors.slice(0, 5), // Keep only first 5 errors per phase duration: Math.round(duration), requestsPerSecond: Math.round((numRequests / duration) * 1000), }; this.results.testPhases.push(phaseResults); // Update totals this.results.totalRequests.successful += successfulRequests; this.results.totalRequests.failed += failedRequests; this.results.totalRequests.errors.push(...errors.slice(0, 3)); if (phaseName === "warmup") { this.results.totalRequests.warmup = numRequests; } else if (phaseName === "load-test-1") { this.results.totalRequests.phase1 = numRequests; } else if (phaseName === "load-test-2") { this.results.totalRequests.phase2 = numRequests; } this.log( `${phaseName} completed: ${successfulRequests}/${numRequests} successful (${Math.round( duration )}ms total)` ); return phaseResults; } analyzeMemoryUsage() { if (this.results.snapshots.length < 3) { this.log("Need at least 3 snapshots to analyze memory usage", "warn"); return; } const snapshot1 = this.results.snapshots[0]; // after warmup const snapshot2 = this.results.snapshots[1]; // after first load test const snapshot3 = this.results.snapshots[2]; // after second load test let analysis = {}; // Handle different snapshot types if ( snapshot1.type === "v8-heapsnapshot" && snapshot2.type === "v8-heapsnapshot" && snapshot3.type === "v8-heapsnapshot" ) { const size1 = snapshot1.size || 0; const size2 = snapshot2.size || 0; const size3 = snapshot3.size || 0; const growth1to2 = size2 - size1; const growth2to3 = size3 - size2; const totalGrowth = size3 - size1; const growth1to2Percent = size1 > 0 ? (growth1to2 / size1) * 100 : 0; const growth2to3Percent = size2 > 0 ? (growth2to3 / size2) * 100 : 0; const totalGrowthPercent = size1 > 0 ? (totalGrowth / size1) * 100 : 0; analysis = { snapshotAnalysis: { type: "v8-heapsnapshot", phase1: { size: Math.round(size1 / 1024 / 1024), file: snapshot1.filename, }, phase2: { size: Math.round(size2 / 1024 / 1024), file: snapshot2.filename, growthFromPhase1: Math.round(growth1to2 / 1024 / 1024), growthPercentFromPhase1: Math.round(growth1to2Percent * 100) / 100, }, phase3: { size: Math.round(size3 / 1024 / 1024), file: snapshot3.filename, growthFromPhase2: Math.round(growth2to3 / 1024 / 1024), growthPercentFromPhase2: Math.round(growth2to3Percent * 100) / 100, }, total: { growth: Math.round(totalGrowth / 1024 / 1024), growthPercent: Math.round(totalGrowthPercent * 100) / 100, }, }, snapshots: this.results.snapshots.length, snapshotPaths: this.results.snapshots.map((s) => s.filepath), }; // Use total growth for recommendations var heapGrowth = totalGrowth; var heapGrowthPercent = totalGrowthPercent; } else if (snapshot1.processMemory && snapshot2.processMemory && snapshot3.processMemory) { // Traditional process memory analysis with 3 snapshots const heap1 = snapshot1.processMemory.heapUsed; const heap2 = snapshot2.processMemory.heapUsed; const heap3 = snapshot3.processMemory.heapUsed; const rss1 = snapshot1.processMemory.rss; const rss2 = snapshot2.processMemory.rss; const rss3 = snapshot3.processMemory.rss; const heapGrowth1to2 = heap2 - heap1; const heapGrowth2to3 = heap3 - heap2; const totalHeapGrowth = heap3 - heap1; const rssGrowth1to2 = rss2 - rss1; const rssGrowth2to3 = rss3 - rss2; const totalRssGrowth = rss3 - rss1; analysis = { heapUsage: { phase1: Math.round(heap1 / 1024 / 1024), phase2: Math.round(heap2 / 1024 / 1024), phase3: Math.round(heap3 / 1024 / 1024), growth1to2: Math.round(heapGrowth1to2 / 1024 / 1024), growth2to3: Math.round(heapGrowth2to3 / 1024 / 1024), totalGrowth: Math.round(totalHeapGrowth / 1024 / 1024), totalGrowthPercent: Math.round((totalHeapGrowth / heap1) * 100 * 100) / 100, }, rssUsage: { phase1: Math.round(rss1 / 1024 / 1024), phase2: Math.round(rss2 / 1024 / 1024), phase3: Math.round(rss3 / 1024 / 1024), growth1to2: Math.round(rssGrowth1to2 / 1024 / 1024), growth2to3: Math.round(rssGrowth2to3 / 1024 / 1024), totalGrowth: Math.round(totalRssGrowth / 1024 / 1024), totalGrowthPercent: Math.round((totalRssGrowth / rss1) * 100 * 100) / 100, }, snapshots: this.results.snapshots.length, }; var heapGrowth = totalHeapGrowth; var heapGrowthPercent = (totalHeapGrowth / heap1) * 100; } else { this.log("Mixed or incompatible snapshot types - cannot analyze memory growth", "warn"); analysis = { error: "Incompatible snapshot types", snapshots: this.results.snapshots.length, snapshotTypes: this.results.snapshots.map((s) => ({ label: s.label, type: s.type })), }; this.results.memoryAnalysis = analysis; return; } this.results.memoryAnalysis = analysis; // Generate recommendations const recommendations = []; if (Math.abs(heapGrowth) > this.options.memoryLeakThreshold * 1024 * 1024) { recommendations.push({ type: "warning", message: `Significant heap growth detected: ${Math.round( heapGrowth / 1024 / 1024 )}MB increase`, suggestion: "Consider investigating object retention and event listener cleanup", }); } if (Math.abs(heapGrowthPercent) > this.options.heapGrowthThreshold * 100) { recommendations.push({ type: "warning", message: `High heap growth percentage: ${heapGrowthPercent.toFixed(1)}%`, suggestion: "Review memory allocation patterns and garbage collection behavior", }); } if ( this.results.totalRequests.failed > (this.results.totalRequests.phase1 + this.results.totalRequests.phase2) * 0.1 ) { recommendations.push({ type: "error", message: `High request failure rate: ${this.results.totalRequests.failed}/${ this.results.totalRequests.phase1 + this.results.totalRequests.phase2 }`, suggestion: "Fix failing endpoints before analyzing memory patterns", }); } if (recommendations.length === 0) { recommendations.push({ type: "info", message: "No obvious memory leaks detected in this test run", suggestion: "Consider running longer tests or testing different API endpoints", }); } this.results.recommendations = recommendations; // Log analysis results this.log("=== Memory Analysis Results ==="); if (this.results.memoryAnalysis.snapshotAnalysis) { const analysis = this.results.memoryAnalysis.snapshotAnalysis; this.log(`V8 Heap Snapshot Analysis:`); this.log(` Phase 1 (after warmup): ${analysis.phase1.size}MB`); this.log( ` Phase 2 (after load test 1): ${analysis.phase2.size}MB (${ analysis.phase2.growthPercentFromPhase1 > 0 ? "+" : "" }${analysis.phase2.growthPercentFromPhase1}%)` ); this.log( ` Phase 3 (after load test 2): ${analysis.phase3.size}MB (${ analysis.phase3.growthPercentFromPhase2 > 0 ? "+" : "" }${analysis.phase3.growthPercentFromPhase2}%)` ); this.log( ` Total Growth: ${analysis.total.growthPercent > 0 ? "+" : ""}${ analysis.total.growth }MB (${analysis.total.growthPercent}%)` ); this.log(`\nSnapshot files saved:`); this.log(` 1. ${analysis.phase1.file}`); this.log(` 2. ${analysis.phase2.file}`); this.log(` 3. ${analysis.phase3.file}`); this.log(`\nšŸ’” Analyze snapshots in Chrome DevTools:`); this.log(` 1. Open Chrome DevTools → Memory tab`); this.log(` 2. Load all 3 .heapsnapshot files`); this.log(` 3. Compare snapshots to identify memory leaks`); this.log(` 4. Focus on comparing snapshot 1 vs 3 for overall growth`); } else if (this.results.memoryAnalysis.heapUsage) { const heap = this.results.memoryAnalysis.heapUsage; const rss = this.results.memoryAnalysis.rssUsage; this.log(`Heap Usage Analysis:`); this.log(` Phase 1: ${heap.phase1}MB`); this.log( ` Phase 2: ${heap.phase2}MB (${heap.growth1to2 > 0 ? "+" : ""}${heap.growth1to2}MB)` ); this.log( ` Phase 3: ${heap.phase3}MB (${heap.growth2to3 > 0 ? "+" : ""}${heap.growth2to3}MB)` ); this.log( ` Total Growth: ${heap.totalGrowthPercent > 0 ? "+" : ""}${heap.totalGrowth}MB (${ heap.totalGrowthPercent }%)` ); this.log(`RSS Usage Analysis:`); this.log(` Phase 1: ${rss.phase1}MB`); this.log(` Phase 2: ${rss.phase2}MB (${rss.growth1to2 > 0 ? "+" : ""}${rss.growth1to2}MB)`); this.log(` Phase 3: ${rss.phase3}MB (${rss.growth2to3 > 0 ? "+" : ""}${rss.growth2to3}MB)`); this.log( ` Total Growth: ${rss.totalGrowthPercent > 0 ? "+" : ""}${rss.totalGrowth}MB (${ rss.totalGrowthPercent }%)` ); } recommendations.forEach((rec, i) => { this.log( `${i + 1}. [${rec.type.toUpperCase()}] ${rec.message}`, rec.type === "error" ? "error" : "info" ); this.log(` Suggestion: ${rec.suggestion}`, "info"); }); } async generateReport() { const reportData = { ...this.results, endTime: new Date().toISOString(), configuration: { serverPort: this.options.serverPort, testRequests: this.options.testRequests, warmupRequests: this.options.warmupRequests, apiEndpoints: this.options.apiEndpoints, memoryLeakThreshold: this.options.memoryLeakThreshold, heapGrowthThreshold: this.options.heapGrowthThreshold, adminToken: this.options.adminToken ? "[REDACTED]" : null, runDirectory: this.options.runDir, }, }; try { fs.writeFileSync(this.options.outputFile, JSON.stringify(reportData, null, 2)); this.log(`Report saved to: ${this.options.outputFile}`); } catch (error) { this.log(`Failed to save report: ${error.message}`, "error"); } return reportData; } async delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async cleanup() { if (this.serverProcess && !this.serverProcess.killed) { this.log("Stopping server..."); this.serverProcess.kill("SIGTERM"); // Wait a bit for graceful shutdown await this.delay(2000); if (!this.serverProcess.killed) { this.serverProcess.kill("SIGKILL"); } } } async run() { try { this.log("=== Memory Leak Detection Started ==="); this.log(`Run directory: ${this.options.runDir}`); await this.ensureSnapshotDir(); await this.startServer(); // Wait for server to fully initialize await this.delay(3000); // Phase 1: Warmup requests + first snapshot this.log("\n=== Phase 1: Warmup ==="); await this.performRequestPhase("warmup", this.options.warmupRequests); // Force GC and wait before snapshot if (global.gc) { global.gc(); await this.delay(1000); } await this.takeHeapSnapshot("after-warmup"); // Phase 2: First load test + second snapshot this.log("\n=== Phase 2: Load Test 1 ==="); await this.performRequestPhase("load-test-1", this.options.testRequests); // Force GC and wait before snapshot if (global.gc) { global.gc(); await this.delay(1000); } await this.takeHeapSnapshot("after-load-test-1"); // Phase 3: Second load test + third snapshot this.log("\n=== Phase 3: Load Test 2 ==="); await this.performRequestPhase("load-test-2", this.options.testRequests); // Force GC and wait before final snapshot if (global.gc) { global.gc(); await this.delay(2000); } await this.takeHeapSnapshot("after-load-test-2"); // Analyze results this.analyzeMemoryUsage(); // Generate report await this.generateReport(); this.log("\n=== Memory Leak Detection Completed ==="); this.log(`šŸ“ All results saved to: ${this.options.runDir}`); this.log(`šŸ“Š Report: ${this.options.outputFile}`); this.log(`šŸ“ˆ Snapshots: ${this.options.heapSnapshotPath}`); if ( this.results.snapshots.length === 3 && this.results.snapshots[0].type === "v8-heapsnapshot" ) { this.log(`\nšŸ” Next steps:`); this.log(`1. Analyze snapshots in Chrome DevTools Memory tab`); this.log(`2. Compare snapshot 1 vs 3 to identify memory leaks`); this.log(`3. Look for growing Sentry-related objects`); this.log(`4. Run with SENTRY_DSN enabled to compare results`); } } catch (error) { this.log(`Detection failed: ${error.message}`, "error"); throw error; } finally { await this.cleanup(); } } } // CLI interface function parseArgs() { const args = process.argv.slice(2); const options = {}; for (let i = 0; i < args.length; i += 2) { const arg = args[i]; const value = args[i + 1]; switch (arg) { case "--port": options.serverPort = parseInt(value); break; case "--requests": options.testRequests = parseInt(value); break; case "--delay": options.requestDelay = parseInt(value); break; case "--endpoints": options.apiEndpoints = value.split(","); break; case "--threshold": options.memoryLeakThreshold = parseFloat(value); break; case "--output": options.outputFile = value; break; case "--token": options.adminToken = value; break; case "--api-key": options.apiKey = value; break; case "--sentry-dsn": options.sentryDsn = value; break; case "--label": options.label = value; break; case "--verbose": options.verbose = true; i--; // No value for this flag break; case "--help": console.log(` Memory Leak Detector Usage: node memory-leak-detector.js [options] Options: --port Server port (default: 3030) --requests Number of test requests (default: 100) --delay Delay between requests (default: 50) --endpoints Comma-separated API endpoints to test --label Label for the run --threshold Memory leak threshold in MB (default: 50) --token Admin Bearer token for V8 heap snapshots --api-key API key for API requests --sentry-dsn Sentry DSN for memory leak detection --output Output report file (default: memory-leak-report.json) --verbose Enable verbose logging --help Show this help Examples: node memory-leak-detector.js --verbose --requests 200 node memory-leak-detector.js --token "your-admin-token" --api-key "your-api-key" --verbose node memory-leak-detector.js --endpoints "/api/v1/whoami,/api/v1/projects" --threshold 25 `); process.exit(0); } } return options; } // Main execution if (require.main === module) { const options = parseArgs(); const detector = new MemoryLeakDetector(options); detector .run() .then(() => { console.log("\nāœ… Memory leak detection completed successfully!"); console.log(`šŸ“ Results directory: ${detector.options.runDir}`); console.log(`šŸ“Š Report: ${detector.options.outputFile}`); console.log(`šŸ“ˆ Snapshots: ${detector.options.heapSnapshotPath}`); process.exit(0); }) .catch((error) => { console.error("\nāŒ Memory leak detection failed:", error.message); process.exit(1); }); } module.exports = MemoryLeakDetector;