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