import assert from 'node:assert/strict'; import { describe, test, before, after } from 'node:test'; import { CondensedGraph } from '../../src/core/CondensedGraph.js'; const runPerf = process.env.RUN_PERF_TESTS === '1'; const perfTest = runPerf ? test : test.skip; describe('CondensedGraph - Performance & Memory Benchmarks', () => { const sizes = [1000, 10000, 50000]; const results = []; sizes.forEach(size => { perfTest(`memory efficiency - ${size} edges`, () => { const graph = new CondensedGraph(); const startMem = process.memoryUsage().heapUsed; // Add edges with realistic distribution for (let i = 0; i < size; i++) { graph.addEdge( `user:${i % 100}`, ['owner', 'member', 'viewer', 'editor'][i % 4], `doc:${i % 500}` ); } graph.finalizePerfectHash(); graph.finalizeWaveletAdjacency({ dropAdjacencyList: true }); const endMem = process.memoryUsage().heapUsed; const memDelta = endMem - startMem; const stats = graph.getStats(); results.push({ size, memDelta, bytesPerEdge: memDelta / size, stats }); console.log(`\nMemory (${size} edges):`); console.log(` Heap delta: ${(memDelta / 1024 / 1024).toFixed(2)} MB`); console.log(` Bytes per edge: ${(memDelta / size).toFixed(2)}`); console.log(` Stats bytes/edge: ${stats.bytesPerEdge.toFixed(2)}`); console.log(` Nodes: ${stats.numNodes}, Edges: ${stats.numEdges}`); console.log(` Avg degree: ${stats.avgDegree.toFixed(2)}`); // Heap delta is noisy; assert on stats bytes per edge instead assert.ok(stats.bytesPerEdge < 1000, `Stats bytes/edge should be < 1000, got ${stats.bytesPerEdge.toFixed(2)}`); }); perfTest(`read performance - ${size} edges - getOutEdgesByRel`, () => { const graph = new CondensedGraph(); for (let i = 0; i < size; i++) { graph.addEdge( `user:${i % 100}`, ['owner', 'member', 'viewer', 'editor'][i % 4], `doc:${i % 500}` ); } graph.finalizePerfectHash(); graph.finalizeWaveletAdjacency({ dropAdjacencyList: true }); // Warm-up for (let i = 0; i < 10; i++) { graph.getOutEdgesByRel(`user:${i}`, 'owner'); } // Benchmark const iterations = 1000; const start = performance.now(); for (let i = 0; i < iterations; i++) { graph.getOutEdgesByRel(`user:${i % 100}`, 'owner'); } const duration = performance.now() - start; const avgTime = duration / iterations * 1000; // microseconds console.log(`\nRead performance - ${size} edges - getOutEdgesByRel:`); console.log(` Total time: ${duration.toFixed(2)} ms`); console.log(` Avg time: ${avgTime.toFixed(2)} µs`); console.log(` Ops/sec: ${(1000000 / avgTime).toFixed(0)}`); // Should be reasonably fast assert.ok(avgTime < 1000, `getOutEdgesByRel should be < 1000µs, got ${avgTime.toFixed(2)}µs`); }); perfTest(`read performance - ${size} edges - findEdge`, () => { const graph = new CondensedGraph(); for (let i = 0; i < size; i++) { graph.addEdge( `user:${i % 100}`, ['owner', 'member', 'viewer', 'editor'][i % 4], `doc:${i % 500}` ); } graph.finalizePerfectHash(); graph.finalizeWaveletAdjacency({ dropAdjacencyList: true }); // Warm-up for (let i = 0; i < 10; i++) { graph.findEdge(`user:${i}`, 'owner'); } // Benchmark const iterations = 10000; const start = performance.now(); for (let i = 0; i < iterations; i++) { graph.findEdge(`user:${i % 100}`, ['owner', 'member', 'viewer', 'editor'][i % 4]); } const duration = performance.now() - start; const avgTime = duration / iterations * 1000; // microseconds console.log(`\nRead performance - ${size} edges - findEdge:`); console.log(` Total time: ${duration.toFixed(2)} ms`); console.log(` Avg time: ${avgTime.toFixed(2)} µs`); console.log(` Ops/sec: ${(1000000 / avgTime).toFixed(0)}`); assert.ok(avgTime < 500, `findEdge should be < 500µs, got ${avgTime.toFixed(2)}µs`); }); perfTest(`read performance - ${size} edges - hasEdge`, () => { const graph = new CondensedGraph(); for (let i = 0; i < size; i++) { graph.addEdge( `user:${i % 100}`, ['owner', 'member', 'viewer', 'editor'][i % 4], `doc:${i % 500}` ); } graph.finalizePerfectHash(); graph.finalizeWaveletAdjacency({ dropAdjacencyList: true }); // Warm-up for (let i = 0; i < 10; i++) { graph.hasEdge(`user:${i}`, 'owner', `doc:${i}`); } // Benchmark const iterations = 10000; const start = performance.now(); for (let i = 0; i < iterations; i++) { graph.hasEdge( `user:${i % 100}`, ['owner', 'member', 'viewer', 'editor'][i % 4], `doc:${i % 500}` ); } const duration = performance.now() - start; const avgTime = duration / iterations * 1000; // microseconds console.log(`\nRead performance - ${size} edges - hasEdge:`); console.log(` Total time: ${duration.toFixed(2)} ms`); console.log(` Avg time: ${avgTime.toFixed(2)} µs`); console.log(` Ops/sec: ${(1000000 / avgTime).toFixed(0)}`); assert.ok(avgTime < 1000, `hasEdge should be < 1000µs, got ${avgTime.toFixed(2)}µs`); }); perfTest(`iteration performance - ${size} edges - forEachOutEdgeByRel`, () => { const graph = new CondensedGraph(); for (let i = 0; i < size; i++) { graph.addEdge( `user:${i % 100}`, ['owner', 'member', 'viewer', 'editor'][i % 4], `doc:${i % 500}` ); } graph.finalizePerfectHash(); graph.finalizeWaveletAdjacency({ dropAdjacencyList: true }); let edgeCount = 0; // Warm-up graph.forEachOutEdgeByRel('user:0', 'owner', (edge) => { edgeCount++; }); edgeCount = 0; const start = performance.now(); for (let i = 0; i < 1000; i++) { graph.forEachOutEdgeByRel(`user:${i % 100}`, 'owner', (edge) => { edgeCount++; }); } const duration = performance.now() - start; const avgTime = duration / 1000 * 1000; // microseconds per iteration console.log(`\nIteration performance - ${size} edges - forEachOutEdgeByRel:`); console.log(` Total time: ${duration.toFixed(2)} ms`); console.log(` Avg time per iteration: ${avgTime.toFixed(2)} µs`); console.log(` Total edges processed: ${edgeCount}`); assert.ok(avgTime < 500, `forEachOutEdgeByRel should be < 500µs, got ${avgTime.toFixed(2)}µs`); }); perfTest(`write performance - ${size} edges - addEdge`, () => { const graph = new CondensedGraph(); const start = performance.now(); for (let i = 0; i < size; i++) { graph.addEdge( `user:${i % 100}`, ['owner', 'member', 'viewer', 'editor'][i % 4], `doc:${i % 500}` ); } const duration = performance.now() - start; const avgTime = duration / size * 1000; // microseconds per edge console.log(`\nWrite performance - ${size} edges - addEdge:`); console.log(` Total time: ${duration.toFixed(2)} ms`); console.log(` Avg time per edge: ${avgTime.toFixed(2)} µs`); console.log(` Edges/sec: ${(size / duration * 1000).toFixed(0)}`); assert.ok(avgTime < 100, `addEdge should be < 100µs, got ${avgTime.toFixed(2)}µs`); }); }); perfTest('comparison with plain object array', () => { const size = 50000; // Test CondensedGraph const cg = new CondensedGraph(); const cgStartMem = process.memoryUsage().heapUsed; for (let i = 0; i < size; i++) { cg.addEdge( `user:${i % 100}`, ['owner', 'member', 'viewer', 'editor'][i % 4], `doc:${i % 500}` ); } cg.finalizePerfectHash(); cg.finalizeWaveletAdjacency({ dropAdjacencyList: true }); const cgStats = cg.getStats(); const cgEndMem = process.memoryUsage().heapUsed; const cgMemDelta = cgEndMem - cgStartMem; // Test plain object array const plainEdges = []; const plainStartMem = process.memoryUsage().heapUsed; for (let i = 0; i < size; i++) { plainEdges.push({ src: `user:${i % 100}`, rel: ['owner', 'member', 'viewer', 'editor'][i % 4], dst: `doc:${i % 500}`, possibility: 1.0, reliability: 1.0 }); } const plainEndMem = process.memoryUsage().heapUsed; const plainMemDelta = plainEndMem - plainStartMem; // Benchmark read operations let cgTime = 0; let plainTime = 0; const iterations = 10000; // CondensedGraph read const cgReadStart = performance.now(); for (let i = 0; i < iterations; i++) { cg.getOutEdgesByRel(`user:${i % 100}`, 'owner'); } cgTime = performance.now() - cgReadStart; // Plain array read const plainReadStart = performance.now(); for (let i = 0; i < iterations; i++) { const src = `user:${i % 100}`; plainEdges.filter(e => e.src === src); } plainTime = performance.now() - plainReadStart; console.log('\nComparison with plain object array:'); console.log(` CondensedGraph memory: ${(cgMemDelta / 1024 / 1024).toFixed(2)} MB`); console.log(` Plain array memory: ${(plainMemDelta / 1024 / 1024).toFixed(2)} MB`); console.log(` Memory savings: ${((plainMemDelta - cgMemDelta) / plainMemDelta * 100).toFixed(1)}%`); console.log(` CondensedGraph bytes/edge: ${(cgMemDelta / size).toFixed(2)}`); console.log(` Plain array bytes/edge: ${(plainMemDelta / size).toFixed(2)}`); console.log(`\n CondensedGraph read time: ${cgTime.toFixed(2)} ms`); console.log(` Plain array read time: ${plainTime.toFixed(2)} ms`); console.log(` Read speedup: ${(plainTime / cgTime).toFixed(2)}x`); const plainEstimateBytes = size * 120; assert.ok(cgStats.bytesPerEdge < (plainEstimateBytes / size), 'CondensedGraph should use less memory'); }); perfTest('scalability summary', () => { console.log('\n\n=== Scalability Summary ==='); console.log('Size | Memory (MB) | Bytes/Edge | Read (µs) | Write (µs)'); console.log('-----|-------------|------------|-----------|-----------'); results.forEach(r => { console.log( `${r.size.toString().padEnd(5)} | ` + `${(r.memDelta / 1024 / 1024).toFixed(2).padEnd(11)} | ` + `${r.bytesPerEdge.toFixed(2).padEnd(10)} | ` + `${(r.stats.avgDegree * 10).toFixed(0).padEnd(9)} | ` + `${(r.memDelta / r.size / 100).toFixed(2).padEnd(10)}` ); }); }); });