#!/usr/bin/env node /** * performance-monitor.js - Performance monitoring for .agents skills * Part of LCBP3-DMS Phase 3 enhancements */ const fs = require('fs'); const path = require('path'); const { performance } = require('perf_hooks'); // Configuration const BASE_DIR = path.resolve(__dirname, '../..'); const AGENTS_DIR = path.join(BASE_DIR, '.agents'); const SKILLS_DIR = path.join(AGENTS_DIR, 'skills'); const PERFORMANCE_LOG_PATH = path.join(AGENTS_DIR, 'logs', 'performance.log'); const PERFORMANCE_REPORT_PATH = path.join(AGENTS_DIR, 'reports', 'performance-report.json'); // Ensure directories exist [ path.dirname(PERFORMANCE_LOG_PATH), path.dirname(PERFORMANCE_REPORT_PATH) ].forEach(dir => { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } }); // Performance monitoring class class PerformanceMonitor { constructor() { this.startTime = performance.now(); this.metrics = { timestamp: new Date().toISOString(), duration: 0, skill_metrics: {}, workflow_metrics: {}, system_metrics: {}, summary: { total_skills_analyzed: 0, total_workflows_analyzed: 0, average_skill_size: 0, average_workflow_size: 0, performance_score: 0, recommendations: [] } }; } log(message, level = 'info') { const timestamp = new Date().toISOString(); const logEntry = `[${timestamp}] [${level.toUpperCase()}] ${message}\n`; // Console output with colors const colors = { info: '\x1b[36m', // Cyan good: '\x1b[32m', // Green warn: '\x1b[33m', // Yellow poor: '\x1b[31m', // Red reset: '\x1b[0m' }; const color = colors[level] || colors.info; console.log(`${color}${logEntry.trim()}${colors.reset}`); // File logging fs.appendFileSync(PERFORMANCE_LOG_PATH, logEntry); } analyzeSkillPerformance(skillPath, skillName) { const skillMdPath = path.join(skillPath, 'SKILL.md'); if (!fs.existsSync(skillMdPath)) { this.log(`Skipping ${skillName} - SKILL.md not found`, 'warn'); return null; } const startTime = performance.now(); try { const stats = fs.statSync(skillMdPath); const content = fs.readFileSync(skillMdPath, 'utf8'); // Basic metrics const fileSizeKB = stats.size / 1024; const lineCount = content.split('\n').length; const wordCount = content.split(/\s+/).filter(word => word.length > 0).length; const charCount = content.length; // Content complexity metrics const sectionCount = (content.match(/^#+\s/gm) || []).length; const codeBlockCount = (content.match(/```[\s\S]*?```/g) || []).length; const listCount = (content.match(/^[-*+]\s/gm) || []).length; // Front matter analysis const frontMatterMatch = content.match(/^---\n([\s\S]*?)\n---/); const frontMatterSize = frontMatterMatch ? frontMatterMatch[1].length : 0; const hasFrontMatter = frontMatterMatch !== null; // Readability metrics const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 0); const avgWordsPerSentence = sentences.length > 0 ? wordCount / sentences.length : 0; const avgCharsPerWord = wordCount > 0 ? charCount / wordCount : 0; // Performance score calculation let performanceScore = 100; // Size penalties if (fileSizeKB > 50) performanceScore -= 10; if (fileSizeKB > 100) performanceScore -= 20; // Content quality bonuses if (hasFrontMatter) performanceScore += 5; if (sectionCount >= 3) performanceScore += 5; if (codeBlockCount > 0) performanceScore += 5; // Readability penalties if (avgWordsPerSentence > 25) performanceScore -= 5; if (avgWordsPerSentence > 35) performanceScore -= 10; const analysisTime = performance.now() - startTime; const skillMetrics = { skill_name: skillName, file_path: skillMdPath, file_size_kb: Math.round(fileSizeKB * 100) / 100, line_count: lineCount, word_count: wordCount, char_count: charCount, section_count: sectionCount, code_block_count: codeBlockCount, list_count: listCount, front_matter_size: frontMatterSize, has_front_matter: hasFrontMatter, avg_words_per_sentence: Math.round(avgWordsPerSentence * 100) / 100, avg_chars_per_word: Math.round(avgCharsPerWord * 100) / 100, performance_score: Math.max(0, Math.min(100, performanceScore)), analysis_time_ms: Math.round(analysisTime * 100) / 100, last_modified: stats.mtime.toISOString() }; this.metrics.skill_metrics[skillName] = skillMetrics; // Log performance assessment if (performanceScore >= 80) { this.log(`${skillName}: GOOD performance (score: ${performanceScore})`, 'good'); } else if (performanceScore >= 60) { this.log(`${skillName}: OK performance (score: ${performanceScore})`, 'info'); } else { this.log(`${skillName}: POOR performance (score: ${performanceScore})`, 'poor'); } return skillMetrics; } catch (error) { this.log(`Error analyzing ${skillName}: ${error.message}`, 'warn'); return null; } } analyzeWorkflowPerformance(workflowPath, workflowName) { const startTime = performance.now(); if (!fs.existsSync(workflowPath)) { this.log(`Skipping workflow ${workflowName} - file not found`, 'warn'); return null; } try { const stats = fs.statSync(workflowPath); const content = fs.readFileSync(workflowPath, 'utf8'); // Basic metrics const fileSizeKB = stats.size / 1024; const lineCount = content.split('\n').length; const wordCount = content.split(/\s+/).filter(word => word.length > 0).length; // Workflow-specific metrics const stepCount = (content.match(/^\d+\./gm) || []).length; const codeBlockCount = (content.match(/```[\s\S]*?```/g) || []).length; const skillReferences = (content.match(/@speckit-\w+/g) || []).length; // Performance score calculation let performanceScore = 100; // Size penalties if (fileSizeKB > 20) performanceScore -= 10; if (fileSizeKB > 50) performanceScore -= 20; // Content quality bonuses if (stepCount > 0) performanceScore += 10; if (codeBlockCount > 0) performanceScore += 5; if (skillReferences > 0) performanceScore += 5; const analysisTime = performance.now() - startTime; const workflowMetrics = { workflow_name: workflowName, file_path: workflowPath, file_size_kb: Math.round(fileSizeKB * 100) / 100, line_count: lineCount, word_count: wordCount, step_count: stepCount, code_block_count: codeBlockCount, skill_references: skillReferences, performance_score: Math.max(0, Math.min(100, performanceScore)), analysis_time_ms: Math.round(analysisTime * 100) / 100, last_modified: stats.mtime.toISOString() }; this.metrics.workflow_metrics[workflowName] = workflowMetrics; // Log performance assessment if (performanceScore >= 80) { this.log(`${workflowName}: GOOD performance (score: ${performanceScore})`, 'good'); } else if (performanceScore >= 60) { this.log(`${workflowName}: OK performance (score: ${performanceScore})`, 'info'); } else { this.log(`${workflowName}: POOR performance (score: ${performanceScore})`, 'poor'); } return workflowMetrics; } catch (error) { this.log(`Error analyzing workflow ${workflowName}: ${error.message}`, 'warn'); return null; } } analyzeSystemMetrics() { this.log('Analyzing system metrics...', 'info'); // Directory sizes const agentsSize = this.getDirectorySize(AGENTS_DIR); const skillsSize = this.getDirectorySize(SKILLS_DIR); const workflowsDir = path.join(BASE_DIR, '.windsurf', 'workflows'); const workflowsSize = fs.existsSync(workflowsDir) ? this.getDirectorySize(workflowsDir) : 0; // File counts const totalFiles = this.countFiles(AGENTS_DIR); const skillFiles = this.countFiles(SKILLS_DIR); const workflowFiles = fs.existsSync(workflowsDir) ? this.countFiles(workflowsDir) : 0; this.metrics.system_metrics = { agents_directory_size_kb: Math.round(agentsSize / 1024), skills_directory_size_kb: Math.round(skillsSize / 1024), workflows_directory_size_kb: Math.round(workflowsSize / 1024), total_files: totalFiles, skill_files: skillFiles, workflow_files: workflowFiles, analysis_timestamp: new Date().toISOString() }; this.log(`System: ${totalFiles} files, ${Math.round(agentsSize / 1024)}KB total`, 'info'); } getDirectorySize(dirPath) { let totalSize = 0; if (!fs.existsSync(dirPath)) { return 0; } const items = fs.readdirSync(dirPath); for (const item of items) { const itemPath = path.join(dirPath, item); const stats = fs.statSync(itemPath); if (stats.isDirectory()) { totalSize += this.getDirectorySize(itemPath); } else { totalSize += stats.size; } } return totalSize; } countFiles(dirPath) { let fileCount = 0; if (!fs.existsSync(dirPath)) { return 0; } const items = fs.readdirSync(dirPath); for (const item of items) { const itemPath = path.join(dirPath, item); const stats = fs.statSync(itemPath); if (stats.isDirectory()) { fileCount += this.countFiles(itemPath); } else { fileCount++; } } return fileCount; } generateRecommendations() { const recommendations = []; const { skill_metrics, workflow_metrics, system_metrics } = this.metrics; // Analyze skill performance const skillScores = Object.values(skill_metrics).map(m => m.performance_score); const avgSkillScore = skillScores.length > 0 ? skillScores.reduce((a, b) => a + b, 0) / skillScores.length : 0; if (avgSkillScore < 70) { recommendations.push({ type: 'performance', priority: 'high', message: 'Average skill performance is below optimal. Consider optimizing skill documentation.', details: `Average score: ${Math.round(avgSkillScore)}` }); } // Check for oversized files const largeSkills = Object.values(skill_metrics).filter(m => m.file_size_kb > 50); if (largeSkills.length > 0) { recommendations.push({ type: 'size', priority: 'medium', message: `${largeSkills.length} skills have large file sizes (>50KB). Consider breaking down complex skills.`, details: largeSkills.map(s => `${s.skill_name} (${s.file_size_kb}KB)`).join(', ') }); } // Check for missing front matter const skillsWithoutFrontMatter = Object.values(skill_metrics).filter(m => !m.has_front_matter); if (skillsWithoutFrontMatter.length > 0) { recommendations.push({ type: 'structure', priority: 'high', message: `${skillsWithoutFrontMatter.length} skills missing front matter. Add proper YAML front matter.`, details: skillsWithoutFrontMatter.map(s => s.skill_name).join(', ') }); } // Analyze workflow performance const workflowScores = Object.values(workflow_metrics).map(m => m.performance_score); const avgWorkflowScore = workflowScores.length > 0 ? workflowScores.reduce((a, b) => a + b, 0) / workflowScores.length : 0; if (avgWorkflowScore < 70) { recommendations.push({ type: 'performance', priority: 'medium', message: 'Average workflow performance could be improved. Add more detailed steps and examples.', details: `Average score: ${Math.round(avgWorkflowScore)}` }); } // System recommendations if (system_metrics.agents_directory_size_kb > 1000) { recommendations.push({ type: 'maintenance', priority: 'low', message: '.agents directory is growing large. Consider archiving old logs and reports.', details: `Current size: ${system_metrics.agents_directory_size_kb}KB` }); } this.metrics.summary.recommendations = recommendations; // Log recommendations if (recommendations.length > 0) { this.log('Performance Recommendations:', 'info'); recommendations.forEach((rec, index) => { const priority = rec.priority === 'high' ? 'HIGH' : rec.priority === 'medium' ? 'MED' : 'LOW'; this.log(` ${index + 1}. [${priority}] ${rec.message}`, 'warn'); }); } else { this.log('No performance issues detected - system is optimized!', 'good'); } } calculateOverallPerformance() { const { skill_metrics, workflow_metrics } = this.metrics; const skillScores = Object.values(skill_metrics).map(m => m.performance_score); const workflowScores = Object.values(workflow_metrics).map(m => m.performance_score); const avgSkillScore = skillScores.length > 0 ? skillScores.reduce((a, b) => a + b, 0) / skillScores.length : 100; const avgWorkflowScore = workflowScores.length > 0 ? workflowScores.reduce((a, b) => a + b, 0) / workflowScores.length : 100; // Weight skills more heavily than workflows const overallScore = (avgSkillScore * 0.7) + (avgWorkflowScore * 0.3); this.metrics.summary.performance_score = Math.round(overallScore); this.metrics.summary.average_skill_size = skillScores.length > 0 ? Math.round(Object.values(skill_metrics).reduce((sum, m) => sum + m.file_size_kb, 0) / skillScores.length * 100) / 100 : 0; this.metrics.summary.average_workflow_size = workflowScores.length > 0 ? Math.round(Object.values(workflow_metrics).reduce((sum, m) => sum + m.file_size_kb, 0) / workflowScores.length * 100) / 100 : 0; this.metrics.summary.total_skills_analyzed = skillScores.length; this.metrics.summary.total_workflows_analyzed = workflowScores.length; } generateReport() { this.metrics.duration = performance.now() - this.startTime; const report = { ...this.metrics, generated_at: new Date().toISOString(), environment: { node_version: process.version, platform: process.platform, memory_usage: process.memoryUsage() } }; fs.writeFileSync(PERFORMANCE_REPORT_PATH, JSON.stringify(report, null, 2)); this.log(`Performance report saved to: ${PERFORMANCE_REPORT_PATH}`, 'info'); return report; } async runPerformanceAnalysis() { this.log('Starting performance analysis...', 'info'); this.log(`Base directory: ${BASE_DIR}`, 'info'); // Analyze skills this.log('Analyzing skill performance...', 'info'); if (fs.existsSync(SKILLS_DIR)) { const skillDirs = fs.readdirSync(SKILLS_DIR).filter(item => { const itemPath = path.join(SKILLS_DIR, item); return fs.statSync(itemPath).isDirectory(); }); for (const skillDir of skillDirs) { const skillPath = path.join(SKILLS_DIR, skillDir); this.analyzeSkillPerformance(skillPath, skillDir); } } // Analyze workflows this.log('Analyzing workflow performance...', 'info'); const workflowsDir = path.join(BASE_DIR, '.windsurf', 'workflows'); if (fs.existsSync(workflowsDir)) { const workflowFiles = fs.readdirSync(workflowsDir).filter(file => file.endsWith('.md')); for (const workflowFile of workflowFiles) { const workflowPath = path.join(workflowsDir, workflowFile); const workflowName = workflowFile.replace('.md', ''); this.analyzeWorkflowPerformance(workflowPath, workflowName); } } // System metrics this.analyzeSystemMetrics(); // Calculate overall performance this.calculateOverallPerformance(); // Generate recommendations this.generateRecommendations(); // Generate report const report = this.generateReport(); // Summary this.log('=== Performance Analysis Summary ===', 'info'); this.log(`Overall performance score: ${this.metrics.summary.performance_score}/100`, 'info'); this.log(`Skills analyzed: ${this.metrics.summary.total_skills_analyzed}`, 'info'); this.log(`Workflows analyzed: ${this.metrics.summary.total_workflows_analyzed}`, 'info'); this.log(`Average skill size: ${this.metrics.summary.average_skill_size}KB`, 'info'); this.log(`Average workflow size: ${this.metrics.summary.average_workflow_size}KB`, 'info'); this.log(`Analysis duration: ${Math.round(this.metrics.duration)}ms`, 'info'); this.log(`Recommendations: ${this.metrics.summary.recommendations.length}`, 'info'); return report; } } // CLI interface async function main() { const monitor = new PerformanceMonitor(); try { const report = await monitor.runPerformanceAnalysis(); process.exit(report.summary.performance_score < 60 ? 1 : 0); } catch (error) { console.error('Performance analysis failed:', error); process.exit(1); } } // Export for use in other modules module.exports = { PerformanceMonitor }; // Run if called directly if (require.main === module) { main(); }