/** * Test chain caching performance improvements */ 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('measures QPS improvement with chain caching', async () => { if (process.env.TEST_DEBUG === '1') console.log('🚀 Testing chain caching QPS improvements...'); const generator = new BigGraphGenerator(); const graphData = generator.generateGraph('enterprise'); const arbiter = generator.loadIntoArbiter(graphData); const rule = new ChainRule(arbiter); // Create a test chain rule const chainRule = { type: 'chain', steps: [ { relation: 'role_membership', direction: 'out' }, { relation: 'role_permission', direction: 'out' } ], collectValues: false, valueAggregation: 'sum' }; // Test with multiple user-object pairs to create cache hits const testPairs = [ { user: 'user:Alice Smith-0', object: 'doc:secret-999' }, { user: 'user:Bob Johnson-1', object: 'doc:report-54' }, { user: 'user:Charlie Brown-2', object: 'doc:contract-480' }, { user: 'user:Diana Martinez-3', object: 'doc:policy-115' }, { user: 'user:Eve Wilson-4', object: 'doc:analysis-200' } ]; if (process.env.TEST_DEBUG === '1') console.log(` Testing with ${testPairs.length} user-object pairs...`); // Test 1: Without caching (clear cache between runs) if (process.env.TEST_DEBUG === '1') console.log(' Test 1: Without caching...'); const start1 = Date.now(); let queryCount1 = 0; const end1 = start1 + 2000; // 2 seconds while (Date.now() < end1) { for (const pair of testPairs) { // Clear cache before each query to simulate no caching rule.chainResultCache.clear(); rule.chainPathCache.clear(); rule.evaluate( arbiter.nodeIdByKey.get(pair.user), pair.user, arbiter.nodeIdByKey.get(pair.object), pair.object, chainRule, new Set(), 'can_read_via_role', {} ); queryCount1++; } } const duration1 = Date.now() - start1; const qps1 = (queryCount1 / duration1) * 1000; if (process.env.TEST_DEBUG === '1') console.log(` Without caching: ${qps1.toFixed(2)} QPS (${queryCount1} queries in ${duration1}ms)`); // Test 2: With caching (let cache accumulate) if (process.env.TEST_DEBUG === '1') console.log(' Test 2: With caching...'); rule.chainResultCache.clear(); rule.chainPathCache.clear(); const start2 = Date.now(); let queryCount2 = 0; const end2 = start2 + 2000; // 2 seconds while (Date.now() < end2) { for (const pair of testPairs) { rule.evaluate( arbiter.nodeIdByKey.get(pair.user), pair.user, arbiter.nodeIdByKey.get(pair.object), pair.object, chainRule, new Set(), 'can_read_via_role', {} ); queryCount2++; } } const duration2 = Date.now() - start2; const qps2 = (queryCount2 / duration2) * 1000; if (process.env.TEST_DEBUG === '1') console.log(` With caching: ${qps2.toFixed(2)} QPS (${queryCount2} queries in ${duration2}ms)`); if (process.env.TEST_DEBUG === '1') console.log(` Cache size: ${rule.chainResultCache.size} results, ${rule.chainPathCache.size} paths`); // Calculate improvement const improvement = qps2 / qps1; if (process.env.TEST_DEBUG === '1') console.log(` QPS improvement: ${improvement.toFixed(2)}x faster`); // Verify improvement assert.ok(improvement > 1, `Caching should improve QPS (${improvement.toFixed(2)}x)`); assert.ok(rule.chainResultCache.size > 0, 'Cache should be populated'); if (process.env.TEST_DEBUG === '1') console.log(' ✅ Chain caching improves QPS'); }); test('measures QPS improvement with repeated queries', async () => { if (process.env.TEST_DEBUG === '1') console.log('🔄 Testing QPS improvement with repeated queries...'); const generator = new BigGraphGenerator(); const graphData = generator.generateGraph('enterprise'); const arbiter = generator.loadIntoArbiter(graphData); const rule = new ChainRule(arbiter); // Create a test chain rule const chainRule = { type: 'chain', steps: [ { relation: 'role_membership', direction: 'out' }, { relation: 'role_permission', direction: 'out' } ], collectValues: false, valueAggregation: 'sum' }; // Test with same user-object pair repeated many times const testUser = 'user:Alice Smith-0'; const testObject = 'doc:secret-999'; if (process.env.TEST_DEBUG === '1') console.log(` Testing repeated queries: ${testUser} -> ${testObject}`); // Test 1: First run (no cache) if (process.env.TEST_DEBUG === '1') console.log(' First run (no cache)...'); const start1 = Date.now(); let queryCount1 = 0; const end1 = start1 + 1000; // 1 second while (Date.now() < end1) { rule.evaluate( arbiter.nodeIdByKey.get(testUser), testUser, arbiter.nodeIdByKey.get(testObject), testObject, chainRule, new Set(), 'can_read_via_role', {} ); queryCount1++; } const duration1 = Date.now() - start1; const qps1 = (queryCount1 / duration1) * 1000; if (process.env.TEST_DEBUG === '1') console.log(` First run: ${qps1.toFixed(2)} QPS (${queryCount1} queries)`); if (process.env.TEST_DEBUG === '1') console.log(` Cache size after first run: ${rule.chainResultCache.size}`); // Test 2: Second run (with cache) if (process.env.TEST_DEBUG === '1') console.log(' Second run (with cache)...'); const start2 = Date.now(); let queryCount2 = 0; const end2 = start2 + 1000; // 1 second while (Date.now() < end2) { rule.evaluate( arbiter.nodeIdByKey.get(testUser), testUser, arbiter.nodeIdByKey.get(testObject), testObject, chainRule, new Set(), 'can_read_via_role', {} ); queryCount2++; } const duration2 = Date.now() - start2; const qps2 = (queryCount2 / duration2) * 1000; if (process.env.TEST_DEBUG === '1') console.log(` Second run: ${qps2.toFixed(2)} QPS (${queryCount2} queries)`); if (process.env.TEST_DEBUG === '1') console.log(` Cache size after second run: ${rule.chainResultCache.size}`); // Calculate improvement const improvement = qps2 / qps1; if (process.env.TEST_DEBUG === '1') console.log(` QPS improvement: ${improvement.toFixed(2)}x faster`); // Verify improvement assert.ok(improvement > 1, `Caching should improve QPS (${improvement.toFixed(2)}x)`); assert.ok(rule.chainResultCache.size > 0, 'Cache should be populated'); if (process.env.TEST_DEBUG === '1') console.log(' ✅ Repeated query caching improves QPS'); }); test('measures QPS improvement with mixed query patterns', async () => { if (process.env.TEST_DEBUG === '1') console.log('🎯 Testing QPS improvement with mixed query patterns...'); const generator = new BigGraphGenerator(); const graphData = generator.generateGraph('enterprise'); const arbiter = generator.loadIntoArbiter(graphData); const rule = new ChainRule(arbiter); // Create a test chain rule const chainRule = { type: 'chain', steps: [ { relation: 'role_membership', direction: 'out' }, { relation: 'role_permission', direction: 'out' } ], collectValues: false, valueAggregation: 'sum' }; // Create a mix of repeated and unique queries const repeatedPairs = [ { user: 'user:Alice Smith-0', object: 'doc:secret-999' }, { user: 'user:Bob Johnson-1', object: 'doc:report-54' } ]; const uniquePairs = [ { user: 'user:Charlie Brown-2', object: 'doc:contract-480' }, { user: 'user:Diana Martinez-3', object: 'doc:policy-115' }, { user: 'user:Eve Wilson-4', object: 'doc:analysis-200' }, { user: 'user:Frank Davis-5', object: 'doc:spec-300' }, { user: 'user:Grace Miller-6', object: 'doc:proposal-400' } ]; if (process.env.TEST_DEBUG === '1') console.log(` Testing with ${repeatedPairs.length} repeated pairs and ${uniquePairs.length} unique pairs...`); // Test 1: Without caching if (process.env.TEST_DEBUG === '1') console.log(' Test 1: Without caching...'); const start1 = Date.now(); let queryCount1 = 0; const end1 = start1 + 3000; // 3 seconds while (Date.now() < end1) { // Mix of repeated and unique queries for (const pair of repeatedPairs) { rule.chainResultCache.clear(); // Clear cache to simulate no caching rule.evaluate( arbiter.nodeIdByKey.get(pair.user), pair.user, arbiter.nodeIdByKey.get(pair.object), pair.object, chainRule, new Set(), 'can_read_via_role', {} ); queryCount1++; } for (const pair of uniquePairs) { rule.chainResultCache.clear(); // Clear cache to simulate no caching rule.evaluate( arbiter.nodeIdByKey.get(pair.user), pair.user, arbiter.nodeIdByKey.get(pair.object), pair.object, chainRule, new Set(), 'can_read_via_role', {} ); queryCount1++; } } const duration1 = Date.now() - start1; const qps1 = (queryCount1 / duration1) * 1000; if (process.env.TEST_DEBUG === '1') console.log(` Without caching: ${qps1.toFixed(2)} QPS (${queryCount1} queries)`); // Test 2: With caching if (process.env.TEST_DEBUG === '1') console.log(' Test 2: With caching...'); rule.chainResultCache.clear(); rule.chainPathCache.clear(); const start2 = Date.now(); let queryCount2 = 0; const end2 = start2 + 3000; // 3 seconds while (Date.now() < end2) { // Mix of repeated and unique queries for (const pair of repeatedPairs) { rule.evaluate( arbiter.nodeIdByKey.get(pair.user), pair.user, arbiter.nodeIdByKey.get(pair.object), pair.object, chainRule, new Set(), 'can_read_via_role', {} ); queryCount2++; } for (const pair of uniquePairs) { rule.evaluate( arbiter.nodeIdByKey.get(pair.user), pair.user, arbiter.nodeIdByKey.get(pair.object), pair.object, chainRule, new Set(), 'can_read_via_role', {} ); queryCount2++; } } const duration2 = Date.now() - start2; const qps2 = (queryCount2 / duration2) * 1000; if (process.env.TEST_DEBUG === '1') console.log(` With caching: ${qps2.toFixed(2)} QPS (${queryCount2} queries)`); if (process.env.TEST_DEBUG === '1') console.log(` Cache size: ${rule.chainResultCache.size} results, ${rule.chainPathCache.size} paths`); // Calculate improvement const improvement = qps2 / qps1; if (process.env.TEST_DEBUG === '1') console.log(` QPS improvement: ${improvement.toFixed(2)}x faster`); // Verify improvement assert.ok(improvement > 1, `Caching should improve QPS (${improvement.toFixed(2)}x)`); assert.ok(rule.chainResultCache.size > 0, 'Cache should be populated'); if (process.env.TEST_DEBUG === '1') console.log(' ✅ Mixed query pattern caching improves QPS'); });