import { test, describe, before, after } from 'node:test'; import assert from 'node:assert/strict'; import { Arbiter } from '../../src/index.js'; import { BigGraphGenerator } from '../helpers/big-graph-generator.js'; describe.skip('Memory Scaling Tests', () => { let baselineMemory; before(() => { // Force garbage collection if available if (global.gc) { global.gc(); } baselineMemory = process.memoryUsage(); }); after(() => { // Cleanup if (global.gc) { global.gc(); } }); /** * Get current memory usage in MB */ function getMemoryUsage() { const memUsage = process.memoryUsage(); return { heapUsed: memUsage.heapUsed / 1024 / 1024, heapTotal: memUsage.heapTotal / 1024 / 1024, external: memUsage.external / 1024 / 1024, rss: memUsage.rss / 1024 / 1024 }; } /** * Measure memory usage for an operation */ function measureMemoryUsage(operation, description) { if (global.gc) global.gc(); const before = getMemoryUsage(); const result = operation(); if (global.gc) global.gc(); const after = getMemoryUsage(); const delta = { heapUsed: after.heapUsed - before.heapUsed, heapTotal: after.heapTotal - before.heapTotal, external: after.external - before.external, rss: after.rss - before.rss }; if (process.env.TEST_DEBUG === '1') console.log(`šŸ“Š ${description}:`); if (process.env.TEST_DEBUG === '1') console.log(` Before: ${before.heapUsed.toFixed(2)}MB heap, ${before.rss.toFixed(2)}MB RSS`); if (process.env.TEST_DEBUG === '1') console.log(` After: ${after.heapUsed.toFixed(2)}MB heap, ${after.rss.toFixed(2)}MB RSS`); if (process.env.TEST_DEBUG === '1') console.log(` Delta: ${delta.heapUsed.toFixed(2)}MB heap, ${delta.rss.toFixed(2)}MB RSS`); return { before, after, delta, result }; } test('Memory scaling: Node count', async () => { if (process.env.TEST_DEBUG === '1') console.log('🧠 Testing memory scaling with node count...'); const nodeCounts = [100, 500, 1000, 2000, 5000]; const memoryResults = []; for (const nodeCount of nodeCounts) { const result = measureMemoryUsage(() => { const arbiter = new Arbiter({ fastConstructionMode: false }); // Add nodes for (let i = 0; i < nodeCount; i++) { arbiter.addNode(`user:${i}`, 'user'); arbiter.addNode(`doc:${i}`, 'document'); } return arbiter; }, `Adding ${nodeCount * 2} nodes`); memoryResults.push({ nodeCount: nodeCount * 2, heapUsed: result.after.heapUsed, heapDelta: result.delta.heapUsed, rss: result.after.rss, rssDelta: result.delta.rss }); } // Analyze scaling if (process.env.TEST_DEBUG === '1') console.log('\nšŸ“ˆ Node Count Memory Scaling Analysis:'); for (let i = 1; i < memoryResults.length; i++) { const prev = memoryResults[i - 1]; const curr = memoryResults[i]; const nodeRatio = curr.nodeCount / prev.nodeCount; const memoryRatio = curr.heapDelta / prev.heapDelta; const scalingFactor = memoryRatio / nodeRatio; if (process.env.TEST_DEBUG === '1') console.log(` ${prev.nodeCount} → ${curr.nodeCount} nodes: ${scalingFactor.toFixed(2)}x memory scaling`); } // Verify reasonable scaling (should be roughly linear) const lastResult = memoryResults[memoryResults.length - 1]; assert.ok(lastResult.heapUsed < 100, `Memory usage ${lastResult.heapUsed.toFixed(2)}MB too high for ${lastResult.nodeCount} nodes`); }); test('Memory scaling: Relation count', async () => { if (process.env.TEST_DEBUG === '1') console.log('🧠 Testing memory scaling with relation count...'); const relationCounts = [500, 1000, 2000, 5000, 10000]; const memoryResults = []; for (const relationCount of relationCounts) { const result = measureMemoryUsage(() => { const arbiter = new Arbiter({ fastConstructionMode: false }); // Add nodes first for (let i = 0; i < relationCount; i++) { arbiter.addNode(`user:${i}`, 'user'); arbiter.addNode(`doc:${i}`, 'document'); } // Add relations for (let i = 0; i < relationCount; i++) { arbiter.addRelation(`user:${i}`, 'can_read', `doc:${i}`, { possibility: 1.0 }); } return arbiter; }, `Adding ${relationCount} relations`); memoryResults.push({ relationCount, heapUsed: result.after.heapUsed, heapDelta: result.delta.heapUsed, rss: result.after.rss, rssDelta: result.delta.rss }); } // Analyze scaling if (process.env.TEST_DEBUG === '1') console.log('\nšŸ“ˆ Relation Count Memory Scaling Analysis:'); for (let i = 1; i < memoryResults.length; i++) { const prev = memoryResults[i - 1]; const curr = memoryResults[i]; const relationRatio = curr.relationCount / prev.relationCount; const memoryRatio = curr.heapDelta / prev.heapDelta; const scalingFactor = memoryRatio / relationRatio; if (process.env.TEST_DEBUG === '1') console.log(` ${prev.relationCount} → ${curr.relationCount} relations: ${scalingFactor.toFixed(2)}x memory scaling`); } // Verify reasonable scaling const lastResult = memoryResults[memoryResults.length - 1]; assert.ok(lastResult.heapUsed < 200, `Memory usage ${lastResult.heapUsed.toFixed(2)}MB too high for ${lastResult.relationCount} relations`); }); test('Memory scaling: Graph complexity', async () => { if (process.env.TEST_DEBUG === '1') console.log('🧠 Testing memory scaling with graph complexity...'); const scales = ['small', 'medium', 'large']; const memoryResults = []; for (const scale of scales) { const result = measureMemoryUsage(() => { const generator = new BigGraphGenerator({ scale, seed: 12345 }); const graphData = generator.generateGraph('enterprise'); const arbiter = generator.loadIntoArbiter(graphData); return { arbiter, graphData }; }, `Loading ${scale} scale graph`); memoryResults.push({ scale, userCount: result.result.graphData.users.length, docCount: result.result.graphData.documents.length, relationCount: result.result.graphData.relations.length, heapUsed: result.after.heapUsed, heapDelta: result.delta.heapUsed, rss: result.after.rss, rssDelta: result.delta.rss }); } // Analyze scaling if (process.env.TEST_DEBUG === '1') console.log('\nšŸ“ˆ Graph Complexity Memory Scaling Analysis:'); for (let i = 1; i < memoryResults.length; i++) { const prev = memoryResults[i - 1]; const curr = memoryResults[i]; const relationRatio = curr.relationCount / prev.relationCount; const memoryRatio = curr.heapDelta / prev.heapDelta; const scalingFactor = memoryRatio / relationRatio; if (process.env.TEST_DEBUG === '1') console.log(` ${prev.scale} → ${curr.scale}: ${prev.relationCount} → ${curr.relationCount} relations`); if (process.env.TEST_DEBUG === '1') console.log(` Memory scaling: ${scalingFactor.toFixed(2)}x (${prev.heapDelta.toFixed(2)}MB → ${curr.heapDelta.toFixed(2)}MB)`); } // Verify reasonable scaling const lastResult = memoryResults[memoryResults.length - 1]; assert.ok(lastResult.heapUsed < 500, `Memory usage ${lastResult.heapUsed.toFixed(2)}MB too high for ${lastResult.scale} scale`); }); test('Memory scaling: Cache growth', async () => { if (process.env.TEST_DEBUG === '1') console.log('🧠 Testing memory scaling with cache growth...'); // Set up a graph const generator = new BigGraphGenerator({ scale: 'medium', seed: 12345 }); const graphData = generator.generateGraph('enterprise'); const arbiter = generator.loadIntoArbiter(graphData); const queryCounts = [100, 500, 1000, 2000, 5000]; const memoryResults = []; for (const queryCount of queryCounts) { const result = measureMemoryUsage(() => { // Run queries to populate caches const relations = graphData.relations.slice(0, queryCount); for (const relation of relations) { try { arbiter.check(relation.src, relation.relation, relation.dst); } catch (error) { // Ignore errors } } }, `Running ${queryCount} queries (cache population)`); memoryResults.push({ queryCount, heapUsed: result.after.heapUsed, heapDelta: result.delta.heapUsed, rss: result.after.rss, rssDelta: result.delta.rss }); } // Analyze cache scaling if (process.env.TEST_DEBUG === '1') console.log('\nšŸ“ˆ Cache Memory Scaling Analysis:'); for (let i = 1; i < memoryResults.length; i++) { const prev = memoryResults[i - 1]; const curr = memoryResults[i]; const queryRatio = curr.queryCount / prev.queryCount; const memoryRatio = curr.heapDelta / prev.heapDelta; const scalingFactor = memoryRatio / queryRatio; if (process.env.TEST_DEBUG === '1') console.log(` ${prev.queryCount} → ${curr.queryCount} queries: ${scalingFactor.toFixed(2)}x memory scaling`); } // Verify cache doesn't grow excessively const lastResult = memoryResults[memoryResults.length - 1]; assert.ok(lastResult.heapDelta < 50, `Cache memory growth ${lastResult.heapDelta.toFixed(2)}MB too high for ${lastResult.queryCount} queries`); }); test('Memory scaling: Indices memory usage', async () => { if (process.env.TEST_DEBUG === '1') console.log('🧠 Testing memory scaling with indices...'); const scales = ['small', 'medium']; const memoryResults = []; for (const scale of scales) { // Test without indices const resultWithoutIndices = measureMemoryUsage(() => { const arbiter = new Arbiter({ fastConstructionMode: true }); const generator = new BigGraphGenerator({ scale, seed: 12345 }); const graphData = generator.generateGraph('enterprise'); // Add relations without building indices for (const relation of graphData.relations) { arbiter.addRelation(relation.src, relation.relation, relation.dst, { possibility: relation.possibility }); } return arbiter; }, `${scale} scale without indices`); // Test with indices const resultWithIndices = measureMemoryUsage(() => { const arbiter = new Arbiter({ fastConstructionMode: false }); const generator = new BigGraphGenerator({ scale, seed: 12345 }); const graphData = generator.generateGraph('enterprise'); const loadedArbiter = generator.loadIntoArbiter(graphData); return loadedArbiter; }, `${scale} scale with indices`); const indexMemoryOverhead = resultWithIndices.after.heapUsed - resultWithoutIndices.after.heapUsed; memoryResults.push({ scale, relationCount: resultWithIndices.result.relations.length, withoutIndices: resultWithoutIndices.after.heapUsed, withIndices: resultWithIndices.after.heapUsed, indexOverhead: indexMemoryOverhead, overheadPerRelation: indexMemoryOverhead / resultWithIndices.result.relations.length }); } // Analyze index memory overhead if (process.env.TEST_DEBUG === '1') console.log('\nšŸ“ˆ Index Memory Overhead Analysis:'); for (const result of memoryResults) { if (process.env.TEST_DEBUG === '1') console.log(` ${result.scale} scale (${result.relationCount} relations):`); if (process.env.TEST_DEBUG === '1') console.log(` Without indices: ${result.withoutIndices.toFixed(2)}MB`); if (process.env.TEST_DEBUG === '1') console.log(` With indices: ${result.withIndices.toFixed(2)}MB`); if (process.env.TEST_DEBUG === '1') console.log(` Index overhead: ${result.indexOverhead.toFixed(2)}MB (${result.overheadPerRelation.toFixed(4)}MB per relation)`); } // Verify reasonable index overhead const lastResult = memoryResults[memoryResults.length - 1]; assert.ok(lastResult.overheadPerRelation < 0.01, `Index overhead ${lastResult.overheadPerRelation.toFixed(4)}MB per relation too high`); }); test('Memory scaling: Long-running operations', async () => { if (process.env.TEST_DEBUG === '1') console.log('🧠 Testing memory scaling with long-running operations...'); const arbiter = new Arbiter({ fastConstructionMode: false }); const generator = new BigGraphGenerator({ scale: 'medium', seed: 12345 }); const graphData = generator.generateGraph('enterprise'); const loadedArbiter = generator.loadIntoArbiter(graphData); const operationCounts = [1000, 5000, 10000, 20000]; const memoryResults = []; for (const opCount of operationCounts) { const result = measureMemoryUsage(() => { // Mix of operations for (let i = 0; i < opCount; i++) { const opType = i % 4; const relation = graphData.relations[i % graphData.relations.length]; switch (opType) { case 0: // Add relation loadedArbiter.addRelation(`${relation.src}-${i}`, relation.relation, `${relation.dst}-${i}`, { possibility: 1.0 }); break; case 1: // Remove relation loadedArbiter.removeRelation(`${relation.src}-${i}`, relation.relation, `${relation.dst}-${i}`); break; case 2: // Query loadedArbiter.check(relation.src, relation.relation, relation.dst); break; case 3: // Update relation loadedArbiter.addRelation(`${relation.src}-${i}`, relation.relation, `${relation.dst}-${i}`, { possibility: 0.8 }); break; } } }, `Running ${opCount} mixed operations`); memoryResults.push({ operationCount: opCount, heapUsed: result.after.heapUsed, heapDelta: result.delta.heapUsed, rss: result.after.rss, rssDelta: result.delta.rss }); } // Analyze long-running memory scaling if (process.env.TEST_DEBUG === '1') console.log('\nšŸ“ˆ Long-running Operations Memory Scaling Analysis:'); for (let i = 1; i < memoryResults.length; i++) { const prev = memoryResults[i - 1]; const curr = memoryResults[i]; const opRatio = curr.operationCount / prev.operationCount; const memoryRatio = curr.heapDelta / prev.heapDelta; const scalingFactor = memoryRatio / opRatio; if (process.env.TEST_DEBUG === '1') console.log(` ${prev.operationCount} → ${curr.operationCount} operations: ${scalingFactor.toFixed(2)}x memory scaling`); } // Verify no memory leaks const lastResult = memoryResults[memoryResults.length - 1]; assert.ok(lastResult.heapDelta < 100, `Memory growth ${lastResult.heapDelta.toFixed(2)}MB too high for ${lastResult.operationCount} operations`); }); test('Memory scaling: Memory pressure test', async () => { if (process.env.TEST_DEBUG === '1') console.log('🧠 Testing memory under pressure...'); const pressureLevels = [1000, 5000, 10000, 20000, 50000]; const memoryResults = []; for (const pressureLevel of pressureLevels) { const result = measureMemoryUsage(() => { const arbiter = new Arbiter({ fastConstructionMode: false }); // Create memory pressure by adding many relations for (let i = 0; i < pressureLevel; i++) { arbiter.addNode(`user:${i}`, 'user'); arbiter.addNode(`doc:${i}`, 'document'); arbiter.addRelation(`user:${i}`, 'can_read', `doc:${i}`, { possibility: 1.0 }); arbiter.addRelation(`user:${i}`, 'can_write', `doc:${i}`, { possibility: 0.8 }); arbiter.addRelation(`user:${i}`, 'can_delete', `doc:${i}`, { possibility: 0.6 }); } return arbiter; }, `Memory pressure with ${pressureLevel} entities`); memoryResults.push({ entityCount: pressureLevel, heapUsed: result.after.heapUsed, heapDelta: result.delta.heapUsed, rss: result.after.rss, rssDelta: result.delta.rss, memoryPerEntity: result.delta.heapUsed / pressureLevel }); } // Analyze memory pressure scaling if (process.env.TEST_DEBUG === '1') console.log('\nšŸ“ˆ Memory Pressure Scaling Analysis:'); for (const result of memoryResults) { if (process.env.TEST_DEBUG === '1') console.log(` ${result.entityCount} entities: ${result.heapUsed.toFixed(2)}MB total, ${result.memoryPerEntity.toFixed(4)}MB per entity`); } // Verify reasonable memory usage under pressure const lastResult = memoryResults[memoryResults.length - 1]; assert.ok(lastResult.memoryPerEntity < 0.1, `Memory per entity ${lastResult.memoryPerEntity.toFixed(4)}MB too high`); assert.ok(lastResult.heapUsed < 1000, `Total memory usage ${lastResult.heapUsed.toFixed(2)}MB too high under pressure`); }); test('Memory scaling: Garbage collection impact', async () => { if (process.env.TEST_DEBUG === '1') console.log('🧠 Testing garbage collection impact...'); // Test without explicit GC const resultWithoutGC = measureMemoryUsage(() => { const arbiter = new Arbiter({ fastConstructionMode: false }); const generator = new BigGraphGenerator({ scale: 'medium', seed: 12345 }); const graphData = generator.generateGraph('enterprise'); const loadedArbiter = generator.loadIntoArbiter(graphData); // Run many operations without GC for (let i = 0; i < 10000; i++) { const relation = graphData.relations[i % graphData.relations.length]; loadedArbiter.check(relation.src, relation.relation, relation.dst); } return loadedArbiter; }, 'Operations without explicit GC'); // Test with explicit GC const resultWithGC = measureMemoryUsage(() => { const arbiter = new Arbiter({ fastConstructionMode: false }); const generator = new BigGraphGenerator({ scale: 'medium', seed: 12345 }); const graphData = generator.generateGraph('enterprise'); const loadedArbiter = generator.loadIntoArbiter(graphData); // Run many operations with periodic GC for (let i = 0; i < 10000; i++) { const relation = graphData.relations[i % graphData.relations.length]; loadedArbiter.check(relation.src, relation.relation, relation.dst); if (i % 1000 === 0 && global.gc) { global.gc(); } } return loadedArbiter; }, 'Operations with periodic GC'); if (process.env.TEST_DEBUG === '1') console.log('\nšŸ“ˆ Garbage Collection Impact Analysis:'); if (process.env.TEST_DEBUG === '1') console.log(` Without GC: ${resultWithoutGC.after.heapUsed.toFixed(2)}MB heap, ${resultWithoutGC.after.rss.toFixed(2)}MB RSS`); if (process.env.TEST_DEBUG === '1') console.log(` With GC: ${resultWithGC.after.heapUsed.toFixed(2)}MB heap, ${resultWithGC.after.rss.toFixed(2)}MB RSS`); const gcBenefit = resultWithoutGC.after.heapUsed - resultWithGC.after.heapUsed; if (process.env.TEST_DEBUG === '1') console.log(` GC Benefit: ${gcBenefit.toFixed(2)}MB heap reduction`); // Verify GC helps (if available) if (global.gc) { assert.ok(gcBenefit >= 0, 'Garbage collection should not increase memory usage'); } }); });