Files

330 lines
12 KiB
JavaScript
Raw Permalink Normal View History

import { Arbiter } from '../src/core/Arbiter.js';
import fs from 'node:fs';
import { performance } from 'node:perf_hooks';
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;
}
const args = parseArgs(process.argv);
const edges = Number(args.get('edges') || 200000);
const users = Number(args.get('users') || 10000);
const docs = Number(args.get('docs') || 50000);
const groups = Number(args.get('groups') || 500);
const samples = Number(args.get('samples') || 5000);
const snapshotPath = args.get('snapshot-path') || null;
const debug = args.has('debug');
const fastConstruction = args.has('no-fast') ? false : true;
const ensureHits = args.has('ensure-hits');
const fixedPair = ensureHits;
const buildIndices = args.has('no-indices') ? false : true;
const hitPairs = [];
console.log('Arbiter snapshot boot bench');
console.log(` edges: ${edges}`);
console.log(` users: ${users}`);
console.log(` docs: ${docs}`);
console.log(` groups: ${groups}`);
console.log(` samples: ${samples}`);
if (snapshotPath) {
console.log(` snapshot path: ${snapshotPath}`);
}
console.log(` fast construction: ${fastConstruction}`);
console.log(` ensure hits: ${ensureHits}`);
console.log(` fixed pair: ${fixedPair}`);
console.log(` build indices: ${buildIndices}`);
let snapshotBuffer = null;
if (snapshotPath && fs.existsSync(snapshotPath)) {
snapshotBuffer = fs.readFileSync(snapshotPath).buffer;
}
let buildTime = 0;
let snapshotTime = 0;
if (!snapshotBuffer) {
const arbiter = new Arbiter({
fastConstructionMode: fastConstruction,
disableCaching: fastConstruction
});
if (debug) console.log(' building nodes...');
const nodeStart = performance.now();
for (let i = 0; i < users; i++) {
arbiter.addNode(`user:${i}`, 'user');
}
for (let i = 0; i < docs; i++) {
arbiter.addNode(`doc:${i}`, 'doc');
}
for (let i = 0; i < groups; i++) {
arbiter.addNode(`group:${i}`, 'group');
}
const nodeTime = performance.now() - nodeStart;
if (debug) console.log(` nodes ready (${nodeTime.toFixed(2)} ms)`);
if (debug) console.log(' building relations...');
const buildStart = performance.now();
for (let i = 0; i < edges; i++) {
const user = `user:${i % users}`;
const doc = `doc:${i % docs}`;
const group = `group:${i % groups}`;
const rel = ['owner', 'editor', 'viewer'][i % 3];
if (i % 5 === 0) {
arbiter.addRelation(user, 'member', group);
}
if (i % 7 === 0) {
arbiter.addRelation(group, 'viewer', doc);
}
arbiter.addRelation(user, rel, doc);
}
buildTime = performance.now() - buildStart;
if (debug) console.log(` relations ready (${buildTime.toFixed(2)} ms)`);
if (debug) console.log(' compiling rules...');
arbiter.setRelationConfig('member', { type: 'direct' });
arbiter.setRelationConfig('viewer', { type: 'direct' });
arbiter.setRelationConfig('risk', { type: 'direct' });
arbiter.setRelationConfig('threshold', { type: 'direct' });
arbiter.setRelationConfig('can_view', {
union: {
rules: [
{ type: 'direct', relation: 'owner' },
{ type: 'direct', relation: 'editor' },
{ type: 'tuple_to_userset', tuplesetRelation: 'viewer', tuplesetDirection: 'in', computedRelation: 'member' }
]
}
});
arbiter.setRelationConfig('complex_view', {
intersection: {
rules: [
{ type: 'tuple_to_userset', tuplesetRelation: 'viewer', tuplesetDirection: 'in', computedRelation: 'member' },
{
type: 'chain',
steps: [
{ relation: 'member', direction: 'out' },
{ relation: 'viewer', direction: 'out' }
]
},
{
type: 'relational_comparator',
comparator: '>=',
left: {
rule: { type: 'direct', relation: 'risk' },
valueRelation: 'risk',
aggregator: 'max',
evaluateFrom: 'auto'
},
right: {
rule: { type: 'direct', relation: 'threshold' },
valueRelation: 'threshold',
aggregator: 'max',
evaluateFrom: 'auto'
}
}
]
}
});
if (!ensureHits) {
for (let i = 0; i < Math.min(20000, edges); i++) {
arbiter.addRelation(`user:${i % users}`, 'risk', `doc:${i % docs}`, { value: (i % 100) / 100 });
arbiter.addRelation(`user:${i % users}`, 'threshold', `doc:${i % docs}`, { value: 0.5 });
}
}
if (ensureHits) {
const hitCount = Math.min(200, users, docs, groups);
for (let i = 0; i < hitCount; i++) {
const user = `user:${i}`;
const doc = `doc:${i}`;
const group = `group:${i}`;
arbiter.addRelation(user, 'member', group);
arbiter.addRelation(group, 'viewer', doc);
arbiter.addRelation(user, 'risk', doc, { value: 0.9 });
arbiter.addRelation(user, 'threshold', doc, { value: 0.1 });
hitPairs.push({ user, doc });
}
}
if (debug) console.log(' snapshotting...');
const snapshotStart = performance.now();
snapshotBuffer = arbiter.toSnapshotBinary({ dropAdjacencyList: true });
snapshotTime = performance.now() - snapshotStart;
if (debug) console.log(` snapshot ready (${snapshotTime.toFixed(2)} ms)`);
console.log(` node build time: ${nodeTime.toFixed(2)} ms`);
}
const snapshotSize = snapshotBuffer.byteLength;
if (snapshotPath && !fs.existsSync(snapshotPath)) {
fs.writeFileSync(snapshotPath, new Uint8Array(snapshotBuffer));
}
const memBefore = process.memoryUsage();
const loadStart = performance.now();
const booted = Arbiter.fromSnapshotBinary(snapshotBuffer, { buildSnapshotIndices: buildIndices });
const loadTime = performance.now() - loadStart;
const memAfter = process.memoryUsage();
const usersArr = Array.from({ length: users }, (_, i) => `user:${i}`);
const docsArr = Array.from({ length: docs }, (_, i) => `doc:${i}`);
let effectiveHitPairs = hitPairs;
if (ensureHits && effectiveHitPairs.length === 0) {
effectiveHitPairs = [{ user: 'user:0', doc: 'doc:0' }];
}
const hitUsers = ensureHits
? (fixedPair ? [effectiveHitPairs[0].user] : effectiveHitPairs.map(pair => pair.user))
: usersArr;
const hitDocs = ensureHits
? (fixedPair ? [effectiveHitPairs[0].doc] : effectiveHitPairs.map(pair => pair.doc))
: docsArr;
const hitUserIds = hitUsers.map(key => booted.resolveNodeId(key));
const hitDocIds = hitDocs.map(key => booted.resolveNodeId(key));
const complexConfig = booted.relationConfigs.get('complex_view');
const simpleStart = performance.now();
let simpleHits = 0;
for (let i = 0; i < samples; i++) {
const user = hitUsers[i % hitUsers.length];
const doc = hitDocs[i % hitDocs.length];
const result = booted.check(user, 'can_view', doc, { fastPath: true });
if (result?.possibility > 0) simpleHits++;
}
const simpleTime = performance.now() - simpleStart;
const complexStart = performance.now();
let complexHits = 0;
for (let i = 0; i < samples; i++) {
const idx = i % hitUsers.length;
const user = hitUsers[idx];
const doc = hitDocs[idx];
const userId = hitUserIds[idx];
const docId = hitDocIds[idx];
const result = booted.authChecker.ruleEvaluator.evaluateRule(
userId,
user,
docId,
doc,
complexConfig,
new Set(),
'complex_view',
{ fastPath: false, collectValues: true, cacheRuleResult: false }
);
if (result?.possibility > 0) complexHits++;
}
const complexTime = performance.now() - complexStart;
console.log('\nResults');
console.log(` build time: ${buildTime.toFixed(2)} ms`);
console.log(` snapshot time: ${snapshotTime.toFixed(2)} ms`);
console.log(` snapshot size: ${(snapshotSize / 1024 / 1024).toFixed(2)} MB`);
console.log(` load time: ${loadTime.toFixed(2)} ms`);
console.log(` memory rss before: ${(memBefore.rss / 1024 / 1024).toFixed(2)} MB`);
console.log(` memory rss after: ${(memAfter.rss / 1024 / 1024).toFixed(2)} MB`);
console.log(` memory heap before: ${(memBefore.heapUsed / 1024 / 1024).toFixed(2)} MB`);
console.log(` memory heap after: ${(memAfter.heapUsed / 1024 / 1024).toFixed(2)} MB`);
console.log(` simple avg: ${(simpleTime / samples * 1000).toFixed(3)} µs`);
console.log(` complex avg: ${(complexTime / samples * 1000).toFixed(3)} µs`);
console.log(` simple hits: ${simpleHits}`);
console.log(` complex hits: ${complexHits}`);
if (ensureHits && debug) {
const sampleUser = hitUsers[0];
const sampleDoc = hitDocs[0];
const sampleSimple = booted.check(sampleUser, 'can_view', sampleDoc, { fastPath: true });
const sampleComplex = booted.check(sampleUser, 'complex_view', sampleDoc, { fastPath: false, collectValues: true, includeMeta: true });
console.log(` sample can_view possibility: ${sampleSimple?.possibility ?? 'null'}`);
console.log(` sample complex_view possibility: ${sampleComplex?.possibility ?? 'null'}`);
if (sampleComplex?.meta) {
console.log(` sample complex meta: ${JSON.stringify(sampleComplex.meta)}`);
}
const complexConfig = booted.relationConfigs.get('complex_view');
const userId = booted.resolveNodeId(sampleUser);
const objectId = booted.resolveNodeId(sampleDoc);
if (userId !== undefined && objectId !== undefined) {
const evaluator = booted.authChecker.ruleEvaluator;
const visited = new Set();
const tupleRule = { type: 'tuple_to_userset', tuplesetRelation: 'viewer', tuplesetDirection: 'in', computedRelation: 'member' };
const chainRule = {
type: 'chain',
steps: [
{ relation: 'member', direction: 'out' },
{ relation: 'viewer', direction: 'out' }
]
};
const compRule = {
type: 'relational_comparator',
comparator: '>=',
left: {
rule: { type: 'direct', relation: 'risk' },
valueRelation: 'risk',
aggregator: 'max',
evaluateFrom: 'auto'
},
right: {
rule: { type: 'direct', relation: 'threshold' },
valueRelation: 'threshold',
aggregator: 'max',
evaluateFrom: 'auto'
}
};
const tupleResult = evaluator.evaluateRule(userId, sampleUser, objectId, sampleDoc, tupleRule, visited, 'tuple_debug', { fastPath: false, collectValues: true });
const chainResult = evaluator.evaluateRule(userId, sampleUser, objectId, sampleDoc, chainRule, visited, 'chain_debug', { fastPath: false, collectValues: true });
const compResult = evaluator.evaluateRule(userId, sampleUser, objectId, sampleDoc, compRule, visited, 'comp_debug', { fastPath: false, collectValues: true, includeMeta: true });
console.log(` tuple_to_userset possibility: ${tupleResult?.possibility ?? 'null'}`);
console.log(` chain possibility: ${chainResult?.possibility ?? 'null'}`);
console.log(` comparator possibility: ${compResult?.possibility ?? 'null'}`);
if (compResult?.meta) {
console.log(` comparator meta: ${JSON.stringify(compResult.meta)}`);
}
const riskRel = booted.relationManager.getDirectRelation(userId, 'risk', objectId);
const thresholdRel = booted.relationManager.getDirectRelation(userId, 'threshold', objectId);
console.log(` risk relation value: ${riskRel?.value ?? 'null'}`);
console.log(` threshold relation value: ${thresholdRel?.value ?? 'null'}`);
if (complexConfig) {
const compiled = complexConfig._compiled;
if (compiled && compiled.type === 'logical') {
const childTypes = compiled.children.map(child => child?.type || 'null');
console.log(` complex compiled children: ${JSON.stringify(childTypes)}`);
const compiledEvaluator = evaluator.compiledEvaluator;
const childPoss = compiled.children.map(child => {
const res = compiledEvaluator.evaluate(child, userId, sampleUser, objectId, sampleDoc, new Set(), 'complex_child', { fastPath: false, collectValues: true, includeMeta: true });
return res?.possibility ?? null;
});
console.log(` complex compiled child possibilities: ${JSON.stringify(childPoss)}`);
}
const directComplex = evaluator.evaluateRule(userId, sampleUser, objectId, sampleDoc, complexConfig, new Set(), 'complex_debug', { fastPath: false, collectValues: true, includeMeta: true });
console.log(` direct complex possibility: ${directComplex?.possibility ?? 'null'}`);
if (directComplex?.meta) {
console.log(` direct complex meta: ${JSON.stringify(directComplex.meta)}`);
}
}
}
}