initial commit: @arbiter/core authorization engine with js-rigor hardening
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.
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
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`);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user