initial commit: @arbiter/core authorization engine with js-rigor hardening
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.
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
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]
|
||||
}
|
||||
});
|
||||
|
||||
// HEAVY WARMUP - this is key for stable measurements
|
||||
console.log('Warming up (10k iterations)...');
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
arbiter.check(`user:${i % SIZE}`, 'risk_ok_owa', `resource:${i % SIZE}`, { fastPath: true });
|
||||
}
|
||||
console.log('Warmup complete\n');
|
||||
|
||||
const ITERATIONS = 5000;
|
||||
|
||||
function measure(label, fn) {
|
||||
// Run 3 times and take median
|
||||
const times = [];
|
||||
for (let run = 0; run < 3; run++) {
|
||||
const start = process.hrtime.bigint();
|
||||
for (let i = 0; i < ITERATIONS; i++) fn(i % SIZE);
|
||||
times.push(Number(process.hrtime.bigint() - start) / 1e6);
|
||||
}
|
||||
times.sort((a, b) => a - b);
|
||||
const ms = times[1]; // median
|
||||
return { label, ms, qps: Math.round((ITERATIONS / ms) * 1000), perOp: ms / ITERATIONS };
|
||||
}
|
||||
|
||||
console.log('=== FINAL PERFORMANCE ANALYSIS (POST-WARMUP) ===\n');
|
||||
|
||||
const config = arbiter.relationConfigs.get('risk_ok_owa');
|
||||
const ruleEval = arbiter.authChecker.ruleEvaluator;
|
||||
const comparatorHandler = ruleEval.ruleHandlers.relational_comparator.numericRule;
|
||||
|
||||
// Baseline
|
||||
const directCheck = measure('Direct check (single relation)', (idx) => {
|
||||
arbiter.check(`user:${idx}`, 'risk_score', `resource:${idx}`, { fastPath: true });
|
||||
});
|
||||
console.log(`${directCheck.label}: ${directCheck.qps.toLocaleString()} QPS`);
|
||||
|
||||
// Full comparator
|
||||
const fullCheck = measure('Full comparator (via arbiter.check)', (idx) => {
|
||||
arbiter.check(`user:${idx}`, 'risk_ok_owa', `resource:${idx}`, { fastPath: true });
|
||||
});
|
||||
console.log(`${fullCheck.label}: ${fullCheck.qps.toLocaleString()} QPS`);
|
||||
|
||||
console.log(`\nSlowdown: ${(fullCheck.perOp / directCheck.perOp).toFixed(1)}x`);
|
||||
|
||||
console.log('\n--- Component Timings (µs per operation) ---\n');
|
||||
|
||||
// Individual components
|
||||
const results = [];
|
||||
|
||||
results.push(measure('Node ID lookup (x2)', (idx) => {
|
||||
arbiter.nodeIdByKey.get(`user:${idx}`);
|
||||
arbiter.nodeIdByKey.get(`resource:${idx}`);
|
||||
}));
|
||||
|
||||
results.push(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);
|
||||
}));
|
||||
|
||||
results.push(measure('Single direct rule via DirectRule', (idx) => {
|
||||
const userId = arbiter.nodeIdByKey.get(`user:${idx}`);
|
||||
const objectId = arbiter.nodeIdByKey.get(`resource:${idx}`);
|
||||
ruleEval.ruleHandlers.direct.evaluate(userId, `user:${idx}`, objectId, `resource:${idx}`,
|
||||
{ type: 'direct', relation: 'risk_score' }, new Set(), 'test', { fastPath: true });
|
||||
}));
|
||||
|
||||
results.push(measure('Union (3 direct rules)', (idx) => {
|
||||
const userId = arbiter.nodeIdByKey.get(`user:${idx}`);
|
||||
const objectId = arbiter.nodeIdByKey.get(`resource:${idx}`);
|
||||
ruleEval.logicalOperators.evaluateUnion(userId, `user:${idx}`, objectId, `resource:${idx}`,
|
||||
config.left.rule, new Set(), 'test', { fastPath: true });
|
||||
}));
|
||||
|
||||
results.push(measure('Union (2 direct rules)', (idx) => {
|
||||
const objectId = arbiter.nodeIdByKey.get(`resource:${idx}`);
|
||||
ruleEval.logicalOperators.evaluateUnion(objectId, `resource:${idx}`, objectId, `resource:${idx}`,
|
||||
config.right.rule, new Set(), 'test', { fastPath: true });
|
||||
}));
|
||||
|
||||
results.push(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);
|
||||
}));
|
||||
|
||||
results.push(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);
|
||||
}));
|
||||
|
||||
results.push(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 });
|
||||
}));
|
||||
|
||||
for (const r of results) {
|
||||
console.log(`${r.label.padEnd(40)} ${(r.perOp * 1000).toFixed(2).padStart(8)}µs ${r.qps.toLocaleString().padStart(10)} QPS`);
|
||||
}
|
||||
|
||||
console.log(`${'Full arbiter.check'.padEnd(40)} ${(fullCheck.perOp * 1000).toFixed(2).padStart(8)}µs ${fullCheck.qps.toLocaleString().padStart(10)} QPS`);
|
||||
|
||||
console.log('\n=== BOTTLENECK IDENTIFICATION ===\n');
|
||||
|
||||
const nodeIdTime = results[0].perOp * 1000;
|
||||
const indexTime = results[1].perOp * 1000;
|
||||
const singleDirectTime = results[2].perOp * 1000;
|
||||
const union3Time = results[3].perOp * 1000;
|
||||
const union2Time = results[4].perOp * 1000;
|
||||
const leftOpTime = results[5].perOp * 1000;
|
||||
const rightOpTime = results[6].perOp * 1000;
|
||||
const evalRuleTime = results[7].perOp * 1000;
|
||||
const fullTime = fullCheck.perOp * 1000;
|
||||
|
||||
// What SHOULD the times be?
|
||||
console.log('Expected vs Actual:');
|
||||
console.log(` Single direct rule: 1 index lookup + overhead`);
|
||||
console.log(` Expected: ~${indexTime.toFixed(2)}µs, Actual: ${singleDirectTime.toFixed(2)}µs`);
|
||||
console.log(` Union (3 rules): 3 direct rules + OWA fusion`);
|
||||
console.log(` Expected: ~${(singleDirectTime * 3).toFixed(2)}µs, Actual: ${union3Time.toFixed(2)}µs (+${(union3Time - singleDirectTime * 3).toFixed(2)}µs)`);
|
||||
console.log(` Union (2 rules): 2 direct rules + OWA fusion`);
|
||||
console.log(` Expected: ~${(singleDirectTime * 2).toFixed(2)}µs, Actual: ${union2Time.toFixed(2)}µs (+${(union2Time - singleDirectTime * 2).toFixed(2)}µs)`);
|
||||
console.log(` Left operand: union3 + extract + aggregate`);
|
||||
console.log(` Actual: ${leftOpTime.toFixed(2)}µs, Union was: ${union3Time.toFixed(2)}µs (+${(leftOpTime - union3Time).toFixed(2)}µs)`);
|
||||
console.log(` Right operand: union2 + extract + aggregate`);
|
||||
console.log(` Actual: ${rightOpTime.toFixed(2)}µs, Union was: ${union2Time.toFixed(2)}µs (+${(rightOpTime - union2Time).toFixed(2)}µs)`);
|
||||
console.log(` _evaluateRule: left + right + compare`);
|
||||
console.log(` Expected: ~${(leftOpTime + rightOpTime).toFixed(2)}µs, Actual: ${evalRuleTime.toFixed(2)}µs`);
|
||||
console.log(` Full check: _evaluateRule + AuthChecker overhead`);
|
||||
console.log(` Actual: ${fullTime.toFixed(2)}µs, _evaluateRule was: ${evalRuleTime.toFixed(2)}µs (+${(fullTime - evalRuleTime).toFixed(2)}µs)`);
|
||||
|
||||
console.log('\n=== KEY BOTTLENECKS ===\n');
|
||||
console.log(`1. OWA fusion overhead in unions: ~${((union3Time - singleDirectTime * 3) + (union2Time - singleDirectTime * 2)).toFixed(2)}µs total`);
|
||||
console.log(`2. Extract+Aggregate overhead: ~${((leftOpTime - union3Time) + (rightOpTime - union2Time)).toFixed(2)}µs total`);
|
||||
console.log(`3. AuthChecker overhead: ~${(fullTime - evalRuleTime).toFixed(2)}µs`);
|
||||
console.log(`4. Object allocations: ~${(nodeIdTime * 3 + 2).toFixed(2)}µs (Sets, visitKeys, meta objects)`);
|
||||
|
||||
const totalBottleneck = (union3Time - singleDirectTime * 3) + (union2Time - singleDirectTime * 2) +
|
||||
(leftOpTime - union3Time) + (rightOpTime - union2Time) +
|
||||
(fullTime - evalRuleTime);
|
||||
console.log(`\nTotal identified overhead: ~${totalBottleneck.toFixed(2)}µs`);
|
||||
console.log(`Direct baseline (5 lookups): ~${(indexTime * 5).toFixed(2)}µs`);
|
||||
console.log(`Actual total: ${fullTime.toFixed(2)}µs`);
|
||||
console.log(`Overhead factor: ${(fullTime / (indexTime * 5)).toFixed(1)}x`);
|
||||
|
||||
console.log('\n=== PRODUCTION SPEED ESTIMATE ===\n');
|
||||
const targetQPS = 100000;
|
||||
const currentQPS = fullCheck.qps;
|
||||
console.log(`Current: ${currentQPS.toLocaleString()} QPS`);
|
||||
console.log(`Target: ${targetQPS.toLocaleString()} QPS`);
|
||||
console.log(`Need ${(targetQPS / currentQPS).toFixed(1)}x improvement`);
|
||||
console.log(`Need to reduce per-op time from ${fullTime.toFixed(2)}µs to ${(1000000 / targetQPS).toFixed(2)}µs`);
|
||||
console.log(`That's removing ${(fullTime - 1000000 / targetQPS).toFixed(2)}µs of overhead`);
|
||||
Reference in New Issue
Block a user