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.
This commit is contained in:
@@ -0,0 +1,506 @@
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user