import assert from 'node:assert/strict'; import { describe, test } 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 - Reliable Memory & Performance', () => { perfTest('memory efficiency - multiple sizes', () => { const sizes = [1000, 10000, 50000]; sizes.forEach(size => { if (global.gc) global.gc(); const baseline = process.memoryUsage(); const graph = new CondensedGraph(); const afterCreate = process.memoryUsage(); 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 afterAdd = process.memoryUsage(); const stats = graph.getStats(); const typedArrayMem = stats.memoryUsage.typedArrays; const nodeMapMem = stats.memoryUsage.nodeMap; const totalMem = stats.memoryUsage.total; console.log(`\nMemory (${size} edges):`); console.log(` Nodes: ${stats.numNodes}`); console.log(` Edges: ${stats.numEdges}`); console.log(` Avg degree: ${stats.avgDegree.toFixed(2)}`); console.log(` Typed arrays: ${(typedArrayMem / 1024 / 1024).toFixed(2)} MB`); console.log(` Node map: ${(nodeMapMem / 1024 / 1024).toFixed(2)} MB`); console.log(` Total: ${(totalMem / 1024 / 1024).toFixed(2)} MB`); console.log(` Bytes/edge: ${stats.bytesPerEdge.toFixed(2)}`); console.log(` Capacity: ${stats.capacity}, Utilization: ${(stats.utilization * 100).toFixed(1)}%`); assert.ok(stats.numEdges === size, `Should have ${size} edges`); assert.ok(totalMem < size * 1000, `Memory should be reasonable`); }); }); perfTest('read performance - warm cache', () => { const sizes = [1000, 10000, 50000]; sizes.forEach(size => { 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 }); const operations = [ { name: 'getOutEdgesByRel', fn: () => graph.getOutEdgesByRel('user:0', 'owner') }, { name: 'findEdge', fn: () => graph.findEdge('user:0', 'owner') }, { name: 'hasEdge', fn: () => graph.hasEdge('user:0', 'owner', 'doc:0') }, { name: 'getDegree', fn: () => graph.getDegree('user:0') } ]; console.log(`\nRead performance (${size} edges):`); operations.forEach(op => { const iterations = 100000; let count = 0; const start = performance.now(); for (let i = 0; i < iterations; i++) { const result = op.fn(); if (result) count++; } const duration = performance.now() - start; const avgMicros = (duration / iterations) * 1000; const opsPerSec = iterations / (duration / 1000); console.log(` ${op.name}: ${avgMicros.toFixed(3)} µs (${opsPerSec.toFixed(0)} ops/sec)`); assert.ok(duration < 5000, `${op.name} should be fast`); }); }); }); perfTest('iteration performance', () => { const sizes = [1000, 10000, 50000]; sizes.forEach(size => { 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 }); console.log(`\nIteration performance (${size} edges):`); let totalEdges = 0; const iterations = 10000; const start = performance.now(); for (let i = 0; i < iterations; i++) { graph.forEachOutEdgeByRel(`user:${i % 100}`, 'owner', (edge) => { totalEdges++; }); } const duration = performance.now() - start; const avgMicros = (duration / iterations) * 1000; console.log(` forEachOutEdgeByRel: ${avgMicros.toFixed(3)} µs/iteration`); console.log(` Total edges processed: ${totalEdges}`); console.log(` Avg edges/node: ${(totalEdges / iterations).toFixed(2)}`); assert.ok(duration < 5000, 'Iteration should be fast'); }); }); perfTest('write performance', () => { const sizes = [1000, 10000, 50000]; sizes.forEach(size => { 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 avgMicros = (duration / size) * 1000; const edgesPerSec = size / (duration / 1000); console.log(`\nWrite performance (${size} edges):`); console.log(` Total time: ${duration.toFixed(2)} ms`); console.log(` Avg time/edge: ${avgMicros.toFixed(3)} µs`); console.log(` Edges/sec: ${edgesPerSec.toFixed(0)}`); assert.ok(avgMicros < 100, `addEdge should be fast`); }); }); perfTest('comparison with plain array - memory', () => { const size = 50000; // CondensedGraph const cg = new CondensedGraph(); 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 cgMem = cgStats.memoryUsage.total; // Plain array const plainEdges = []; 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 }); } // Estimate plain array memory (rough estimate) const plainMem = size * 120; // ~120 bytes per object + overhead console.log('\nMemory comparison (50K edges):'); console.log(` CondensedGraph: ${(cgMem / 1024 / 1024).toFixed(2)} MB`); console.log(` Plain array (est): ${(plainMem / 1024 / 1024).toFixed(2)} MB`); console.log(` Savings: ${((plainMem - cgMem) / plainMem * 100).toFixed(1)}%`); console.log(` CondensedGraph bytes/edge: ${cgStats.bytesPerEdge.toFixed(2)}`); console.log(` Plain array bytes/edge (est): ${(plainMem / size).toFixed(2)}`); assert.ok(cgMem < plainMem, 'CondensedGraph should use less memory'); }); perfTest('comparison with plain array - performance', () => { const size = 50000; // CondensedGraph const cg = new CondensedGraph(); 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 }); // Plain array const plainEdges = []; 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 iterations = 1000; // CondensedGraph: getOutEdges const cgStart = performance.now(); for (let i = 0; i < iterations; i++) { cg.getOutEdgesByRel(`user:${i % 100}`, 'owner'); } const cgTime = performance.now() - cgStart; // Plain array: filter const plainStart = performance.now(); for (let i = 0; i < iterations; i++) { plainEdges.filter(e => e.src === `user:${i % 100}`); } const plainTime = performance.now() - plainStart; console.log('\nPerformance comparison (50K edges, 1K lookups):'); console.log(` CondensedGraph: ${cgTime.toFixed(2)} ms (${(cgTime / iterations).toFixed(3)} µs/lookup)`); console.log(` Plain array filter: ${plainTime.toFixed(2)} ms (${(plainTime / iterations).toFixed(3)} µs/lookup)`); console.log(` Speedup: ${(plainTime / cgTime).toFixed(2)}x`); assert.ok(cgTime < plainTime, 'CondensedGraph should be faster'); }); perfTest('scalability trends', () => { console.log('\n\n=== Scalability Analysis ===\n'); const sizes = [1000, 10000, 50000]; console.log('Size | Nodes | Edges | Avg Deg | Memory (MB) | Bytes/Edge'); console.log('------|-------|-------|---------|-------------|-------------'); sizes.forEach(size => { 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 }); const stats = graph.getStats(); console.log( `${size.toString().padStart(5)} | ` + `${stats.numNodes.toString().padStart(5)} | ` + `${stats.numEdges.toString().padStart(5)} | ` + `${stats.avgDegree.toFixed(2).padStart(7)} | ` + `${(stats.memoryUsage.total / 1024 / 1024).toFixed(2).padStart(11)} | ` + `${stats.bytesPerEdge.toFixed(2).padStart(11)}` ); }); }); });