import { test, describe, before, after } from 'node:test'; import assert from 'node:assert/strict'; import { Arbiter } from '../../src/index.js'; describe('Clean Memory Scaling Tests', () => { /** * 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 with proper GC handling */ function measureMemoryUsage(operation, description) { // Force multiple GC cycles to get clean baseline if (global.gc) { global.gc(); global.gc(); global.gc(); } const before = getMemoryUsage(); const result = operation(); // Force multiple GC cycles to get clean measurement if (global.gc) { global.gc(); 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`); if (process.env.TEST_DEBUG === '1') console.log(` Heap utilization: ${((after.heapUsed / after.heapTotal) * 100).toFixed(1)}%`); return { before, after, delta, result }; } test('Clean node memory scaling test', async () => { if (process.env.TEST_DEBUG === '1') console.log('🧠 Clean node memory scaling test...'); const nodeCounts = [100, 200, 500, 1000, 2000, 5000]; const memoryResults = []; for (const nodeCount of nodeCounts) { const result = measureMemoryUsage(() => { const arbiter = new Arbiter({ fastConstructionMode: true }); for (let i = 0; i < nodeCount; i++) { arbiter.addNode(`user:${i}`, 'user'); } return arbiter; }, `Adding ${nodeCount} nodes`); memoryResults.push({ nodeCount, heapUsed: result.after.heapUsed, heapDelta: result.delta.heapUsed, memoryPerNode: result.delta.heapUsed / nodeCount, heapUtilization: (result.after.heapUsed / result.after.heapTotal) * 100 }); } // Analyze scaling if (process.env.TEST_DEBUG === '1') console.log('\nšŸ“ˆ Clean Node Memory Scaling Analysis:'); if (process.env.TEST_DEBUG === '1') console.log('Nodes | Heap Delta | Memory/Node | Heap Util | Scaling Factor'); if (process.env.TEST_DEBUG === '1') console.log('------|------------|-------------|-----------|---------------'); for (let i = 0; i < memoryResults.length; i++) { const result = memoryResults[i]; const scalingFactor = i > 0 ? (result.memoryPerNode / memoryResults[i-1].memoryPerNode).toFixed(2) : 'N/A'; if (process.env.TEST_DEBUG === '1') console.log(`${result.nodeCount.toString().padStart(5)} | ${result.heapDelta.toFixed(2).padStart(10)}MB | ${result.memoryPerNode.toFixed(4).padStart(11)}MB | ${result.heapUtilization.toFixed(1).padStart(8)}% | ${scalingFactor.padStart(13)}x`); } // Check for linear scaling const memoryPerNodeValues = memoryResults.map(r => r.memoryPerNode); const avgMemoryPerNode = memoryPerNodeValues.reduce((a, b) => a + b, 0) / memoryPerNodeValues.length; const maxDeviation = Math.max(...memoryPerNodeValues.map(v => Math.abs(v - avgMemoryPerNode))); const deviationPercent = (maxDeviation / avgMemoryPerNode) * 100; if (process.env.TEST_DEBUG === '1') console.log(`\nAverage memory per node: ${avgMemoryPerNode.toFixed(4)}MB`); if (process.env.TEST_DEBUG === '1') console.log(`Maximum deviation: ${maxDeviation.toFixed(4)}MB (${deviationPercent.toFixed(1)}%)`); if (deviationPercent < 20) { if (process.env.TEST_DEBUG === '1') console.log('āœ… Memory scaling is approximately linear'); } else { if (process.env.TEST_DEBUG === '1') console.log('āš ļø Memory scaling shows significant nonlinearity'); } }); test('Clean relation memory scaling test', async () => { if (process.env.TEST_DEBUG === '1') console.log('🧠 Clean relation memory scaling test...'); const relationCounts = [100, 200, 500, 1000, 2000, 5000]; const memoryResults = []; for (const relationCount of relationCounts) { const result = measureMemoryUsage(() => { const arbiter = new Arbiter({ fastConstructionMode: true }); // 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, memoryPerRelation: result.delta.heapUsed / relationCount, heapUtilization: (result.after.heapUsed / result.after.heapTotal) * 100 }); } // Analyze scaling if (process.env.TEST_DEBUG === '1') console.log('\nšŸ“ˆ Clean Relation Memory Scaling Analysis:'); if (process.env.TEST_DEBUG === '1') console.log('Rels | Heap Delta | Memory/Rel | Heap Util | Scaling Factor'); if (process.env.TEST_DEBUG === '1') console.log('------|------------|------------|-----------|---------------'); for (let i = 0; i < memoryResults.length; i++) { const result = memoryResults[i]; const scalingFactor = i > 0 ? (result.memoryPerRelation / memoryResults[i-1].memoryPerRelation).toFixed(2) : 'N/A'; if (process.env.TEST_DEBUG === '1') console.log(`${result.relationCount.toString().padStart(5)} | ${result.heapDelta.toFixed(2).padStart(10)}MB | ${result.memoryPerRelation.toFixed(4).padStart(10)}MB | ${result.heapUtilization.toFixed(1).padStart(8)}% | ${scalingFactor.padStart(13)}x`); } // Check for linear scaling const memoryPerRelationValues = memoryResults.map(r => r.memoryPerRelation); const avgMemoryPerRelation = memoryPerRelationValues.reduce((a, b) => a + b, 0) / memoryPerRelationValues.length; const maxDeviation = Math.max(...memoryPerRelationValues.map(v => Math.abs(v - avgMemoryPerRelation))); const deviationPercent = (maxDeviation / avgMemoryPerRelation) * 100; if (process.env.TEST_DEBUG === '1') console.log(`\nAverage memory per relation: ${avgMemoryPerRelation.toFixed(4)}MB`); if (process.env.TEST_DEBUG === '1') console.log(`Maximum deviation: ${maxDeviation.toFixed(4)}MB (${deviationPercent.toFixed(1)}%)`); if (deviationPercent < 20) { if (process.env.TEST_DEBUG === '1') console.log('āœ… Memory scaling is approximately linear'); } else { if (process.env.TEST_DEBUG === '1') console.log('āš ļø Memory scaling shows significant nonlinearity'); } }); test('Index memory overhead analysis', async () => { if (process.env.TEST_DEBUG === '1') console.log('🧠 Index memory overhead analysis...'); const relationCounts = [1000, 2000, 5000]; for (const relationCount of relationCounts) { if (process.env.TEST_DEBUG === '1') console.log(`\n--- Testing with ${relationCount} relations ---`); // Fast construction mode (no indices) const resultFast = measureMemoryUsage(() => { const arbiter = new Arbiter({ fastConstructionMode: true }); for (let i = 0; i < relationCount; i++) { arbiter.addNode(`user:${i}`, 'user'); arbiter.addNode(`doc:${i}`, 'document'); arbiter.addRelation(`user:${i}`, 'can_read', `doc:${i}`, { possibility: 1.0 }); } return arbiter; }, `Fast construction mode (${relationCount} relations)`); // Normal mode (with indices) const resultNormal = measureMemoryUsage(() => { const arbiter = new Arbiter({ fastConstructionMode: false }); for (let i = 0; i < relationCount; i++) { arbiter.addNode(`user:${i}`, 'user'); arbiter.addNode(`doc:${i}`, 'document'); arbiter.addRelation(`user:${i}`, 'can_read', `doc:${i}`, { possibility: 1.0 }); } return arbiter; }, `Normal mode with indices (${relationCount} relations)`); const indexOverhead = resultNormal.after.heapUsed - resultFast.after.heapUsed; const overheadPerRelation = indexOverhead / relationCount; if (process.env.TEST_DEBUG === '1') console.log(` Index overhead: ${indexOverhead.toFixed(2)}MB`); if (process.env.TEST_DEBUG === '1') console.log(` Overhead per relation: ${overheadPerRelation.toFixed(4)}MB`); if (process.env.TEST_DEBUG === '1') console.log(` Overhead percentage: ${((indexOverhead / resultFast.after.heapUsed) * 100).toFixed(1)}%`); } }); test('Memory efficiency comparison', async () => { if (process.env.TEST_DEBUG === '1') console.log('🧠 Memory efficiency comparison...'); const relationCount = 5000; // Test different configurations const configs = [ { name: 'Fast construction', fastMode: true }, { name: 'Normal mode', fastMode: false }, { name: 'Fast + manual indices', fastMode: true, manualIndices: true } ]; for (const config of configs) { const result = measureMemoryUsage(() => { const arbiter = new Arbiter({ fastConstructionMode: config.fastMode }); for (let i = 0; i < relationCount; i++) { arbiter.addNode(`user:${i}`, 'user'); arbiter.addNode(`doc:${i}`, 'document'); arbiter.addRelation(`user:${i}`, 'can_read', `doc:${i}`, { possibility: 1.0 }); } if (config.manualIndices) { arbiter.setFastConstructionMode(false); } return arbiter; }, `${config.name} (${relationCount} relations)`); const memoryPerRelation = result.delta.heapUsed / relationCount; if (process.env.TEST_DEBUG === '1') console.log(` ${config.name}: ${memoryPerRelation.toFixed(4)}MB per relation`); } }); });