UNPKG

ursamu-mud

Version:

Ursamu - Modular MUD Engine with sandboxed scripting and plugin system

731 lines (729 loc) • 31.4 kB
/** * Performance profiling commands */ import chalk from 'chalk'; import inquirer from 'inquirer'; import { BaseCommand } from './BaseCommand.js'; import { RestClient } from '../clients/RestClient.js'; export class ProfileCommands extends BaseCommand { restClient; inquirer = inquirer; activeProfile; constructor(config) { super(config); this.restClient = new RestClient(config); } register(program) { const profile = program .command('profile') .description('Performance profiling commands'); // Start profiling profile .command('start') .description('Start profiling a script') .argument('<script>', 'script name') .option('-i, --interval <ms>', 'sampling interval in milliseconds', '100') .option('-d, --duration <ms>', 'max profiling duration', '60000') .option('--memory', 'include memory profiling') .option('--cpu', 'include CPU profiling') .option('--functions', 'profile function calls') .action(async (scriptName, options) => { await this.handleCommand(() => this.startProfiling(scriptName, options)); }); // Stop profiling profile .command('stop') .description('Stop current profiling session') .option('-r, --report', 'generate report immediately') .option('-o, --output <file>', 'save report to file') .action(async (options) => { await this.handleCommand(() => this.stopProfiling(options)); }); // Generate report profile .command('report') .description('Generate profiling report') .argument('[script]', 'script name (if not currently profiling)') .option('-f, --format <type>', 'report format (text, json, html)', 'text') .option('-o, --output <file>', 'output file path') .option('--top <n>', 'show top N functions', '10') .option('--threshold <ms>', 'minimum time threshold', '1') .action(async (scriptName, options) => { await this.handleCommand(() => this.generateReport(scriptName, options)); }); // Benchmark script profile .command('benchmark') .alias('bench') .description('Benchmark script performance') .argument('<script>', 'script name') .option('-n, --runs <count>', 'number of benchmark runs', '10') .option('-w, --warmup <count>', 'warmup runs', '3') .option('-i, --input <data>', 'benchmark input data (JSON)') .option('--compare <baseline>', 'compare against baseline script') .action(async (scriptName, options) => { await this.handleCommand(() => this.benchmarkScript(scriptName, options)); }); // Memory analysis profile .command('memory') .description('Analyze memory usage patterns') .argument('<script>', 'script name') .option('-d, --duration <ms>', 'analysis duration', '30000') .option('--gc', 'include garbage collection analysis') .option('--leaks', 'detect memory leaks') .action(async (scriptName, options) => { await this.handleCommand(() => this.analyzeMemory(scriptName, options)); }); // CPU analysis profile .command('cpu') .description('Analyze CPU usage patterns') .argument('<script>', 'script name') .option('-d, --duration <ms>', 'analysis duration', '30000') .option('-s, --samples <count>', 'number of samples', '1000') .action(async (scriptName, options) => { await this.handleCommand(() => this.analyzeCPU(scriptName, options)); }); // Trace execution profile .command('trace') .description('Trace script execution flow') .argument('<script>', 'script name') .option('-v, --verbose', 'verbose tracing') .option('--filter <pattern>', 'filter functions by pattern') .option('--depth <n>', 'max call depth', '10') .action(async (scriptName, options) => { await this.handleCommand(() => this.traceExecution(scriptName, options)); }); // Compare profiles profile .command('compare') .description('Compare profiling results') .argument('<baseline>', 'baseline script or profile file') .argument('<comparison>', 'comparison script or profile file') .option('-m, --metric <type>', 'comparison metric (time, memory, calls)', 'time') .option('-t, --threshold <percent>', 'significance threshold', '5') .action(async (baseline, comparison, options) => { await this.handleCommand(() => this.compareProfiles(baseline, comparison, options)); }); // List profiles profile .command('list') .description('List saved profiling reports') .option('-s, --sort <field>', 'sort by field (date, script, duration)', 'date') .option('-n, --limit <count>', 'limit results', '20') .action(async (options) => { await this.handleCommand(() => this.listProfiles(options)); }); // Delete profiles profile .command('delete') .description('Delete profiling reports') .argument('<pattern>', 'script name pattern or profile ID') .option('--force', 'skip confirmation') .action(async (pattern, options) => { await this.handleCommand(() => this.deleteProfiles(pattern, options)); }); } async startProfiling(scriptName, options = {}) { if (this.activeProfile) { this.warn('Profiling session already active. Stop current session first.'); return; } const spinner = this.startSpinner('Starting profiling session...'); try { const interval = parseInt(options.interval) || 100; const duration = parseInt(options.duration) || 60000; this.updateSpinner('Initializing profiler...'); // Start profiling on server const response = await this.restClient.makeRequest('POST', '/api/profile/start', { scriptName, options: { interval, duration, includeMemory: options.memory, includeCPU: options.cpu, includeFunctions: options.functions } }); if (!response.success) { this.failSpinner('Failed to start profiling'); return; } // Track active profiling session this.activeProfile = { scriptName, startTime: Date.now(), sampleInterval: interval }; // Set automatic stop timer setTimeout(async () => { if (this.activeProfile?.scriptName === scriptName) { console.log(chalk.yellow('\nProfiling duration reached, stopping...')); await this.stopProfiling({ report: true }); } }, duration); this.succeedSpinner(`Profiling started: ${chalk.green(scriptName)}`); console.log(chalk.gray(`Sample interval: ${interval}ms`)); console.log(chalk.gray(`Max duration: ${this.formatDuration(duration)}`)); console.log(chalk.gray('Use `mud profile stop` to end session')); } catch (error) { this.failSpinner('Failed to start profiling'); throw error; } } async stopProfiling(options = {}) { if (!this.activeProfile) { this.warn('No active profiling session'); return; } const spinner = this.startSpinner('Stopping profiling session...'); try { const duration = Date.now() - this.activeProfile.startTime; this.updateSpinner('Collecting profiling data...'); // Stop profiling on server const response = await this.restClient.makeRequest('POST', '/api/profile/stop', { scriptName: this.activeProfile.scriptName }); this.succeedSpinner(`Profiling stopped: ${chalk.yellow(this.activeProfile.scriptName)}`); console.log(chalk.gray(`Total duration: ${this.formatDuration(duration)}`)); // Generate report if requested if (options.report) { console.log(); await this.generateReport(this.activeProfile.scriptName, options); } this.activeProfile = undefined; } catch (error) { this.failSpinner('Error stopping profiling'); this.activeProfile = undefined; throw error; } } async generateReport(scriptName, options = {}) { const targetScript = scriptName || this.activeProfile?.scriptName; if (!targetScript) { this.error('No script specified and no active profiling session'); return; } const spinner = this.startSpinner('Generating profiling report...'); try { this.updateSpinner('Analyzing profiling data...'); // Get profiling data from server const profileData = await this.restClient.makeRequest('GET', `/api/profile/data/${targetScript}`); if (!profileData.success) { this.failSpinner('No profiling data available for script'); return; } this.updateSpinner('Processing performance metrics...'); const report = this.processProfileData(profileData.data); this.succeedSpinner('Profiling report generated'); // Display or save report if (options.output) { await this.saveReport(report, options.output, options.format); console.log(chalk.green(`Report saved to: ${options.output}`)); } else { this.displayReport(report, options); } } catch (error) { this.failSpinner('Failed to generate report'); throw error; } } async benchmarkScript(scriptName, options = {}) { const runs = parseInt(options.runs) || 10; const warmup = parseInt(options.warmup) || 3; const spinner = this.startSpinner('Starting benchmark...'); try { let inputData = {}; if (options.input) { inputData = JSON.parse(options.input); } this.updateSpinner('Running warmup iterations...'); // Warmup runs for (let i = 0; i < warmup; i++) { await this.restClient.testScript(scriptName, { input: inputData, timeout: 10000 }); } this.updateSpinner('Running benchmark iterations...'); const results = []; const progress = this.logger.progress(runs, 'Benchmark Progress'); // Actual benchmark runs for (let i = 0; i < runs; i++) { const startTime = Date.now(); const result = await this.restClient.testScript(scriptName, { input: inputData, timeout: 10000 }); const duration = Date.now() - startTime; if (result.success) { results.push(duration); } else { this.warn(`Run ${i + 1} failed: ${result.error}`); } progress.update(i + 1); } progress.complete(); if (results.length === 0) { this.failSpinner('All benchmark runs failed'); return; } this.succeedSpinner('Benchmark completed'); // Calculate statistics const stats = this.calculateBenchmarkStats(results); this.displayBenchmarkResults(scriptName, stats); // Compare with baseline if provided if (options.compare) { await this.compareBenchmarks(scriptName, stats, options.compare); } } catch (error) { this.failSpinner('Benchmark failed'); throw error; } } async analyzeMemory(scriptName, options = {}) { const duration = parseInt(options.duration) || 30000; const spinner = this.startSpinner('Starting memory analysis...'); try { this.updateSpinner('Monitoring memory usage...'); // Start memory monitoring const response = await this.restClient.makeRequest('POST', '/api/profile/memory', { scriptName, duration, includeGC: options.gc, detectLeaks: options.leaks }); if (!response.success) { this.failSpinner('Failed to start memory analysis'); return; } // Wait for analysis to complete await this.sleep(duration + 1000); this.updateSpinner('Analyzing memory patterns...'); // Get memory analysis results const memoryData = await this.restClient.makeRequest('GET', `/api/profile/memory/${scriptName}/results`); this.succeedSpinner('Memory analysis completed'); this.displayMemoryAnalysis(memoryData.data); } catch (error) { this.failSpinner('Memory analysis failed'); throw error; } } async analyzeCPU(scriptName, options = {}) { const duration = parseInt(options.duration) || 30000; const samples = parseInt(options.samples) || 1000; const spinner = this.startSpinner('Starting CPU analysis...'); try { this.updateSpinner('Monitoring CPU usage...'); const response = await this.restClient.makeRequest('POST', '/api/profile/cpu', { scriptName, duration, samples }); if (!response.success) { this.failSpinner('Failed to start CPU analysis'); return; } // Wait for analysis await this.sleep(duration + 1000); this.updateSpinner('Processing CPU data...'); const cpuData = await this.restClient.makeRequest('GET', `/api/profile/cpu/${scriptName}/results`); this.succeedSpinner('CPU analysis completed'); this.displayCPUAnalysis(cpuData.data); } catch (error) { this.failSpinner('CPU analysis failed'); throw error; } } async traceExecution(scriptName, options = {}) { const spinner = this.startSpinner('Starting execution trace...'); try { this.updateSpinner('Tracing script execution...'); const traceOptions = { verbose: options.verbose, filter: options.filter, maxDepth: parseInt(options.depth) || 10 }; const response = await this.restClient.makeRequest('POST', '/api/profile/trace', { scriptName, options: traceOptions }); if (!response.success) { this.failSpinner('Failed to start execution trace'); return; } this.succeedSpinner('Execution trace completed'); this.displayExecutionTrace(response.data); } catch (error) { this.failSpinner('Execution trace failed'); throw error; } } async compareProfiles(baseline, comparison, options = {}) { const spinner = this.startSpinner('Comparing profiles...'); try { this.updateSpinner('Loading profile data...'); const metric = options.metric || 'time'; const threshold = parseFloat(options.threshold) || 5.0; const response = await this.restClient.makeRequest('POST', '/api/profile/compare', { baseline, comparison, metric, threshold }); if (!response.success) { this.failSpinner('Failed to compare profiles'); return; } this.succeedSpinner('Profile comparison completed'); this.displayProfileComparison(response.data); } catch (error) { this.failSpinner('Profile comparison failed'); throw error; } } async listProfiles(options = {}) { const spinner = this.startSpinner('Loading profile list...'); try { const profiles = await this.restClient.makeRequest('GET', '/api/profile/list', { sort: options.sort || 'date', limit: parseInt(options.limit) || 20 }); this.succeedSpinner(`Found ${profiles.length} profiles`); if (profiles.length === 0) { console.log(chalk.yellow('No profiling reports found')); return; } this.displayProfileList(profiles); } catch (error) { this.failSpinner('Failed to load profiles'); throw error; } } async deleteProfiles(pattern, options = {}) { const spinner = this.startSpinner('Finding matching profiles...'); try { const profiles = await this.restClient.makeRequest('GET', `/api/profile/search/${pattern}`); if (profiles.length === 0) { this.warnSpinner('No matching profiles found'); return; } this.infoSpinner(`Found ${profiles.length} matching profiles`); // Confirm deletion unless forced if (!options.force) { const { confirmed } = await this.inquirer.prompt([ { type: 'confirm', name: 'confirmed', message: `Delete ${profiles.length} profiling reports?`, default: false } ]); if (!confirmed) { this.warnSpinner('Deletion cancelled'); return; } } this.startSpinner('Deleting profiles...'); await this.restClient.makeRequest('DELETE', '/api/profile/bulk', { profileIds: profiles.map((p) => p.id) }); this.succeedSpinner(`Deleted ${profiles.length} profiles`); } catch (error) { this.failSpinner('Failed to delete profiles'); throw error; } } processProfileData(data) { // Process raw profiling data into structured report const functions = data.functions?.map((func) => ({ functionName: func.name, callCount: func.callCount, totalTime: func.totalTime, averageTime: func.averageTime, maxTime: func.maxTime, minTime: func.minTime, percentage: func.percentage })) || []; const hotspots = functions .filter(f => f.percentage > 5) .map(f => f.functionName); const recommendations = this.generateRecommendations(functions, data); return { scriptName: data.scriptName, duration: data.duration, totalInstructions: data.totalInstructions, functions, memory: data.memory || [], hotspots, recommendations }; } generateRecommendations(functions, data) { const recommendations = []; // Find expensive functions const expensiveFunctions = functions.filter(f => f.percentage > 10); if (expensiveFunctions.length > 0) { recommendations.push(`Consider optimizing: ${expensiveFunctions.map(f => f.functionName).join(', ')}`); } // Check for excessive function calls const highCallCount = functions.filter(f => f.callCount > 1000); if (highCallCount.length > 0) { recommendations.push(`High call count functions may need caching: ${highCallCount.map(f => f.functionName).join(', ')}`); } // Memory recommendations if (data.memory && data.memory.length > 0) { const maxMemory = Math.max(...data.memory.map((m) => m.heapUsed)); if (maxMemory > 50 * 1024 * 1024) { // 50MB recommendations.push('Consider memory optimization - peak usage exceeds 50MB'); } } return recommendations; } displayReport(report, options = {}) { const topN = parseInt(options.top) || 10; const threshold = parseFloat(options.threshold) || 1; console.log(chalk.bold(`\nšŸ“Š Profiling Report: ${report.scriptName}`)); console.log(chalk.gray(`Duration: ${this.formatDuration(report.duration)}`)); console.log(chalk.gray(`Instructions: ${report.totalInstructions.toLocaleString()}`)); if (report.functions.length > 0) { console.log(chalk.bold('\nšŸ”„ Top Functions by Time:')); console.log('Function'.padEnd(25) + 'Calls'.padEnd(10) + 'Total (ms)'.padEnd(12) + 'Avg (ms)'.padEnd(10) + '%'); console.log('─'.repeat(70)); const topFunctions = report.functions .filter(f => f.totalTime >= threshold) .slice(0, topN); for (const func of topFunctions) { const percentage = func.percentage.toFixed(1) + '%'; const color = func.percentage > 10 ? chalk.red : func.percentage > 5 ? chalk.yellow : chalk.white; console.log(color(func.functionName.substring(0, 24).padEnd(25)) + func.callCount.toString().padEnd(10) + func.totalTime.toFixed(1).padEnd(12) + func.averageTime.toFixed(2).padEnd(10) + percentage); } } if (report.hotspots.length > 0) { console.log(chalk.bold('\nšŸŽÆ Performance Hotspots:')); for (const hotspot of report.hotspots) { console.log(chalk.red(` • ${hotspot}`)); } } if (report.recommendations.length > 0) { console.log(chalk.bold('\nšŸ’” Recommendations:')); for (const rec of report.recommendations) { console.log(chalk.blue(` • ${rec}`)); } } console.log(); } calculateBenchmarkStats(results) { results.sort((a, b) => a - b); const mean = results.reduce((sum, val) => sum + val, 0) / results.length; const median = results[Math.floor(results.length / 2)]; const min = results[0]; const max = results[results.length - 1]; // Calculate standard deviation const variance = results.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / results.length; const stdDev = Math.sqrt(variance); return { mean, median, min, max, stdDev, count: results.length }; } displayBenchmarkResults(scriptName, stats) { console.log(chalk.bold(`\n⚔ Benchmark Results: ${scriptName}`)); console.log('─'.repeat(50)); console.log(`Runs: ${stats.count}`); console.log(`Mean: ${stats.mean.toFixed(2)}ms`); console.log(`Median: ${stats.median.toFixed(2)}ms`); console.log(`Min: ${stats.min.toFixed(2)}ms`); console.log(`Max: ${stats.max.toFixed(2)}ms`); console.log(`Std Dev: ${stats.stdDev.toFixed(2)}ms`); console.log(`Throughput: ${(1000 / stats.mean).toFixed(2)} ops/sec`); console.log(); } displayMemoryAnalysis(data) { console.log(chalk.bold('\n🧠 Memory Analysis')); console.log('─'.repeat(40)); if (data.peakUsage) { console.log(`Peak Usage: ${this.formatBytes(data.peakUsage)}`); } if (data.averageUsage) { console.log(`Average Usage: ${this.formatBytes(data.averageUsage)}`); } if (data.gcCount) { console.log(`GC Cycles: ${data.gcCount}`); } if (data.leaks && data.leaks.length > 0) { console.log(chalk.red('\nāš ļø Potential Memory Leaks:')); for (const leak of data.leaks) { console.log(chalk.red(` • ${leak}`)); } } console.log(); } displayCPUAnalysis(data) { console.log(chalk.bold('\nšŸ–„ļø CPU Analysis')); console.log('─'.repeat(40)); if (data.averageUsage !== undefined) { console.log(`Average CPU: ${data.averageUsage.toFixed(1)}%`); } if (data.peakUsage !== undefined) { console.log(`Peak CPU: ${data.peakUsage.toFixed(1)}%`); } if (data.samples) { console.log(`Samples: ${data.samples}`); } console.log(); } displayExecutionTrace(data) { console.log(chalk.bold('\nšŸ” Execution Trace')); console.log('─'.repeat(50)); if (data.trace && Array.isArray(data.trace)) { for (const entry of data.trace) { const indent = ' '.repeat(entry.depth || 0); const duration = entry.duration ? `(${entry.duration}ms)` : ''; console.log(`${indent}${chalk.cyan(entry.function)} ${chalk.gray(duration)}`); } } console.log(); } displayProfileComparison(data) { console.log(chalk.bold('\nšŸ“ˆ Profile Comparison')); console.log('─'.repeat(50)); if (data.summary) { console.log(`Baseline: ${data.summary.baseline}`); console.log(`Comparison: ${data.summary.comparison}`); console.log(`Metric: ${data.summary.metric}`); const change = data.summary.change; const color = change > 0 ? chalk.red : change < 0 ? chalk.green : chalk.gray; console.log(`Change: ${color(change.toFixed(1) + '%')}`); } if (data.differences && data.differences.length > 0) { console.log(chalk.bold('\nSignificant Differences:')); for (const diff of data.differences) { const color = diff.change > 0 ? chalk.red : chalk.green; console.log(` ${diff.function}: ${color(diff.change.toFixed(1) + '%')}`); } } console.log(); } displayProfileList(profiles) { console.log(chalk.bold('Script'.padEnd(20) + 'Date'.padEnd(20) + 'Duration'.padEnd(12) + 'Status')); console.log('─'.repeat(70)); for (const profile of profiles) { const date = new Date(profile.createdAt).toLocaleDateString(); const duration = this.formatDuration(profile.duration); const status = profile.status === 'completed' ? chalk.green('āœ“') : chalk.yellow('ā³'); console.log(profile.scriptName.padEnd(20) + date.padEnd(20) + duration.padEnd(12) + status); } } async saveReport(report, filePath, format) { const { writeFile } = await import('fs/promises'); let content; switch (format) { case 'json': content = JSON.stringify(report, null, 2); break; case 'html': content = this.generateHTMLReport(report); break; default: content = this.generateTextReport(report); } await writeFile(filePath, content); } generateTextReport(report) { let text = `Profiling Report: ${report.scriptName}\n`; text += `Duration: ${this.formatDuration(report.duration)}\n`; text += `Instructions: ${report.totalInstructions.toLocaleString()}\n\n`; if (report.functions.length > 0) { text += 'Top Functions by Time:\n'; text += 'Function'.padEnd(25) + 'Calls'.padEnd(10) + 'Total (ms)'.padEnd(12) + 'Avg (ms)'.padEnd(10) + '%\n'; text += '─'.repeat(70) + '\n'; for (const func of report.functions.slice(0, 10)) { text += func.functionName.substring(0, 24).padEnd(25) + func.callCount.toString().padEnd(10) + func.totalTime.toFixed(1).padEnd(12) + func.averageTime.toFixed(2).padEnd(10) + func.percentage.toFixed(1) + '%\n'; } } return text; } generateHTMLReport(report) { return `<!DOCTYPE html> <html> <head> <title>Profiling Report: ${report.scriptName}</title> <style> body { font-family: Arial, sans-serif; margin: 20px; } .header { background: #f5f5f5; padding: 10px; border-radius: 5px; } table { width: 100%; border-collapse: collapse; margin: 20px 0; } th, td { border: 1px solid #ddd; padding: 8px; text-align: left; } th { background: #f2f2f2; } .hotspot { color: #d73527; font-weight: bold; } .recommendation { color: #0066cc; } </style> </head> <body> <div class="header"> <h1>Profiling Report: ${report.scriptName}</h1> <p>Duration: ${this.formatDuration(report.duration)}</p> <p>Instructions: ${report.totalInstructions.toLocaleString()}</p> </div> <h2>Function Performance</h2> <table> <tr><th>Function</th><th>Calls</th><th>Total Time (ms)</th><th>Average Time (ms)</th><th>Percentage</th></tr> ${report.functions.slice(0, 10).map(func => ` <tr> <td>${func.functionName}</td> <td>${func.callCount}</td> <td>${func.totalTime.toFixed(1)}</td> <td>${func.averageTime.toFixed(2)}</td> <td>${func.percentage.toFixed(1)}%</td> </tr> `).join('')} </table> ${report.recommendations.length > 0 ? ` <h2>Recommendations</h2> <ul> ${report.recommendations.map(rec => `<li class="recommendation">${rec}</li>`).join('')} </ul> ` : ''} </body> </html>`; } sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async compareBenchmarks(scriptName, currentStats, baselineName) { try { // This is a placeholder implementation for benchmark comparison // In a full implementation, this would compare current results with stored baselines console.log(`\n=== Benchmark Comparison ===`); console.log(`Script: ${scriptName}`); console.log(`Baseline: ${baselineName}`); console.log(`Current performance comparison would be displayed here`); } catch (error) { this.warn(`Failed to compare with baseline: ${baselineName}`); } } } //# sourceMappingURL=ProfileCommands.js.map