import { Arbiter } from '../src/core/Arbiter.js'; import { OWAFusion } from '../src/utils/OWAFusion.js'; function parseArgNumber(args, name, fallback) { const idx = args.indexOf(name); if (idx === -1 || idx + 1 >= args.length) return fallback; const value = Number(args[idx + 1]); return Number.isFinite(value) ? value : fallback; } function hasArg(args, name) { return args.includes(name); } class Profiler { constructor() { this.stats = new Map(); } _record(label, deltaMs) { let entry = this.stats.get(label); if (!entry) { entry = { calls: 0, totalMs: 0 }; this.stats.set(label, entry); } entry.calls += 1; entry.totalMs += deltaMs; } wrap(obj, methodName, label) { if (!obj || typeof obj[methodName] !== 'function') return; const original = obj[methodName]; if (original.__profiled) return; const profiler = this; const wrapped = function(...args) { const start = process.hrtime.bigint(); try { return original.apply(this, args); } finally { const deltaMs = Number(process.hrtime.bigint() - start) / 1e6; profiler._record(label, deltaMs); } }; wrapped.__profiled = true; obj[methodName] = wrapped; } report() { const entries = Array.from(this.stats.entries()).map(([label, data]) => ({ label, calls: data.calls, totalMs: data.totalMs, avgMs: data.calls ? data.totalMs / data.calls : 0 })); entries.sort((a, b) => b.totalMs - a.totalMs); console.log('profile:function_ms'); for (const entry of entries) { console.log(` ${entry.label} calls=${entry.calls} total_ms=${entry.totalMs.toFixed(3)} avg_ms=${entry.avgMs.toFixed(6)}`); } } } function buildOwaComparatorScenario(arbiter, size) { for (let i = 0; i < size; i++) { arbiter.addNode(`user:${i}`, 'user'); arbiter.addNode(`resource:${i}`, 'resource'); arbiter.addRelation(`user:${i}`, 'risk_score', `resource:${i}`, 1.0, { value: i % 2 === 0 ? 20 : 80 }); arbiter.addRelation(`user:${i}`, 'risk_bonus', `resource:${i}`, 1.0, { value: i % 2 === 0 ? 5 : 15 }); arbiter.addRelation(`user:${i}`, 'risk_noise', `resource:${i}`, 1.0, { value: i % 2 === 0 ? 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 }); } 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_union_left', { 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] } }); arbiter.setRelationConfig('risk_union_right', { union: { rules: [ { type: 'direct', relation: 'risk_limit' }, { type: 'direct', relation: 'risk_cap' } ], aggregator: 'owa', owaWeights: [0.6, 0.4] } }); 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] } }); } function resetSortCounter() { OWAFusion.sortCounter = { total: 0, byMode: {}, byMethod: {} }; return OWAFusion.sortCounter; } function snapshotSortCounter() { const counter = OWAFusion.sortCounter || { total: 0, byMode: {}, byMethod: {} }; return { total: counter.total || 0, byMode: { ...counter.byMode }, byMethod: { ...counter.byMethod } }; } function runTrace(arbiter, relation, userKey, objectKey, label) { resetSortCounter(); const start = process.hrtime.bigint(); const result = arbiter.check(userKey, relation, objectKey, { fastPath: true, includeMeta: true, cacheRuleResult: false }); const durationMs = Number(process.hrtime.bigint() - start) / 1e6; const counts = snapshotSortCounter(); console.log(`trace:${label}`); console.log(` result=${result.possibility.toFixed(3)} duration_ms=${durationMs.toFixed(3)}`); console.log(` sorts_total=${counts.total}`); console.log(` sorts_by_method=${JSON.stringify(counts.byMethod)}`); console.log(` sorts_by_mode=${JSON.stringify(counts.byMode)}`); } function runBench(arbiter, relation, size, runs, label, useResourceKey = false) { const userKey = `user:${Math.floor(size / 2)}`; const objectKey = `resource:${Math.floor(size / 2)}`; const subjectKey = useResourceKey ? objectKey : userKey; const targetKey = useResourceKey ? objectKey : objectKey; resetSortCounter(); for (let i = 0; i < 200; i++) { arbiter.check(subjectKey, relation, targetKey, { fastPath: true, cacheRuleResult: false }); } const start = process.hrtime.bigint(); for (let i = 0; i < runs; i++) { arbiter.check(subjectKey, relation, targetKey, { fastPath: true, cacheRuleResult: false }); } const durationMs = Number(process.hrtime.bigint() - start) / 1e6; const counts = snapshotSortCounter(); const perQuery = durationMs / runs; const sortsPerQuery = runs > 0 ? counts.total / runs : 0; console.log(`bench:${label}`); console.log(` runs=${runs} total_ms=${durationMs.toFixed(2)} per_query_ms=${perQuery.toFixed(4)}`); console.log(` sorts_total=${counts.total} sorts_per_query=${sortsPerQuery.toFixed(3)}`); console.log(` sorts_by_method=${JSON.stringify(counts.byMethod)}`); console.log(` sorts_by_mode=${JSON.stringify(counts.byMode)}`); } const args = process.argv.slice(2); const size = parseArgNumber(args, '--size', 10000); const runs = parseArgNumber(args, '--runs', 20000); const traceOne = hasArg(args, '--trace-one'); const profile = hasArg(args, '--profile'); const arbiter = new Arbiter({ enableRuleResultCache: false, disableCaching: true }); buildOwaComparatorScenario(arbiter, size); let profiler = null; if (profile) { profiler = new Profiler(); profiler.wrap(arbiter, 'check', 'Arbiter.check'); profiler.wrap(arbiter.authChecker, 'check', 'AuthorizationChecker.check'); profiler.wrap(arbiter.authChecker.ruleEvaluator, 'evaluateRule', 'RuleEvaluator.evaluateRule'); profiler.wrap(arbiter.authChecker.ruleEvaluator.logicalOperators, 'evaluateUnion', 'LogicalOperators.evaluateUnion'); profiler.wrap(arbiter.authChecker.ruleEvaluator.ruleHandlers.direct, 'evaluate', 'DirectRule.evaluate'); const comparator = arbiter.authChecker.ruleEvaluator.ruleHandlers.relational_comparator?.rule; if (comparator) { profiler.wrap(comparator, '_evaluateOperand', 'RelationalComparatorRule._evaluateOperand'); profiler.wrap(comparator, '_extractValues', 'RelationalComparatorRule._extractValues'); profiler.wrap(comparator, '_aggregateCrispValues', 'RelationalComparatorRule._aggregateCrispValues'); profiler.wrap(comparator, '_compareBlurredValues', 'RelationalComparatorRule._compareBlurredValues'); } profiler.wrap(OWAFusion, 'fuseWithMeta', 'OWAFusion.fuseWithMeta'); profiler.wrap(OWAFusion, 'fuseTriplesWithMeta', 'OWAFusion.fuseTriplesWithMeta'); } if (traceOne) { const userKey = `user:${Math.floor(size / 2)}`; const objectKey = `resource:${Math.floor(size / 2)}`; const resourceKey = objectKey; runTrace(arbiter, 'risk_union_left', userKey, objectKey, 'union_left'); runTrace(arbiter, 'risk_union_right', resourceKey, resourceKey, 'union_right'); runTrace(arbiter, 'risk_ok_owa', userKey, objectKey, 'comparator_nested'); } else { runBench(arbiter, 'risk_union_left', size, Math.max(1000, Math.floor(runs / 2)), 'union_left'); runBench(arbiter, 'risk_union_right', size, Math.max(1000, Math.floor(runs / 2)), 'union_right', true); runBench(arbiter, 'risk_ok_owa', size, runs, 'comparator_nested'); } if (profiler) { profiler.report(); }