@ospm/eslint-plugin-react-signals-hooks
Version:
ESLint plugin for React Signals hooks - enforces best practices, performance optimizations, and integration patterns for @preact/signals-react usage in React projects
183 lines • 7.54 kB
JavaScript
class MetricsAggregator {
metrics = [];
static instance;
constructor() { }
static getInstance() {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/strict-boolean-expressions
if (!MetricsAggregator.instance) {
MetricsAggregator.instance = new MetricsAggregator();
}
return MetricsAggregator.instance;
}
addMetrics(metrics) {
this.metrics.push(metrics);
}
getAggregatedMetrics() {
if (this.metrics.length === 0) {
return this.getEmptyAggregatedMetrics();
}
const aggregated = {
totalFiles: this.metrics.length,
totalNodes: 0,
totalDuration: 0,
operationCounts: {},
avgDuration: 0,
maxDuration: 0,
minDuration: Infinity,
avgMemoryDelta: 0,
maxMemoryDelta: 0,
minMemoryDelta: Infinity,
filesExceededBudget: 0,
commonExceededOperations: [],
performanceByFileSize: [],
};
// Group metrics by file size ranges
const sizeGroups = new Map();
const sizeRanges = [
{ name: '0-100', min: 0, max: 100 },
{ name: '101-500', min: 101, max: 500 },
{ name: '501-1000', min: 501, max: 1000 },
{ name: '1001-5000', min: 1001, max: 5000 },
{ name: '5001+', min: 5001, max: Infinity },
];
// Initialize size groups
sizeRanges.forEach((range) => {
sizeGroups.set(range.name, { count: 0, totalDuration: 0 });
});
// Process each metric
const operationCounts = new Map();
const exceededOperations = new Map();
this.metrics.forEach((metric) => {
// Basic metrics
aggregated.totalNodes += metric.nodeCount || 0;
const duration = metric.duration ?? 0;
aggregated.totalDuration += duration;
aggregated.maxDuration = Math.max(aggregated.maxDuration, duration);
aggregated.minDuration = Math.min(aggregated.minDuration, duration);
// Memory metrics
const memoryDelta = metric.memoryDelta ?? 0;
aggregated.maxMemoryDelta = Math.max(aggregated.maxMemoryDelta, memoryDelta);
aggregated.minMemoryDelta = Math.min(aggregated.minMemoryDelta, memoryDelta);
// Budget exceedance
if (metric.exceededBudget === true) {
aggregated.filesExceededBudget++;
}
// Operation counts
Object.entries(metric.operationCounts).forEach(([op, count]) => {
const current = operationCounts.get(op) ?? 0;
operationCounts.set(op, current + count);
// Track operations that caused budget exceedance
if (metric.exceededBudget === true &&
count >
(metric.perfBudget?.maxOperations?.[op] ?? 0)) {
const currentExceeded = exceededOperations.get(op) ?? 0;
exceededOperations.set(op, currentExceeded + 1);
}
});
// Group by file size (node count)
const nodeCount = metric.nodeCount || 0;
for (const range of sizeRanges) {
if (nodeCount >= range.min && nodeCount <= range.max) {
const group = sizeGroups.get(range.name);
if (typeof group !== 'undefined') {
group.count++;
group.totalDuration += duration;
}
break;
}
}
});
// Calculate averages
aggregated.avgDuration = aggregated.totalDuration / aggregated.totalFiles;
aggregated.avgMemoryDelta =
this.metrics.reduce((sum, m) => sum + (m.memoryDelta ?? 0), 0) / aggregated.totalFiles;
// Convert operation counts to object
operationCounts.forEach((count, op) => {
// eslint-disable-next-line security/detect-object-injection
aggregated.operationCounts[op] = count;
});
// Sort and get top 5 most common exceeded operations
aggregated.commonExceededOperations = Array.from(exceededOperations.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([operation, count]) => ({ operation, count }));
// Calculate performance by file size
aggregated.performanceByFileSize = sizeRanges
.map((range) => {
const group = sizeGroups.get(range.name);
if (typeof group === 'undefined') {
return null;
}
return {
sizeRange: range.name,
...group,
};
})
.filter((group) => {
return group !== null && typeof group.count !== 'undefined' && group.count > 0;
})
.map((group) => {
return {
sizeRange: group.sizeRange,
count: group.count,
avgDuration: group.totalDuration / group.count,
};
});
return aggregated;
}
clear() {
this.metrics = [];
}
getEmptyAggregatedMetrics() {
return {
totalFiles: 0,
totalNodes: 0,
totalDuration: 0,
operationCounts: {},
avgDuration: 0,
maxDuration: 0,
minDuration: 0,
avgMemoryDelta: 0,
maxMemoryDelta: 0,
minMemoryDelta: 0,
filesExceededBudget: 0,
commonExceededOperations: [],
performanceByFileSize: [],
};
}
}
export const metricsAggregator = MetricsAggregator.getInstance();
export function logAggregatedMetrics() {
const aggregated = metricsAggregator.getAggregatedMetrics();
console.info('\n=== ESLint Performance Metrics ===');
console.info(`Total files analyzed: ${aggregated.totalFiles}`);
console.info(`Total nodes processed: ${aggregated.totalNodes}`);
console.info(`Average duration: ${aggregated.avgDuration.toFixed(2)}ms`);
console.info(`Max duration: ${aggregated.maxDuration.toFixed(2)}ms`);
console.info(`Min duration: ${aggregated.minDuration.toFixed(2)}ms`);
if (aggregated.filesExceededBudget > 0) {
console.info(`\n⚠️ ${aggregated.filesExceededBudget} files exceeded performance budget`);
if (aggregated.commonExceededOperations.length > 0) {
console.info('Most common exceeded operations:');
aggregated.commonExceededOperations.forEach(({ operation, count }) => {
console.info(` - ${operation}: ${count} files`);
});
}
}
if (aggregated.performanceByFileSize.length > 0) {
console.info('\nPerformance by file size (nodes):');
console.table(aggregated.performanceByFileSize);
}
if (Object.keys(aggregated.operationCounts).length > 0) {
console.info('\nOperation counts:');
console.table(Object.entries(aggregated.operationCounts)
.sort((a, b) => b[1] - a[1])
.map(([operation, count]) => ({
Operation: operation,
Count: count,
'Avg per file': (count / aggregated.totalFiles).toFixed(2),
})));
}
console.info('==================================\n');
}
//# sourceMappingURL=metrics-aggregator.js.map