717ae1031e
Zanzibar-style authorization graph engine (direct/chain/TTU/defeasible/ binary modes, condensed snapshots, value relations) with 39 rigor test campaigns. Includes fixes for snapshot binary writer/reader format mismatch (snapshot-of-snapshot corruption), possibility write-boundary validation, empty-graph snapshot serialization, relation lookup cache direction collision, config-redefinition cache invalidation, binary threshold semantics, defeasible compiled routing, and comparator reason whitelisting.
231 lines
8.8 KiB
JavaScript
231 lines
8.8 KiB
JavaScript
import { Arbiter } from '../src/core/Arbiter.js';
|
|
|
|
const arbiter = new Arbiter();
|
|
const SIZE = 5000;
|
|
|
|
// Build nodes and relations
|
|
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' }));
|
|
|
|
const leftRule = {
|
|
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]
|
|
}
|
|
};
|
|
const rightRule = {
|
|
union: {
|
|
rules: [
|
|
{ type: 'direct', relation: 'risk_limit' },
|
|
{ type: 'direct', relation: 'risk_cap' }
|
|
],
|
|
aggregator: 'owa',
|
|
owaWeights: [0.6, 0.4]
|
|
}
|
|
};
|
|
|
|
arbiter.setRelationConfig('risk_ok_owa', {
|
|
type: 'relational_comparator',
|
|
comparator: '<=',
|
|
fallbackBehavior: 'deny',
|
|
left: {
|
|
rule: leftRule,
|
|
extractValue: true,
|
|
valueRelation: 'risk_score',
|
|
aggregator: 'owa',
|
|
owaWeights: [0.5, 0.3, 0.2]
|
|
},
|
|
right: {
|
|
rule: rightRule,
|
|
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;
|
|
const comparatorRule = arbiter.authChecker.ruleEvaluator.ruleHandlers.relational_comparator.numericRule;
|
|
|
|
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) };
|
|
}
|
|
|
|
// Profile each step of the comparator
|
|
const results = [];
|
|
|
|
// 1. Node ID lookup
|
|
results.push(measure('nodeIdLookup', (idx) => {
|
|
arbiter.nodeIdByKey.get(`user:${idx}`);
|
|
arbiter.nodeIdByKey.get(`resource:${idx}`);
|
|
}));
|
|
|
|
// 2. Left union evaluation
|
|
results.push(measure('leftUnionEval', (idx) => {
|
|
const userId = arbiter.nodeIdByKey.get(`user:${idx}`);
|
|
const objectId = arbiter.nodeIdByKey.get(`resource:${idx}`);
|
|
arbiter.authChecker.ruleEvaluator.evaluateRule(
|
|
userId, `user:${idx}`, objectId, `resource:${idx}`,
|
|
leftRule, new Set(), 'risk_ok_owa', { fastPath: true }
|
|
);
|
|
}));
|
|
|
|
// 3. Right union evaluation
|
|
results.push(measure('rightUnionEval', (idx) => {
|
|
const objectId = arbiter.nodeIdByKey.get(`resource:${idx}`);
|
|
arbiter.authChecker.ruleEvaluator.evaluateRule(
|
|
objectId, `resource:${idx}`, objectId, `resource:${idx}`,
|
|
rightRule, new Set(), 'risk_ok_owa', { fastPath: true }
|
|
);
|
|
}));
|
|
|
|
// 4. _extractValues (left)
|
|
results.push(measure('leftExtract', (idx) => {
|
|
const userId = arbiter.nodeIdByKey.get(`user:${idx}`);
|
|
const objectId = arbiter.nodeIdByKey.get(`resource:${idx}`);
|
|
const ruleResult = arbiter.authChecker.ruleEvaluator.evaluateRule(
|
|
userId, `user:${idx}`, objectId, `resource:${idx}`,
|
|
leftRule, new Set(), 'risk_ok_owa', { fastPath: true }
|
|
);
|
|
comparatorRule._extractValues(
|
|
userId, `user:${idx}`, objectId, `resource:${idx}`,
|
|
leftRule, 'risk_score', ruleResult, 'auto', null, 24*60*60*1000, null
|
|
);
|
|
}));
|
|
|
|
// 5. _extractValues (right)
|
|
results.push(measure('rightExtract', (idx) => {
|
|
const objectId = arbiter.nodeIdByKey.get(`resource:${idx}`);
|
|
const ruleResult = arbiter.authChecker.ruleEvaluator.evaluateRule(
|
|
objectId, `resource:${idx}`, objectId, `resource:${idx}`,
|
|
rightRule, new Set(), 'risk_ok_owa', { fastPath: true }
|
|
);
|
|
comparatorRule._extractValues(
|
|
objectId, `resource:${idx}`, objectId, `resource:${idx}`,
|
|
rightRule, 'risk_limit', ruleResult, 'object', null, 24*60*60*1000, null
|
|
);
|
|
}));
|
|
|
|
// 6. Full _evaluateOperand (includes eval + extract + aggregate)
|
|
results.push(measure('leftOperand', (idx) => {
|
|
const userId = arbiter.nodeIdByKey.get(`user:${idx}`);
|
|
const objectId = arbiter.nodeIdByKey.get(`resource:${idx}`);
|
|
comparatorRule._evaluateOperand(
|
|
userId, `user:${idx}`, objectId, `resource:${idx}`,
|
|
{
|
|
rule: leftRule,
|
|
extractValue: true,
|
|
valueRelation: 'risk_score',
|
|
aggregator: 'owa',
|
|
owaWeights: [0.5, 0.3, 0.2]
|
|
},
|
|
new Set(), 'risk_ok_owa', { fastPath: true }, 'left', 1.0, null
|
|
);
|
|
}));
|
|
|
|
results.push(measure('rightOperand', (idx) => {
|
|
const userId = arbiter.nodeIdByKey.get(`user:${idx}`);
|
|
const objectId = arbiter.nodeIdByKey.get(`resource:${idx}`);
|
|
comparatorRule._evaluateOperand(
|
|
userId, `user:${idx}`, objectId, `resource:${idx}`,
|
|
{
|
|
rule: rightRule,
|
|
extractValue: true,
|
|
valueRelation: 'risk_limit',
|
|
evaluateFrom: 'object',
|
|
aggregator: 'owa',
|
|
owaWeights: [0.6, 0.4]
|
|
},
|
|
new Set(), 'risk_ok_owa', { fastPath: true }, 'right', 1.0, null
|
|
);
|
|
}));
|
|
|
|
// 7. Full comparator evaluation
|
|
results.push(measure('fullComparator', (idx) => {
|
|
const userId = arbiter.nodeIdByKey.get(`user:${idx}`);
|
|
const objectId = arbiter.nodeIdByKey.get(`resource:${idx}`);
|
|
comparatorRule._evaluateRule(
|
|
userId, `user:${idx}`, objectId, `resource:${idx}`,
|
|
arbiter.relationConfigs.get('risk_ok_owa'),
|
|
new Set(), 'risk_ok_owa', { fastPath: true }
|
|
);
|
|
}));
|
|
|
|
// 8. Through authChecker (adds visited set, config lookup, etc)
|
|
results.push(measure('viaAuthChecker', (idx) => {
|
|
arbiter.check(`user:${idx}`, 'risk_ok_owa', `resource:${idx}`, { fastPath: true });
|
|
}));
|
|
|
|
console.log('Profile Results (per iteration):');
|
|
console.log('================================');
|
|
for (const r of results) {
|
|
console.log(`${r.label.padEnd(20)} ${r.ms.toFixed(2).padStart(8)}ms ${r.qps.toString().padStart(8)} QPS`);
|
|
}
|
|
|
|
// Calculate incremental costs
|
|
console.log('\n--- Incremental Analysis ---');
|
|
const nodeIdMs = results[0].ms;
|
|
const leftUnionMs = results[1].ms;
|
|
const rightUnionMs = results[2].ms;
|
|
const leftExtractMs = results[3].ms;
|
|
const rightExtractMs = results[4].ms;
|
|
const leftOperandMs = results[5].ms;
|
|
const rightOperandMs = results[6].ms;
|
|
const fullComparatorMs = results[7].ms;
|
|
const viaAuthCheckerMs = results[8].ms;
|
|
|
|
console.log(`Node ID lookup: ${nodeIdMs.toFixed(2)}ms`);
|
|
console.log(`Left union eval only: ${(leftUnionMs - nodeIdMs).toFixed(2)}ms`);
|
|
console.log(`Right union eval only: ${(rightUnionMs - nodeIdMs).toFixed(2)}ms`);
|
|
console.log(`Extract overhead (left): ${(leftExtractMs - leftUnionMs).toFixed(2)}ms`);
|
|
console.log(`Extract overhead (right): ${(rightExtractMs - rightUnionMs).toFixed(2)}ms`);
|
|
console.log(`Aggregate overhead (left): ${(leftOperandMs - leftExtractMs).toFixed(2)}ms`);
|
|
console.log(`Aggregate overhead (right): ${(rightOperandMs - rightExtractMs).toFixed(2)}ms`);
|
|
console.log(`Compare + meta overhead: ${(fullComparatorMs - leftOperandMs - rightOperandMs + nodeIdMs).toFixed(2)}ms`);
|
|
console.log(`AuthChecker overhead: ${(viaAuthCheckerMs - fullComparatorMs).toFixed(2)}ms`);
|
|
|
|
// Check what percentage each part takes
|
|
console.log('\n--- Time Distribution (% of total) ---');
|
|
const total = viaAuthCheckerMs;
|
|
console.log(`Node ID lookup: ${((nodeIdMs / total) * 100).toFixed(1)}%`);
|
|
console.log(`Left union: ${(((leftUnionMs - nodeIdMs) / total) * 100).toFixed(1)}%`);
|
|
console.log(`Right union: ${(((rightUnionMs - nodeIdMs) / total) * 100).toFixed(1)}%`);
|
|
console.log(`Left extract: ${(((leftExtractMs - leftUnionMs) / total) * 100).toFixed(1)}%`);
|
|
console.log(`Right extract: ${(((rightExtractMs - rightUnionMs) / total) * 100).toFixed(1)}%`);
|
|
console.log(`Left aggregate: ${(((leftOperandMs - leftExtractMs) / total) * 100).toFixed(1)}%`);
|
|
console.log(`Right aggregate: ${(((rightOperandMs - rightExtractMs) / total) * 100).toFixed(1)}%`);
|
|
console.log(`Compare + meta: ${(((fullComparatorMs - leftOperandMs - rightOperandMs + nodeIdMs) / total) * 100).toFixed(1)}%`);
|
|
console.log(`AuthChecker overhead: ${(((viaAuthCheckerMs - fullComparatorMs) / total) * 100).toFixed(1)}%`);
|
|
|
|
// Check cost of new Set() creation
|
|
console.log('\n--- Set Creation Overhead ---');
|
|
const setCreation = measure('new Set()', () => new Set());
|
|
console.log(`new Set() per iteration: ${(setCreation.ms / ITERATIONS * 1000).toFixed(4)}µs`);
|
|
console.log(`Set creations per comparator check: ~4-6 (left union, right union, left operand, right operand, authChecker)`);
|
|
console.log(`Estimated Set overhead: ${(setCreation.ms * 5 / ITERATIONS * 1000).toFixed(2)}µs per check`);
|