mega-minds
Version:
Enhanced multi-agent workflow system for Claude Code projects with automated handoff management and Claude Code hooks integration
573 lines (501 loc) • 16.4 kB
JavaScript
/**
* Performance Monitor for Mega-Minds Variable-Driven Agent System
* Tracks system performance, metrics, and optimization scores
*/
class PerformanceMonitor {
constructor(config = {}) {
this.config = {
maxHistorySize: config.maxHistorySize || 1000,
sampleWindow: config.sampleWindow || 60000, // 1 minute
alertThresholds: {
loadTime: config.alertThresholds?.loadTime || 1000, // 1 second
memoryUsage: config.alertThresholds?.memoryUsage || 80, // 80%
errorRate: config.alertThresholds?.errorRate || 5 // 5%
},
...config
};
// Core metrics storage
this.metrics = {
loadTimes: [],
cacheHits: 0,
cacheMisses: 0,
errors: [],
operations: [],
systemHealth: {
status: 'healthy',
lastCheck: Date.now()
},
optimizationScore: 8.5
};
// Performance tracking
this.activeTimers = new Map();
this.metricHistory = new Map();
this.alerts = [];
// Initialize
this.startTime = Date.now();
this.lastCleanup = Date.now();
}
/**
* Start timing an operation
* @param {string} operationId - Unique identifier for the operation
* @returns {string} Timer ID for ending the operation
*/
startTiming(operationId = null) {
const timerId = operationId || `timer_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
this.activeTimers.set(timerId, {
startTime: performance.now(),
realStartTime: Date.now(),
operationId: operationId || 'unknown'
});
return timerId;
}
/**
* End timing and record the operation
* @param {string} operation - Operation name for categorization
* @param {string} timerId - Timer ID from startTiming (optional)
* @returns {number} Duration in milliseconds
*/
endTiming(operation, timerId = null) {
let timer;
if (timerId && this.activeTimers.has(timerId)) {
timer = this.activeTimers.get(timerId);
this.activeTimers.delete(timerId);
} else {
// Find the most recent timer if no ID provided
const timers = Array.from(this.activeTimers.entries());
if (timers.length > 0) {
const [id, timerData] = timers[timers.length - 1];
timer = timerData;
this.activeTimers.delete(id);
}
}
if (!timer) {
console.warn('No active timer found for operation:', operation);
return 0;
}
const duration = performance.now() - timer.startTime;
// Record the metric
this.recordMetric(operation, duration, {
timestamp: Date.now(),
operationId: timer.operationId
});
return duration;
}
/**
* Record a performance metric
* @param {string} operation - Operation name
* @param {number} value - Metric value
* @param {Object} metadata - Additional metadata
*/
recordMetric(operation, value, metadata = {}) {
const metric = {
operation: operation,
value: value,
timestamp: Date.now(),
metadata: metadata
};
// Store in appropriate category
switch (operation) {
case 'section-load':
case 'template-load':
case 'variable-generation':
case 'agent-invocation':
this.metrics.loadTimes.push(metric);
break;
case 'cache-hit':
this.metrics.cacheHits++;
break;
case 'cache-miss':
this.metrics.cacheMisses++;
break;
case 'error':
this.metrics.errors.push(metric);
break;
default:
this.metrics.operations.push(metric);
}
// Store in history for trend analysis
if (!this.metricHistory.has(operation)) {
this.metricHistory.set(operation, []);
}
this.metricHistory.get(operation).push(metric);
// Check for alerts
this.checkForAlerts(operation, value, metric);
// Cleanup old data periodically
if (Date.now() - this.lastCleanup > 300000) { // 5 minutes
this.cleanup();
}
}
/**
* Get current metrics snapshot
* @returns {Object} Current metrics
*/
getCurrentMetrics() {
return {
loadTimes: this.getLoadTimeStats(),
cacheHitRate: this.getCacheHitRate(),
errorRate: this.getErrorRate(),
systemHealth: this.metrics.systemHealth,
optimizationScore: this.metrics.optimizationScore,
uptime: this.getUptime(),
alerts: this.getActiveAlerts(),
timestamp: Date.now()
};
}
/**
* Get load time statistics
* @returns {Object} Load time stats
*/
getLoadTimeStats() {
if (this.metrics.loadTimes.length === 0) {
return {
average: 0,
min: 0,
max: 0,
recent: 0,
count: 0
};
}
const times = this.metrics.loadTimes.map(m => m.value);
const recentTimes = this.getRecentMetrics('loadTimes', 60000).map(m => m.value); // Last minute
return {
average: this.average(times),
min: Math.min(...times),
max: Math.max(...times),
recent: recentTimes.length > 0 ? this.average(recentTimes) : 0,
count: times.length,
p95: this.percentile(times, 95),
p99: this.percentile(times, 99)
};
}
/**
* Get cache hit rate
* @returns {number} Cache hit rate as percentage
*/
getCacheHitRate() {
const total = this.metrics.cacheHits + this.metrics.cacheMisses;
return total > 0 ? Math.round((this.metrics.cacheHits / total) * 100 * 100) / 100 : 0;
}
/**
* Get error rate
* @returns {number} Error rate as percentage
*/
getErrorRate() {
const totalOperations = this.metrics.loadTimes.length + this.metrics.operations.length;
const errorCount = this.metrics.errors.length;
return totalOperations > 0 ? Math.round((errorCount / totalOperations) * 100 * 100) / 100 : 0;
}
/**
* Get system uptime
* @returns {number} Uptime in milliseconds
*/
getUptime() {
return Date.now() - this.startTime;
}
/**
* Get last recorded load time
* @returns {number} Last load time in ms
*/
getLastLoadTime() {
if (this.metrics.loadTimes.length === 0) {
return 50; // Default estimation
}
const lastMetric = this.metrics.loadTimes[this.metrics.loadTimes.length - 1];
return Math.round(lastMetric.value);
}
/**
* Update system health status
* @param {string} status - Health status ('healthy', 'degraded', 'critical')
* @param {string} reason - Reason for status change
*/
updateSystemHealth(status, reason = null) {
this.metrics.systemHealth = {
status: status,
reason: reason,
lastCheck: Date.now(),
previousStatus: this.metrics.systemHealth.status
};
// Log significant changes
if (status !== this.metrics.systemHealth.previousStatus) {
console.log(`System health changed: ${this.metrics.systemHealth.previousStatus} → ${status}${reason ? ` (${reason})` : ''}`);
}
}
/**
* Calculate and update optimization score
* @param {Object} factors - Factors affecting optimization
* @returns {number} New optimization score
*/
calculateOptimizationScore(factors = {}) {
let score = this.metrics.optimizationScore;
// Adjust based on performance metrics
const loadStats = this.getLoadTimeStats();
const cacheHitRate = this.getCacheHitRate();
const errorRate = this.getErrorRate();
// Load time factor (-2 to +1)
if (loadStats.average < 100) score += 0.5;
else if (loadStats.average < 200) score += 0.2;
else if (loadStats.average > 1000) score -= 0.8;
else if (loadStats.average > 500) score -= 0.4;
// Cache hit rate factor (-1 to +1)
if (cacheHitRate > 90) score += 0.3;
else if (cacheHitRate > 80) score += 0.1;
else if (cacheHitRate < 50) score -= 0.5;
// Error rate factor (-2 to +0.5)
if (errorRate === 0) score += 0.2;
else if (errorRate < 1) score += 0.1;
else if (errorRate > 10) score -= 1.0;
else if (errorRate > 5) score -= 0.5;
// External factors
if (factors.memoryUsage) {
if (factors.memoryUsage < 50) score += 0.2;
else if (factors.memoryUsage > 90) score -= 1.0;
else if (factors.memoryUsage > 75) score -= 0.3;
}
if (factors.coordinationSuccess) {
if (factors.coordinationSuccess > 95) score += 0.3;
else if (factors.coordinationSuccess < 80) score -= 0.5;
}
// Keep score in bounds
score = Math.max(1, Math.min(10, score));
// Smooth changes
this.metrics.optimizationScore = Math.round((this.metrics.optimizationScore * 0.8 + score * 0.2) * 10) / 10;
return this.metrics.optimizationScore;
}
/**
* Check for performance alerts
* @param {string} operation - Operation name
* @param {number} value - Metric value
* @param {Object} metric - Full metric object
*/
checkForAlerts(operation, value, metric) {
const alerts = [];
// Load time alerts
if (['section-load', 'template-load', 'variable-generation'].includes(operation)) {
if (value > this.config.alertThresholds.loadTime) {
alerts.push({
type: 'performance',
severity: value > this.config.alertThresholds.loadTime * 2 ? 'critical' : 'warning',
message: `Slow ${operation}: ${Math.round(value)}ms`,
operation: operation,
value: value,
threshold: this.config.alertThresholds.loadTime,
timestamp: metric.timestamp
});
}
}
// Error rate alerts
const errorRate = this.getErrorRate();
if (errorRate > this.config.alertThresholds.errorRate) {
alerts.push({
type: 'error-rate',
severity: errorRate > this.config.alertThresholds.errorRate * 2 ? 'critical' : 'warning',
message: `High error rate: ${errorRate}%`,
value: errorRate,
threshold: this.config.alertThresholds.errorRate,
timestamp: Date.now()
});
}
// Add alerts
for (const alert of alerts) {
this.addAlert(alert);
}
}
/**
* Add a performance alert
* @param {Object} alert - Alert object
*/
addAlert(alert) {
// Avoid duplicate alerts
const exists = this.alerts.some(existing =>
existing.type === alert.type &&
existing.operation === alert.operation &&
(Date.now() - existing.timestamp) < 60000 // Same alert within 1 minute
);
if (!exists) {
alert.id = `alert_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
this.alerts.push(alert);
// Log critical alerts
if (alert.severity === 'critical') {
console.warn(`🚨 Critical Performance Alert: ${alert.message}`);
}
}
}
/**
* Get active alerts
* @param {number} maxAge - Maximum age of alerts in ms (default: 5 minutes)
* @returns {Array} Active alerts
*/
getActiveAlerts(maxAge = 300000) {
const now = Date.now();
return this.alerts.filter(alert => (now - alert.timestamp) < maxAge);
}
/**
* Clear alerts
* @param {string} type - Alert type to clear (optional)
*/
clearAlerts(type = null) {
if (type) {
this.alerts = this.alerts.filter(alert => alert.type !== type);
} else {
this.alerts = [];
}
}
/**
* Get recent metrics for a category
* @param {string} category - Metric category
* @param {number} timeWindow - Time window in ms
* @returns {Array} Recent metrics
*/
getRecentMetrics(category, timeWindow) {
const now = Date.now();
const metrics = this.metricHistory.get(category) || [];
return metrics.filter(metric => (now - metric.timestamp) < timeWindow);
}
/**
* Get performance trends
* @param {string} operation - Operation to analyze
* @param {number} timeWindow - Time window in ms
* @returns {Object} Trend analysis
*/
getTrend(operation, timeWindow = 3600000) { // 1 hour default
const metrics = this.getRecentMetrics(operation, timeWindow);
if (metrics.length < 2) {
return { trend: 'insufficient-data', change: 0 };
}
const values = metrics.map(m => m.value);
const midpoint = Math.floor(values.length / 2);
const firstHalf = values.slice(0, midpoint);
const secondHalf = values.slice(midpoint);
const firstAvg = this.average(firstHalf);
const secondAvg = this.average(secondHalf);
const change = ((secondAvg - firstAvg) / firstAvg) * 100;
let trend = 'stable';
if (Math.abs(change) > 20) {
trend = change > 0 ? 'deteriorating' : 'improving';
} else if (Math.abs(change) > 10) {
trend = change > 0 ? 'slightly-worse' : 'slightly-better';
}
return { trend, change: Math.round(change * 100) / 100 };
}
/**
* Generate performance report
* @returns {Object} Comprehensive performance report
*/
generateReport() {
const now = Date.now();
const loadStats = this.getLoadTimeStats();
return {
timestamp: now,
uptime: this.getUptime(),
summary: {
optimizationScore: this.metrics.optimizationScore,
systemHealth: this.metrics.systemHealth.status,
cacheHitRate: this.getCacheHitRate(),
errorRate: this.getErrorRate()
},
performance: {
loadTimes: loadStats,
trends: {
loadTime: this.getTrend('section-load'),
cachePerformance: this.getTrend('cache-hit'),
errors: this.getTrend('error')
}
},
metrics: {
totalOperations: this.metrics.loadTimes.length + this.metrics.operations.length,
cacheHits: this.metrics.cacheHits,
cacheMisses: this.metrics.cacheMisses,
errors: this.metrics.errors.length
},
alerts: {
active: this.getActiveAlerts(),
total: this.alerts.length
}
};
}
/**
* Cleanup old metrics to prevent memory leaks
*/
cleanup() {
const maxAge = 3600000; // 1 hour
const now = Date.now();
// Clean up load times
this.metrics.loadTimes = this.metrics.loadTimes.filter(m => (now - m.timestamp) < maxAge);
// Clean up errors
this.metrics.errors = this.metrics.errors.filter(m => (now - m.timestamp) < maxAge);
// Clean up operations
this.metrics.operations = this.metrics.operations.filter(m => (now - m.timestamp) < maxAge);
// Clean up alerts
this.alerts = this.alerts.filter(a => (now - a.timestamp) < 300000); // 5 minutes
// Clean up metric history
for (const [key, history] of this.metricHistory) {
const filtered = history.filter(m => (now - m.timestamp) < maxAge);
if (filtered.length === 0) {
this.metricHistory.delete(key);
} else {
this.metricHistory.set(key, filtered.slice(-this.config.maxHistorySize));
}
}
this.lastCleanup = now;
}
/**
* Reset all metrics
*/
reset() {
this.metrics = {
loadTimes: [],
cacheHits: 0,
cacheMisses: 0,
errors: [],
operations: [],
systemHealth: {
status: 'healthy',
lastCheck: Date.now()
},
optimizationScore: 8.5
};
this.activeTimers.clear();
this.metricHistory.clear();
this.alerts = [];
this.startTime = Date.now();
}
/**
* Export metrics for external analysis
* @returns {Object} Exportable metrics data
*/
exportMetrics() {
return {
config: this.config,
metrics: this.metrics,
history: Object.fromEntries(this.metricHistory),
alerts: this.alerts,
uptime: this.getUptime(),
exportedAt: Date.now()
};
}
// Utility methods
average(values) {
return values.length > 0 ? values.reduce((sum, val) => sum + val, 0) / values.length : 0;
}
percentile(values, p) {
if (values.length === 0) return 0;
const sorted = [...values].sort((a, b) => a - b);
const index = Math.ceil((p / 100) * sorted.length) - 1;
return sorted[Math.max(0, index)];
}
/**
* Get monitor statistics
* @returns {Object} Monitor statistics
*/
getStats() {
return {
activeTimers: this.activeTimers.size,
metricCategories: Array.from(this.metricHistory.keys()),
totalMetrics: Array.from(this.metricHistory.values()).reduce((sum, arr) => sum + arr.length, 0),
alerts: this.alerts.length,
uptime: this.getUptime(),
optimizationScore: this.metrics.optimizationScore
};
}
}
module.exports = { PerformanceMonitor };