import { Arbiter } from '../src/core/Arbiter.js'; const arbiter = new Arbiter(); const SIZE = 5000; // Build scenario for (let i = 0; i < SIZE; i++) { arbiter.addNode(`user:${i}`, 'user'); arbiter.addNode(`resource:${i}`, 'resource'); const isSafe = i % 2 === 0; arbiter.addRelation(`user:${i}`, 'risk_score', `resource:${i}`, 1.0, { value: isSafe ? 20 : 80 }); arbiter.addRelation(`user:${i}`, 'risk_bonus', `resource:${i}`, 1.0, { value: isSafe ? 5 : 15 }); arbiter.addRelation(`user:${i}`, 'risk_noise', `resource:${i}`, 1.0, { value: isSafe ? 0 : 10 }); arbiter.addRelation(`resource:${i}`, 'risk_limit', `resource:${i}`, 1.0, { value: 40 }); arbiter.addRelation(`resource:${i}`, 'risk_cap', `resource:${i}`, 1.0, { value: 45 }); } ['risk_score', 'risk_bonus', 'risk_noise', 'risk_limit', 'risk_cap'].forEach(r => arbiter.setRelationConfig(r, { type: 'direct' })); arbiter.setRelationConfig('risk_ok_owa', { type: 'relational_comparator', comparator: '<=', fallbackBehavior: 'deny', left: { rule: { union: { rules: [ { type: 'direct', relation: 'risk_score' }, { type: 'direct', relation: 'risk_bonus' }, { type: 'direct', relation: 'risk_noise' } ], aggregator: 'owa', owaWeights: [0.5, 0.3, 0.2] } }, extractValue: true, valueRelation: 'risk_score', aggregator: 'owa', owaWeights: [0.5, 0.3, 0.2] }, right: { rule: { union: { rules: [ { type: 'direct', relation: 'risk_limit' }, { type: 'direct', relation: 'risk_cap' } ], aggregator: 'owa', owaWeights: [0.6, 0.4] } }, extractValue: true, valueRelation: 'risk_limit', evaluateFrom: 'object', aggregator: 'owa', owaWeights: [0.6, 0.4] } }); // Warm up for (let i = 0; i < 100; i++) { arbiter.check(`user:${i}`, 'risk_ok_owa', `resource:${i}`, { fastPath: true }); } const ITERATIONS = 2000; function measure(label, fn) { const start = process.hrtime.bigint(); for (let i = 0; i < ITERATIONS; i++) fn(i % SIZE); const ms = Number(process.hrtime.bigint() - start) / 1e6; return { label, ms, qps: Math.round((ITERATIONS / ms) * 1000), perOp: ms / ITERATIONS }; } console.log('=== BASELINE MEASUREMENTS ===\n'); // Baseline: single direct check const directCheck = measure('Direct check (single)', (idx) => { arbiter.check(`user:${idx}`, 'risk_score', `resource:${idx}`, { fastPath: true }); }); console.log(`${directCheck.label}: ${directCheck.qps} QPS (${(directCheck.perOp * 1000).toFixed(2)}µs/op)`); // Full comparator check const fullCheck = measure('Full comparator check', (idx) => { arbiter.check(`user:${idx}`, 'risk_ok_owa', `resource:${idx}`, { fastPath: true }); }); console.log(`${fullCheck.label}: ${fullCheck.qps} QPS (${(fullCheck.perOp * 1000).toFixed(2)}µs/op)`); console.log(`\nSlowdown: ${(fullCheck.perOp / directCheck.perOp).toFixed(1)}x`); console.log(`Extra time per check: ${((fullCheck.perOp - directCheck.perOp) * 1000).toFixed(2)}µs`); console.log('\n=== COMPONENT BREAKDOWN ===\n'); // What does a comparator check actually do? // 1. Resolve node IDs (2x) // 2. Get relation config // 3. Cycle detection (visited set scan) // 4. Left operand: evaluate union (3 direct rules) + extract values + aggregate // 5. Right operand: evaluate union (2 direct rules) + extract values + aggregate // 6. Compare intervals // 7. Build result meta // Measure individual components const ruleEval = arbiter.authChecker.ruleEvaluator; const comparatorHandler = ruleEval.ruleHandlers.relational_comparator.numericRule; const config = arbiter.relationConfigs.get('risk_ok_owa'); // Component 1: Node ID resolution (2 lookups) const nodeIdLookup = measure('Node ID lookup (2x)', (idx) => { arbiter.nodeIdByKey.get(`user:${idx}`); arbiter.nodeIdByKey.get(`resource:${idx}`); }); console.log(`${nodeIdLookup.label}: ${(nodeIdLookup.perOp * 1000).toFixed(3)}µs`); // Component 2: Config lookup const configLookup = measure('Config lookup', () => { arbiter.relationConfigs.get('risk_ok_owa'); }); console.log(`${configLookup.label}: ${(configLookup.perOp * 1000).toFixed(3)}µs`); // Component 3: Direct check via indices (what union does internally per rule) const directIndex = measure('Direct index lookup', (idx) => { const userId = arbiter.nodeIdByKey.get(`user:${idx}`); const objectId = arbiter.nodeIdByKey.get(`resource:${idx}`); arbiter.indices.getDirectRelation(userId, 'risk_score', objectId); }); console.log(`${directIndex.label}: ${(directIndex.perOp * 1000).toFixed(3)}µs`); // Component 4: Evaluate a single direct rule via ruleEvaluator const singleDirect = measure('Single direct rule eval', (idx) => { const userId = arbiter.nodeIdByKey.get(`user:${idx}`); const objectId = arbiter.nodeIdByKey.get(`resource:${idx}`); ruleEval.evaluateRule(userId, `user:${idx}`, objectId, `resource:${idx}`, { type: 'direct', relation: 'risk_score' }, new Set(), 'test', { fastPath: true }); }); console.log(`${singleDirect.label}: ${(singleDirect.perOp * 1000).toFixed(3)}µs`); // Component 5: Union of 3 direct rules const union3 = measure('Union (3 direct rules)', (idx) => { const userId = arbiter.nodeIdByKey.get(`user:${idx}`); const objectId = arbiter.nodeIdByKey.get(`resource:${idx}`); ruleEval.evaluateRule(userId, `user:${idx}`, objectId, `resource:${idx}`, config.left.rule, new Set(), 'test', { fastPath: true }); }); console.log(`${union3.label}: ${(union3.perOp * 1000).toFixed(3)}µs`); // Component 6: Union of 2 direct rules const union2 = measure('Union (2 direct rules)', (idx) => { const objectId = arbiter.nodeIdByKey.get(`resource:${idx}`); ruleEval.evaluateRule(objectId, `resource:${idx}`, objectId, `resource:${idx}`, config.right.rule, new Set(), 'test', { fastPath: true }); }); console.log(`${union2.label}: ${(union2.perOp * 1000).toFixed(3)}µs`); // Component 7: _evaluateOperand (left) - includes union + extract + aggregate const leftOp = measure('Left _evaluateOperand', (idx) => { const userId = arbiter.nodeIdByKey.get(`user:${idx}`); const objectId = arbiter.nodeIdByKey.get(`resource:${idx}`); comparatorHandler._evaluateOperand( userId, `user:${idx}`, objectId, `resource:${idx}`, config.left, new Set(), 'test', { fastPath: true }, 'left', 1.0, null ); }); console.log(`${leftOp.label}: ${(leftOp.perOp * 1000).toFixed(3)}µs`); // Component 8: _evaluateOperand (right) const rightOp = measure('Right _evaluateOperand', (idx) => { const userId = arbiter.nodeIdByKey.get(`user:${idx}`); const objectId = arbiter.nodeIdByKey.get(`resource:${idx}`); comparatorHandler._evaluateOperand( userId, `user:${idx}`, objectId, `resource:${idx}`, config.right, new Set(), 'test', { fastPath: true }, 'right', 1.0, null ); }); console.log(`${rightOp.label}: ${(rightOp.perOp * 1000).toFixed(3)}µs`); // Component 9: _evaluateRule (both operands + compare) const evalRule = measure('comparator._evaluateRule', (idx) => { const userId = arbiter.nodeIdByKey.get(`user:${idx}`); const objectId = arbiter.nodeIdByKey.get(`resource:${idx}`); comparatorHandler._evaluateRule( userId, `user:${idx}`, objectId, `resource:${idx}`, config, new Set(), 'test', { fastPath: true } ); }); console.log(`${evalRule.label}: ${(evalRule.perOp * 1000).toFixed(3)}µs`); console.log('\n=== OVERHEAD ANALYSIS ===\n'); const directIdxTime = directIndex.perOp * 1000; const singleDirectTime = singleDirect.perOp * 1000; const union3Time = union3.perOp * 1000; const leftOpTime = leftOp.perOp * 1000; const rightOpTime = rightOp.perOp * 1000; const evalRuleTime = evalRule.perOp * 1000; const fullCheckTime = fullCheck.perOp * 1000; console.log(`Index lookup: ${directIdxTime.toFixed(2)}µs`); console.log(`Single direct rule: ${singleDirectTime.toFixed(2)}µs (+${(singleDirectTime - directIdxTime).toFixed(2)}µs overhead)`); console.log(`Union (3 rules): ${union3Time.toFixed(2)}µs (expected ~${(singleDirectTime * 3).toFixed(2)}µs, actual overhead: ${(union3Time - singleDirectTime * 3).toFixed(2)}µs)`); console.log(`Left operand: ${leftOpTime.toFixed(2)}µs (+${(leftOpTime - union3Time).toFixed(2)}µs for extract+aggregate)`); console.log(`Right operand: ${rightOpTime.toFixed(2)}µs`); console.log(`_evaluateRule: ${evalRuleTime.toFixed(2)}µs (expected ~${(leftOpTime + rightOpTime).toFixed(2)}µs, actual: ${evalRuleTime.toFixed(2)}µs)`); console.log(`Full check: ${fullCheckTime.toFixed(2)}µs (+${(fullCheckTime - evalRuleTime).toFixed(2)}µs AuthChecker overhead)`); console.log('\n=== THEORETICAL VS ACTUAL ===\n'); const theoreticalMin = (singleDirectTime * 5) + 2; // 5 direct lookups + some overhead console.log(`Theoretical minimum (5 direct lookups): ~${theoreticalMin.toFixed(2)}µs`); console.log(`Actual: ${fullCheckTime.toFixed(2)}µs`); console.log(`Overhead factor: ${(fullCheckTime / theoreticalMin).toFixed(1)}x`); // Check Set creation overhead console.log('\n=== SET CREATION OVERHEAD ===\n'); const setCreate = measure('new Set()', () => new Set()); const setWithAdd = measure('new Set() + 1 add', () => { const s = new Set(); s.add({ userKey: 'user:0', relation: 'test', objectKey: 'resource:0' }); }); console.log(`new Set(): ${(setCreate.perOp * 1000).toFixed(3)}µs`); console.log(`new Set() + 1 object add: ${(setWithAdd.perOp * 1000).toFixed(3)}µs`); console.log(`Sets created per check: ~6-8`); console.log(`Total Set overhead: ~${((setCreate.perOp * 7) * 1000).toFixed(2)}µs`); // Check object allocation in visited scan console.log('\n=== VISITED SCAN OVERHEAD ===\n'); const visited = new Set(); for (let i = 0; i < 10; i++) { visited.add({ userKey: `user:${i}`, relation: 'test', objectKey: `resource:${i}` }); } const visitedScan = measure('Scan visited (10 entries)', () => { const userKey = 'user:5'; const relation = 'test'; const objectKey = 'resource:5'; for (const v of visited) { if (v.userKey === userKey && v.relation === relation && v.objectKey === objectKey) break; } }); console.log(`Scan 10-entry visited set: ${(visitedScan.perOp * 1000).toFixed(3)}µs`); console.log(`(This happens at each level of rule evaluation)`);