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.
321 lines
14 KiB
JavaScript
321 lines
14 KiB
JavaScript
import { test, describe, before, after } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { Arbiter } from '../../src/index.js';
|
|
|
|
describe.skip('Simple Million Node Performance Benchmark', () => {
|
|
let arbiter;
|
|
let testArbiter;
|
|
let validPaths = [];
|
|
let startTime;
|
|
let endTime;
|
|
|
|
before(() => {
|
|
if (process.env.TEST_DEBUG === '1') console.log('🚀 Starting simple million-node benchmark setup...');
|
|
startTime = Date.now();
|
|
|
|
// Create arbiter with caching enabled for realistic performance
|
|
arbiter = new Arbiter({
|
|
fastConstructionMode: false,
|
|
disableCaching: false, // Keep caching enabled for realistic performance
|
|
disableChainCaching: false,
|
|
disableDirectCaching: false
|
|
});
|
|
|
|
// Create a simpler million-node graph directly
|
|
if (process.env.TEST_DEBUG === '1') console.log('📊 Creating million-node graph...');
|
|
const graphStartTime = Date.now();
|
|
|
|
// Add 1 million nodes (100K users + 900K documents)
|
|
const userCount = 100000;
|
|
const docCount = 900000;
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Adding ${userCount.toLocaleString()} users...`);
|
|
for (let i = 0; i < userCount; i++) {
|
|
arbiter.addNode(`user:${i}`, 'user');
|
|
}
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Adding ${docCount.toLocaleString()} documents...`);
|
|
for (let i = 0; i < docCount; i++) {
|
|
arbiter.addNode(`doc:${i}`, 'document');
|
|
}
|
|
|
|
const graphEndTime = Date.now();
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Node creation took: ${(graphEndTime - graphStartTime).toFixed(0)}ms`);
|
|
|
|
// Add 2 million relations (much simpler than complex chains)
|
|
if (process.env.TEST_DEBUG === '1') console.log('📊 Adding 2 million relations...');
|
|
const relationStartTime = Date.now();
|
|
|
|
const relationCount = 2000000;
|
|
for (let i = 0; i < relationCount; i++) {
|
|
const userId = Math.floor(Math.random() * userCount);
|
|
const docId = Math.floor(Math.random() * docCount);
|
|
const relationType = ['can_read', 'can_write', 'can_delete'][Math.floor(Math.random() * 3)];
|
|
|
|
arbiter.addRelation(`user:${userId}`, relationType, `doc:${docId}`, {
|
|
possibility: 0.8 + Math.random() * 0.2
|
|
});
|
|
}
|
|
|
|
const relationEndTime = Date.now();
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Relation creation took: ${(relationEndTime - relationStartTime).toFixed(0)}ms`);
|
|
|
|
// Configure simple chain rules
|
|
arbiter.setRelationConfig('can_read_via_role', {
|
|
type: 'chain',
|
|
steps: [
|
|
{ relation: 'member_of', direction: 'out' },
|
|
{ relation: 'can_read', direction: 'out' }
|
|
]
|
|
});
|
|
|
|
// Add some role-based relations for chain testing
|
|
if (process.env.TEST_DEBUG === '1') console.log('📊 Adding role-based relations for chain testing...');
|
|
const roleStartTime = Date.now();
|
|
|
|
// Add 10K role nodes
|
|
for (let i = 0; i < 10000; i++) {
|
|
arbiter.addNode(`role:${i}`, 'role');
|
|
}
|
|
|
|
// Add member_of relations (users to roles)
|
|
for (let i = 0; i < 50000; i++) {
|
|
const userId = Math.floor(Math.random() * userCount);
|
|
const roleId = Math.floor(Math.random() * 10000);
|
|
arbiter.addRelation(`user:${userId}`, 'member_of', `role:${roleId}`, {
|
|
possibility: 0.9
|
|
});
|
|
}
|
|
|
|
// Add can_read relations (roles to documents)
|
|
for (let i = 0; i < 50000; i++) {
|
|
const roleId = Math.floor(Math.random() * 10000);
|
|
const docId = Math.floor(Math.random() * docCount);
|
|
arbiter.addRelation(`role:${roleId}`, 'can_read', `doc:${docId}`, {
|
|
possibility: 0.8
|
|
});
|
|
}
|
|
|
|
const roleEndTime = Date.now();
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Role relations took: ${(roleEndTime - roleStartTime).toFixed(0)}ms`);
|
|
|
|
testArbiter = arbiter;
|
|
|
|
// Pre-find valid paths that actually exist in the graph
|
|
if (process.env.TEST_DEBUG === '1') console.log('🔍 Finding valid chain paths in million-node graph...');
|
|
const pathStartTime = Date.now();
|
|
validPaths = findValidChainPaths(testArbiter, userCount, docCount);
|
|
const pathEndTime = Date.now();
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Found ${validPaths.length} valid paths in ${(pathEndTime - pathStartTime).toFixed(0)}ms`);
|
|
|
|
endTime = Date.now();
|
|
if (process.env.TEST_DEBUG === '1') console.log(`✅ Setup completed in ${(endTime - startTime).toFixed(0)}ms`);
|
|
});
|
|
|
|
after(() => {
|
|
arbiter = null;
|
|
testArbiter = null;
|
|
validPaths = null;
|
|
});
|
|
|
|
/**
|
|
* Find valid chain paths that actually exist in the graph
|
|
*/
|
|
function findValidChainPaths(arbiter, userCount, docCount) {
|
|
const validPaths = [];
|
|
|
|
// Sample a reasonable number of combinations to test
|
|
const maxTests = Math.min(10000, userCount * docCount);
|
|
let tested = 0;
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Testing up to ${maxTests} user-document combinations...`);
|
|
|
|
for (let i = 0; i < Math.min(100, userCount); i++) {
|
|
if (tested >= maxTests) break;
|
|
|
|
for (let j = 0; j < Math.min(100, docCount); j++) {
|
|
if (tested >= maxTests) break;
|
|
|
|
// Test if this path actually exists
|
|
try {
|
|
const result = arbiter.check(`user:${i}`, 'can_read_via_role', `doc:${j}`);
|
|
if (result.possibility > 0) {
|
|
validPaths.push({ user: `user:${i}`, doc: `doc:${j}`, relation: 'can_read_via_role', result });
|
|
}
|
|
} catch (error) {
|
|
// Skip invalid paths
|
|
}
|
|
|
|
tested++;
|
|
|
|
// Progress indicator for large graphs
|
|
if (tested % 1000 === 0) {
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Tested ${tested}/${maxTests} combinations, found ${validPaths.length} valid paths`);
|
|
}
|
|
}
|
|
}
|
|
|
|
return validPaths;
|
|
}
|
|
|
|
/**
|
|
* Measure QPS for a given operation with detailed analysis
|
|
*/
|
|
function measureQPSWithAnalysis(operation, duration = 5000, operationName = 'Operation') {
|
|
const startTime = Date.now();
|
|
const endTime = startTime + duration;
|
|
let operationCount = 0;
|
|
const latencies = [];
|
|
let successCount = 0;
|
|
let failureCount = 0;
|
|
|
|
while (Date.now() < endTime) {
|
|
const opStart = process.hrtime.bigint();
|
|
|
|
try {
|
|
const result = operation();
|
|
operationCount++;
|
|
|
|
if (result && result.possibility > 0) {
|
|
successCount++;
|
|
} else {
|
|
failureCount++;
|
|
}
|
|
|
|
const opEnd = process.hrtime.bigint();
|
|
const latency = Number(opEnd - opStart) / 1000000; // Convert to milliseconds
|
|
latencies.push(latency);
|
|
} catch (error) {
|
|
operationCount++;
|
|
failureCount++;
|
|
const opEnd = process.hrtime.bigint();
|
|
const latency = Number(opEnd - opStart) / 1000000;
|
|
latencies.push(latency);
|
|
}
|
|
}
|
|
|
|
const actualDuration = Date.now() - startTime;
|
|
const qps = (operationCount / actualDuration) * 1000;
|
|
const avgLatency = latencies.reduce((a, b) => a + b, 0) / latencies.length;
|
|
|
|
// Sort once and reuse
|
|
const sortedLatencies = latencies.sort((a, b) => a - b);
|
|
const p95Latency = sortedLatencies[Math.floor(latencies.length * 0.95)];
|
|
const p99Latency = sortedLatencies[Math.floor(latencies.length * 0.99)];
|
|
|
|
return {
|
|
qps,
|
|
operationCount,
|
|
duration: actualDuration,
|
|
avgLatency,
|
|
p95Latency,
|
|
p99Latency,
|
|
maxLatency: sortedLatencies[sortedLatencies.length - 1],
|
|
minLatency: sortedLatencies[0],
|
|
successCount,
|
|
failureCount,
|
|
successRate: successCount / operationCount
|
|
};
|
|
}
|
|
|
|
test('Million Node Chain Query Performance', async () => {
|
|
if (process.env.TEST_DEBUG === '1') console.log('📊 Testing Million Node Chain Query QPS...');
|
|
|
|
if (validPaths.length === 0) {
|
|
if (process.env.TEST_DEBUG === '1') console.log(' ⚠️ No valid paths found - skipping test');
|
|
return;
|
|
}
|
|
|
|
let queryIndex = 0;
|
|
const result = measureQPSWithAnalysis(() => {
|
|
const path = validPaths[queryIndex % validPaths.length];
|
|
return testArbiter.check(path.user, path.relation, path.doc);
|
|
}, 5000, 'Million Node Chain Query');
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(` QPS: ${result.qps.toFixed(2)}`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Operations: ${result.operationCount}`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Duration: ${result.duration}ms`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Avg Latency: ${result.avgLatency.toFixed(3)}ms`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` P95 Latency: ${result.p95Latency.toFixed(3)}ms`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` P99 Latency: ${result.p99Latency.toFixed(3)}ms`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Max Latency: ${result.maxLatency.toFixed(3)}ms`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Min Latency: ${result.minLatency.toFixed(3)}ms`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Success Rate: ${(result.successRate * 100).toFixed(1)}%`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Success Count: ${result.successCount}`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Failure Count: ${result.failureCount}`);
|
|
|
|
// More lenient expectations for million-node graph
|
|
assert.ok(result.qps > 100, `Million node chain query QPS ${result.qps.toFixed(0)} below 100 threshold`);
|
|
assert.ok(result.avgLatency < 500, `Million node chain query latency ${result.avgLatency.toFixed(3)}ms too high`);
|
|
assert.ok(result.successRate > 0.1, `Success rate ${(result.successRate * 100).toFixed(1)}% too low for valid paths`);
|
|
});
|
|
|
|
test('Million Node Direct Query Performance', async () => {
|
|
if (process.env.TEST_DEBUG === '1') console.log('📊 Testing Million Node Direct Query QPS...');
|
|
|
|
// Test direct queries (should be much faster)
|
|
let queryIndex = 0;
|
|
const result = measureQPSWithAnalysis(() => {
|
|
const userId = queryIndex % 100000;
|
|
const docId = (queryIndex + 1000) % 900000;
|
|
const relationType = ['can_read', 'can_write', 'can_delete'][queryIndex % 3];
|
|
return testArbiter.check(`user:${userId}`, relationType, `doc:${docId}`);
|
|
}, 5000, 'Million Node Direct Query');
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(` QPS: ${result.qps.toFixed(2)}`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Operations: ${result.operationCount}`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Duration: ${result.duration}ms`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Avg Latency: ${result.avgLatency.toFixed(3)}ms`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` P95 Latency: ${result.p95Latency.toFixed(3)}ms`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` P99 Latency: ${result.p99Latency.toFixed(3)}ms`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Max Latency: ${result.maxLatency.toFixed(3)}ms`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Min Latency: ${result.minLatency.toFixed(3)}ms`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Success Rate: ${(result.successRate * 100).toFixed(1)}%`);
|
|
|
|
// Direct queries should be much faster than chain queries
|
|
assert.ok(result.qps > 1000, `Million node direct query QPS ${result.qps.toFixed(0)} below 1000 threshold`);
|
|
assert.ok(result.avgLatency < 50, `Million node direct query latency ${result.avgLatency.toFixed(3)}ms too high`);
|
|
});
|
|
|
|
test('Memory Usage Analysis', async () => {
|
|
if (process.env.TEST_DEBUG === '1') console.log('📊 Memory Usage Analysis...');
|
|
|
|
const memUsage = process.memoryUsage();
|
|
const memUsageMB = {
|
|
rss: (memUsage.rss / 1024 / 1024).toFixed(2),
|
|
heapTotal: (memUsage.heapTotal / 1024 / 1024).toFixed(2),
|
|
heapUsed: (memUsage.heapUsed / 1024 / 1024).toFixed(2),
|
|
external: (memUsage.external / 1024 / 1024).toFixed(2)
|
|
};
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(` RSS Memory: ${memUsageMB.rss} MB`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Heap Total: ${memUsageMB.heapTotal} MB`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Heap Used: ${memUsageMB.heapUsed} MB`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` External: ${memUsageMB.external} MB`);
|
|
|
|
// Check if we're within reasonable memory limits
|
|
const heapUsedMB = parseFloat(memUsageMB.heapUsed);
|
|
assert.ok(heapUsedMB < 16384, `Heap usage ${heapUsedMB}MB too high for million-node graph`);
|
|
});
|
|
|
|
test('Graph Statistics', async () => {
|
|
if (process.env.TEST_DEBUG === '1') console.log('📊 Million Node Graph Statistics:');
|
|
|
|
// Get stats from the arbiter
|
|
const stats = testArbiter.getStats();
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Total Nodes: ${stats.totalNodes.toLocaleString()}`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Total Relations: ${stats.totalRelations.toLocaleString()}`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Users: 100,000`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Documents: 900,000`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Roles: 10,000`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Valid Chain Paths Found: ${validPaths.length.toLocaleString()}`);
|
|
|
|
// Verify we have a million-node graph
|
|
const totalNodes = stats.totalNodes;
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Total Nodes: ${totalNodes.toLocaleString()}`);
|
|
|
|
assert.ok(totalNodes >= 1000000, `Graph too small: ${totalNodes.toLocaleString()} nodes (expected 1M+)`);
|
|
assert.ok(stats.totalRelations >= 2000000, `Graph too small: ${stats.totalRelations.toLocaleString()} relations (expected 2M+)`);
|
|
});
|
|
});
|