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 buildFeatureFlagScenario(arbiter, size) { const flagCount = Math.max(1, Math.floor(size / 10)); buildUsers(arbiter, size, 'user'); buildUsers(arbiter, size, 'resource'); buildUsers(arbiter, flagCount, 'flag'); for (let i = 0; i < size; i++) { const flagId = i % flagCount; arbiter.addRelation(`user:${i}`, 'has_flag', `flag:${flagId}`); if (i % 2 === 0) { arbiter.addRelation(`resource:${i}`, 'feature_flag', `flag:${flagId}`); } } arbiter.setRelationConfig('has_flag', { type: 'direct' }); arbiter.setRelationConfig('feature_flag', { type: 'direct' }); arbiter.setRelationConfig('can_feature', { type: 'tuple_to_userset', tuplesetRelation: 'feature_flag', tuplesetDirection: 'out', computedRelation: 'has_flag' }); } function buildQueries(size, rng, samples) { const queries = []; const half = Math.floor(samples / 2); for (let i = 0; i < half; i++) { const userId = randInt(rng, Math.floor(size / 2)) * 2; queries.push({ userKey: `user:${userId}`, objectKey: `resource:${userId}`, flagId: userId % Math.max(1, Math.floor(size / 10)), expected: true }); } for (let i = 0; i < samples - half; i++) { const userId = randInt(rng, Math.floor(size / 2)) * 2 + 1; queries.push({ userKey: `user:${userId}`, objectKey: `resource:${userId}`, flagId: userId % Math.max(1, Math.floor(size / 10)), expected: false }); } 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, qps: median, elapsedMs: null }; } const args = parseArgs(process.argv); const config = { sizes: parseSizes(args.get('sizes')) || [5000, 10000, 20000, 40000], 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'), relationGraphTraversal: args.has('relation-graph'), relationGraphMinDegree: Number(args.get('relation-graph-min-degree') || 200), relationCsr: args.has('relation-csr'), relationCsrMinDegree: Number(args.get('relation-csr-min-degree') || 200), relationCsrDeltaThreshold: Number(args.get('relation-csr-delta-threshold') || 1000), warmupRuns: Number(args.get('warmup-runs') || 2), medianRuns: Number(args.get('median-runs') || 5), verify: args.has('verify') }; const rng = createRng(config.seed); const startTime = Date.now(); console.log('feature_flag_micro_bench'); console.log('size,samples,meta,values,relation_graph,relation_csr,metric,elapsed_ms,qps'); for (const size of config.sizes) { if ((Date.now() - startTime) / 1000 > config.maxSeconds) break; const arbiter = new Arbiter({ useRelationGraphTraversal: config.relationGraphTraversal, relationGraphMinDegree: config.relationGraphMinDegree, useRelationCsrIndex: config.relationCsr, relationCsrMinDegree: config.relationCsrMinDegree, relationCsrDeltaThreshold: config.relationCsrDeltaThreshold }); buildFeatureFlagScenario(arbiter, size); const queries = buildQueries(size, rng, config.samples); const visited = new Set([{ userKey: 'seed', relation: 'can_feature', objectKey: 'seed' }]); const baseOptions = { fastPath: true, includeMeta: config.includeMeta, collectValues: config.collectValues }; if (config.verify) { for (const q of queries) { const result = arbiter.check(q.userKey, 'can_feature', q.objectKey, { fastPath: false }); const predicted = result && result.possibility > 0; if (predicted !== q.expected) { throw new Error(`Verification failed size ${size}: ${q.userKey} -> ${q.objectKey} expected=${q.expected}`); } } } const directFast = measureMedian('direct_fast', queries.length, () => { const q = queries[randInt(rng, queries.length)]; arbiter.authChecker.check(q.userKey, 'has_flag', `flag:${q.flagId}`, baseOptions); }, config.warmupRuns, config.medianRuns); const directNested = measureMedian('direct_nested', queries.length, () => { const q = queries[randInt(rng, queries.length)]; arbiter.authChecker.check(q.userKey, 'has_flag', `flag:${q.flagId}`, { ...baseOptions, _visited: visited, _currentRelation: 'can_feature' }); }, config.warmupRuns, config.medianRuns); const tuplesetFetch = measureMedian('tupleset_fetch', queries.length, () => { const q = queries[randInt(rng, queries.length)]; const objectId = arbiter.nodeIdByKey.get(q.objectKey); arbiter.relationManager.getRelationsFromSrc(objectId, 'feature_flag'); }, config.warmupRuns, config.medianRuns); const tuplesetInner = measureMedian('tupleset_inner', queries.length, () => { const q = queries[randInt(rng, queries.length)]; const objectId = arbiter.nodeIdByKey.get(q.objectKey); const tuples = arbiter.relationManager.getRelationsFromSrc(objectId, 'feature_flag'); if (!tuples.length) return; const flagKey = arbiter.keyByNodeId.get(tuples[0].dst); if (!flagKey) return; arbiter.authChecker.check(q.userKey, 'has_flag', flagKey, { ...baseOptions, _visited: visited, _currentRelation: 'can_feature' }); }, config.warmupRuns, config.medianRuns); const tupleToUserset = measureMedian('tuple_to_userset', queries.length, () => { const q = queries[randInt(rng, queries.length)]; arbiter.check(q.userKey, 'can_feature', q.objectKey, baseOptions); }, config.warmupRuns, config.medianRuns); const mode = config.relationGraphTraversal ? 'on' : 'off'; const csrMode = config.relationCsr ? 'on' : 'off'; for (const metric of [directFast, directNested, tuplesetFetch, tuplesetInner, tupleToUserset]) { console.log([ size, queries.length, config.includeMeta ? 'on' : 'off', config.collectValues ? 'on' : 'off', mode, csrMode, metric.label, metric.elapsedMs === null ? 'median' : Math.round(metric.elapsedMs), metric.qps ].join(',')); } }