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.
197 lines
6.3 KiB
JavaScript
197 lines
6.3 KiB
JavaScript
import { Arbiter } from '../src/core/Arbiter.js';
|
|
|
|
function parseArgs(argv) {
|
|
const args = new Map();
|
|
for (let i = 2; i < argv.length; i++) {
|
|
const value = argv[i];
|
|
if (!value.startsWith('--')) continue;
|
|
const [key, inline] = value.slice(2).split('=');
|
|
if (inline !== undefined) {
|
|
args.set(key, inline);
|
|
continue;
|
|
}
|
|
const next = argv[i + 1];
|
|
if (next && !next.startsWith('--')) {
|
|
args.set(key, next);
|
|
i++;
|
|
} else {
|
|
args.set(key, true);
|
|
}
|
|
}
|
|
return args;
|
|
}
|
|
|
|
function createRng(seed) {
|
|
let state = seed >>> 0;
|
|
return () => {
|
|
state = (1664525 * state + 1013904223) >>> 0;
|
|
return state / 0x100000000;
|
|
};
|
|
}
|
|
|
|
function parseSizes(value) {
|
|
if (!value) return null;
|
|
return value.split(',').map(item => Number(item.trim())).filter(Number.isFinite);
|
|
}
|
|
|
|
function formatBytes(bytes) {
|
|
if (bytes === 0) return '0B';
|
|
const units = ['B', 'KB', 'MB', 'GB'];
|
|
const index = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)));
|
|
const scaled = bytes / Math.pow(1024, index);
|
|
return `${scaled.toFixed(2)}${units[index]}`;
|
|
}
|
|
|
|
function buildGraph({ nodes, edgesPerNode, rng, enableReachability, authMode, maxDepth }) {
|
|
const arbiter = new Arbiter();
|
|
for (let i = 0; i < nodes; i++) {
|
|
arbiter.addNode(`node:${i}`, 'node');
|
|
}
|
|
|
|
const totalEdges = Math.max(1, Math.round(nodes * edgesPerNode));
|
|
for (let i = 0; i < totalEdges; i++) {
|
|
const src = Math.floor(rng() * nodes);
|
|
const dst = Math.floor(rng() * nodes);
|
|
if (src === dst) continue;
|
|
arbiter.addRelation(`node:${src}`, 'link', `node:${dst}`, 1.0);
|
|
}
|
|
|
|
arbiter.setRelationConfig('link', { type: 'direct' });
|
|
arbiter.setRelationConfig('can_reach', {
|
|
type: 'multi_hop',
|
|
relation: 'link',
|
|
maxDepth,
|
|
skipReachabilityCheck: true,
|
|
collectValues: false,
|
|
trackPaths: false,
|
|
fallbackToBasicPaths: false
|
|
});
|
|
|
|
if (enableReachability) {
|
|
arbiter.graphManager.initializeReachabilityChecker({ enableBackwardIndex: true });
|
|
}
|
|
|
|
return { arbiter, totalEdges };
|
|
}
|
|
|
|
function runAuthChecks({ arbiter, nodes, rng, samples, authMode }) {
|
|
const start = process.hrtime.bigint();
|
|
for (let i = 0; i < samples; i++) {
|
|
const src = Math.floor(rng() * nodes);
|
|
const dst = Math.floor(rng() * nodes);
|
|
const relation = authMode === 'direct' ? 'link' : 'can_reach';
|
|
arbiter.check(`node:${src}`, relation, `node:${dst}`, { fastPath: true });
|
|
}
|
|
const elapsedMs = Number(process.hrtime.bigint() - start) / 1e6;
|
|
const qps = elapsedMs > 0 ? Math.round((samples / elapsedMs) * 1000) : 0;
|
|
return { elapsedMs, qps };
|
|
}
|
|
|
|
function runMutationBench({ arbiter, nodes, rng, samples }) {
|
|
const keys = [];
|
|
const insertStart = process.hrtime.bigint();
|
|
for (let i = 0; i < samples; i++) {
|
|
const src = Math.floor(rng() * nodes);
|
|
const dst = Math.floor(rng() * nodes);
|
|
const srcKey = `node:${src}`;
|
|
const dstKey = `node:${dst}`;
|
|
keys.push([srcKey, dstKey]);
|
|
arbiter.addRelation(srcKey, 'link', dstKey, 1.0);
|
|
}
|
|
const insertMs = Number(process.hrtime.bigint() - insertStart) / 1e6;
|
|
const insertQps = insertMs > 0 ? Math.round((samples / insertMs) * 1000) : 0;
|
|
|
|
const updateStart = process.hrtime.bigint();
|
|
for (const [srcKey, dstKey] of keys) {
|
|
arbiter.addRelation(srcKey, 'link', dstKey, { possibility: 0.7 });
|
|
}
|
|
const updateMs = Number(process.hrtime.bigint() - updateStart) / 1e6;
|
|
const updateQps = updateMs > 0 ? Math.round((samples / updateMs) * 1000) : 0;
|
|
|
|
const deleteStart = process.hrtime.bigint();
|
|
for (const [srcKey, dstKey] of keys) {
|
|
arbiter.removeRelation(srcKey, 'link', dstKey);
|
|
}
|
|
const deleteMs = Number(process.hrtime.bigint() - deleteStart) / 1e6;
|
|
const deleteQps = deleteMs > 0 ? Math.round((samples / deleteMs) * 1000) : 0;
|
|
|
|
return { insertMs, insertQps, updateMs, updateQps, deleteMs, deleteQps };
|
|
}
|
|
|
|
function captureMemory() {
|
|
if (global.gc) {
|
|
global.gc();
|
|
}
|
|
const memory = process.memoryUsage();
|
|
return {
|
|
rss: memory.rss,
|
|
heapUsed: memory.heapUsed,
|
|
heapTotal: memory.heapTotal
|
|
};
|
|
}
|
|
|
|
const args = parseArgs(process.argv);
|
|
const config = {
|
|
sizes: parseSizes(args.get('sizes')) || [5000, 10000, 25000, 50000, 100000, 200000],
|
|
edgesPerNode: Number(args.get('edges-per-node') || 4),
|
|
seed: Number(args.get('seed') || 42),
|
|
maxSeconds: Number(args.get('max-seconds') || 60),
|
|
reachability: args.get('reachability') === 'true',
|
|
authSamples: Number(args.get('auth-samples') || 20000),
|
|
mutationSamples: Number(args.get('mutation-samples') || 5000),
|
|
authMode: args.get('auth-mode') || 'multi-hop',
|
|
maxDepth: Number(args.get('max-depth') || 4)
|
|
};
|
|
|
|
const rng = createRng(config.seed);
|
|
const start = Date.now();
|
|
|
|
console.log('arbiter_scaling');
|
|
console.log(`edges_per_node=${config.edgesPerNode} reachability=${config.reachability}`);
|
|
console.log(`auth_mode=${config.authMode} max_depth=${config.maxDepth}`);
|
|
console.log('nodes,edges,build_ms,reachability_ms,auth_ms,auth_qps,insert_ms,insert_qps,update_ms,update_qps,delete_ms,delete_qps,rss,heap_used,heap_total');
|
|
|
|
for (const nodes of config.sizes) {
|
|
if ((Date.now() - start) / 1000 > config.maxSeconds) break;
|
|
const beforeMemory = captureMemory();
|
|
const buildStart = Date.now();
|
|
const { arbiter, totalEdges } = buildGraph({
|
|
nodes,
|
|
edgesPerNode: config.edgesPerNode,
|
|
rng,
|
|
enableReachability: false,
|
|
authMode: config.authMode,
|
|
maxDepth: config.maxDepth
|
|
});
|
|
const buildMs = Date.now() - buildStart;
|
|
|
|
let reachabilityMs = 0;
|
|
if (config.reachability) {
|
|
const reachStart = Date.now();
|
|
arbiter.graphManager.initializeReachabilityChecker({ enableBackwardIndex: true });
|
|
reachabilityMs = Date.now() - reachStart;
|
|
}
|
|
|
|
const auth = runAuthChecks({ arbiter, nodes, rng, samples: config.authSamples, authMode: config.authMode });
|
|
const mutations = runMutationBench({ arbiter, nodes, rng, samples: config.mutationSamples });
|
|
|
|
const afterMemory = captureMemory();
|
|
console.log([
|
|
nodes,
|
|
totalEdges,
|
|
buildMs,
|
|
reachabilityMs,
|
|
Math.round(auth.elapsedMs),
|
|
auth.qps,
|
|
Math.round(mutations.insertMs),
|
|
mutations.insertQps,
|
|
Math.round(mutations.updateMs),
|
|
mutations.updateQps,
|
|
Math.round(mutations.deleteMs),
|
|
mutations.deleteQps,
|
|
formatBytes(afterMemory.rss - beforeMemory.rss),
|
|
formatBytes(afterMemory.heapUsed - beforeMemory.heapUsed),
|
|
formatBytes(afterMemory.heapTotal - beforeMemory.heapTotal)
|
|
].join(','));
|
|
}
|