Files
core/tests/helpers/performance-metrics.js
John Dvorak 717ae1031e initial commit: @arbiter/core authorization engine with js-rigor hardening
Zanzibar-style authorization graph engine (direct/chain/TTU/defeasible/
binary modes, condensed snapshots, value relations) with 39 rigor test
campaigns. Includes fixes for snapshot binary writer/reader format
mismatch (snapshot-of-snapshot corruption), possibility write-boundary
validation, empty-graph snapshot serialization, relation lookup cache
direction collision, config-redefinition cache invalidation, binary
threshold semantics, defeasible compiled routing, and comparator
reason whitelisting.
2026-07-31 13:44:06 -07:00

507 lines
14 KiB
JavaScript

/**
* Performance Metrics Collection and Analysis
*
* Comprehensive performance metrics collection for zanzibar-graph
* performance testing with statistical analysis and reporting.
*/
export class PerformanceMetrics {
constructor() {
this.metrics = new Map();
this.startTime = Date.now();
this.testCount = 0;
}
/**
* Record a performance metric
*/
record(testName, data) {
if (!this.metrics.has(testName)) {
this.metrics.set(testName, []);
}
const metric = {
timestamp: Date.now(),
testName,
data,
testId: ++this.testCount
};
this.metrics.get(testName).push(metric);
}
/**
* Get metrics for a specific test
*/
getMetrics(testName) {
return this.metrics.get(testName) || [];
}
/**
* Get all metrics
*/
getAllMetrics() {
const allMetrics = {};
for (const [testName, metrics] of this.metrics) {
allMetrics[testName] = metrics;
}
return allMetrics;
}
/**
* Calculate statistical summary for a test
*/
calculateSummary(testName) {
const metrics = this.getMetrics(testName);
if (metrics.length === 0) return null;
const values = metrics.map(m => m.data);
// Extract numeric values for statistical analysis
const numericValues = this._extractNumericValues(values);
if (numericValues.length === 0) return null;
return {
count: numericValues.length,
min: Math.min(...numericValues),
max: Math.max(...numericValues),
mean: this._calculateMean(numericValues),
median: this._calculateMedian(numericValues),
p95: this._calculatePercentile(numericValues, 95),
p99: this._calculatePercentile(numericValues, 99),
stdDev: this._calculateStdDev(numericValues),
variance: this._calculateVariance(numericValues)
};
}
/**
* Generate comprehensive performance report
*/
generateReport() {
const report = {
summary: this._generateSummary(),
testResults: {},
performanceTargets: this._getPerformanceTargets(),
recommendations: this._generateRecommendations(),
generatedAt: new Date().toISOString(),
duration: Date.now() - this.startTime
};
// Generate test-specific results
for (const [testName, metrics] of this.metrics) {
report.testResults[testName] = {
summary: this.calculateSummary(testName),
rawData: metrics,
analysis: this._analyzeTest(testName, metrics)
};
}
return report;
}
/**
* Generate overall summary
*/
_generateSummary() {
const allMetrics = this.getAllMetrics();
const totalTests = Object.keys(allMetrics).length;
// Calculate overall performance metrics
let totalLatency = 0;
let totalMemory = 0;
let totalQueries = 0;
let testCount = 0;
for (const [testName, metrics] of Object.entries(allMetrics)) {
for (const metric of metrics) {
if (metric.data.avgLatency) {
totalLatency += metric.data.avgLatency;
testCount++;
}
if (metric.data.memoryUsed) {
totalMemory += metric.data.memoryUsed;
}
if (metric.data.queryCount) {
totalQueries += metric.data.queryCount;
}
}
}
return {
totalTests,
avgLatency: testCount > 0 ? totalLatency / testCount : 0,
totalMemoryUsage: totalMemory,
totalQueries,
testCount,
duration: Date.now() - this.startTime
};
}
/**
* Get performance targets
*/
_getPerformanceTargets() {
return {
latency: {
avg: 100, // ms
p95: 200, // ms
p99: 500 // ms
},
memory: {
small: 512, // MB
medium: 1024, // MB
large: 2048, // MB
enterprise: 4096 // MB
},
throughput: {
qps: 1000, // queries per second
tps: 500 // transactions per second
},
cache: {
hitRate: 0.8, // 80%
evictionRate: 0.1 // 10%
}
};
}
/**
* Generate performance recommendations
*/
_generateRecommendations() {
const recommendations = [];
const summary = this._generateSummary();
const targets = this._getPerformanceTargets();
// Latency recommendations
if (summary.avgLatency > targets.latency.avg) {
recommendations.push({
type: 'latency',
severity: 'high',
message: `Average latency ${summary.avgLatency}ms exceeds target ${targets.latency.avg}ms`,
suggestion: 'Consider optimizing authorization logic or increasing cache size'
});
}
// Memory recommendations
if (summary.totalMemoryUsage > targets.memory.medium) {
recommendations.push({
type: 'memory',
severity: 'medium',
message: `Memory usage ${summary.totalMemoryUsage}MB exceeds target ${targets.memory.medium}MB`,
suggestion: 'Consider implementing memory optimization or increasing heap size'
});
}
// Throughput recommendations
if (summary.totalQueries > 0) {
const avgQPS = (summary.totalQueries / summary.duration) * 1000;
if (avgQPS < targets.throughput.qps * 0.8) {
recommendations.push({
type: 'throughput',
severity: 'medium',
message: `Average QPS ${avgQPS} below target ${targets.throughput.qps}`,
suggestion: 'Consider optimizing query performance or increasing concurrency'
});
}
}
return recommendations;
}
/**
* Analyze specific test results
*/
_analyzeTest(testName, metrics) {
const analysis = {
performance: 'good',
issues: [],
suggestions: []
};
// Analyze based on test type
switch (testName) {
case 'graph_loading':
this._analyzeGraphLoading(metrics, analysis);
break;
case 'authorization_qps':
this._analyzeAuthorizationQPS(metrics, analysis);
break;
case 'memory_leak_test':
this._analyzeMemoryLeak(metrics, analysis);
break;
case 'cache_performance':
this._analyzeCachePerformance(metrics, analysis);
break;
default:
this._analyzeGeneric(metrics, analysis);
}
return analysis;
}
/**
* Analyze graph loading performance
*/
_analyzeGraphLoading(metrics, analysis) {
for (const metric of metrics) {
const data = metric.data;
if (data.loadTime > 30000) {
analysis.issues.push('Graph loading time exceeds 30s limit');
analysis.suggestions.push('Consider optimizing graph construction or using lazy loading');
analysis.performance = 'poor';
}
if (data.memoryUsed > 1024) {
analysis.issues.push('Memory usage exceeds 1GB limit');
analysis.suggestions.push('Consider implementing memory optimization or reducing graph size');
analysis.performance = 'poor';
}
}
}
/**
* Analyze authorization QPS performance
*/
_analyzeAuthorizationQPS(metrics, analysis) {
for (const metric of metrics) {
const data = metric.data;
if (data.actualQPS < data.targetQPS * 0.8) {
analysis.issues.push(`QPS ${data.actualQPS} below 80% of target ${data.targetQPS}`);
analysis.suggestions.push('Consider optimizing authorization logic or increasing concurrency');
analysis.performance = 'poor';
}
if (data.avgLatency > 100) {
analysis.issues.push(`Average latency ${data.avgLatency}ms exceeds 100ms limit`);
analysis.suggestions.push('Consider optimizing query performance or increasing cache size');
analysis.performance = 'poor';
}
if (data.p95Latency > 200) {
analysis.issues.push(`P95 latency ${data.p95Latency}ms exceeds 200ms limit`);
analysis.suggestions.push('Consider optimizing worst-case performance or reducing query complexity');
analysis.performance = 'poor';
}
}
}
/**
* Analyze memory leak test results
*/
_analyzeMemoryLeak(metrics, analysis) {
for (const metric of metrics) {
const data = metric.data;
if (data.totalGrowth > 200) {
analysis.issues.push(`Memory growth ${data.totalGrowth}MB exceeds 200MB limit`);
analysis.suggestions.push('Investigate potential memory leaks in authorization logic');
analysis.performance = 'poor';
}
if (data.totalGrowth > 100) {
analysis.issues.push(`Memory growth ${data.totalGrowth}MB exceeds 100MB limit`);
analysis.suggestions.push('Monitor memory usage and consider implementing garbage collection');
analysis.performance = 'fair';
}
}
}
/**
* Analyze cache performance
*/
_analyzeCachePerformance(metrics, analysis) {
for (const metric of metrics) {
const data = metric.data;
if (data.speedup < 1.5) {
analysis.issues.push(`Cache speedup ${data.speedup}x below 1.5x threshold`);
analysis.suggestions.push('Consider optimizing cache implementation or increasing cache size');
analysis.performance = 'poor';
}
if (data.speedup < 2.0) {
analysis.issues.push(`Cache speedup ${data.speedup}x below 2.0x threshold`);
analysis.suggestions.push('Consider optimizing cache hit rate or cache eviction strategy');
analysis.performance = 'fair';
}
}
}
/**
* Generic analysis for unknown test types
*/
_analyzeGeneric(metrics, analysis) {
const summary = this.calculateSummary(metrics[0]?.testName);
if (!summary) return;
if (summary.mean > 1000) {
analysis.issues.push(`Average performance ${summary.mean}ms exceeds 1000ms threshold`);
analysis.suggestions.push('Consider optimizing performance or reducing complexity');
analysis.performance = 'poor';
}
}
/**
* Extract numeric values from metric data
*/
_extractNumericValues(values) {
const numericValues = [];
for (const value of values) {
if (typeof value === 'number') {
numericValues.push(value);
} else if (typeof value === 'object' && value !== null) {
// Extract numeric values from objects
for (const [key, val] of Object.entries(value)) {
if (typeof val === 'number') {
numericValues.push(val);
}
}
}
}
return numericValues;
}
/**
* Calculate mean
*/
_calculateMean(values) {
return values.reduce((sum, val) => sum + val, 0) / values.length;
}
/**
* Calculate median
*/
_calculateMedian(values) {
const sorted = [...values].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2 === 0
? (sorted[mid - 1] + sorted[mid]) / 2
: sorted[mid];
}
/**
* Calculate percentile
*/
_calculatePercentile(values, percentile) {
const sorted = [...values].sort((a, b) => a - b);
const index = Math.ceil((percentile / 100) * sorted.length) - 1;
return sorted[Math.max(0, index)];
}
/**
* Calculate standard deviation
*/
_calculateStdDev(values) {
const mean = this._calculateMean(values);
const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length;
return Math.sqrt(variance);
}
/**
* Calculate variance
*/
_calculateVariance(values) {
const mean = this._calculateMean(values);
return values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length;
}
/**
* Export metrics to JSON
*/
exportToJSON() {
return JSON.stringify(this.generateReport(), null, 2);
}
/**
* Export metrics to CSV
*/
exportToCSV() {
const csv = [];
csv.push('TestName,Timestamp,TestId,Data');
for (const [testName, metrics] of this.metrics) {
for (const metric of metrics) {
csv.push(`${testName},${metric.timestamp},${metric.testId},"${JSON.stringify(metric.data)}"`);
}
}
return csv.join('\n');
}
/**
* Clear all metrics
*/
clear() {
this.metrics.clear();
this.startTime = Date.now();
this.testCount = 0;
}
/**
* Get metrics for a specific time range
*/
getMetricsInRange(startTime, endTime) {
const filteredMetrics = new Map();
for (const [testName, metrics] of this.metrics) {
const filtered = metrics.filter(m =>
m.timestamp >= startTime && m.timestamp <= endTime
);
if (filtered.length > 0) {
filteredMetrics.set(testName, filtered);
}
}
return filteredMetrics;
}
/**
* Get performance trends over time
*/
getPerformanceTrends(testName, windowSize = 1000) {
const metrics = this.getMetrics(testName);
if (metrics.length === 0) return null;
const trends = [];
const window = Math.min(windowSize, metrics.length);
for (let i = window; i <= metrics.length; i++) {
const windowMetrics = metrics.slice(i - window, i);
const trend = this._calculateTrend(windowMetrics);
trends.push({
timestamp: metrics[i - 1].timestamp,
trend: trend
});
}
return trends;
}
/**
* Calculate trend for a window of metrics
*/
_calculateTrend(metrics) {
if (metrics.length < 2) return null;
const values = this._extractNumericValues(metrics.map(m => m.data));
if (values.length < 2) return null;
const firstHalf = values.slice(0, Math.floor(values.length / 2));
const secondHalf = values.slice(Math.floor(values.length / 2));
const firstMean = this._calculateMean(firstHalf);
const secondMean = this._calculateMean(secondHalf);
return {
direction: secondMean > firstMean ? 'increasing' : 'decreasing',
change: secondMean - firstMean,
changePercent: ((secondMean - firstMean) / firstMean) * 100
};
}
}