@calmhive/calmhive-cli
Version:
🐝 Intelligent Claude CLI wrapper with rule injection, AFk background processing, voice interface, and adaptive retry
732 lines (609 loc) • 19.2 kB
JavaScript
/**
* TUIQualityAnalyzer - Evaluates TUI performance and user experience
*
* Monitors and analyzes the Terminal User Interface for:
* - Responsiveness (render times, input lag)
* - Usability (navigation smoothness, modal state management)
* - Data integrity (session sync accuracy, refresh throttling)
* - Error handling (graceful failure modes, recovery)
* - Performance (memory usage, screen update efficiency)
*/
const EventEmitter = require('events');
class TUIQualityAnalyzer extends EventEmitter {
constructor(tuiManager) {
super();
this.tuiManager = tuiManager;
this.metrics = new Map();
this.performanceData = {
renderTimes: [],
inputLagTimes: [],
memoryUsage: [],
refreshCycles: [],
errorCount: 0,
navigationEvents: []
};
this.isMonitoring = false;
this.monitoringStartTime = null;
this.lastMetricsTime = Date.now();
// Quality thresholds
this.thresholds = {
maxRenderTime: 100, // milliseconds
maxInputLag: 50, // milliseconds
maxMemoryGrowth: 50, // MB per hour
maxRefreshTime: 200, // milliseconds
minSuccessRate: 95 // percentage
};
}
/**
* Start monitoring TUI quality metrics
*/
startMonitoring() {
if (this.isMonitoring) {
console.log('⚠️ TUI quality monitoring already active');
return;
}
console.log('🔍 Starting TUI quality monitoring...');
this.isMonitoring = true;
this.monitoringStartTime = Date.now();
this.attachTUIHooks();
this.startPerformanceTracking();
this.startMemoryMonitoring();
console.log('✅ TUI quality monitoring active');
this.emit('monitoringStarted');
}
/**
* Stop monitoring TUI quality metrics
*/
stopMonitoring() {
if (!this.isMonitoring) {
console.log('⚠️ TUI quality monitoring not active');
return;
}
console.log('🛑 Stopping TUI quality monitoring...');
this.isMonitoring = false;
this.detachTUIHooks();
this.stopPerformanceTracking();
this.stopMemoryMonitoring();
console.log('✅ TUI quality monitoring stopped');
this.emit('monitoringStopped');
}
/**
* Attach hooks to TUI manager for monitoring
*/
attachTUIHooks() {
if (!this.tuiManager) {
console.warn('⚠️ No TUI manager available for monitoring');
return;
}
// Monitor render performance
this.originalRender = this.tuiManager.render?.bind(this.tuiManager);
if (this.originalRender) {
this.tuiManager.render = (...args) => {
return this.measureRenderTime(() => this.originalRender(...args));
};
}
// Monitor view switches
this.originalSwitchView = this.tuiManager.switchView?.bind(this.tuiManager);
if (this.originalSwitchView) {
this.tuiManager.switchView = (...args) => {
return this.measureNavigationTime(() => this.originalSwitchView(...args), 'viewSwitch');
};
}
// Monitor refresh cycles
this.originalRefreshData = this.tuiManager.refreshData?.bind(this.tuiManager);
if (this.originalRefreshData) {
this.tuiManager.refreshData = (...args) => {
return this.measureRefreshTime(() => this.originalRefreshData(...args));
};
}
// Monitor modal operations
if (this.tuiManager.activeModals) {
this.monitorModalState();
}
// Monitor key handling
if (this.tuiManager.screen) {
this.monitorKeyHandling();
}
}
/**
* Detach hooks from TUI manager
*/
detachTUIHooks() {
if (!this.tuiManager) {return;}
// Restore original methods
if (this.originalRender) {
this.tuiManager.render = this.originalRender;
}
if (this.originalSwitchView) {
this.tuiManager.switchView = this.originalSwitchView;
}
if (this.originalRefreshData) {
this.tuiManager.refreshData = this.originalRefreshData;
}
}
/**
* Measure render time performance
*/
measureRenderTime(renderFunction) {
const startTime = process.hrtime.bigint();
try {
const result = renderFunction();
const endTime = process.hrtime.bigint();
const renderTimeMs = Number(endTime - startTime) / 1000000;
this.recordRenderTime(renderTimeMs);
return result;
} catch (error) {
this.recordError('render', error);
throw error;
}
}
/**
* Measure navigation time performance
*/
measureNavigationTime(navigationFunction, type) {
const startTime = process.hrtime.bigint();
try {
const result = navigationFunction();
const endTime = process.hrtime.bigint();
const navTimeMs = Number(endTime - startTime) / 1000000;
this.recordNavigationTime(navTimeMs, type);
return result;
} catch (error) {
this.recordError('navigation', error);
throw error;
}
}
/**
* Measure refresh cycle performance
*/
measureRefreshTime(refreshFunction) {
const startTime = process.hrtime.bigint();
try {
const result = refreshFunction();
const endTime = process.hrtime.bigint();
const refreshTimeMs = Number(endTime - startTime) / 1000000;
this.recordRefreshTime(refreshTimeMs);
return result;
} catch (error) {
this.recordError('refresh', error);
throw error;
}
}
/**
* Monitor modal state consistency
*/
monitorModalState() {
if (!this.tuiManager.activeModals) {return;}
setInterval(() => {
if (!this.isMonitoring) {return;}
const modalCount = this.tuiManager.activeModals.size;
const hasActiveModal = this.tuiManager.hasActiveModal?.() || false;
// Check for state inconsistency
if ((modalCount > 0) !== hasActiveModal) {
this.recordError('modal_state', new Error('Modal state inconsistency detected'));
}
this.recordMetric('modalCount', modalCount);
}, 1000);
}
/**
* Monitor key handling responsiveness
*/
monitorKeyHandling() {
if (!this.tuiManager.screen) {return;}
this.tuiManager.screen.on('keypress', (ch, key) => {
if (!this.isMonitoring) {return;}
const timestamp = Date.now();
// Measure input lag (time since last screen update)
const inputLag = timestamp - (this.lastScreenUpdate || timestamp);
this.recordInputLag(inputLag);
// Track navigation patterns
if (key && key.name) {
this.recordNavigationEvent(key.name, timestamp);
}
});
// Track screen updates
const originalRender = this.tuiManager.screen.render?.bind(this.tuiManager.screen);
if (originalRender) {
this.tuiManager.screen.render = (...args) => {
this.lastScreenUpdate = Date.now();
return originalRender(...args);
};
}
}
/**
* Start performance tracking
*/
startPerformanceTracking() {
this.performanceTimer = setInterval(() => {
if (!this.isMonitoring) {return;}
const memUsage = process.memoryUsage();
this.recordMetric('memoryHeapUsed', memUsage.heapUsed);
this.recordMetric('memoryRSS', memUsage.rss);
// Check for memory leaks
this.checkMemoryGrowth();
}, 5000); // Every 5 seconds
}
/**
* Stop performance tracking
*/
stopPerformanceTracking() {
if (this.performanceTimer) {
clearInterval(this.performanceTimer);
this.performanceTimer = null;
}
}
/**
* Start memory monitoring
*/
startMemoryMonitoring() {
this.memoryBaseline = process.memoryUsage().heapUsed;
this.memoryCheckInterval = setInterval(() => {
if (!this.isMonitoring) {return;}
const currentMemory = process.memoryUsage().heapUsed;
const growth = currentMemory - this.memoryBaseline;
this.performanceData.memoryUsage.push({
timestamp: Date.now(),
heapUsed: currentMemory,
growth
});
// Keep only last 100 measurements
if (this.performanceData.memoryUsage.length > 100) {
this.performanceData.memoryUsage.shift();
}
}, 10000); // Every 10 seconds
}
/**
* Stop memory monitoring
*/
stopMemoryMonitoring() {
if (this.memoryCheckInterval) {
clearInterval(this.memoryCheckInterval);
this.memoryCheckInterval = null;
}
}
/**
* Record render time metric
*/
recordRenderTime(timeMs) {
this.performanceData.renderTimes.push({
timestamp: Date.now(),
duration: timeMs
});
// Keep only last 100 measurements
if (this.performanceData.renderTimes.length > 100) {
this.performanceData.renderTimes.shift();
}
// Check threshold
if (timeMs > this.thresholds.maxRenderTime) {
this.emit('performanceIssue', {
type: 'slowRender',
value: timeMs,
threshold: this.thresholds.maxRenderTime
});
}
}
/**
* Record input lag metric
*/
recordInputLag(lagMs) {
this.performanceData.inputLagTimes.push({
timestamp: Date.now(),
lag: lagMs
});
// Keep only last 50 measurements
if (this.performanceData.inputLagTimes.length > 50) {
this.performanceData.inputLagTimes.shift();
}
// Check threshold
if (lagMs > this.thresholds.maxInputLag) {
this.emit('performanceIssue', {
type: 'inputLag',
value: lagMs,
threshold: this.thresholds.maxInputLag
});
}
}
/**
* Record refresh cycle time
*/
recordRefreshTime(timeMs) {
this.performanceData.refreshCycles.push({
timestamp: Date.now(),
duration: timeMs
});
// Keep only last 50 measurements
if (this.performanceData.refreshCycles.length > 50) {
this.performanceData.refreshCycles.shift();
}
// Check threshold
if (timeMs > this.thresholds.maxRefreshTime) {
this.emit('performanceIssue', {
type: 'slowRefresh',
value: timeMs,
threshold: this.thresholds.maxRefreshTime
});
}
}
/**
* Record navigation event
*/
recordNavigationEvent(keyName, timestamp) {
this.performanceData.navigationEvents.push({
key: keyName,
timestamp
});
// Keep only last 100 events
if (this.performanceData.navigationEvents.length > 100) {
this.performanceData.navigationEvents.shift();
}
}
/**
* Record navigation time
*/
recordNavigationTime(timeMs, type) {
this.recordMetric(`navigation_${type}`, timeMs);
if (timeMs > this.thresholds.maxRenderTime) {
this.emit('performanceIssue', {
type: 'slowNavigation',
navigationMode: type,
value: timeMs,
threshold: this.thresholds.maxRenderTime
});
}
}
/**
* Record error
*/
recordError(context, error) {
this.performanceData.errorCount++;
this.emit('error', {
context,
error: error.message,
timestamp: Date.now()
});
}
/**
* Record generic metric
*/
recordMetric(name, value) {
if (!this.metrics.has(name)) {
this.metrics.set(name, []);
}
const metricArray = this.metrics.get(name);
metricArray.push({
timestamp: Date.now(),
value
});
// Keep only last 100 measurements per metric
if (metricArray.length > 100) {
metricArray.shift();
}
}
/**
* Check for memory growth issues
*/
checkMemoryGrowth() {
if (this.performanceData.memoryUsage.length < 10) {return;}
const recent = this.performanceData.memoryUsage.slice(-10);
const growthRate = (recent[recent.length - 1].heapUsed - recent[0].heapUsed) /
(recent.length * 10); // Growth per 10 seconds
const hourlyGrowthMB = (growthRate * 360) / (1024 * 1024); // Extrapolate to hourly
if (hourlyGrowthMB > this.thresholds.maxMemoryGrowth) {
this.emit('performanceIssue', {
type: 'memoryLeak',
value: hourlyGrowthMB,
threshold: this.thresholds.maxMemoryGrowth
});
}
}
/**
* Generate comprehensive quality report
*/
generateQualityReport() {
const now = Date.now();
const monitoringDuration = this.monitoringStartTime ?
(now - this.monitoringStartTime) / 1000 : 0;
const report = {
timestamp: now,
monitoringDuration,
responsiveness: this.analyzeResponsiveness(),
usability: this.analyzeUsability(),
dataIntegrity: this.analyzeDataIntegrity(),
errorHandling: this.analyzeErrorHandling(),
performance: this.analyzePerformance(),
overallScore: 0,
recommendations: []
};
// Calculate overall score (weighted average)
const weights = {
responsiveness: 0.3,
usability: 0.25,
dataIntegrity: 0.2,
errorHandling: 0.15,
performance: 0.1
};
report.overallScore = Object.keys(weights).reduce((score, metric) => {
return score + (report[metric].score * weights[metric]);
}, 0);
// Generate recommendations
report.recommendations = this.generateRecommendations(report);
return report;
}
/**
* Analyze responsiveness metrics
*/
analyzeResponsiveness() {
const renderTimes = this.performanceData.renderTimes.map(r => r.duration);
const inputLags = this.performanceData.inputLagTimes.map(i => i.lag);
const avgRenderTime = renderTimes.length > 0 ?
renderTimes.reduce((a, b) => a + b, 0) / renderTimes.length : 0;
const avgInputLag = inputLags.length > 0 ?
inputLags.reduce((a, b) => a + b, 0) / inputLags.length : 0;
const maxRenderTime = Math.max(...renderTimes, 0);
const slowRenders = renderTimes.filter(t => t > this.thresholds.maxRenderTime).length;
// Score based on performance vs thresholds
let score = 100;
if (avgRenderTime > this.thresholds.maxRenderTime * 0.5) {score -= 20;}
if (avgInputLag > this.thresholds.maxInputLag * 0.5) {score -= 20;}
if (maxRenderTime > this.thresholds.maxRenderTime * 2) {score -= 30;}
if (slowRenders > renderTimes.length * 0.1) {score -= 30;}
return {
score: Math.max(0, score),
avgRenderTime,
avgInputLag,
maxRenderTime,
slowRenderCount: slowRenders,
totalRenders: renderTimes.length
};
}
/**
* Analyze usability metrics
*/
analyzeUsability() {
const navEvents = this.performanceData.navigationEvents;
const refreshCycles = this.performanceData.refreshCycles;
// Check navigation patterns for usability issues
const recentNavs = navEvents.slice(-20);
const backtrackCount = this.countBacktrackPatterns(recentNavs);
const avgRefreshTime = refreshCycles.length > 0 ?
refreshCycles.reduce((a, b) => a + b.duration, 0) / refreshCycles.length : 0;
let score = 100;
if (backtrackCount > recentNavs.length * 0.3) {score -= 40;} // Too much backtracking
if (avgRefreshTime > this.thresholds.maxRefreshTime) {score -= 30;}
return {
score: Math.max(0, score),
navigationEvents: navEvents.length,
backtrackRate: recentNavs.length > 0 ? backtrackCount / recentNavs.length : 0,
avgRefreshTime
};
}
/**
* Analyze data integrity
*/
analyzeDataIntegrity() {
const modalMetrics = this.metrics.get('modalCount') || [];
// Check for data consistency issues
let score = 100;
// Modal state consistency
const modalStateErrors = this.performanceData.errorCount; // Simplified
if (modalStateErrors > 0) {score -= 50;}
return {
score: Math.max(0, score),
modalStateConsistency: modalStateErrors === 0,
totalDataChecks: modalMetrics.length
};
}
/**
* Analyze error handling
*/
analyzeErrorHandling() {
const totalErrors = this.performanceData.errorCount;
const totalOperations = this.performanceData.renderTimes.length +
this.performanceData.refreshCycles.length +
this.performanceData.navigationEvents.length;
const errorRate = totalOperations > 0 ? (totalErrors / totalOperations) * 100 : 0;
const successRate = 100 - errorRate;
let score = successRate;
if (successRate < this.thresholds.minSuccessRate) {
score = Math.max(0, score - 20);
}
return {
score: Math.max(0, score),
totalErrors,
errorRate,
successRate
};
}
/**
* Analyze performance metrics
*/
analyzePerformance() {
const memData = this.performanceData.memoryUsage;
let score = 100;
// Memory usage analysis
if (memData.length > 5) {
const recent = memData.slice(-5);
const growth = recent[recent.length - 1].growth;
const growthMB = growth / (1024 * 1024);
if (growthMB > this.thresholds.maxMemoryGrowth / 10) {score -= 30;} // Per measurement
}
return {
score: Math.max(0, score),
memoryGrowth: memData.length > 0 ? memData[memData.length - 1].growth : 0,
memoryMeasurements: memData.length
};
}
/**
* Count backtrack navigation patterns
*/
countBacktrackPatterns(navEvents) {
if (navEvents.length < 3) {return 0;}
let backtrackCount = 0;
for (let i = 2; i < navEvents.length; i++) {
const current = navEvents[i].key;
const prev = navEvents[i - 1].key;
const prevPrev = navEvents[i - 2].key;
// Pattern: A -> B -> A (backtrack)
if (current === prevPrev && current !== prev) {
backtrackCount++;
}
}
return backtrackCount;
}
/**
* Generate improvement recommendations
*/
generateRecommendations(report) {
const recommendations = [];
if (report.responsiveness.score < 80) {
recommendations.push({
priority: 'high',
category: 'responsiveness',
issue: 'Slow render times detected',
suggestion: 'Optimize screen updates and reduce DOM complexity'
});
}
if (report.usability.backtrackRate > 0.2) {
recommendations.push({
priority: 'medium',
category: 'usability',
issue: 'High backtrack rate in navigation',
suggestion: 'Improve navigation flow and add breadcrumbs'
});
}
if (report.errorHandling.successRate < 95) {
recommendations.push({
priority: 'high',
category: 'reliability',
issue: 'Low success rate detected',
suggestion: 'Add better error handling and recovery mechanisms'
});
}
if (report.performance.memoryGrowth > 1024 * 1024 * 10) { // 10MB
recommendations.push({
priority: 'medium',
category: 'performance',
issue: 'Memory usage growing',
suggestion: 'Check for memory leaks in event handlers'
});
}
return recommendations;
}
/**
* Get current metrics snapshot
*/
getMetricsSnapshot() {
return {
timestamp: Date.now(),
isMonitoring: this.isMonitoring,
renderTimes: [...this.performanceData.renderTimes],
inputLagTimes: [...this.performanceData.inputLagTimes],
refreshCycles: [...this.performanceData.refreshCycles],
navigationEvents: [...this.performanceData.navigationEvents],
memoryUsage: [...this.performanceData.memoryUsage],
errorCount: this.performanceData.errorCount,
customMetrics: Object.fromEntries(this.metrics)
};
}
}
module.exports = TUIQualityAnalyzer;