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 buildNodes(arbiter, count, prefix) { for (let i = 0; i < count; i++) { arbiter.addNode(`${prefix}:${i}`, prefix); } } function buildScenario(arbiter, size, edgeFactor, rng) { const nodes = size; buildNodes(arbiter, nodes, 'user'); buildNodes(arbiter, nodes, 'resource'); for (let i = 0; i < nodes; i++) { arbiter.addRelation(`user:${i}`, 'link', `resource:${i}`, 1.0); } const extraEdges = Math.max(0, Math.floor(nodes * edgeFactor)); for (let i = 0; i < extraEdges; i++) { const src = randInt(rng, nodes); const dst = randInt(rng, nodes); if (src === dst) continue; arbiter.addRelation(`user:${src}`, 'link', `resource:${dst}`, 1.0, { value: (src + dst) % 100 }); } arbiter.setRelationConfig('link', { type: 'direct' }); arbiter.setRelationConfig('multi_hop_link', { type: 'multi_hop', relation: 'link', maxDepth: 3, pathAggregation: 'max', collectValues: false, trackPaths: false }); } function buildQueries(size, rng, samples) { const queries = []; for (let i = 0; i < samples; i++) { const userId = randInt(rng, size); const objectId = randInt(rng, size); queries.push({ userKey: `user:${userId}`, objectKey: `resource:${objectId}` }); } 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++) { samples.push(measure(label, iterations, fn).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')) || [2000, 5000, 10000], samples: Number(args.get('samples') || 2000), seed: Number(args.get('seed') || 42), edgeFactor: Number(args.get('edge-factor') || 1.5), includeMeta: !args.has('no-meta'), collectValues: !args.has('no-values'), warmupRuns: Number(args.get('warmup-runs') || 2), medianRuns: Number(args.get('median-runs') || 5), relationGraph: args.has('relation-graph') }; console.log('multi_hop_rule_bench'); console.log('size,samples,meta,values,relation_graph,metric,elapsed_ms,qps'); for (const size of config.sizes) { const rng = createRng(config.seed + size); const arbiter = new Arbiter({ useRelationGraphTraversal: config.relationGraph }); buildScenario(arbiter, size, config.edgeFactor, rng); const queries = buildQueries(size, rng, config.samples); const baseOptions = { fastPath: true, includeMeta: config.includeMeta, collectValues: config.collectValues }; const multiHop = measureMedian('multi_hop', queries.length, () => { const q = queries[randInt(rng, queries.length)]; arbiter.check(q.userKey, 'multi_hop_link', q.objectKey, baseOptions); }, config.warmupRuns, config.medianRuns); console.log([ size, queries.length, config.includeMeta ? 'on' : 'off', config.collectValues ? 'on' : 'off', config.relationGraph ? 'on' : 'off', multiHop.label, multiHop.elapsedMs === null ? 'median' : Math.round(multiHop.elapsedMs), multiHop.qps ].join(',')); }