717ae1031e
Zanzibar-style authorization graph engine (direct/chain/TTU/defeasible/ binary modes, condensed snapshots, value relations) with 39 rigor test campaigns. Includes fixes for snapshot binary writer/reader format mismatch (snapshot-of-snapshot corruption), possibility write-boundary validation, empty-graph snapshot serialization, relation lookup cache direction collision, config-redefinition cache invalidation, binary threshold semantics, defeasible compiled routing, and comparator reason whitelisting.
468 lines
19 KiB
JavaScript
468 lines
19 KiB
JavaScript
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');
|
|
}
|
|
});
|
|
});
|