/** * Test performance of different reachability indexing strategies */ import { test as _test } from 'node:test'; const test = process.env.RUN_PERF_TESTS === '1' ? _test : _test.skip; import assert from 'node:assert/strict'; import { BigGraphGenerator } from '../helpers/big-graph-generator.js'; import { ChainRule } from '../../src/authorization/rules/ChainRule.js'; test('compares reachability strategy performance', async () => { if (process.env.TEST_DEBUG === '1') console.log('🔍 Comparing reachability strategy performance...'); const generator = new BigGraphGenerator(); const graphData = generator.generateGraph('enterprise'); // Test different strategies const strategies = ['auto', 'twohop', 'treecover', 'hybrid']; const results = {}; for (const strategy of strategies) { if (process.env.TEST_DEBUG === '1') console.log(` Testing strategy: ${strategy}`); // Create new arbiter for each strategy const arbiter = generator.loadIntoArbiter(graphData); // Initialize reachability checker with specific strategy await arbiter.reachabilityChecker.initialize({ strategy: strategy, twoHopOptions: { maxNodes: 1000 }, treeCoverOptions: { maxTrees: 3 } }); const rule = new ChainRule(arbiter); // Create a chain rule const chainRule = { type: 'chain', steps: [ { relation: 'member_of', direction: 'out' }, { relation: 'can_read', direction: 'out' } ], collectValues: false, valueAggregation: 'sum' }; // Find actual chain paths const actualChains = []; const roleMemberships = graphData.relations.filter(r => r.relation === 'member_of' && r.dst.startsWith('role:') ); for (const membership of roleMemberships) { const userId = membership.src; const roleId = membership.dst; const roleReadPermissions = graphData.relations.filter(r => r.relation === 'can_read' && r.src === roleId ); for (const permission of roleReadPermissions) { actualChains.push({ user: userId, object: permission.dst }); } } if (process.env.TEST_DEBUG === '1') console.log(` Found ${actualChains.length} chain paths`); // Test performance const start = Date.now(); let queryCount = 0; let positiveResults = 0; const end = start + 2000; // 2 seconds while (Date.now() < end) { for (const chain of actualChains) { const result = rule.evaluate( arbiter.nodeIdByKey.get(chain.user), chain.user, arbiter.nodeIdByKey.get(chain.object), chain.object, chainRule, new Set(), 'can_read_via_role', {} ); if (result.possibility > 0) { positiveResults++; } queryCount++; } } const duration = Date.now() - start; const qps = (queryCount / duration) * 1000; const positiveRate = (positiveResults / queryCount) * 100; results[strategy] = { qps: qps, queryCount: queryCount, positiveRate: positiveRate, duration: duration, cacheSize: rule.chainResultCache.size }; if (process.env.TEST_DEBUG === '1') console.log(` ${strategy}: ${qps.toFixed(2)} QPS (${queryCount} queries, ${positiveRate.toFixed(1)}% positive)`); if (process.env.TEST_DEBUG === '1') console.log(` Cache size: ${rule.chainResultCache.size} results`); // Get reachability stats const reachabilityStats = arbiter.reachabilityChecker.getStats(); if (process.env.TEST_DEBUG === '1') console.log(` Reachability stats:`, reachabilityStats); } // Compare results if (process.env.TEST_DEBUG === '1') console.log(' 📊 Strategy comparison:'); const sortedResults = Object.entries(results).sort((a, b) => b[1].qps - a[1].qps); sortedResults.forEach(([strategy, result], index) => { const rank = index + 1; const improvement = index === 0 ? '🏆' : `-${((sortedResults[0][1].qps - result.qps) / sortedResults[0][1].qps * 100).toFixed(1)}%`; if (process.env.TEST_DEBUG === '1') console.log(` ${rank}. ${strategy}: ${result.qps.toFixed(2)} QPS ${improvement}`); }); // Verify that we have results assert.ok(Object.keys(results).length > 0, 'Should have results for all strategies'); if (process.env.TEST_DEBUG === '1') console.log(' ✅ Reachability strategy comparison completed'); }); test('measures strategy performance with different graph sizes', async () => { if (process.env.TEST_DEBUG === '1') console.log('📈 Testing strategy performance with different graph sizes...'); const graphSizes = ['small', 'medium', 'enterprise']; const strategies = ['auto', 'twohop', 'treecover', 'hybrid']; const results = {}; for (const size of graphSizes) { if (process.env.TEST_DEBUG === '1') console.log(` Testing ${size} graph...`); results[size] = {}; for (const strategy of strategies) { if (process.env.TEST_DEBUG === '1') console.log(` Strategy: ${strategy}`); const generator = new BigGraphGenerator(); const graphData = generator.generateGraph(size); const arbiter = generator.loadIntoArbiter(graphData); // Initialize reachability checker await arbiter.reachabilityChecker.initialize({ strategy: strategy, twoHopOptions: { maxNodes: 1000 }, treeCoverOptions: { maxTrees: 3 } }); const rule = new ChainRule(arbiter); // Create a chain rule const chainRule = { type: 'chain', steps: [ { relation: 'member_of', direction: 'out' }, { relation: 'can_read', direction: 'out' } ], collectValues: false, valueAggregation: 'sum' }; // Find actual chain paths const actualChains = []; const roleMemberships = graphData.relations.filter(r => r.relation === 'member_of' && r.dst.startsWith('role:') ); for (const membership of roleMemberships) { const userId = membership.src; const roleId = membership.dst; const roleReadPermissions = graphData.relations.filter(r => r.relation === 'can_read' && r.src === roleId ); for (const permission of roleReadPermissions) { actualChains.push({ user: userId, object: permission.dst }); } } // Test performance const start = Date.now(); let queryCount = 0; let positiveResults = 0; const end = start + 1000; // 1 second while (Date.now() < end) { for (const chain of actualChains) { const result = rule.evaluate( arbiter.nodeIdByKey.get(chain.user), chain.user, arbiter.nodeIdByKey.get(chain.object), chain.object, chainRule, new Set(), 'can_read_via_role', {} ); if (result.possibility > 0) { positiveResults++; } queryCount++; } } const duration = Date.now() - start; const qps = (queryCount / duration) * 1000; const positiveRate = (positiveResults / queryCount) * 100; results[size][strategy] = { qps: qps, queryCount: queryCount, positiveRate: positiveRate, chainCount: actualChains.length }; if (process.env.TEST_DEBUG === '1') console.log(` ${strategy}: ${qps.toFixed(2)} QPS (${actualChains.length} chains)`); } } // Compare results across graph sizes if (process.env.TEST_DEBUG === '1') console.log(' 📊 Results by graph size:'); for (const size of graphSizes) { if (process.env.TEST_DEBUG === '1') console.log(` ${size}:`); const sortedResults = Object.entries(results[size]).sort((a, b) => b[1].qps - a[1].qps); sortedResults.forEach(([strategy, result], index) => { const rank = index + 1; if (process.env.TEST_DEBUG === '1') console.log(` ${rank}. ${strategy}: ${result.qps.toFixed(2)} QPS (${result.chainCount} chains)`); }); } if (process.env.TEST_DEBUG === '1') console.log(' ✅ Graph size strategy comparison completed'); }); test('measures strategy performance with different chain lengths', async () => { if (process.env.TEST_DEBUG === '1') console.log('🔗 Testing strategy performance with different chain lengths...'); const generator = new BigGraphGenerator(); const graphData = generator.generateGraph('enterprise'); const strategies = ['auto', 'twohop', 'treecover', 'hybrid']; const chainLengths = [2, 3, 4]; const results = {}; for (const length of chainLengths) { if (process.env.TEST_DEBUG === '1') console.log(` Testing chain length: ${length}`); results[length] = {}; for (const strategy of strategies) { if (process.env.TEST_DEBUG === '1') console.log(` Strategy: ${strategy}`); const arbiter = generator.loadIntoArbiter(graphData); // Initialize reachability checker await arbiter.reachabilityChecker.initialize({ strategy: strategy, twoHopOptions: { maxNodes: 1000 }, treeCoverOptions: { maxTrees: 3 } }); const rule = new ChainRule(arbiter); // Create a chain rule with specified length const chainRule = { type: 'chain', steps: Array.from({ length }, (_, i) => ({ relation: i === 0 ? 'member_of' : 'can_read', direction: 'out' })), collectValues: false, valueAggregation: 'sum' }; // Find actual chain paths const actualChains = []; const roleMemberships = graphData.relations.filter(r => r.relation === 'member_of' && r.dst.startsWith('role:') ); for (const membership of roleMemberships) { const userId = membership.src; const roleId = membership.dst; const roleReadPermissions = graphData.relations.filter(r => r.relation === 'can_read' && r.src === roleId ); for (const permission of roleReadPermissions) { actualChains.push({ user: userId, object: permission.dst }); } } // Test performance const start = Date.now(); let queryCount = 0; let positiveResults = 0; const end = start + 1000; // 1 second while (Date.now() < end) { for (const chain of actualChains) { const result = rule.evaluate( arbiter.nodeIdByKey.get(chain.user), chain.user, arbiter.nodeIdByKey.get(chain.object), chain.object, chainRule, new Set(), 'can_read_via_role', {} ); if (result.possibility > 0) { positiveResults++; } queryCount++; } } const duration = Date.now() - start; const qps = (queryCount / duration) * 1000; const positiveRate = (positiveResults / queryCount) * 100; results[length][strategy] = { qps: qps, queryCount: queryCount, positiveRate: positiveRate, chainCount: actualChains.length }; if (process.env.TEST_DEBUG === '1') console.log(` ${strategy}: ${qps.toFixed(2)} QPS (${actualChains.length} chains)`); } } // Compare results across chain lengths if (process.env.TEST_DEBUG === '1') console.log(' 📊 Results by chain length:'); for (const length of chainLengths) { if (process.env.TEST_DEBUG === '1') console.log(` Length ${length}:`); const sortedResults = Object.entries(results[length]).sort((a, b) => b[1].qps - a[1].qps); sortedResults.forEach(([strategy, result], index) => { const rank = index + 1; if (process.env.TEST_DEBUG === '1') console.log(` ${rank}. ${strategy}: ${result.qps.toFixed(2)} QPS (${result.chainCount} chains)`); }); } if (process.env.TEST_DEBUG === '1') console.log(' ✅ Chain length strategy comparison completed'); });