717ae1031e
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.
196 lines
5.9 KiB
JavaScript
196 lines
5.9 KiB
JavaScript
import { performance } from 'node:perf_hooks';
|
|
import { CondensedGraph } from '../src/core/CondensedGraph.js';
|
|
import { WaveletTree } from '../src/core/graph/succinct/WaveletTree.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;
|
|
}
|
|
|
|
const args = parseArgs(process.argv);
|
|
const edges = Number(args.get('edges') || 200000);
|
|
const users = Number(args.get('users') || 10000);
|
|
const docs = Number(args.get('docs') || 50000);
|
|
const bucketSize = Number(args.get('bucket') || 4096);
|
|
|
|
console.log('Shard size bench (relation + source-range)');
|
|
console.log(` edges: ${edges}`);
|
|
console.log(` users: ${users}`);
|
|
console.log(` docs: ${docs}`);
|
|
console.log(` bucket size: ${bucketSize}`);
|
|
|
|
const graph = new CondensedGraph();
|
|
const relations = ['owner', 'editor', 'viewer'];
|
|
|
|
const buildStart = performance.now();
|
|
for (let i = 0; i < edges; i++) {
|
|
graph.addEdge(`user:${i % users}`, relations[i % 3], `doc:${i % docs}`);
|
|
}
|
|
const buildTime = performance.now() - buildStart;
|
|
console.log(` build time: ${buildTime.toFixed(2)} ms`);
|
|
|
|
const relCount = graph.nextRelationId;
|
|
const bucketCount = Math.ceil(users / bucketSize);
|
|
const shardBuckets = new Array(relCount);
|
|
for (let r = 0; r < relCount; r++) {
|
|
shardBuckets[r] = new Array(bucketCount);
|
|
}
|
|
|
|
const adjacency = graph.adjacency;
|
|
for (let edgeIdx = 0; edgeIdx < graph.edgeIndex; edgeIdx++) {
|
|
const base = edgeIdx * 6;
|
|
const srcId = adjacency[base + 0];
|
|
const relId = adjacency[base + 1];
|
|
const dstId = adjacency[base + 2];
|
|
const bucket = Math.floor(srcId / bucketSize);
|
|
if (!shardBuckets[relId][bucket]) {
|
|
shardBuckets[relId][bucket] = { dstsBySrc: [], edgeCount: 0 };
|
|
}
|
|
const shard = shardBuckets[relId][bucket];
|
|
const local = srcId - bucket * bucketSize;
|
|
let list = shard.dstsBySrc[local];
|
|
if (!list) {
|
|
list = [];
|
|
shard.dstsBySrc[local] = list;
|
|
}
|
|
list.push(dstId);
|
|
shard.edgeCount++;
|
|
}
|
|
|
|
function sizeWaveletAdjacency(params) {
|
|
const {
|
|
sourceCount,
|
|
edgeCount,
|
|
boundaryWordCount,
|
|
boundaryBlockRankLength,
|
|
nodeCount,
|
|
totalWords,
|
|
totalRank
|
|
} = params;
|
|
let offset = 0;
|
|
const align = (n) => {
|
|
const pad = (n - (offset % n)) % n;
|
|
offset += pad;
|
|
};
|
|
|
|
offset += 4; // numNodes
|
|
offset += 4; // boundary length
|
|
offset += 4; // boundary word count
|
|
align(4);
|
|
offset += boundaryWordCount * 4;
|
|
offset += 4; // blockSize
|
|
offset += 4; // blockRank length
|
|
align(4);
|
|
offset += boundaryBlockRankLength * 4;
|
|
offset += 4; // edgeCount
|
|
align(4);
|
|
offset += edgeCount * 4; // edge indices
|
|
offset += 1; // hasTree
|
|
|
|
if (nodeCount > 0) {
|
|
offset += 4; // nodeCount
|
|
align(4);
|
|
offset += nodeCount * 4 * 11; // 11 uint32/int32 arrays
|
|
offset += 4; // isLeaf length
|
|
offset += nodeCount; // isLeaf bytes
|
|
offset += 4; // words length
|
|
align(4);
|
|
offset += totalWords * 4;
|
|
offset += 4; // rank length
|
|
align(4);
|
|
offset += totalRank * 4;
|
|
}
|
|
|
|
return offset;
|
|
}
|
|
|
|
function computeWaveletFlatSize(dstSequence, alphabetSize) {
|
|
if (!dstSequence.length) {
|
|
return { nodeCount: 0, totalWords: 0, totalRank: 0 };
|
|
}
|
|
const tree = new WaveletTree(new Uint32Array(dstSequence), alphabetSize);
|
|
const stack = [tree];
|
|
let nodeCount = 0;
|
|
let totalWords = 0;
|
|
let totalRank = 0;
|
|
while (stack.length) {
|
|
const node = stack.pop();
|
|
if (!node) continue;
|
|
nodeCount++;
|
|
if (node.bitvector) {
|
|
totalWords += node.bitvector.bitvector.length;
|
|
totalRank += node.bitvector.rankSelect.blockRank.length;
|
|
}
|
|
if (node.leftChild) stack.push(node.leftChild);
|
|
if (node.rightChild) stack.push(node.rightChild);
|
|
}
|
|
return { nodeCount, totalWords, totalRank };
|
|
}
|
|
|
|
const shardSizes = [];
|
|
const alphabetSize = graph.numNodes;
|
|
const blockSize = 512;
|
|
|
|
for (let relId = 0; relId < relCount; relId++) {
|
|
for (let b = 0; b < bucketCount; b++) {
|
|
const shard = shardBuckets[relId][b];
|
|
if (!shard || shard.edgeCount === 0) continue;
|
|
const sourceStart = b * bucketSize;
|
|
const sourceCount = Math.min(bucketSize, users - sourceStart);
|
|
const dstSequence = [];
|
|
for (let i = 0; i < sourceCount; i++) {
|
|
const list = shard.dstsBySrc[i];
|
|
if (!list) continue;
|
|
for (let j = 0; j < list.length; j++) {
|
|
dstSequence.push(list[j]);
|
|
}
|
|
}
|
|
|
|
const boundaryLength = sourceCount + shard.edgeCount + 1;
|
|
const boundaryWordCount = Math.ceil(boundaryLength / 32);
|
|
const boundaryBlockRankLength = Math.ceil(boundaryLength / blockSize) + 1;
|
|
const { nodeCount, totalWords, totalRank } = computeWaveletFlatSize(dstSequence, alphabetSize);
|
|
const bytes = sizeWaveletAdjacency({
|
|
sourceCount,
|
|
edgeCount: shard.edgeCount,
|
|
boundaryWordCount,
|
|
boundaryBlockRankLength,
|
|
nodeCount,
|
|
totalWords,
|
|
totalRank
|
|
});
|
|
shardSizes.push({ relId, bucket: b, edges: shard.edgeCount, bytes });
|
|
}
|
|
}
|
|
|
|
shardSizes.sort((a, b) => a.bytes - b.bytes);
|
|
const totalBytes = shardSizes.reduce((sum, shard) => sum + shard.bytes, 0);
|
|
const p = (q) => shardSizes[Math.floor((shardSizes.length - 1) * q)] || shardSizes[0];
|
|
const toMb = (bytes) => (bytes / 1024 / 1024).toFixed(2);
|
|
|
|
console.log('\nShard sizes');
|
|
console.log(` shards: ${shardSizes.length}`);
|
|
console.log(` total: ${toMb(totalBytes)} MB`);
|
|
console.log(` avg: ${toMb(totalBytes / shardSizes.length)} MB`);
|
|
console.log(` p50: ${toMb(p(0.5).bytes)} MB`);
|
|
console.log(` p95: ${toMb(p(0.95).bytes)} MB`);
|
|
console.log(` p99: ${toMb(p(0.99).bytes)} MB`);
|
|
console.log(` max: ${toMb(shardSizes[shardSizes.length - 1].bytes)} MB`);
|
|
console.log(` bytes/edge (avg): ${(totalBytes / edges).toFixed(2)}`);
|