ursamu-mud
Version:
Ursamu - Modular MUD Engine with sandboxed scripting and plugin system
731 lines (729 loc) ⢠31.4 kB
JavaScript
/**
* 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