import { performance } from 'perf_hooks'; import { Arbiter } from '../src/index.js'; class Benchmark { constructor(name) { this.name = name; this.results = []; this.labels = []; } async run(fn, iterations = 1000, label = '') { for (let i = 0; i < 10; i++) await fn(); const times = []; for (let i = 0; i < iterations; i++) { const start = performance.now(); await fn(); const end = performance.now(); times.push(end - start); } const avg = times.reduce((a, b) => a + b, 0) / times.length; const min = Math.min(...times); const max = Math.max(...times); const p95 = times.sort((a, b) => a - b)[Math.floor(times.length * 0.95)]; this.results.push({ avg: avg.toFixed(3), min: min.toFixed(3), max: max.toFixed(3), p95: p95.toFixed(3) }); this.labels.push(label); return { avg, min, max, p95 }; } report() { console.log('Configuration | Avg (ms) | Min (ms) | Max (ms) | P95 (ms) | Speedup'); console.log('---------------------------------|----------|----------|----------|----------|--------'); const baselineTime = parseFloat(this.results[0].avg); this.results.forEach((result, i) => { const label = this.labels[i] || `Test ${i + 1}`; const speedup = i === 0 ? '1.00x' : `${(baselineTime / parseFloat(result.avg)).toFixed(2)}x`; console.log(`${label.padEnd(32)} | ${result.avg.padStart(8)} | ${result.min.padStart(8)} | ${result.max.padStart(8)} | ${result.p95.padStart(8)} | ${speedup.padStart(6)}`); }); } } function setupGraph(scale = 'medium') { const scales = { medium: { users: 2000, resources: 1000 }, xlarge: { users: 20000, resources: 5000 } }; const config = scales[scale]; const arbiter = new Arbiter({ fastConstructionMode: true }); const users = [], resources = []; for (let i = 0; i < config.users; i++) { const userKey = `user:user${i}`; arbiter.addNode(userKey, 'user'); // Assign a random balance between 100 and 10000 arbiter.addRelation(userKey, 'has_balance', 'balance', { value: Math.floor(Math.random() * 9900) + 100 }); users.push(userKey); } for (let i = 0; i < config.resources; i++) { const resKey = `resource:res${i}`; arbiter.addNode(resKey, 'resource'); // Assign a random price between 50 and 5000 arbiter.addRelation(resKey, 'has_price', 'price', { value: Math.floor(Math.random() * 4950) + 50 }); resources.push(resKey); } // Direct access for a subset for (let i = 0; i < Math.floor(config.users * 0.1); i++) { const user = users[i]; const res = resources[i % resources.length]; arbiter.addRelation(user, 'can_access', res); } // ChainRule: user->can_access->resource arbiter.setRelationConfig('can_access', { type: 'direct' }); arbiter.setRelationConfig('chain_access', { type: 'chain', steps: [ { relation: 'can_access', direction: 'out' } ] }); // RelationalComparatorRule: user.balance >= resource.price arbiter.setRelationConfig('balance_check', { type: 'relational_comparator', leftOperand: { rule: { type: 'direct', relation: 'has_balance' }, extractValue: true, valueRelation: 'has_balance', aggregator: 'max' }, rightOperand: { rule: { type: 'direct', relation: 'has_price' }, extractValue: true, valueRelation: 'has_price', aggregator: 'max' }, comparator: '>=', fallbackBehavior: 'deny' }); return { arbiter, users, resources }; } async function runRelationalComparatorBenchmarks() { for (const scale of ['medium', 'xlarge']) { console.log(`\n🔬 RelationalComparatorRule Benchmark (${scale.toUpperCase()})`); const { arbiter, users, resources } = setupGraph(scale); const benchmark = new Benchmark(`RelationalComparatorRule (${scale})`); // Direct access baseline await benchmark.run(() => { const user = users[Math.floor(Math.random() * users.length)]; const res = resources[Math.floor(Math.random() * resources.length)]; arbiter.check(user, 'can_access', res); }, scale === 'medium' ? 500 : 100, 'Direct Access (baseline)'); // ChainRule existence await benchmark.run(() => { const user = users[Math.floor(Math.random() * users.length)]; const res = resources[Math.floor(Math.random() * resources.length)]; arbiter.check(user, 'chain_access', res); }, scale === 'medium' ? 500 : 100, 'ChainRule Existence'); // RelationalComparatorRule (value-based) await benchmark.run(() => { const user = users[Math.floor(Math.random() * users.length)]; const res = resources[Math.floor(Math.random() * resources.length)]; arbiter.check(user, 'balance_check', res); }, scale === 'medium' ? 500 : 100, 'RelationalComparatorRule (balance >= price)'); // --- Complex, realistic scenario: sum of all balances user can debit vs. min plan/feature price --- // Setup: users, accounts, features, plans, prices, debit rights const accounts = [], plans = [], features = []; for (let i = 0; i < (scale === 'medium' ? 1000 : 5000); i++) { const accKey = `account:acc${i}`; accounts.push(accKey); arbiter.addNode(accKey, 'account'); // Each account has a balance (with timestamp for decay) arbiter.addRelation(accKey, 'has_balance', 'unit:usd', { value: Math.floor(Math.random() * 9900) + 100, updated_last_at: Date.now() - Math.floor(Math.random() * 48 * 60 * 60 * 1000) // up to 48h old }); } for (let i = 0; i < (scale === 'medium' ? 100 : 500); i++) { const planKey = `plan:plan${i}`; plans.push(planKey); arbiter.addNode(planKey, 'plan'); arbiter.addRelation(planKey, 'has_price', 'unit:usd', { value: Math.floor(Math.random() * 4950) + 50 }); } for (let i = 0; i < (scale === 'medium' ? 500 : 2000); i++) { const featKey = `feature:feat${i}`; features.push(featKey); arbiter.addNode(featKey, 'feature'); // Each feature has a price arbiter.addRelation(featKey, 'has_price', 'unit:usd', { value: Math.floor(Math.random() * 4950) + 50 }); // Some features belong to a plan if (Math.random() < 0.5) { const plan = plans[Math.floor(Math.random() * plans.length)]; arbiter.addRelation(featKey, 'belongs_to_plan', plan); } } // Each user can debit a random subset of accounts for (const user of users) { for (let j = 0; j < (scale === 'medium' ? 3 : 5); j++) { const acc = accounts[Math.floor(Math.random() * accounts.length)]; arbiter.addRelation(user, 'can_debit', acc); } } arbiter.setRelationConfig('can_debit', { type: 'direct' }); arbiter.setRelationConfig('belongs_to_plan', { type: 'direct' }); arbiter.setRelationConfig('has_balance', { type: 'direct' }); arbiter.setRelationConfig('has_price', { type: 'direct' }); // Simplified RelationalComparatorRule: user balance vs feature price arbiter.setRelationConfig('can_afford_feature', { type: 'relational_comparator', leftOperand: { rule: { type: 'direct', relation: 'has_balance' }, extractValue: true, valueRelation: 'has_balance', aggregator: 'max' }, rightOperand: { rule: { type: 'direct', relation: 'has_price' }, extractValue: true, valueRelation: 'has_price', aggregator: 'max' }, comparator: '>=', fallbackBehavior: 'deny' }); // Benchmark: can user afford feature (complex, multi-hop, aggregated, decayed) await benchmark.run(() => { const user = users[Math.floor(Math.random() * users.length)]; const feature = features[Math.floor(Math.random() * features.length)]; arbiter.check(user, 'can_afford_feature', feature); }, scale === 'medium' ? 200 : 30, 'Complex: can_debit sum >= min(plan/feature price)'); // Optionally: Nested logical operator (e.g., require both can afford AND recent 2FA) // For brevity, just add a dummy 2FA relation and a logical AND for (const user of users) { arbiter.addRelation(user, 'last_2fa', '2fa:recent', { value: Date.now() - Math.floor(Math.random() * 60 * 60 * 1000) }); } arbiter.addNode('2fa:recent', '2fa'); arbiter.setRelationConfig('last_2fa', { type: 'direct' }); arbiter.setRelationConfig('recent_2fa_check', { type: 'relational_comparator', leftOperand: { rule: { type: 'direct', relation: 'last_2fa' }, extractValue: true }, rightOperand: { rule: { type: 'computed', value: Date.now() - 10 * 60 * 1000 }, // 10 minutes ago extractValue: true }, comparator: '>=', // last_2fa >= threshold (i.e., more recent) fallbackBehavior: 'deny' }); arbiter.setRelationConfig('can_afford_and_recent_2fa', { type: 'intersection', rules: [ { type: 'relational_comparator', leftOperand: { rule: { type: 'direct', relation: 'has_balance' }, extractValue: true, valueRelation: 'has_balance', aggregator: 'max' }, rightOperand: { rule: { type: 'direct', relation: 'has_price' }, extractValue: true, valueRelation: 'has_price', aggregator: 'max' }, comparator: '>=', fallbackBehavior: 'deny' }, { type: 'relational_comparator', leftOperand: { rule: { type: 'direct', relation: 'last_2fa' }, extractValue: true }, rightOperand: { rule: { type: 'computed', value: Date.now() - 10 * 60 * 1000 }, extractValue: true }, comparator: '>=', fallbackBehavior: 'deny' } ] }); await benchmark.run(() => { const user = users[Math.floor(Math.random() * users.length)]; const feature = features[Math.floor(Math.random() * features.length)]; arbiter.check(user, 'can_afford_and_recent_2fa', feature); }, scale === 'medium' ? 200 : 30, 'Complex: can_afford AND recent_2FA'); benchmark.report(); } } if (import.meta.url === `file://${process.argv[1]}`) { runRelationalComparatorBenchmarks(); } export { runRelationalComparatorBenchmarks };