@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
332 lines ⢠16.4 kB
JavaScript
/**
* 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