229 lines
8.4 KiB
JavaScript
229 lines
8.4 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('=== 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`);
|