Files
core/benchmarks/profile-comparator.js
John Dvorak 717ae1031e 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.
2026-07-31 13:44:06 -07:00

178 lines
6.5 KiB
JavaScript

import { Arbiter } from '../src/core/Arbiter.js';
// Create scenario matching owa_comparator_nested
const arbiter = new Arbiter();
const SIZE = 5000;
// Build nodes
for (let i = 0; i < SIZE; i++) {
arbiter.addNode(`user:${i}`, 'user');
arbiter.addNode(`resource:${i}`, 'resource');
}
// Build relations (same as owa-comparator-micro-bench)
for (let i = 0; i < SIZE; i++) {
const isSafe = i % 2 === 0;
const score = isSafe ? 20 : 80;
const bonus = isSafe ? 5 : 15;
const noise = isSafe ? 0 : 10;
arbiter.addRelation(`user:${i}`, 'risk_score', `resource:${i}`, 1.0, { value: score });
arbiter.addRelation(`user:${i}`, 'risk_bonus', `resource:${i}`, 1.0, { value: bonus });
arbiter.addRelation(`user:${i}`, 'risk_noise', `resource:${i}`, 1.0, { value: noise });
arbiter.addRelation(`resource:${i}`, 'risk_limit', `resource:${i}`, 1.0, { value: 40 });
arbiter.addRelation(`resource:${i}`, 'risk_cap', `resource:${i}`, 1.0, { value: 45 });
}
// Set relation configs
arbiter.setRelationConfig('risk_score', { type: 'direct' });
arbiter.setRelationConfig('risk_bonus', { type: 'direct' });
arbiter.setRelationConfig('risk_noise', { type: 'direct' });
arbiter.setRelationConfig('risk_limit', { type: 'direct' });
arbiter.setRelationConfig('risk_cap', { 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 });
}
// Profile with timing at each step
const ITERATIONS = 1000;
let totalMs = 0;
// Detailed timing breakdown
const timings = {
total: 0,
nodeIdLookup: 0,
configGet: 0,
visitedCheck: 0,
ruleEval: 0
};
const start = process.hrtime.bigint();
for (let i = 0; i < ITERATIONS; i++) {
const idx = i % SIZE;
arbiter.check(`user:${idx}`, 'risk_ok_owa', `resource:${idx}`, { fastPath: true });
}
totalMs = Number(process.hrtime.bigint() - start) / 1e6;
const qps = Math.round((ITERATIONS / totalMs) * 1000);
console.log(`Total: ${totalMs.toFixed(2)}ms for ${ITERATIONS} checks = ${qps} QPS`);
console.log(`Per check: ${(totalMs / ITERATIONS).toFixed(4)}ms`);
// Now let's profile object allocations
console.log('\n--- Checking allocation patterns ---');
// Check visited set behavior
const visited = new Set();
const visitKey1 = { userKey: 'user:0', relation: 'test', objectKey: 'resource:0' };
const visitKey2 = { userKey: 'user:0', relation: 'test', objectKey: 'resource:0' };
visited.add(visitKey1);
console.log(`Same keys, different objects in Set: ${visited.has(visitKey2)}`); // false - reference equality
// Count how many allocations happen per check
console.log('\n--- Checking comparison: direct vs comparator ---');
// Profile direct check
const directStart = process.hrtime.bigint();
for (let i = 0; i < ITERATIONS; i++) {
const idx = i % SIZE;
arbiter.check(`user:${idx}`, 'risk_score', `resource:${idx}`, { fastPath: true });
}
const directMs = Number(process.hrtime.bigint() - directStart) / 1e6;
console.log(`Direct: ${directMs.toFixed(2)}ms = ${Math.round((ITERATIONS / directMs) * 1000)} QPS`);
// Profile union eval only
const unionRule = {
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 unionStart = process.hrtime.bigint();
for (let i = 0; i < ITERATIONS; i++) {
const idx = i % SIZE;
const userId = arbiter.nodeIdByKey.get(`user:${idx}`);
const objectId = arbiter.nodeIdByKey.get(`resource:${idx}`);
arbiter.authChecker.ruleEvaluator.evaluateRule(
userId, `user:${idx}`, objectId, `resource:${idx}`,
unionRule, new Set(), 'risk_ok_owa', { fastPath: true }
);
}
const unionMs = Number(process.hrtime.bigint() - unionStart) / 1e6;
console.log(`Union eval: ${unionMs.toFixed(2)}ms = ${Math.round((ITERATIONS / unionMs) * 1000)} QPS`);
// What's the overhead of authChecker.check vs direct ruleEvaluator call?
const checkStart = process.hrtime.bigint();
for (let i = 0; i < ITERATIONS; i++) {
const idx = i % SIZE;
arbiter.authChecker.check(`user:${idx}`, 'risk_ok_owa', `resource:${idx}`, { fastPath: true });
}
const checkMs = Number(process.hrtime.bigint() - checkStart) / 1e6;
console.log(`authChecker.check: ${checkMs.toFixed(2)}ms = ${Math.round((ITERATIONS / checkMs) * 1000)} QPS`);
// Direct to evaluateRule for relational_comparator
const comparatorRule = arbiter.relationConfigs.get('risk_ok_owa');
const comparatorStart = process.hrtime.bigint();
for (let i = 0; i < ITERATIONS; i++) {
const idx = i % SIZE;
const userId = arbiter.nodeIdByKey.get(`user:${idx}`);
const objectId = arbiter.nodeIdByKey.get(`resource:${idx}`);
arbiter.authChecker.ruleEvaluator.evaluateRule(
userId, `user:${idx}`, objectId, `resource:${idx}`,
comparatorRule, new Set(), 'risk_ok_owa', { fastPath: true }
);
}
const comparatorMs = Number(process.hrtime.bigint() - comparatorStart) / 1e6;
console.log(`ruleEvaluator.evaluateRule (comparator): ${comparatorMs.toFixed(2)}ms = ${Math.round((ITERATIONS / comparatorMs) * 1000)} QPS`);
console.log('\n--- Summary ---');
console.log(`Direct check baseline: ${Math.round((ITERATIONS / directMs) * 1000)} QPS`);
console.log(`Union eval: ${Math.round((ITERATIONS / unionMs) * 1000)} QPS (${(unionMs/directMs).toFixed(1)}x slower than direct)`);
console.log(`Full comparator via ruleEvaluator: ${Math.round((ITERATIONS / comparatorMs) * 1000)} QPS (${(comparatorMs/directMs).toFixed(1)}x slower than direct)`);
console.log(`Full comparator via authChecker: ${Math.round((ITERATIONS / checkMs) * 1000)} QPS (${(checkMs/directMs).toFixed(1)}x slower than direct)`);