UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

332 lines • 16.4 kB
/** * Performance Comparator for Sync Operations * @description Compares performance metrics between different sync runs, * enabling tracking of optimization improvements over time. * * Features: * - Baseline vs current comparison * - Trend analysis across multiple runs * - Performance regression detection * - Improvement percentage calculations * - Visual comparison charts (ASCII) * * @author Optimizely MCP Server * @version 1.0.0 */ import chalk from 'chalk'; import { readFileSync, writeFileSync, existsSync } from 'fs'; import path from 'path'; /** * Performance Comparator Class */ export class PerformanceComparator { historyFile; maxHistorySize = 100; constructor(dataDir = './data/performance') { this.historyFile = path.join(dataDir, 'performance-history.json'); this.ensureDataDirectory(dataDir); } /** * Ensure data directory exists */ ensureDataDirectory(dataDir) { const { mkdirSync } = require('fs'); try { mkdirSync(dataDir, { recursive: true }); } catch (error) { // Directory might already exist } } /** * Save performance snapshot */ saveSnapshot(snapshot) { const history = this.loadHistory(); // Add new snapshot history.snapshots.push(snapshot); // Maintain max history size if (history.snapshots.length > this.maxHistorySize) { history.snapshots = history.snapshots.slice(-this.maxHistorySize); } // Update best performances this.updateBestPerformances(history); // Save to file writeFileSync(this.historyFile, JSON.stringify(history, null, 2), 'utf-8'); } /** * Load performance history */ loadHistory() { if (!existsSync(this.historyFile)) { return { snapshots: [], bestPerformance: { fastestSync: null, highestReduction: null, highestThroughput: null } }; } try { const data = readFileSync(this.historyFile, 'utf-8'); return JSON.parse(data); } catch (error) { console.error(chalk.yellow('āš ļø Could not load performance history')); return { snapshots: [], bestPerformance: { fastestSync: null, highestReduction: null, highestThroughput: null } }; } } /** * Update best performance records */ updateBestPerformances(history) { if (history.snapshots.length === 0) return; // Find fastest sync history.bestPerformance.fastestSync = history.snapshots.reduce((prev, curr) => curr.totalDuration < prev.totalDuration ? curr : prev); // Find highest reduction history.bestPerformance.highestReduction = history.snapshots.reduce((prev, curr) => curr.reductionPercent > prev.reductionPercent ? curr : prev); // Find highest throughput (records per second) history.bestPerformance.highestThroughput = history.snapshots.reduce((prev, curr) => { const prevThroughput = prev.totalRecords / (prev.totalDuration / 1000); const currThroughput = curr.totalRecords / (curr.totalDuration / 1000); return currThroughput > prevThroughput ? curr : prev; }); } /** * Compare current performance with baseline */ compare(current, baseline) { if (!baseline) { const history = this.loadHistory(); if (history.snapshots.length === 0) { return null; } // Use previous run as baseline baseline = history.snapshots[history.snapshots.length - 1]; } // Calculate improvements const durationChange = baseline.totalDuration - current.totalDuration; const durationChangePercent = (durationChange / baseline.totalDuration) * 100; const reductionChange = current.reductionPercent - baseline.reductionPercent; const baselineThroughput = baseline.totalRecords / (baseline.totalDuration / 1000); const currentThroughput = current.totalRecords / (current.totalDuration / 1000); const throughputChange = currentThroughput - baselineThroughput; const throughputChangePercent = (throughputChange / baselineThroughput) * 100; // Detect regressions const regressions = []; if (durationChangePercent < -10) { regressions.push(`Duration increased by ${Math.abs(durationChangePercent).toFixed(1)}%`); } if (reductionChange < -5) { regressions.push(`SQL reduction decreased by ${Math.abs(reductionChange).toFixed(1)}%`); } if (throughputChangePercent < -10) { regressions.push(`Throughput decreased by ${Math.abs(throughputChangePercent).toFixed(1)}%`); } // Identify highlights const highlights = []; if (durationChangePercent > 20) { highlights.push(`šŸš€ ${durationChangePercent.toFixed(1)}% faster sync time!`); } if (reductionChange > 5) { highlights.push(`šŸ“ˆ ${reductionChange.toFixed(1)}% better SQL reduction!`); } if (throughputChangePercent > 20) { highlights.push(`⚔ ${throughputChangePercent.toFixed(1)}% higher throughput!`); } if (current.reductionPercent >= 98) { highlights.push('šŸŽÆ Achieved 98%+ SQL reduction target!'); } return { baseline, current, improvements: { durationChange, durationChangePercent, reductionChange, throughputChange, throughputChangePercent }, regressions, highlights }; } /** * Generate comparison report */ generateComparisonReport(comparison) { console.log('\n' + chalk.bold.cyan('šŸ“Š PERFORMANCE COMPARISON')); console.log(chalk.gray('─'.repeat(60))); // Time comparison console.log(chalk.bold('\nā±ļø Sync Duration:')); console.log(` Baseline: ${(comparison.baseline.totalDuration / 1000).toFixed(2)}s`); console.log(` Current: ${(comparison.current.totalDuration / 1000).toFixed(2)}s`); const durationColor = comparison.improvements.durationChangePercent > 0 ? chalk.green : chalk.red; const durationSymbol = comparison.improvements.durationChangePercent > 0 ? '↓' : '↑'; console.log(durationColor(` Change: ${durationSymbol} ${Math.abs(comparison.improvements.durationChangePercent).toFixed(1)}%`)); // Throughput comparison const baselineThroughput = comparison.baseline.totalRecords / (comparison.baseline.totalDuration / 1000); const currentThroughput = comparison.current.totalRecords / (comparison.current.totalDuration / 1000); console.log(chalk.bold('\nšŸ“ˆ Throughput (records/sec):')); console.log(` Baseline: ${baselineThroughput.toFixed(1)}`); console.log(` Current: ${currentThroughput.toFixed(1)}`); const throughputColor = comparison.improvements.throughputChangePercent > 0 ? chalk.green : chalk.red; const throughputSymbol = comparison.improvements.throughputChangePercent > 0 ? '↑' : '↓'; console.log(throughputColor(` Change: ${throughputSymbol} ${Math.abs(comparison.improvements.throughputChangePercent).toFixed(1)}%`)); // SQL reduction comparison console.log(chalk.bold('\nšŸ’¾ SQL Reduction:')); console.log(` Baseline: ${comparison.baseline.reductionPercent.toFixed(1)}%`); console.log(` Current: ${comparison.current.reductionPercent.toFixed(1)}%`); const reductionColor = comparison.improvements.reductionChange > 0 ? chalk.green : chalk.red; const reductionSymbol = comparison.improvements.reductionChange > 0 ? '↑' : '↓'; console.log(reductionColor(` Change: ${reductionSymbol} ${Math.abs(comparison.improvements.reductionChange).toFixed(1)}%`)); // Visual comparison chart this.generateVisualComparison(comparison); // Highlights if (comparison.highlights.length > 0) { console.log(chalk.bold.green('\n✨ Highlights:')); comparison.highlights.forEach(highlight => { console.log(chalk.green(` ${highlight}`)); }); } // Regressions if (comparison.regressions.length > 0) { console.log(chalk.bold.red('\nāš ļø Regressions:')); comparison.regressions.forEach(regression => { console.log(chalk.red(` ${regression}`)); }); } console.log('\n' + chalk.gray('─'.repeat(60))); } /** * Generate visual comparison chart */ generateVisualComparison(comparison) { console.log(chalk.bold('\nšŸ“Š Visual Comparison:')); // Duration bar chart const maxDuration = Math.max(comparison.baseline.totalDuration, comparison.current.totalDuration); const baselineBar = Math.round((comparison.baseline.totalDuration / maxDuration) * 40); const currentBar = Math.round((comparison.current.totalDuration / maxDuration) * 40); console.log('\n Duration:'); console.log(` Baseline: ${'ā–ˆ'.repeat(baselineBar)} ${(comparison.baseline.totalDuration / 1000).toFixed(1)}s`); console.log(` Current: ${chalk.green('ā–ˆ'.repeat(currentBar))} ${(comparison.current.totalDuration / 1000).toFixed(1)}s`); // SQL operations bar chart const maxOps = Math.max(comparison.baseline.sqlOperationsBefore, comparison.current.sqlOperationsBefore); const baselineOpsBar = Math.round((comparison.baseline.sqlOperationsAfter / maxOps) * 40); const currentOpsBar = Math.round((comparison.current.sqlOperationsAfter / maxOps) * 40); console.log('\n SQL Operations (after optimization):'); console.log(` Baseline: ${'ā–ˆ'.repeat(baselineOpsBar || 1)} ${comparison.baseline.sqlOperationsAfter}`); console.log(` Current: ${chalk.green('ā–ˆ'.repeat(currentOpsBar || 1))} ${comparison.current.sqlOperationsAfter}`); } /** * Generate trend analysis */ generateTrendAnalysis(limit = 10) { const history = this.loadHistory(); if (history.snapshots.length < 2) { console.log(chalk.yellow('šŸ“Š Not enough data for trend analysis (need at least 2 runs)')); return; } console.log('\n' + chalk.bold.blue('šŸ“ˆ PERFORMANCE TREND ANALYSIS')); console.log(chalk.gray('═'.repeat(80))); // Get recent snapshots const recentSnapshots = history.snapshots.slice(-limit); console.log(chalk.bold('\nšŸ“Š Recent Performance Trends:')); console.log(chalk.gray('─'.repeat(80))); console.log('Date'.padEnd(20) + 'Duration'.padEnd(12) + 'Records'.padEnd(12) + 'SQL Reduction'.padEnd(15) + 'Throughput'.padEnd(15) + 'Type'); console.log(chalk.gray('─'.repeat(80))); recentSnapshots.forEach((snapshot, index) => { const date = new Date(snapshot.timestamp).toLocaleDateString(); const time = new Date(snapshot.timestamp).toLocaleTimeString(); const duration = (snapshot.totalDuration / 1000).toFixed(1) + 's'; const throughput = (snapshot.totalRecords / (snapshot.totalDuration / 1000)).toFixed(1) + '/s'; // Highlight best performances let rowColor = chalk.white; if (snapshot === history.bestPerformance.fastestSync) { rowColor = chalk.green; } else if (snapshot === history.bestPerformance.highestReduction) { rowColor = chalk.cyan; } else if (snapshot === history.bestPerformance.highestThroughput) { rowColor = chalk.yellow; } console.log(rowColor(`${date} ${time}`.padEnd(20) + duration.padEnd(12) + snapshot.totalRecords.toString().padEnd(12) + `${snapshot.reductionPercent.toFixed(1)}%`.padEnd(15) + throughput.padEnd(15) + snapshot.metadata.syncType)); }); console.log(chalk.gray('─'.repeat(80))); // Best performances if (history.bestPerformance.fastestSync) { console.log(chalk.bold('\nšŸ† Best Performances:')); console.log(chalk.green(` Fastest Sync: ${(history.bestPerformance.fastestSync.totalDuration / 1000).toFixed(1)}s on ${new Date(history.bestPerformance.fastestSync.timestamp).toLocaleDateString()}`)); console.log(chalk.cyan(` Highest SQL Reduction: ${history.bestPerformance.highestReduction.reductionPercent.toFixed(1)}% on ${new Date(history.bestPerformance.highestReduction.timestamp).toLocaleDateString()}`)); const bestThroughput = history.bestPerformance.highestThroughput.totalRecords / (history.bestPerformance.highestThroughput.totalDuration / 1000); console.log(chalk.yellow(` Highest Throughput: ${bestThroughput.toFixed(1)} records/s on ${new Date(history.bestPerformance.highestThroughput.timestamp).toLocaleDateString()}`)); } // Calculate trends if (recentSnapshots.length >= 3) { const firstHalf = recentSnapshots.slice(0, Math.floor(recentSnapshots.length / 2)); const secondHalf = recentSnapshots.slice(Math.floor(recentSnapshots.length / 2)); const avgDurationFirst = firstHalf.reduce((sum, s) => sum + s.totalDuration, 0) / firstHalf.length; const avgDurationSecond = secondHalf.reduce((sum, s) => sum + s.totalDuration, 0) / secondHalf.length; const avgReductionFirst = firstHalf.reduce((sum, s) => sum + s.reductionPercent, 0) / firstHalf.length; const avgReductionSecond = secondHalf.reduce((sum, s) => sum + s.reductionPercent, 0) / secondHalf.length; console.log(chalk.bold('\nšŸ“Š Trend Summary:')); const durationTrend = avgDurationSecond < avgDurationFirst ? '↓ Improving' : '↑ Degrading'; const durationTrendColor = avgDurationSecond < avgDurationFirst ? chalk.green : chalk.red; console.log(` Duration Trend: ${durationTrendColor(durationTrend)} (${Math.abs(((avgDurationSecond - avgDurationFirst) / avgDurationFirst) * 100).toFixed(1)}%)`); const reductionTrend = avgReductionSecond > avgReductionFirst ? '↑ Improving' : '↓ Degrading'; const reductionTrendColor = avgReductionSecond > avgReductionFirst ? chalk.green : chalk.red; console.log(` SQL Reduction Trend: ${reductionTrendColor(reductionTrend)} (${Math.abs(avgReductionSecond - avgReductionFirst).toFixed(1)}%)`); } console.log('\n' + chalk.gray('═'.repeat(80)) + '\n'); } /** * Create snapshot from performance report */ static createSnapshot(report, syncType = 'full') { const entityBreakdown = {}; if (report.entityMetrics) { report.entityMetrics.forEach((metric) => { entityBreakdown[metric.entityType] = { records: metric.recordCount, duration: metric.syncDuration, reduction: metric.reductionPercent }; }); } return { timestamp: new Date().toISOString(), totalDuration: report.summary?.totalDuration || report.duration || 0, totalRecords: report.summary?.totalRecords || report.totalChanges || 0, sqlOperationsBefore: report.summary?.totalSqlBefore || 0, sqlOperationsAfter: report.summary?.totalSqlAfter || 0, reductionPercent: report.summary?.overallReduction || 0, entityBreakdown, metadata: { projectCount: report.configuration?.projectCount || report.projectsSynced || 1, syncType, version: '2.0.0' } }; } } //# sourceMappingURL=PerformanceComparator.js.map