Files
core/benchmarks/profile-authchecker.js
T

229 lines
8.4 KiB
JavaScript
Raw Normal View History

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`);