717ae1031e
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.
507 lines
14 KiB
JavaScript
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
|
|
};
|
|
}
|
|
}
|