434 lines
13 KiB
JavaScript
434 lines
13 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 parseSizes(value) {
|
||
|
|
if (!value) return null;
|
||
|
|
return value.split(',').map(item => Number(item.trim())).filter(Number.isFinite);
|
||
|
|
}
|
||
|
|
|
||
|
|
function createRng(seed) {
|
||
|
|
let state = seed >>> 0;
|
||
|
|
return () => {
|
||
|
|
state = (1664525 * state + 1013904223) >>> 0;
|
||
|
|
return state / 0x100000000;
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function randInt(rng, max) {
|
||
|
|
return Math.floor(rng() * max);
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildUsers(arbiter, count, prefix) {
|
||
|
|
for (let i = 0; i < count; i++) {
|
||
|
|
arbiter.addNode(`${prefix}:${i}`, prefix);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildScenario(arbiter, size) {
|
||
|
|
const trueIds = [];
|
||
|
|
const falseIds = [];
|
||
|
|
buildUsers(arbiter, size, 'user');
|
||
|
|
buildUsers(arbiter, size, 'resource');
|
||
|
|
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 });
|
||
|
|
if (isSafe) trueIds.push(i);
|
||
|
|
else falseIds.push(i);
|
||
|
|
}
|
||
|
|
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]
|
||
|
|
}
|
||
|
|
});
|
||
|
|
return { trueIds, falseIds };
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildQueries(size, rng, samples, trueIds, falseIds) {
|
||
|
|
const queries = [];
|
||
|
|
const half = Math.floor(samples / 2);
|
||
|
|
for (let i = 0; i < half; i++) {
|
||
|
|
const userId = trueIds.length ? trueIds[randInt(rng, trueIds.length)] : randInt(rng, size);
|
||
|
|
queries.push({ userKey: `user:${userId}`, objectKey: `resource:${userId}` });
|
||
|
|
}
|
||
|
|
for (let i = 0; i < samples - half; i++) {
|
||
|
|
const userId = falseIds.length ? falseIds[randInt(rng, falseIds.length)] : randInt(rng, size);
|
||
|
|
queries.push({ userKey: `user:${userId}`, objectKey: `resource:${userId}` });
|
||
|
|
}
|
||
|
|
return queries;
|
||
|
|
}
|
||
|
|
|
||
|
|
function measure(label, iterations, fn) {
|
||
|
|
const start = process.hrtime.bigint();
|
||
|
|
for (let i = 0; i < iterations; i++) {
|
||
|
|
fn();
|
||
|
|
}
|
||
|
|
const elapsedMs = Number(process.hrtime.bigint() - start) / 1e6;
|
||
|
|
return { label, elapsedMs, qps: elapsedMs > 0 ? Math.round((iterations / elapsedMs) * 1000) : 0 };
|
||
|
|
}
|
||
|
|
|
||
|
|
function measureMedian(label, iterations, fn, warmups, runs) {
|
||
|
|
for (let w = 0; w < warmups; w++) {
|
||
|
|
for (let i = 0; i < iterations; i++) fn();
|
||
|
|
}
|
||
|
|
|
||
|
|
const samples = [];
|
||
|
|
for (let r = 0; r < runs; r++) {
|
||
|
|
const result = measure(label, iterations, fn);
|
||
|
|
samples.push(result.qps);
|
||
|
|
}
|
||
|
|
|
||
|
|
samples.sort((a, b) => a - b);
|
||
|
|
const mid = Math.floor(samples.length / 2);
|
||
|
|
const median = samples.length % 2 === 0
|
||
|
|
? Math.round(((samples[mid - 1] + samples[mid]) / 2) * 100) / 100
|
||
|
|
: samples[mid];
|
||
|
|
return { label, elapsedMs: null, qps: median };
|
||
|
|
}
|
||
|
|
|
||
|
|
const args = parseArgs(process.argv);
|
||
|
|
const config = {
|
||
|
|
sizes: parseSizes(args.get('sizes')) || [5000, 10000, 20000],
|
||
|
|
samples: Number(args.get('samples') || 2000),
|
||
|
|
seed: Number(args.get('seed') || 42),
|
||
|
|
maxSeconds: Number(args.get('max-seconds') || 60),
|
||
|
|
includeMeta: !args.has('no-meta'),
|
||
|
|
collectValues: !args.has('no-values'),
|
||
|
|
warmupRuns: Number(args.get('warmup-runs') || 2),
|
||
|
|
medianRuns: Number(args.get('median-runs') || 5)
|
||
|
|
};
|
||
|
|
|
||
|
|
const rng = createRng(config.seed);
|
||
|
|
const startTime = Date.now();
|
||
|
|
|
||
|
|
console.log('owa_comparator_micro_bench');
|
||
|
|
console.log('size,samples,meta,values,metric,elapsed_ms,qps');
|
||
|
|
|
||
|
|
for (const size of config.sizes) {
|
||
|
|
if ((Date.now() - startTime) / 1000 > config.maxSeconds) break;
|
||
|
|
const arbiter = new Arbiter();
|
||
|
|
const { trueIds, falseIds } = buildScenario(arbiter, size);
|
||
|
|
const queries = buildQueries(size, rng, config.samples, trueIds, falseIds);
|
||
|
|
const comparatorRule = arbiter.authChecker.ruleEvaluator.ruleHandlers.relational_comparator.numericRule;
|
||
|
|
const visited = new Set();
|
||
|
|
const baseOptions = { fastPath: true, includeMeta: config.includeMeta, collectValues: config.collectValues };
|
||
|
|
const valuesOptions = { fastPath: true, includeMeta: config.includeMeta, collectValues: true };
|
||
|
|
|
||
|
|
const leftUnionRule = {
|
||
|
|
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 rightUnionRule = {
|
||
|
|
union: {
|
||
|
|
rules: [
|
||
|
|
{ type: 'direct', relation: 'risk_limit' },
|
||
|
|
{ type: 'direct', relation: 'risk_cap' }
|
||
|
|
],
|
||
|
|
aggregator: 'owa',
|
||
|
|
owaWeights: [0.6, 0.4]
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const fullComparator = measureMedian('comparator_full', queries.length, () => {
|
||
|
|
const q = queries[randInt(rng, queries.length)];
|
||
|
|
arbiter.check(q.userKey, 'risk_ok_owa', q.objectKey, baseOptions);
|
||
|
|
}, config.warmupRuns, config.medianRuns);
|
||
|
|
|
||
|
|
const fullComparatorValues = measureMedian('comparator_values_agg', queries.length, () => {
|
||
|
|
const q = queries[randInt(rng, queries.length)];
|
||
|
|
arbiter.check(q.userKey, 'risk_ok_owa', q.objectKey, valuesOptions);
|
||
|
|
}, config.warmupRuns, config.medianRuns);
|
||
|
|
|
||
|
|
const leftUnionEval = measureMedian('left_union_eval', queries.length, () => {
|
||
|
|
const q = queries[randInt(rng, queries.length)];
|
||
|
|
const userId = arbiter.nodeIdByKey.get(q.userKey);
|
||
|
|
const objectId = arbiter.nodeIdByKey.get(q.objectKey);
|
||
|
|
arbiter.authChecker.ruleEvaluator.evaluateRule(
|
||
|
|
userId,
|
||
|
|
q.userKey,
|
||
|
|
objectId,
|
||
|
|
q.objectKey,
|
||
|
|
leftUnionRule,
|
||
|
|
visited,
|
||
|
|
'risk_ok_owa',
|
||
|
|
baseOptions
|
||
|
|
);
|
||
|
|
}, config.warmupRuns, config.medianRuns);
|
||
|
|
|
||
|
|
const rightUnionEval = measureMedian('right_union_eval', queries.length, () => {
|
||
|
|
const q = queries[randInt(rng, queries.length)];
|
||
|
|
const userId = arbiter.nodeIdByKey.get(q.userKey);
|
||
|
|
const objectId = arbiter.nodeIdByKey.get(q.objectKey);
|
||
|
|
arbiter.authChecker.ruleEvaluator.evaluateRule(
|
||
|
|
objectId,
|
||
|
|
q.objectKey,
|
||
|
|
objectId,
|
||
|
|
q.objectKey,
|
||
|
|
rightUnionRule,
|
||
|
|
visited,
|
||
|
|
'risk_ok_owa',
|
||
|
|
baseOptions
|
||
|
|
);
|
||
|
|
}, config.warmupRuns, config.medianRuns);
|
||
|
|
|
||
|
|
const leftExtract = measureMedian('left_extract', queries.length, () => {
|
||
|
|
const q = queries[randInt(rng, queries.length)];
|
||
|
|
const userId = arbiter.nodeIdByKey.get(q.userKey);
|
||
|
|
const objectId = arbiter.nodeIdByKey.get(q.objectKey);
|
||
|
|
const ruleResult = arbiter.authChecker.ruleEvaluator.evaluateRule(
|
||
|
|
userId,
|
||
|
|
q.userKey,
|
||
|
|
objectId,
|
||
|
|
q.objectKey,
|
||
|
|
leftUnionRule,
|
||
|
|
visited,
|
||
|
|
'risk_ok_owa',
|
||
|
|
baseOptions
|
||
|
|
);
|
||
|
|
comparatorRule._extractValues(
|
||
|
|
userId,
|
||
|
|
q.userKey,
|
||
|
|
objectId,
|
||
|
|
q.objectKey,
|
||
|
|
leftUnionRule,
|
||
|
|
'risk_score',
|
||
|
|
ruleResult,
|
||
|
|
'auto',
|
||
|
|
null,
|
||
|
|
24 * 60 * 60 * 1000,
|
||
|
|
null,
|
||
|
|
baseOptions
|
||
|
|
);
|
||
|
|
}, config.warmupRuns, config.medianRuns);
|
||
|
|
|
||
|
|
const rightExtract = measureMedian('right_extract', queries.length, () => {
|
||
|
|
const q = queries[randInt(rng, queries.length)];
|
||
|
|
const userId = arbiter.nodeIdByKey.get(q.userKey);
|
||
|
|
const objectId = arbiter.nodeIdByKey.get(q.objectKey);
|
||
|
|
const ruleResult = arbiter.authChecker.ruleEvaluator.evaluateRule(
|
||
|
|
objectId,
|
||
|
|
q.objectKey,
|
||
|
|
objectId,
|
||
|
|
q.objectKey,
|
||
|
|
rightUnionRule,
|
||
|
|
visited,
|
||
|
|
'risk_ok_owa',
|
||
|
|
baseOptions
|
||
|
|
);
|
||
|
|
comparatorRule._extractValues(
|
||
|
|
objectId,
|
||
|
|
q.objectKey,
|
||
|
|
objectId,
|
||
|
|
q.objectKey,
|
||
|
|
rightUnionRule,
|
||
|
|
'risk_limit',
|
||
|
|
ruleResult,
|
||
|
|
'object',
|
||
|
|
null,
|
||
|
|
24 * 60 * 60 * 1000,
|
||
|
|
null,
|
||
|
|
baseOptions
|
||
|
|
);
|
||
|
|
}, config.warmupRuns, config.medianRuns);
|
||
|
|
|
||
|
|
const leftAggregate = measureMedian('left_aggregate', queries.length, () => {
|
||
|
|
const q = queries[randInt(rng, queries.length)];
|
||
|
|
const userId = arbiter.nodeIdByKey.get(q.userKey);
|
||
|
|
const objectId = arbiter.nodeIdByKey.get(q.objectKey);
|
||
|
|
const ruleResult = arbiter.authChecker.ruleEvaluator.evaluateRule(
|
||
|
|
userId,
|
||
|
|
q.userKey,
|
||
|
|
objectId,
|
||
|
|
q.objectKey,
|
||
|
|
leftUnionRule,
|
||
|
|
visited,
|
||
|
|
'risk_ok_owa',
|
||
|
|
baseOptions
|
||
|
|
);
|
||
|
|
const values = comparatorRule._extractValues(
|
||
|
|
userId,
|
||
|
|
q.userKey,
|
||
|
|
objectId,
|
||
|
|
q.objectKey,
|
||
|
|
leftUnionRule,
|
||
|
|
'risk_score',
|
||
|
|
ruleResult,
|
||
|
|
'auto',
|
||
|
|
null,
|
||
|
|
24 * 60 * 60 * 1000,
|
||
|
|
null,
|
||
|
|
baseOptions
|
||
|
|
);
|
||
|
|
comparatorRule._aggregateCrispValues(values, 'owa', [0.5, 0.3, 0.2], null);
|
||
|
|
}, config.warmupRuns, config.medianRuns);
|
||
|
|
|
||
|
|
const rightAggregate = measureMedian('right_aggregate', queries.length, () => {
|
||
|
|
const q = queries[randInt(rng, queries.length)];
|
||
|
|
const objectId = arbiter.nodeIdByKey.get(q.objectKey);
|
||
|
|
const ruleResult = arbiter.authChecker.ruleEvaluator.evaluateRule(
|
||
|
|
objectId,
|
||
|
|
q.objectKey,
|
||
|
|
objectId,
|
||
|
|
q.objectKey,
|
||
|
|
rightUnionRule,
|
||
|
|
visited,
|
||
|
|
'risk_ok_owa',
|
||
|
|
baseOptions
|
||
|
|
);
|
||
|
|
const values = comparatorRule._extractValues(
|
||
|
|
objectId,
|
||
|
|
q.objectKey,
|
||
|
|
objectId,
|
||
|
|
q.objectKey,
|
||
|
|
rightUnionRule,
|
||
|
|
'risk_limit',
|
||
|
|
ruleResult,
|
||
|
|
'object',
|
||
|
|
null,
|
||
|
|
24 * 60 * 60 * 1000,
|
||
|
|
null,
|
||
|
|
baseOptions
|
||
|
|
);
|
||
|
|
comparatorRule._aggregateCrispValues(values, 'owa', [0.6, 0.4], null);
|
||
|
|
}, config.warmupRuns, config.medianRuns);
|
||
|
|
|
||
|
|
const compareOnly = measureMedian('compare_only', queries.length, () => {
|
||
|
|
const q = queries[randInt(rng, queries.length)];
|
||
|
|
const userId = arbiter.nodeIdByKey.get(q.userKey);
|
||
|
|
const objectId = arbiter.nodeIdByKey.get(q.objectKey);
|
||
|
|
const leftValues = comparatorRule._extractValues(
|
||
|
|
userId,
|
||
|
|
q.userKey,
|
||
|
|
objectId,
|
||
|
|
q.objectKey,
|
||
|
|
leftUnionRule,
|
||
|
|
'risk_score',
|
||
|
|
{ collectedValues: [] },
|
||
|
|
'auto',
|
||
|
|
null,
|
||
|
|
24 * 60 * 60 * 1000,
|
||
|
|
null,
|
||
|
|
baseOptions
|
||
|
|
);
|
||
|
|
const rightValues = comparatorRule._extractValues(
|
||
|
|
objectId,
|
||
|
|
q.objectKey,
|
||
|
|
objectId,
|
||
|
|
q.objectKey,
|
||
|
|
rightUnionRule,
|
||
|
|
'risk_limit',
|
||
|
|
{ collectedValues: [] },
|
||
|
|
'object',
|
||
|
|
null,
|
||
|
|
24 * 60 * 60 * 1000,
|
||
|
|
null,
|
||
|
|
baseOptions
|
||
|
|
);
|
||
|
|
const leftAgg = comparatorRule._aggregateCrispValues(leftValues, 'owa', [0.5, 0.3, 0.2], null);
|
||
|
|
const rightAgg = comparatorRule._aggregateCrispValues(rightValues, 'owa', [0.6, 0.4], null);
|
||
|
|
comparatorRule._compareBlurredValues(
|
||
|
|
{ hasValue: !!leftAgg.interval, valueInterval: leftAgg.interval, operandPossibility: leftAgg.possibility, reliability: leftAgg.reliability },
|
||
|
|
{ hasValue: !!rightAgg.interval, valueInterval: rightAgg.interval, operandPossibility: rightAgg.possibility, reliability: rightAgg.reliability },
|
||
|
|
'<=',
|
||
|
|
'deny',
|
||
|
|
{},
|
||
|
|
{},
|
||
|
|
{}
|
||
|
|
);
|
||
|
|
}, config.warmupRuns, config.medianRuns);
|
||
|
|
|
||
|
|
for (const metric of [
|
||
|
|
fullComparator,
|
||
|
|
fullComparatorValues,
|
||
|
|
leftUnionEval,
|
||
|
|
rightUnionEval,
|
||
|
|
leftExtract,
|
||
|
|
rightExtract,
|
||
|
|
leftAggregate,
|
||
|
|
rightAggregate,
|
||
|
|
compareOnly
|
||
|
|
]) {
|
||
|
|
console.log([
|
||
|
|
size,
|
||
|
|
queries.length,
|
||
|
|
config.includeMeta ? 'on' : 'off',
|
||
|
|
config.collectValues ? 'on' : 'off',
|
||
|
|
metric.label,
|
||
|
|
metric.elapsedMs === null ? 'median' : Math.round(metric.elapsedMs),
|
||
|
|
metric.qps
|
||
|
|
].join(','));
|
||
|
|
}
|
||
|
|
}
|