Files
core/tests/rules/million-node-benchmark.test.js
T
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

298 lines
13 KiB
JavaScript

import { test, describe, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { Arbiter } from '../../src/index.js';
import { BigGraphGenerator } from '../helpers/big-graph-generator.js';
describe.skip('Million Node Performance Benchmark', () => {
let arbiter;
let generator;
let testArbiter;
let graphData;
let validPaths = [];
let startTime;
let endTime;
before(() => {
if (process.env.TEST_DEBUG === '1') console.log('🚀 Starting 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
});
// Use million-node scale
generator = new BigGraphGenerator({ scale: 'million', seed: 12345 });
if (process.env.TEST_DEBUG === '1') console.log('📊 Generating million-node graph...');
const graphStartTime = Date.now();
graphData = generator.generateGraph('enterprise');
const graphEndTime = Date.now();
if (process.env.TEST_DEBUG === '1') console.log(` Graph generation took: ${(graphEndTime - graphStartTime).toFixed(0)}ms`);
if (process.env.TEST_DEBUG === '1') console.log('🔄 Loading graph into arbiter...');
const loadStartTime = Date.now();
testArbiter = generator.loadIntoArbiter(graphData);
const loadEndTime = Date.now();
if (process.env.TEST_DEBUG === '1') console.log(` Graph loading took: ${(loadEndTime - loadStartTime).toFixed(0)}ms`);
// Configure chain rules
testArbiter.setRelationConfig('can_read_via_role', {
type: 'chain',
steps: [
{ relation: 'member_of', direction: 'out' },
{ relation: 'can_read', direction: 'out' }
]
});
testArbiter.setRelationConfig('can_access_multi_hop', {
type: 'chain',
steps: [
{ relation: 'member_of', direction: 'out' },
{ relation: 'member_of', direction: 'out' },
{ relation: 'member_of', direction: 'out' },
{ relation: 'can_read', direction: 'out' }
]
});
// 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, graphData);
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;
generator = null;
testArbiter = null;
graphData = null;
validPaths = null;
});
/**
* Find valid chain paths that actually exist in the graph
*/
function findValidChainPaths(arbiter, graphData) {
const validPaths = [];
// Get all users and documents
const users = graphData.users.map(u => u.id);
const docs = graphData.documents.map(d => d.id);
// Sample a reasonable number of combinations to test (don't test all combinations!)
const maxTests = Math.min(10000, users.length * docs.length);
let tested = 0;
if (process.env.TEST_DEBUG === '1') console.log(` Testing up to ${maxTests} user-document combinations...`);
for (const user of users) {
if (tested >= maxTests) break;
for (const doc of docs) {
if (tested >= maxTests) break;
// Test if this path actually exists
try {
const result = arbiter.check(user, 'can_read_via_role', doc);
if (result.possibility > 0) {
validPaths.push({ user, doc, relation: 'can_read_via_role', result });
}
} catch (error) {
// Skip invalid paths
}
tested++;
// Progress indicator for large graphs
if (tested % 10000 === 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 > 1000, `Million node chain query QPS ${result.qps.toFixed(0)} below 1000 threshold`);
assert.ok(result.avgLatency < 100, `Million node chain query latency ${result.avgLatency.toFixed(3)}ms too high`);
assert.ok(result.successRate > 0.5, `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)
const directRelations = graphData.relations.filter(r =>
r.src.startsWith('user:') && r.dst.startsWith('doc:') &&
['can_read', 'can_write', 'can_delete'].includes(r.relation)
).slice(0, 1000); // Use 1000 direct relations
if (directRelations.length === 0) {
if (process.env.TEST_DEBUG === '1') console.log(' ⚠️ No direct relations found - skipping test');
return;
}
let queryIndex = 0;
const result = measureQPSWithAnalysis(() => {
const relation = directRelations[queryIndex % directRelations.length];
return testArbiter.check(relation.src, relation.relation, relation.dst);
}, 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 > 10000, `Million node direct query QPS ${result.qps.toFixed(0)} below 10000 threshold`);
assert.ok(result.avgLatency < 10, `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:');
if (process.env.TEST_DEBUG === '1') console.log(` Total Relations: ${graphData.relations.length.toLocaleString()}`);
if (process.env.TEST_DEBUG === '1') console.log(` Total Users: ${graphData.users.length.toLocaleString()}`);
if (process.env.TEST_DEBUG === '1') console.log(` Total Documents: ${graphData.documents.length.toLocaleString()}`);
const userCount = graphData.users.length;
const docCount = graphData.documents.length;
const groupCount = graphData.enterprises ? graphData.enterprises.length : 0;
if (process.env.TEST_DEBUG === '1') console.log(` Users: ${userCount.toLocaleString()}`);
if (process.env.TEST_DEBUG === '1') console.log(` Documents: ${docCount.toLocaleString()}`);
if (process.env.TEST_DEBUG === '1') console.log(` Groups: ${groupCount.toLocaleString()}`);
const relationTypes = [...new Set(graphData.relations.map(r => r.relation))];
if (process.env.TEST_DEBUG === '1') console.log(` Relation Types: ${relationTypes.length} (${relationTypes.join(', ')})`);
if (process.env.TEST_DEBUG === '1') console.log(` Valid Chain Paths Found: ${validPaths.length.toLocaleString()}`);
// Verify we have a million-node graph
const totalNodes = userCount + docCount + groupCount;
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(graphData.relations.length >= 1000000, `Graph too small: ${graphData.relations.length.toLocaleString()} relations (expected 1M+)`);
});
});