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('=== AuthChecker.check() Breakdown ===\n'); const config = arbiter.relationConfigs.get('risk_ok_owa'); const ruleEval = arbiter.authChecker.ruleEvaluator; // What AuthChecker.check does for a relational_comparator: // 1. Parse options (backward compat check) // 2. Get config from relationConfigs // 3. Check if fast path (NO - it's relational_comparator, not direct) // 4. Call _ensureIndicesBuilt // 5. Create visitKey object // 6. Scan visited set for cycles // 7. Add visitKey to visited // 8. Get userId and objectId from nodeIdByKey // 9. Check if config exists // 10. Build evaluationPath object // 11. Call ruleEvaluator.evaluateRule (since no union/intersection/exclusion at top level) // 12. Iterate through collected rules and call ruleEvaluator for each // 13. Build result object with meta // Let's measure each step // Step 1-2: Options parsing + config lookup const configGet = measure('Config lookup', () => { arbiter.relationConfigs.get('risk_ok_owa'); }); console.log(`Config lookup: ${(configGet.perOp * 1000).toFixed(3)}µs`); // Step 4: _ensureIndicesBuilt const ensureIndices = measure('_ensureIndicesBuilt', () => { arbiter.relationManager._ensureIndicesBuilt(); }); console.log(`_ensureIndicesBuilt: ${(ensureIndices.perOp * 1000).toFixed(3)}µs`); // Step 5: Create visitKey object const createVisitKey = measure('Create visitKey object', (idx) => { const visitKey = { userKey: `user:${idx}`, relation: 'risk_ok_owa', objectKey: `resource:${idx}` }; }); console.log(`Create visitKey: ${(createVisitKey.perOp * 1000).toFixed(3)}µs`); // Step 6: Scan visited set (empty) const visited = new Set(); const scanEmpty = measure('Scan empty visited', () => { for (const v of visited) { if (v.userKey === 'user:0') break; } }); console.log(`Scan empty visited: ${(scanEmpty.perOp * 1000).toFixed(3)}µs`); // Step 7: Add to visited const addVisited = measure('Add to visited', (idx) => { const s = new Set(); s.add({ userKey: `user:${idx}`, relation: 'risk_ok_owa', objectKey: `resource:${idx}` }); }); console.log(`Add to visited (new Set + add): ${(addVisited.perOp * 1000).toFixed(3)}µs`); // Step 8: Get IDs const getIds = measure('Get user/object IDs', (idx) => { arbiter.nodeIdByKey.get(`user:${idx}`); arbiter.nodeIdByKey.get(`resource:${idx}`); }); console.log(`Get IDs: ${(getIds.perOp * 1000).toFixed(3)}µs`); // Step 10: Build evaluationPath object const buildEvalPath = measure('Build evaluationPath object', (idx) => { const evaluationPath = { userKey: `user:${idx}`, relation: 'risk_ok_owa', objectKey: `resource:${idx}`, config: config, rules: [], visitedPath: [] // Would be Array.from(_visited) but that's expensive }; }); console.log(`Build evaluationPath: ${(buildEvalPath.perOp * 1000).toFixed(3)}µs`); // Step 10b: Array.from(_visited) - THIS IS LIKELY EXPENSIVE const visitedWith5 = new Set(); for (let i = 0; i < 5; i++) { visitedWith5.add({ userKey: `user:${i}`, relation: 'test', objectKey: `resource:${i}` }); } const arrayFrom = measure('Array.from(visited) with 5 entries', () => { Array.from(visitedWith5); }); console.log(`Array.from(visited) x5: ${(arrayFrom.perOp * 1000).toFixed(3)}µs`); // Step 11: ruleEvaluator.evaluateRule for comparator const evalRule = measure('ruleEvaluator.evaluateRule', (idx) => { const userId = arbiter.nodeIdByKey.get(`user:${idx}`); const objectId = arbiter.nodeIdByKey.get(`resource:${idx}`); ruleEval.evaluateRule(userId, `user:${idx}`, objectId, `resource:${idx}`, config, new Set(), 'risk_ok_owa', { fastPath: true }); }); console.log(`ruleEvaluator.evaluateRule: ${(evalRule.perOp * 1000).toFixed(3)}µs`); // Full check for comparison const fullCheck = measure('Full arbiter.check', (idx) => { arbiter.check(`user:${idx}`, 'risk_ok_owa', `resource:${idx}`, { fastPath: true }); }); console.log(`Full arbiter.check: ${(fullCheck.perOp * 1000).toFixed(3)}µs`); // Difference analysis const overhead = fullCheck.perOp - evalRule.perOp; console.log(`\n=== AuthChecker overhead: ${(overhead * 1000).toFixed(2)}µs ===`); // What's causing it? const sumOfParts = configGet.perOp + ensureIndices.perOp + createVisitKey.perOp + addVisited.perOp + getIds.perOp + buildEvalPath.perOp + arrayFrom.perOp; console.log(`Sum of measured parts: ${(sumOfParts * 1000).toFixed(2)}µs`); console.log(`Unaccounted overhead: ${((overhead - sumOfParts) * 1000).toFixed(2)}µs`); // Check what happens inside RuleEvaluator.evaluateRule console.log('\n=== RuleEvaluator.evaluateRule Breakdown ===\n'); // The evaluateRule does: // 1. Extract binary, valueContext from options // 2. Convert string IDs to numeric (already numeric here) // 3. Check if rule needs values (_ruleRequiresValues) - THIS COULD BE EXPENSIVE // 4. Create enhanced options // 5. Route to handler const needsValues = measure('_ruleRequiresValues', () => { ruleEval._ruleRequiresValues(config, new Set()); }); console.log(`_ruleRequiresValues: ${(needsValues.perOp * 1000).toFixed(3)}µs`); // Handler.evaluate directly const handler = ruleEval.ruleHandlers.relational_comparator; const handlerEval = measure('handler.evaluate (direct)', (idx) => { const userId = arbiter.nodeIdByKey.get(`user:${idx}`); const objectId = arbiter.nodeIdByKey.get(`resource:${idx}`); handler.evaluate(userId, `user:${idx}`, objectId, `resource:${idx}`, config, new Set(), 'risk_ok_owa', { fastPath: true }); }); console.log(`handler.evaluate: ${(handlerEval.perOp * 1000).toFixed(3)}µs`); // Check _ruleRequiresValues deeply console.log('\n=== _ruleRequiresValues Analysis ===\n'); // This recursively checks the rule tree // For relational_comparator, it returns true immediately // But does it actually traverse? const visited2 = new Set(); const start = process.hrtime.bigint(); for (let i = 0; i < 10000; i++) { visited2.clear(); ruleEval._ruleRequiresValues(config, visited2); } const requiresMs = Number(process.hrtime.bigint() - start) / 1e6; console.log(`_ruleRequiresValues (10k iterations): ${requiresMs.toFixed(2)}ms`); console.log(`Per call: ${(requiresMs / 10).toFixed(3)}µs`); // Actually look at what the method does for relational_comparator console.log(`\nrule.type = '${config.type}'`); console.log(`_ruleRequiresValues returns true immediately for relational_comparator`);