240 lines
10 KiB
JavaScript
240 lines
10 KiB
JavaScript
|
|
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)`);
|