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(',')); }