283 lines
9.6 KiB
JavaScript
283 lines
9.6 KiB
JavaScript
|
|
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)}`
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|