initial commit: @arbiter/core authorization engine with js-rigor hardening
Zanzibar-style authorization graph engine (direct/chain/TTU/defeasible/ binary modes, condensed snapshots, value relations) with 39 rigor test campaigns. Includes fixes for snapshot binary writer/reader format mismatch (snapshot-of-snapshot corruption), possibility write-boundary validation, empty-graph snapshot serialization, relation lookup cache direction collision, config-redefinition cache invalidation, binary threshold semantics, defeasible compiled routing, and comparator reason whitelisting.
This commit is contained in:
@@ -0,0 +1,727 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { CondensedGraph } from '../src/core/CondensedGraph.js';
|
||||
import { ShardedSnapshotBuilder } from '../src/core/shards/ShardedSnapshotBuilder.js';
|
||||
import { ShardedSnapshot } from '../src/core/shards/ShardedSnapshot.js';
|
||||
import { FileShardStorage } from '../src/core/shards/FileShardStorage.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 makeRng(seed = 1337) {
|
||||
let state = seed >>> 0;
|
||||
return () => {
|
||||
state = (state * 1664525 + 1013904223) >>> 0;
|
||||
return state / 0xffffffff;
|
||||
};
|
||||
}
|
||||
|
||||
function makeZipfSampler(count, skew, seed = 1337) {
|
||||
const weights = new Float64Array(count);
|
||||
let sum = 0;
|
||||
for (let i = 1; i <= count; i++) {
|
||||
const w = 1 / Math.pow(i, skew);
|
||||
weights[i - 1] = w;
|
||||
sum += w;
|
||||
}
|
||||
const cdf = new Float64Array(count);
|
||||
let acc = 0;
|
||||
for (let i = 0; i < count; i++) {
|
||||
acc += weights[i] / sum;
|
||||
cdf[i] = acc;
|
||||
}
|
||||
let state = seed >>> 0;
|
||||
const rand = () => {
|
||||
state = (state * 1664525 + 1013904223) >>> 0;
|
||||
return state / 0xffffffff;
|
||||
};
|
||||
return () => {
|
||||
const r = rand();
|
||||
let lo = 0;
|
||||
let hi = cdf.length - 1;
|
||||
while (lo < hi) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
if (r <= cdf[mid]) {
|
||||
hi = mid;
|
||||
} else {
|
||||
lo = mid + 1;
|
||||
}
|
||||
}
|
||||
return lo;
|
||||
};
|
||||
}
|
||||
|
||||
function nowNs() {
|
||||
return process.hrtime.bigint();
|
||||
}
|
||||
|
||||
function toMs(ns) {
|
||||
return Number(ns) / 1e6;
|
||||
}
|
||||
|
||||
function formatBytes(value) {
|
||||
if (value === null || value === undefined) return 'n/a';
|
||||
if (value < 1024) return `${value} B`;
|
||||
if (value < 1024 * 1024) return `${(value / 1024).toFixed(2)} KB`;
|
||||
if (value < 1024 * 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(2)} MB`;
|
||||
return `${(value / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
||||
}
|
||||
|
||||
function computeNodeTableBytes(graph) {
|
||||
if (!graph.nodeIds || typeof TextEncoder === 'undefined') return null;
|
||||
const encoder = new TextEncoder();
|
||||
let total = 0;
|
||||
for (let i = 0; i < graph.numNodes; i++) {
|
||||
const key = graph.getNodeKey(i);
|
||||
const encoded = encoder.encode(key === undefined || key === null ? '' : String(key));
|
||||
total += encoded.length;
|
||||
}
|
||||
const offsetsBytes = (graph.numNodes + 1) * 4;
|
||||
return total + offsetsBytes;
|
||||
}
|
||||
|
||||
function computeRelationTableBytes(graph) {
|
||||
if (!graph.relationIdToName) return null;
|
||||
const encoder = new TextEncoder();
|
||||
let total = 0;
|
||||
for (let i = 0; i < graph.relationIdToName.length; i++) {
|
||||
const name = graph.relationIdToName[i];
|
||||
const encoded = encoder.encode(name === undefined || name === null ? '' : String(name));
|
||||
total += encoded.length;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
function computeEdgeArrayBytes(graph) {
|
||||
if (graph.edgeSrcIds) {
|
||||
return graph.edgeSrcIds.byteLength
|
||||
+ graph.edgeRelIds.byteLength
|
||||
+ graph.edgeDstIds.byteLength
|
||||
+ graph.edgePossibilityBits.byteLength
|
||||
+ graph.edgeReliabilityBits.byteLength;
|
||||
}
|
||||
if (!graph.adjacency) return null;
|
||||
const edgeCount = graph.edgeIndex;
|
||||
const srcRelDstBytes = edgeCount * 3 * 4;
|
||||
const possRelBytes = edgeCount * 2 * 2;
|
||||
return srcRelDstBytes + possRelBytes;
|
||||
}
|
||||
|
||||
function computeWaveletBytes(graph) {
|
||||
let total = 0;
|
||||
if (graph._waveletAll) total += graph._waveletAll.getMemoryUsage();
|
||||
if (graph._waveletByRelId) {
|
||||
for (const wavelet of graph._waveletByRelId) {
|
||||
if (wavelet) total += wavelet.getMemoryUsage();
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
function computeMphBytes(graph) {
|
||||
if (!graph._mphEnabled || !graph._mph) return 0;
|
||||
const mph = graph._mph;
|
||||
const seedsBytes = mph.seeds ? mph.seeds.byteLength : 0;
|
||||
const tableBytes = mph.table ? mph.table.byteLength : 0;
|
||||
const valuesBytes = mph.values instanceof Int32Array
|
||||
? mph.values.byteLength
|
||||
: mph.values
|
||||
? Int32Array.from(mph.values).byteLength
|
||||
: 0;
|
||||
const keyIndicesBytes = mph.keyIndices instanceof Int32Array
|
||||
? mph.keyIndices.byteLength
|
||||
: mph.keyIndices
|
||||
? Int32Array.from(mph.keyIndices).byteLength
|
||||
: 0;
|
||||
return seedsBytes + tableBytes + valuesBytes + keyIndicesBytes;
|
||||
}
|
||||
|
||||
function logSnapshotBreakdown(graph, snapshotBytes) {
|
||||
const edgeArrayBytes = computeEdgeArrayBytes(graph);
|
||||
const valuesBytes = graph.values ? graph.values.byteLength : 0;
|
||||
const nodeDegreeBytes = graph.nodeDegrees ? graph.nodeDegrees.byteLength : 0;
|
||||
const nodeTableBytes = computeNodeTableBytes(graph);
|
||||
const relationTableBytes = computeRelationTableBytes(graph);
|
||||
const waveletBytes = computeWaveletBytes(graph);
|
||||
const mphBytes = computeMphBytes(graph);
|
||||
const snapshotWithoutValues = snapshotBytes !== null ? snapshotBytes - valuesBytes : null;
|
||||
|
||||
console.log('Snapshot breakdown');
|
||||
console.log(` edge arrays: ${formatBytes(edgeArrayBytes)}`);
|
||||
console.log(` values: ${formatBytes(valuesBytes)} (omit for KV: ${formatBytes(snapshotWithoutValues)})`);
|
||||
console.log(` node degrees: ${formatBytes(nodeDegreeBytes)}`);
|
||||
console.log(` wavelet: ${formatBytes(waveletBytes)}`);
|
||||
console.log(` node table: ${formatBytes(nodeTableBytes)}`);
|
||||
console.log(` relation table: ${formatBytes(relationTableBytes)}`);
|
||||
console.log(` mph: ${formatBytes(mphBytes)}`);
|
||||
}
|
||||
|
||||
class ValueProvider {
|
||||
constructor(seed = 42) {
|
||||
this.rng = makeRng(seed);
|
||||
this._riskCache = new Map();
|
||||
this._limitCache = new Map();
|
||||
}
|
||||
|
||||
risk(userId, docId) {
|
||||
const key = `${userId}:${docId}`;
|
||||
if (this._riskCache.has(key)) return this._riskCache.get(key);
|
||||
const h = (userId * 2654435761 + docId * 2246822519) >>> 0;
|
||||
const value = (h % 1000) / 1000;
|
||||
this._riskCache.set(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
riskLimit(docId) {
|
||||
if (this._limitCache.has(docId)) return this._limitCache.get(docId);
|
||||
const h = (docId * 2654435761) >>> 0;
|
||||
const value = 0.6 + ((h % 300) / 1000);
|
||||
this._limitCache.set(docId, value);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function buildRelationMap(relationIdToName) {
|
||||
const map = new Map();
|
||||
for (let i = 0; i < relationIdToName.length; i++) {
|
||||
map.set(relationIdToName[i], i);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function shardKey(snapshot, relationId, direction, nodeId) {
|
||||
return snapshot.getShardKeyForNode(relationId, direction, nodeId);
|
||||
}
|
||||
|
||||
function directCheck(snapshot, relationId, srcId, dstId, shardSet, missing) {
|
||||
const key = shardKey(snapshot, relationId, 'out', srcId);
|
||||
if (key) shardSet.add(key);
|
||||
return snapshot.executeFindEdgeSync(srcId, relationId, dstId, missing) !== null;
|
||||
}
|
||||
|
||||
function chainCheck(snapshot, relationIds, userId, docId, shardSet, missing, caches) {
|
||||
if (snapshot.sameComponent(userId, docId) === false) return false;
|
||||
const memberRel = relationIds.get('member');
|
||||
const viewerRel = relationIds.get('viewer');
|
||||
const memberDegree = snapshot.executeGetOutDegreeSync(userId, memberRel, missing);
|
||||
const viewerDegree = snapshot.executeGetInDegreeSync(docId, viewerRel, missing);
|
||||
if (memberDegree === null || viewerDegree === null) return null;
|
||||
|
||||
if (viewerDegree < memberDegree) {
|
||||
const viewerKey = shardKey(snapshot, viewerRel, 'in', docId);
|
||||
if (viewerKey) shardSet.add(viewerKey);
|
||||
const cachedViewers = caches?.viewerByDoc?.get(docId);
|
||||
const viewerEdges = cachedViewers || snapshot.executeGetInEdgesSync(docId, viewerRel, missing);
|
||||
if (!viewerEdges) return null;
|
||||
if (caches && !cachedViewers) caches.viewerByDoc.set(docId, viewerEdges);
|
||||
for (const viewer of viewerEdges) {
|
||||
const groupId = viewer.src;
|
||||
const memberKey = shardKey(snapshot, memberRel, 'in', groupId);
|
||||
if (memberKey) shardSet.add(memberKey);
|
||||
const cachedMembers = caches?.memberByGroup?.get(groupId);
|
||||
const memberEdges = cachedMembers || snapshot.executeGetInEdgesSync(groupId, memberRel, missing);
|
||||
if (!memberEdges) return null;
|
||||
if (caches && !cachedMembers) caches.memberByGroup.set(groupId, memberEdges);
|
||||
for (const member of memberEdges) {
|
||||
if (member.src === userId) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const memberKey = shardKey(snapshot, memberRel, 'out', userId);
|
||||
if (memberKey) shardSet.add(memberKey);
|
||||
const cachedMembers = caches?.memberByUser?.get(userId);
|
||||
const memberEdges = cachedMembers || snapshot.executeGetOutEdgesSync(userId, memberRel, missing);
|
||||
if (!memberEdges) return null;
|
||||
if (caches && !cachedMembers) caches.memberByUser.set(userId, memberEdges);
|
||||
for (const member of memberEdges) {
|
||||
const groupId = member.dst;
|
||||
const viewerKey = shardKey(snapshot, viewerRel, 'out', groupId);
|
||||
if (viewerKey) shardSet.add(viewerKey);
|
||||
const cachedViewers = caches?.viewerByGroup?.get(groupId);
|
||||
const viewerEdges = cachedViewers || snapshot.executeGetOutEdgesSync(groupId, viewerRel, missing);
|
||||
if (!viewerEdges) return null;
|
||||
if (caches && !cachedViewers) caches.viewerByGroup.set(groupId, viewerEdges);
|
||||
for (const viewer of viewerEdges) {
|
||||
if (viewer.dst === docId) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function usersetComparatorCheck(snapshot, relationIds, valueProvider, docId, shardSet, missing, caches, limits, stats) {
|
||||
const viewerRel = relationIds.get('viewer');
|
||||
const memberRel = relationIds.get('member');
|
||||
const riskRel = relationIds.get('risk');
|
||||
|
||||
if (snapshot.sameComponent(docId, docId) === false) return false;
|
||||
const viewerKey = shardKey(snapshot, viewerRel, 'in', docId);
|
||||
if (viewerKey) shardSet.add(viewerKey);
|
||||
const cachedViewers = caches?.viewerByDoc?.get(docId);
|
||||
const viewerEdges = cachedViewers || snapshot.executeGetInEdgesSync(docId, viewerRel, missing);
|
||||
if (!viewerEdges) return null;
|
||||
if (caches && !cachedViewers) caches.viewerByDoc.set(docId, viewerEdges);
|
||||
let maxRisk = -Infinity;
|
||||
let groupScanned = 0;
|
||||
let userScanned = 0;
|
||||
|
||||
for (const viewer of viewerEdges) {
|
||||
if (limits.maxGroups && groupScanned >= limits.maxGroups) {
|
||||
if (stats) stats.cappedComparator++;
|
||||
return false;
|
||||
}
|
||||
const groupId = viewer.src;
|
||||
const memberKey = shardKey(snapshot, memberRel, 'in', groupId);
|
||||
if (memberKey) shardSet.add(memberKey);
|
||||
const cachedMembers = caches?.memberByGroup?.get(groupId);
|
||||
const memberEdges = cachedMembers || snapshot.executeGetInEdgesSync(groupId, memberRel, missing);
|
||||
if (!memberEdges) return null;
|
||||
if (caches && !cachedMembers) caches.memberByGroup.set(groupId, memberEdges);
|
||||
groupScanned++;
|
||||
for (const member of memberEdges) {
|
||||
if (limits.maxUsersTotal && userScanned >= limits.maxUsersTotal) {
|
||||
if (stats) stats.cappedComparator++;
|
||||
return false;
|
||||
}
|
||||
if (limits.maxUsersPerGroup && userScanned >= limits.maxUsersPerGroup * groupScanned) {
|
||||
break;
|
||||
}
|
||||
const userId = member.src;
|
||||
const riskShardKey = shardKey(snapshot, riskRel, 'out', userId);
|
||||
if (riskShardKey) shardSet.add(riskShardKey);
|
||||
const riskKey = `${userId}:${docId}`;
|
||||
const cachedRisk = caches?.riskByUserDoc?.get(riskKey);
|
||||
const hasRisk = cachedRisk !== undefined
|
||||
? cachedRisk
|
||||
: snapshot.executeFindEdgeSync(userId, riskRel, docId, missing);
|
||||
if (!hasRisk && missing && missing.size > 0) return null;
|
||||
if (hasRisk) {
|
||||
if (caches && cachedRisk === undefined) caches.riskByUserDoc.set(riskKey, hasRisk);
|
||||
const risk = valueProvider.risk(userId, docId);
|
||||
if (risk > maxRisk) maxRisk = risk;
|
||||
}
|
||||
userScanned++;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxRisk === -Infinity) return false;
|
||||
const limit = valueProvider.riskLimit(docId);
|
||||
return maxRisk <= limit;
|
||||
}
|
||||
|
||||
function buildGraph({ edges, users, groups, docs, seed, userSkew, groupSkew, docSkew, buildWavelet }) {
|
||||
const graph = new CondensedGraph();
|
||||
const rng = makeRng(seed);
|
||||
|
||||
const memberEdges = Math.floor(edges * 0.3);
|
||||
const viewerEdges = Math.floor(edges * 0.3);
|
||||
const ownerEdges = Math.floor(edges * 0.1);
|
||||
const riskEdges = Math.floor(edges * 0.2);
|
||||
let riskLimitEdges = edges - memberEdges - viewerEdges - ownerEdges - riskEdges;
|
||||
if (riskLimitEdges < 0) riskLimitEdges = 0;
|
||||
|
||||
const userSampler = makeZipfSampler(users, userSkew, 7);
|
||||
const groupSampler = makeZipfSampler(groups, groupSkew, 11);
|
||||
const docSampler = makeZipfSampler(docs, docSkew, 13);
|
||||
|
||||
for (let i = 0; i < memberEdges; i++) {
|
||||
const userId = userSampler();
|
||||
const groupId = groupSampler();
|
||||
graph.addEdge(`user:${userId}`, 'member', `group:${groupId}`);
|
||||
}
|
||||
|
||||
for (let i = 0; i < viewerEdges; i++) {
|
||||
const groupId = groupSampler();
|
||||
const docId = docSampler();
|
||||
graph.addEdge(`group:${groupId}`, 'viewer', `doc:${docId}`);
|
||||
}
|
||||
|
||||
for (let i = 0; i < ownerEdges; i++) {
|
||||
const userId = userSampler();
|
||||
const docId = docSampler();
|
||||
graph.addEdge(`user:${userId}`, 'owner', `doc:${docId}`);
|
||||
}
|
||||
|
||||
for (let i = 0; i < riskEdges; i++) {
|
||||
const userId = userSampler();
|
||||
const docId = docSampler();
|
||||
const value = Math.min(1, Math.max(0, rng()));
|
||||
graph.addEdge(`user:${userId}`, 'risk', `doc:${docId}`, { value, possibility: 1.0, reliability: 1.0 });
|
||||
}
|
||||
|
||||
for (let i = 0; i < riskLimitEdges; i++) {
|
||||
const docId = docSampler();
|
||||
graph.addEdge(`doc:${docId}`, 'risk_limit', `doc:${docId}`, { value: 0.5, possibility: 1.0, reliability: 1.0 });
|
||||
}
|
||||
|
||||
graph.finalizePerfectHash();
|
||||
if (buildWavelet) {
|
||||
graph.finalizeWaveletAdjacency({ dropAdjacencyList: true });
|
||||
}
|
||||
return graph;
|
||||
}
|
||||
|
||||
function buildSnapshot(graph, outputDir, options) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
const builder = new ShardedSnapshotBuilder(options);
|
||||
return builder.build(graph, outputDir);
|
||||
}
|
||||
|
||||
function runScenario(snapshot, options) {
|
||||
const relationIds = buildRelationMap(snapshot.relationIdToName || []);
|
||||
const valueProvider = new ValueProvider(options.seed);
|
||||
const rng = makeRng(options.seed);
|
||||
const cachePaths = options.cachePaths;
|
||||
const limits = options.limits;
|
||||
const stats = { cappedComparator: 0 };
|
||||
const caches = cachePaths ? {
|
||||
memberByUser: new Map(),
|
||||
viewerByGroup: new Map(),
|
||||
viewerByDoc: new Map(),
|
||||
memberByGroup: new Map(),
|
||||
riskByUserDoc: new Map()
|
||||
} : null;
|
||||
|
||||
const userIds = [];
|
||||
for (let i = 0; i < Math.min(options.userPool, options.users); i++) {
|
||||
const id = snapshot.resolveNodeId(`user:${i}`);
|
||||
if (id !== -1) userIds.push(id);
|
||||
}
|
||||
|
||||
const docIds = [];
|
||||
for (let i = 0; i < Math.min(options.docPool, options.docs); i++) {
|
||||
const id = snapshot.resolveNodeId(`doc:${i}`);
|
||||
if (id !== -1) docIds.push(id);
|
||||
}
|
||||
|
||||
const directRel = relationIds.get('owner');
|
||||
const memberRel = relationIds.get('member');
|
||||
const viewerRel = relationIds.get('viewer');
|
||||
const riskRel = relationIds.get('risk');
|
||||
|
||||
function sampleDirectPair() {
|
||||
for (let i = 0; i < 200; i++) {
|
||||
const userId = userIds[Math.floor(rng() * userIds.length)];
|
||||
const edges = snapshot.getOutEdgesSync(userId, directRel);
|
||||
if (edges.length > 0) {
|
||||
const edge = edges[0];
|
||||
return { userId, docId: edge.dst };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function sampleChainPair() {
|
||||
for (let i = 0; i < 200; i++) {
|
||||
const userId = userIds[Math.floor(rng() * userIds.length)];
|
||||
const members = snapshot.getOutEdgesSync(userId, memberRel);
|
||||
if (!members.length) continue;
|
||||
const groupId = members[0].dst;
|
||||
const viewers = snapshot.getOutEdgesSync(groupId, viewerRel);
|
||||
if (!viewers.length) continue;
|
||||
return { userId, docId: viewers[0].dst };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function sampleComparatorDoc() {
|
||||
for (let i = 0; i < 200; i++) {
|
||||
const docId = docIds[Math.floor(rng() * docIds.length)];
|
||||
const viewers = snapshot.getInEdgesSync(docId, viewerRel);
|
||||
if (!viewers.length) continue;
|
||||
const groupId = viewers[0].src;
|
||||
const members = snapshot.getInEdgesSync(groupId, memberRel);
|
||||
if (!members.length) continue;
|
||||
const userId = members[0].src;
|
||||
const riskEdge = snapshot.findEdgeSync(userId, riskRel, docId);
|
||||
if (riskEdge) return { docId };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const directPairs = [];
|
||||
const chainPairs = [];
|
||||
const comparatorDocs = [];
|
||||
while (directPairs.length < options.queries) {
|
||||
const pair = sampleDirectPair();
|
||||
if (!pair) break;
|
||||
directPairs.push(pair);
|
||||
}
|
||||
while (chainPairs.length < options.queries) {
|
||||
const pair = sampleChainPair();
|
||||
if (!pair) break;
|
||||
chainPairs.push(pair);
|
||||
}
|
||||
while (comparatorDocs.length < options.queries) {
|
||||
const pair = sampleComparatorDoc();
|
||||
if (!pair) break;
|
||||
comparatorDocs.push(pair);
|
||||
}
|
||||
|
||||
let directHits = 0;
|
||||
let chainHits = 0;
|
||||
let comparatorHits = 0;
|
||||
|
||||
let directShardCount = 0;
|
||||
let chainShardCount = 0;
|
||||
let comparatorShardCount = 0;
|
||||
|
||||
let directStart = nowNs();
|
||||
for (let i = 0; i < directPairs.length; i++) {
|
||||
let ok = null;
|
||||
let replans = 0;
|
||||
while (ok === null && replans < 4) {
|
||||
const missing = new Set();
|
||||
const shards = new Set();
|
||||
ok = directCheck(snapshot, directRel, directPairs[i].userId, directPairs[i].docId, shards, missing);
|
||||
if (missing.size > 0) {
|
||||
snapshot.prefetchPlanSync(missing);
|
||||
replans++;
|
||||
ok = null;
|
||||
continue;
|
||||
}
|
||||
if (ok) directHits++;
|
||||
directShardCount += shards.size;
|
||||
}
|
||||
}
|
||||
let directTime = nowNs() - directStart;
|
||||
|
||||
let chainStart = nowNs();
|
||||
for (let i = 0; i < chainPairs.length; i++) {
|
||||
let ok = null;
|
||||
let replans = 0;
|
||||
while (ok === null && replans < 4) {
|
||||
const missing = new Set();
|
||||
const shards = new Set();
|
||||
ok = chainCheck(snapshot, relationIds, chainPairs[i].userId, chainPairs[i].docId, shards, missing, caches);
|
||||
if (missing.size > 0) {
|
||||
snapshot.prefetchPlanSync(missing);
|
||||
replans++;
|
||||
ok = null;
|
||||
continue;
|
||||
}
|
||||
if (ok) chainHits++;
|
||||
chainShardCount += shards.size;
|
||||
}
|
||||
}
|
||||
let chainTime = nowNs() - chainStart;
|
||||
|
||||
let comparatorStart = nowNs();
|
||||
for (let i = 0; i < comparatorDocs.length; i++) {
|
||||
let ok = null;
|
||||
let replans = 0;
|
||||
while (ok === null && replans < 4) {
|
||||
const missing = new Set();
|
||||
const shards = new Set();
|
||||
ok = usersetComparatorCheck(snapshot, relationIds, valueProvider, comparatorDocs[i].docId, shards, missing, caches, limits, stats);
|
||||
if (missing.size > 0) {
|
||||
snapshot.prefetchPlanSync(missing);
|
||||
replans++;
|
||||
ok = null;
|
||||
continue;
|
||||
}
|
||||
if (ok) comparatorHits++;
|
||||
comparatorShardCount += shards.size;
|
||||
}
|
||||
}
|
||||
let comparatorTime = nowNs() - comparatorStart;
|
||||
|
||||
return {
|
||||
samples: {
|
||||
direct: directPairs.length,
|
||||
chain: chainPairs.length,
|
||||
comparator: comparatorDocs.length
|
||||
},
|
||||
avgMs: {
|
||||
direct: toMs(directTime) / Math.max(1, directPairs.length),
|
||||
chain: toMs(chainTime) / Math.max(1, chainPairs.length),
|
||||
comparator: toMs(comparatorTime) / Math.max(1, comparatorDocs.length)
|
||||
},
|
||||
hits: {
|
||||
direct: directHits,
|
||||
chain: chainHits,
|
||||
comparator: comparatorHits
|
||||
},
|
||||
shardsPerQuery: {
|
||||
direct: directShardCount / Math.max(1, directPairs.length),
|
||||
chain: chainShardCount / Math.max(1, chainPairs.length),
|
||||
comparator: comparatorShardCount / Math.max(1, comparatorDocs.length)
|
||||
},
|
||||
comparatorCapped: stats.cappedComparator,
|
||||
cacheStats: snapshot.getStats()
|
||||
};
|
||||
}
|
||||
|
||||
function componentCount(snapshot) {
|
||||
if (!snapshot._componentByNode) return null;
|
||||
let max = -1;
|
||||
for (let i = 0; i < snapshot._componentByNode.length; i++) {
|
||||
const value = snapshot._componentByNode[i];
|
||||
if (value > max) max = value;
|
||||
}
|
||||
return max + 1;
|
||||
}
|
||||
|
||||
const args = parseArgs(process.argv);
|
||||
const edges = Number(args.get('edges') || 200000);
|
||||
const users = Number(args.get('users') || 50000);
|
||||
const groups = Number(args.get('groups') || 2000);
|
||||
const docs = Number(args.get('docs') || 100000);
|
||||
const bucketSize = Number(args.get('bucket') || 65536);
|
||||
const directions = args.get('directions')
|
||||
? String(args.get('directions')).split(',')
|
||||
: ['out', 'in'];
|
||||
const componentRelations = args.get('component-relations')
|
||||
? String(args.get('component-relations')).split(',')
|
||||
: ['member', 'viewer'];
|
||||
const queries = Number(args.get('queries') || 1000);
|
||||
const seed = Number(args.get('seed') || 1337);
|
||||
const userSkew = Number(args.get('user-skew') || 1.2);
|
||||
const groupSkew = Number(args.get('group-skew') || 1.1);
|
||||
const docSkew = Number(args.get('doc-skew') || 1.15);
|
||||
const userPool = Number(args.get('user-pool') || 5000);
|
||||
const docPool = Number(args.get('doc-pool') || 5000);
|
||||
const cachePaths = args.get('cache-paths') !== 'false';
|
||||
const targetShardBytes = Number(args.get('target-shard-bytes') || 0);
|
||||
const bytesPerEdgeEstimate = Number(args.get('bytes-per-edge') || 32);
|
||||
const buildWavelet = args.get('build-wavelet') === 'true';
|
||||
const streamRelations = args.get('stream-relations') !== 'false';
|
||||
const maxSnapshotBytes = Number(args.get('max-snapshot-bytes') || 52428800);
|
||||
const omitNodeTable = args.get('omit-node-table') !== 'false';
|
||||
const maxGroups = Number(args.get('max-groups') || 0);
|
||||
const maxUsersPerGroup = Number(args.get('max-users-per-group') || 0);
|
||||
const maxUsersTotal = Number(args.get('max-users-total') || 0);
|
||||
const limits = {
|
||||
maxGroups: maxGroups > 0 ? maxGroups : null,
|
||||
maxUsersPerGroup: maxUsersPerGroup > 0 ? maxUsersPerGroup : null,
|
||||
maxUsersTotal: maxUsersTotal > 0 ? maxUsersTotal : null
|
||||
};
|
||||
|
||||
console.log('Sharded component bench');
|
||||
console.log(` edges: ${edges}`);
|
||||
console.log(` users: ${users}`);
|
||||
console.log(` groups: ${groups}`);
|
||||
console.log(` docs: ${docs}`);
|
||||
console.log(` bucket: ${bucketSize}`);
|
||||
console.log(` directions: ${directions.join(',')}`);
|
||||
console.log(` component relations: ${componentRelations.join(',')}`);
|
||||
console.log(` queries: ${queries}`);
|
||||
console.log(` user pool: ${userPool}`);
|
||||
console.log(` doc pool: ${docPool}`);
|
||||
console.log(` cache paths: ${cachePaths}`);
|
||||
console.log(` max groups: ${limits.maxGroups || 'none'}`);
|
||||
console.log(` max users/group: ${limits.maxUsersPerGroup || 'none'}`);
|
||||
console.log(` max users total: ${limits.maxUsersTotal || 'none'}`);
|
||||
console.log(` build wavelet: ${buildWavelet}`);
|
||||
console.log(` stream relations: ${streamRelations}`);
|
||||
console.log(` max snapshot bytes: ${maxSnapshotBytes}`);
|
||||
console.log(` omit node table: ${omitNodeTable}`);
|
||||
if (targetShardBytes > 0) {
|
||||
console.log(` target shard bytes: ${targetShardBytes}`);
|
||||
console.log(` bytes/edge estimate: ${bytesPerEdgeEstimate}`);
|
||||
}
|
||||
|
||||
const graph = buildGraph({ edges, users, groups, docs, seed, userSkew, groupSkew, docSkew, buildWavelet });
|
||||
graph._omitNodeIdTable = omitNodeTable;
|
||||
let snapshotBytes = null;
|
||||
if (buildWavelet) {
|
||||
const snapshotBuffer = graph.toBinary();
|
||||
snapshotBytes = snapshotBuffer.byteLength;
|
||||
console.log(` snapshot bytes: ${snapshotBytes}`);
|
||||
logSnapshotBreakdown(graph, snapshotBytes);
|
||||
} else {
|
||||
console.log(' snapshot bytes: unknown (build wavelet disabled)');
|
||||
}
|
||||
|
||||
if (snapshotBytes !== null && snapshotBytes <= maxSnapshotBytes) {
|
||||
console.log('Snapshot below threshold; sharding skipped.');
|
||||
process.exit(0);
|
||||
}
|
||||
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sharded-component-'));
|
||||
const rangeDir = path.join(baseDir, 'range');
|
||||
const componentDir = path.join(baseDir, 'component');
|
||||
|
||||
const rangeManifest = buildSnapshot(graph, rangeDir, {
|
||||
bucketSize,
|
||||
includeDirections: directions,
|
||||
shardMode: 'range',
|
||||
streamRelations
|
||||
});
|
||||
const componentManifest = buildSnapshot(graph, componentDir, {
|
||||
bucketSize,
|
||||
includeDirections: directions,
|
||||
shardMode: 'component',
|
||||
componentRelations,
|
||||
targetShardBytes: targetShardBytes > 0 ? targetShardBytes : null,
|
||||
bytesPerEdgeEstimate,
|
||||
streamRelations
|
||||
});
|
||||
|
||||
const rangeStorage = new FileShardStorage(rangeDir);
|
||||
const componentStorage = new FileShardStorage(componentDir);
|
||||
const rangeSnapshot = new ShardedSnapshot(rangeManifest, rangeStorage, { cacheLimit: 32, recentLimit: 128 });
|
||||
const componentSnapshot = new ShardedSnapshot(componentManifest, componentStorage, { cacheLimit: 32, recentLimit: 128 });
|
||||
rangeSnapshot.initializeSync();
|
||||
componentSnapshot.initializeSync();
|
||||
|
||||
const scenarioOptions = {
|
||||
users,
|
||||
docs,
|
||||
userPool,
|
||||
docPool,
|
||||
queries,
|
||||
seed,
|
||||
cachePaths,
|
||||
limits
|
||||
};
|
||||
|
||||
const rangeResults = runScenario(rangeSnapshot, scenarioOptions);
|
||||
const componentResults = runScenario(componentSnapshot, scenarioOptions);
|
||||
|
||||
console.log('Results');
|
||||
console.log(` range shards: ${rangeManifest.shards.length}`);
|
||||
console.log(` component shards: ${componentManifest.shards.length}`);
|
||||
console.log(` component count: ${componentCount(componentSnapshot)}`);
|
||||
|
||||
console.log('Range');
|
||||
console.log(` direct avg: ${(rangeResults.avgMs.direct * 1000).toFixed(3)} µs`);
|
||||
console.log(` direct shards/query: ${rangeResults.shardsPerQuery.direct.toFixed(2)}`);
|
||||
console.log(` chain avg: ${(rangeResults.avgMs.chain * 1000).toFixed(3)} µs`);
|
||||
console.log(` chain shards/query: ${rangeResults.shardsPerQuery.chain.toFixed(2)}`);
|
||||
console.log(` comparator avg: ${(rangeResults.avgMs.comparator * 1000).toFixed(3)} µs`);
|
||||
console.log(` comparator shards/query: ${rangeResults.shardsPerQuery.comparator.toFixed(2)}`);
|
||||
console.log(` comparator capped: ${rangeResults.comparatorCapped}`);
|
||||
|
||||
console.log('Component');
|
||||
console.log(` direct avg: ${(componentResults.avgMs.direct * 1000).toFixed(3)} µs`);
|
||||
console.log(` direct shards/query: ${componentResults.shardsPerQuery.direct.toFixed(2)}`);
|
||||
console.log(` chain avg: ${(componentResults.avgMs.chain * 1000).toFixed(3)} µs`);
|
||||
console.log(` chain shards/query: ${componentResults.shardsPerQuery.chain.toFixed(2)}`);
|
||||
console.log(` comparator avg: ${(componentResults.avgMs.comparator * 1000).toFixed(3)} µs`);
|
||||
console.log(` comparator shards/query: ${componentResults.shardsPerQuery.comparator.toFixed(2)}`);
|
||||
console.log(` comparator capped: ${componentResults.comparatorCapped}`);
|
||||
|
||||
console.log('Cache stats (range)');
|
||||
console.log(rangeResults.cacheStats);
|
||||
console.log('Cache stats (component)');
|
||||
console.log(componentResults.cacheStats);
|
||||
Reference in New Issue
Block a user