baseline-lint
Version:
Check web features for Baseline compatibility
624 lines (535 loc) • 16.1 kB
JavaScript
// src/utils/performance.js
// Performance monitoring and metrics collection system
import { logger } from './logger.js';
import fs from 'fs/promises';
import path from 'path';
import { isTestEnvironment } from './test-helpers.js';
/**
* Performance metrics collector
*/
export class PerformanceMetrics {
constructor() {
this.metrics = new Map();
this.timers = new Map();
this.counters = new Map();
this.gauges = new Map();
this.histograms = new Map();
this.startTime = process.hrtime.bigint();
}
/**
* Start timing an operation
*/
startTimer(name, labels = {}) {
// Skip timing in test environment to prevent interference
if (isTestEnvironment()) {
return;
}
const key = this.createKey(name, labels);
this.timers.set(key, {
start: process.hrtime.bigint(),
labels,
name
});
logger.debug(`Started timer: ${name}`, labels);
}
/**
* End timing an operation
*/
endTimer(name, labels = {}) {
// Skip timing in test environment to prevent interference
if (isTestEnvironment()) {
return 0;
}
const key = this.createKey(name, labels);
const timer = this.timers.get(key);
if (!timer) {
logger.warn(`Timer not found: ${name}`, labels);
return null;
}
const end = process.hrtime.bigint();
const duration = Number(end - timer.start) / 1000000; // Convert to milliseconds
this.timers.delete(key);
// Record in metrics
this.recordMetric('timer', name, duration, labels);
logger.debug(`Timer completed: ${name}`, { ...labels, duration });
return duration;
}
/**
* Increment a counter
*/
incrementCounter(name, value = 1, labels = {}) {
const key = this.createKey(name, labels);
const current = this.counters.get(key) || { value: 0, labels, name };
current.value += value;
this.counters.set(key, current);
logger.debug(`Counter incremented: ${name}`, { ...labels, value, total: current.value });
}
/**
* Set a gauge value
*/
setGauge(name, value, labels = {}) {
const key = this.createKey(name, labels);
this.gauges.set(key, { value, labels, name, timestamp: Date.now() });
logger.debug(`Gauge set: ${name}`, { ...labels, value });
}
/**
* Record a histogram value
*/
recordHistogram(name, value, labels = {}) {
const key = this.createKey(name, labels);
const histogram = this.histograms.get(key) || {
values: [],
labels,
name,
sum: 0,
count: 0,
min: Infinity,
max: -Infinity
};
histogram.values.push(value);
histogram.sum += value;
histogram.count++;
histogram.min = Math.min(histogram.min, value);
histogram.max = Math.max(histogram.max, value);
// Keep only last 1000 values to prevent memory issues
if (histogram.values.length > 1000) {
histogram.values = histogram.values.slice(-1000);
}
this.histograms.set(key, histogram);
logger.debug(`Histogram recorded: ${name}`, { ...labels, value });
}
/**
* Record a metric (internal method)
*/
recordMetric(type, name, value, labels = {}) {
const key = this.createKey(`${type}.${name}`, labels);
const existing = this.metrics.get(key) || {
type,
name,
labels,
values: [],
count: 0,
sum: 0
};
existing.values.push({
value,
timestamp: Date.now()
});
existing.count++;
existing.sum += value;
// Keep only last 1000 values
if (existing.values.length > 1000) {
existing.values = existing.values.slice(-1000);
}
this.metrics.set(key, existing);
}
/**
* Create a unique key for metrics
*/
createKey(name, labels) {
const labelStr = Object.entries(labels)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}=${v}`)
.join(',');
return labelStr ? `${name}{${labelStr}}` : name;
}
/**
* Get all metrics
*/
getAllMetrics() {
return {
timers: Object.fromEntries(this.timers),
counters: Object.fromEntries(this.counters),
gauges: Object.fromEntries(this.gauges),
histograms: Object.fromEntries(this.histograms),
metrics: Object.fromEntries(this.metrics),
uptime: this.getUptime()
};
}
/**
* Get uptime in milliseconds
*/
getUptime() {
const now = process.hrtime.bigint();
return Number(now - this.startTime) / 1000000;
}
/**
* Get summary statistics
*/
getSummary() {
const summary = {
uptime: this.getUptime(),
timers: {},
counters: {},
gauges: {},
histograms: {}
};
// Process counters
for (const [key, counter] of this.counters) {
summary.counters[key] = {
value: counter.value,
labels: counter.labels
};
}
// Process gauges
for (const [key, gauge] of this.gauges) {
summary.gauges[key] = {
value: gauge.value,
labels: gauge.labels,
timestamp: gauge.timestamp
};
}
// Process histograms
for (const [key, histogram] of this.histograms) {
summary.histograms[key] = {
count: histogram.count,
sum: histogram.sum,
min: histogram.min === Infinity ? null : histogram.min,
max: histogram.max === -Infinity ? null : histogram.max,
avg: histogram.count > 0 ? histogram.sum / histogram.count : null,
labels: histogram.labels
};
}
// Process metrics (timers)
for (const [key, metric] of this.metrics) {
if (metric.type === 'timer') {
const values = metric.values.map(v => v.value);
summary.timers[key] = {
count: metric.count,
sum: metric.sum,
min: Math.min(...values),
max: Math.max(...values),
avg: metric.count > 0 ? metric.sum / metric.count : null,
labels: metric.labels
};
}
}
return summary;
}
/**
* Clear all metrics
*/
clear() {
this.metrics.clear();
this.timers.clear();
this.counters.clear();
this.gauges.clear();
this.histograms.clear();
this.startTime = process.hrtime.bigint();
logger.info('Performance metrics cleared');
}
/**
* Export metrics to file
*/
async exportToFile(filePath, format = 'json') {
try {
const data = this.getAllMetrics();
let content;
if (format === 'json') {
content = JSON.stringify(data, null, 2);
} else if (format === 'csv') {
content = this.exportToCSV(data);
} else {
throw new Error(`Unsupported format: ${format}`);
}
await fs.writeFile(filePath, content);
logger.info(`Performance metrics exported to: ${filePath}`);
} catch (error) {
logger.error(`Failed to export metrics: ${error.message}`);
throw error;
}
}
/**
* Export metrics to CSV format
*/
exportToCSV(data) {
const lines = ['metric_type,name,value,labels,timestamp'];
// Export counters
for (const [key, counter] of Object.entries(data.counters)) {
const labels = Object.entries(counter.labels)
.map(([k, v]) => `${k}=${v}`)
.join(';');
lines.push(`counter,${key},${counter.value},"${labels}",${Date.now()}`);
}
// Export gauges
for (const [key, gauge] of Object.entries(data.gauges)) {
const labels = Object.entries(gauge.labels)
.map(([k, v]) => `${k}=${v}`)
.join(';');
lines.push(`gauge,${key},${gauge.value},"${labels}",${gauge.timestamp}`);
}
// Export histogram summaries
for (const [key, histogram] of Object.entries(data.histograms)) {
const labels = Object.entries(histogram.labels)
.map(([k, v]) => `${k}=${v}`)
.join(';');
lines.push(`histogram,${key}_count,${histogram.count},"${labels}",${Date.now()}`);
lines.push(`histogram,${key}_avg,${histogram.sum / histogram.count},"${labels}",${Date.now()}`);
lines.push(`histogram,${key}_min,${histogram.min},"${labels}",${Date.now()}`);
lines.push(`histogram,${key}_max,${histogram.max},"${labels}",${Date.now()}`);
}
return lines.join('\n');
}
}
/**
* Performance monitor for operations
*/
export class PerformanceMonitor {
constructor(metrics = null) {
this.metrics = metrics || new PerformanceMetrics();
this.activeOperations = new Map();
this.stopped = false;
}
/**
* Start monitoring an operation
*/
startOperation(name, labels = {}) {
const operationId = `${name}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
this.activeOperations.set(operationId, {
name,
labels,
startTime: process.hrtime.bigint()
});
this.metrics.startTimer(name, labels);
return operationId;
}
/**
* End monitoring an operation
*/
endOperation(operationId, success = true, error = null) {
const operation = this.activeOperations.get(operationId);
if (!operation) {
logger.warn(`Operation not found: ${operationId}`);
return null;
}
const endTime = process.hrtime.bigint();
const duration = Number(endTime - operation.startTime) / 1000000;
this.activeOperations.delete(operationId);
// Record metrics
this.metrics.endTimer(operation.name, operation.labels);
this.metrics.incrementCounter(`${operation.name}_total`, 1, operation.labels);
if (success) {
this.metrics.incrementCounter(`${operation.name}_success`, 1, operation.labels);
} else {
this.metrics.incrementCounter(`${operation.name}_errors`, 1, operation.labels);
}
this.metrics.recordHistogram(`${operation.name}_duration`, duration, operation.labels);
logger.debug(`Operation completed: ${operation.name}`, {
...operation.labels,
duration,
success,
error: error?.message
});
return duration;
}
/**
* Wrap a function with performance monitoring
*/
wrap(name, fn, labels = {}) {
return async (...args) => {
const operationId = this.startOperation(name, labels);
try {
const result = await fn(...args);
this.endOperation(operationId, true);
return result;
} catch (error) {
this.endOperation(operationId, false, error);
throw error;
}
};
}
/**
* Monitor memory usage
*/
monitorMemory() {
const memUsage = process.memoryUsage();
this.metrics.setGauge('memory_heap_used', memUsage.heapUsed, { unit: 'bytes' });
this.metrics.setGauge('memory_heap_total', memUsage.heapTotal, { unit: 'bytes' });
this.metrics.setGauge('memory_external', memUsage.external, { unit: 'bytes' });
this.metrics.setGauge('memory_rss', memUsage.rss, { unit: 'bytes' });
logger.debug('Memory usage recorded', memUsage);
}
/**
* Get performance summary
*/
getSummary() {
const summary = this.metrics.getSummary();
summary.activeOperations = this.activeOperations.size;
return summary;
}
/**
* Stop the performance monitor, marking any active operations as incomplete
* and preventing new ones from being started.
*/
stop() {
if (this.stopped) return;
// Mark any outstanding operations as aborted
for (const [opId] of this.activeOperations) {
try {
this.endOperation(opId, false, new Error('Operation aborted: monitor stopped'));
} catch (_) {
// ignore
}
}
this.activeOperations.clear();
this.stopped = true;
logger.debug('Performance monitor stopped');
}
}
/**
* File analysis performance tracker
*/
export class FileAnalysisTracker {
constructor(monitor) {
this.monitor = monitor;
this.fileStats = new Map();
}
/**
* Start tracking file analysis
*/
startFileAnalysis(filePath, fileType) {
const operationId = this.monitor.startOperation('file_analysis', {
file_type: fileType,
file_path: filePath
});
this.fileStats.set(operationId, {
filePath,
fileType,
startTime: Date.now(),
size: 0
});
return operationId;
}
/**
* Record file size
*/
recordFileSize(operationId, size) {
const stats = this.fileStats.get(operationId);
if (stats) {
stats.size = size;
}
}
/**
* End file analysis tracking
*/
endFileAnalysis(operationId, issues = []) {
const stats = this.fileStats.get(operationId);
if (!stats) {
logger.warn(`File analysis not found: ${operationId}`);
return null;
}
const duration = this.monitor.endOperation(operationId);
if (duration !== null) {
// Record additional metrics
this.monitor.metrics.incrementCounter('files_analyzed', 1, {
file_type: stats.fileType
});
this.monitor.metrics.recordHistogram('file_size', stats.size, {
file_type: stats.fileType
});
this.monitor.metrics.incrementCounter('issues_found', issues.length, {
file_type: stats.fileType,
severity: 'total'
});
issues.forEach(issue => {
this.monitor.metrics.incrementCounter('issues_found', 1, {
file_type: stats.fileType,
severity: issue.severity
});
});
}
this.fileStats.delete(operationId);
return duration;
}
}
/**
* Default performance monitor instance
*/
export const performanceMonitor = new PerformanceMonitor();
export const fileAnalysisTracker = new FileAnalysisTracker(performanceMonitor);
/**
* Performance decorators for automatic monitoring
*/
export function monitorPerformance(name, labels = {}) {
return function(target, propertyKey, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function(...args) {
const operationId = performanceMonitor.startOperation(name, labels);
try {
const result = await originalMethod.apply(this, args);
performanceMonitor.endOperation(operationId, true);
return result;
} catch (error) {
performanceMonitor.endOperation(operationId, false, error);
throw error;
}
};
return descriptor;
};
}
/**
* Memory usage monitoring
*/
export class MemoryMonitor {
constructor(interval = 30000) { // 30 seconds default
this.interval = interval;
this.intervalId = null;
this.baselineMemory = null;
}
start() {
// Skip memory monitoring in test environment
if (isTestEnvironment()) {
logger.debug('Memory monitoring disabled in test environment');
return;
}
this.baselineMemory = process.memoryUsage();
logger.info('Memory monitoring started', this.baselineMemory);
this.intervalId = setInterval(() => {
this.checkMemoryUsage();
}, this.interval);
}
stop() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
logger.info('Memory monitoring stopped');
}
}
checkMemoryUsage() {
const current = process.memoryUsage();
const baseline = this.baselineMemory;
const memoryDiff = {
heapUsed: current.heapUsed - baseline.heapUsed,
heapTotal: current.heapTotal - baseline.heapTotal,
external: current.external - baseline.external,
rss: current.rss - baseline.rss
};
// Log memory usage
performanceMonitor.monitorMemory();
// Check for memory leaks
if (memoryDiff.heapUsed > 100 * 1024 * 1024) { // 100MB increase
logger.warn('Potential memory leak detected', {
baseline,
current,
diff: memoryDiff
});
}
logger.debug('Memory usage check', {
current,
diff: memoryDiff
});
}
}
/**
* Default memory monitor
*/
export const memoryMonitor = new MemoryMonitor();
/**
* Export default instances
*/
export default {
metrics: performanceMonitor.metrics,
monitor: performanceMonitor,
fileTracker: fileAnalysisTracker,
memoryMonitor
};