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+)`); }); });