ctrlshiftleft
Version:
AI-powered toolkit for embedding QA and security testing into development workflows
274 lines (237 loc) • 8.89 kB
JavaScript
/**
* Performance Tracker (JavaScript wrapper)
*
* This is a JavaScript wrapper for the TypeScript performanceTracker module.
* It provides performance tracking capabilities for ctrl.shift.left operations.
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
// Import platform utilities
const { PathUtils, FileUtils } = require('./platformUtils');
/**
* Performance tracker for measuring and reporting operation durations
*/
class PerformanceTracker {
/**
* Create a new performance tracker
* @param {Object} options Configuration options
* @param {string} options.projectName Project name for reporting
* @param {string} options.version Project version for reporting
* @param {string} options.outputDir Directory to save performance reports
* @param {boolean} options.enabled Whether tracking is enabled
*/
constructor(options = {}) {
this.projectName = options.projectName || path.basename(process.cwd());
this.version = options.version || '1.0.0';
this.outputDir = options.outputDir || './performance-reports';
this.enabled = options.enabled !== false;
this.entries = [];
this.activeTimers = new Map();
// Create output directory if enabled
if (this.enabled && !fs.existsSync(this.outputDir)) {
PathUtils.ensureDir(this.outputDir);
}
}
/**
* Enable or disable performance tracking
* @param {boolean} enabled Whether tracking should be enabled
*/
setEnabled(enabled) {
this.enabled = enabled;
}
/**
* Start a performance timer for an operation
* @param {string} type Operation type (e.g., 'generation', 'execution')
* @param {string} target Target of the operation (e.g., filename)
* @returns {string} Timer ID for stopping the timer later
*/
startTimer(type, target) {
if (!this.enabled) {
return '';
}
const timerId = `${type}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
this.activeTimers.set(timerId, {
startTime: Date.now(),
type,
target
});
return timerId;
}
/**
* Stop a performance timer and record the operation
* @param {string} timerId Timer ID from startTimer
* @param {Object} metadata Additional metadata about the operation
* @returns {number} Duration in milliseconds
*/
stopTimer(timerId, metadata = {}) {
if (!this.enabled || !timerId || !this.activeTimers.has(timerId)) {
return -1;
}
const endTime = Date.now();
const timer = this.activeTimers.get(timerId);
const duration = endTime - timer.startTime;
this.activeTimers.delete(timerId);
this.recordOperation(timer.type, timer.target, duration, metadata);
return duration;
}
/**
* Record an operation directly with a known duration
* @param {string} type Operation type
* @param {string} target Target of the operation
* @param {number} duration Duration in milliseconds
* @param {Object} metadata Additional metadata
*/
recordOperation(type, target, duration, metadata = {}) {
if (!this.enabled) {
return;
}
this.entries.push({
type,
target,
duration,
timestamp: new Date().toISOString(),
metadata: {
...metadata,
platform: os.platform(),
arch: os.arch(),
nodeVersion: process.version
}
});
}
/**
* Get all recorded performance entries
* @returns {Array} Array of performance entries
*/
getEntries() {
return this.entries;
}
/**
* Generate a performance report
* @param {string} label Optional label for the report
* @returns {Object} Performance report object
*/
generateReport(label = '') {
const totalDuration = this.entries.reduce((sum, entry) => sum + entry.duration, 0);
const averageDuration = this.entries.length ? totalDuration / this.entries.length : 0;
const successCount = this.entries.filter(entry => entry.metadata?.success !== false).length;
const successRate = this.entries.length ? (successCount / this.entries.length) * 100 : 100;
// Group by type
const byType = {};
this.entries.forEach(entry => {
if (!byType[entry.type]) {
byType[entry.type] = {
count: 0,
totalDuration: 0,
averageDuration: 0,
successCount: 0,
successRate: 0
};
}
byType[entry.type].count++;
byType[entry.type].totalDuration += entry.duration;
if (entry.metadata?.success !== false) {
byType[entry.type].successCount++;
}
});
// Calculate averages and success rates
Object.keys(byType).forEach(type => {
byType[type].averageDuration = byType[type].totalDuration / byType[type].count;
byType[type].successRate = (byType[type].successCount / byType[type].count) * 100;
});
return {
metadata: {
project: this.projectName,
version: this.version,
timestamp: new Date().toISOString(),
label,
platform: os.platform(),
arch: os.arch(),
nodeVersion: process.version
},
entries: this.entries,
summary: {
operationCount: this.entries.length,
totalDuration,
averageDuration,
successCount,
successRate,
byType
}
};
}
/**
* Save performance report to file
* @param {string} format Output format ('json' or 'markdown')
* @param {string} label Optional label for the report
* @returns {string} Path to the saved report
*/
saveReport(format = 'json', label = '') {
if (!this.enabled || this.entries.length === 0) {
return '';
}
// Create output directory if it doesn't exist
PathUtils.ensureDir(this.outputDir);
const timestamp = new Date().toISOString().replace(/:/g, '-');
const labelSuffix = label ? `-${label}` : '';
const report = this.generateReport(label);
if (format === 'json') {
const filePath = path.join(this.outputDir, `perf-report${labelSuffix}-${timestamp}.json`);
fs.writeFileSync(filePath, JSON.stringify(report, null, 2));
return filePath;
} else if (format === 'markdown') {
const filePath = path.join(this.outputDir, `perf-report${labelSuffix}-${timestamp}.md`);
const markdown = this.formatMarkdownReport(report);
fs.writeFileSync(filePath, markdown);
return filePath;
}
return '';
}
/**
* Format a performance report as Markdown
* @param {Object} report Performance report object
* @returns {string} Markdown formatted report
*/
formatMarkdownReport(report) {
const { metadata, summary, entries } = report;
let markdown = `# Performance Report\n\n`;
// Metadata section
markdown += `## Metadata\n\n`;
markdown += `- **Project**: ${metadata.project}\n`;
markdown += `- **Version**: ${metadata.version}\n`;
markdown += `- **Timestamp**: ${metadata.timestamp}\n`;
if (metadata.label) {
markdown += `- **Label**: ${metadata.label}\n`;
}
markdown += `- **Platform**: ${metadata.platform} (${metadata.arch})\n`;
markdown += `- **Node.js**: ${metadata.nodeVersion}\n\n`;
// Summary section
markdown += `## Summary\n\n`;
markdown += `- **Total Operations**: ${summary.operationCount}\n`;
markdown += `- **Total Duration**: ${summary.totalDuration.toFixed(2)}ms\n`;
markdown += `- **Average Duration**: ${summary.averageDuration.toFixed(2)}ms\n`;
markdown += `- **Success Rate**: ${summary.successRate.toFixed(2)}%\n\n`;
// Operations by type
markdown += `## Operations by Type\n\n`;
markdown += `| Type | Count | Total Duration | Average Duration | Success Rate |\n`;
markdown += `| ---- | ----- | -------------- | ---------------- | ------------ |\n`;
Object.keys(summary.byType).forEach(type => {
const stats = summary.byType[type];
markdown += `| ${type} | ${stats.count} | ${stats.totalDuration.toFixed(2)}ms | ${stats.averageDuration.toFixed(2)}ms | ${stats.successRate.toFixed(2)}% |\n`;
});
markdown += `\n## Operation Details\n\n`;
markdown += `| Type | Target | Duration | Timestamp | Status |\n`;
markdown += `| ---- | ------ | -------- | --------- | ------ |\n`;
// Show the first 20 operations for brevity
const displayEntries = entries.slice(0, 20);
displayEntries.forEach(entry => {
const status = entry.metadata?.success === false ? '❌ Failed' : '✅ Success';
markdown += `| ${entry.type} | ${entry.target} | ${entry.duration.toFixed(2)}ms | ${entry.timestamp} | ${status} |\n`;
});
if (entries.length > 20) {
markdown += `\n*...and ${entries.length - 20} more operations*\n`;
}
return markdown;
}
}
module.exports = { PerformanceTracker };