Files
core/benchmarks/shard-size-bench.js
T

196 lines
5.9 KiB
JavaScript
Raw Normal View History

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)}`);