Files
core/tests/engine/condensed-graph-perf.test.js
T

326 lines
11 KiB
JavaScript
Raw Normal View History

import assert from 'node:assert/strict';
import { describe, test, before, after } 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 - Performance & Memory Benchmarks', () => {
const sizes = [1000, 10000, 50000];
const results = [];
sizes.forEach(size => {
perfTest(`memory efficiency - ${size} edges`, () => {
const graph = new CondensedGraph();
const startMem = process.memoryUsage().heapUsed;
// Add edges with realistic distribution
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 endMem = process.memoryUsage().heapUsed;
const memDelta = endMem - startMem;
const stats = graph.getStats();
results.push({
size,
memDelta,
bytesPerEdge: memDelta / size,
stats
});
console.log(`\nMemory (${size} edges):`);
console.log(` Heap delta: ${(memDelta / 1024 / 1024).toFixed(2)} MB`);
console.log(` Bytes per edge: ${(memDelta / size).toFixed(2)}`);
console.log(` Stats bytes/edge: ${stats.bytesPerEdge.toFixed(2)}`);
console.log(` Nodes: ${stats.numNodes}, Edges: ${stats.numEdges}`);
console.log(` Avg degree: ${stats.avgDegree.toFixed(2)}`);
// Heap delta is noisy; assert on stats bytes per edge instead
assert.ok(stats.bytesPerEdge < 1000, `Stats bytes/edge should be < 1000, got ${stats.bytesPerEdge.toFixed(2)}`);
});
perfTest(`read performance - ${size} edges - getOutEdgesByRel`, () => {
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 });
// Warm-up
for (let i = 0; i < 10; i++) {
graph.getOutEdgesByRel(`user:${i}`, 'owner');
}
// Benchmark
const iterations = 1000;
const start = performance.now();
for (let i = 0; i < iterations; i++) {
graph.getOutEdgesByRel(`user:${i % 100}`, 'owner');
}
const duration = performance.now() - start;
const avgTime = duration / iterations * 1000; // microseconds
console.log(`\nRead performance - ${size} edges - getOutEdgesByRel:`);
console.log(` Total time: ${duration.toFixed(2)} ms`);
console.log(` Avg time: ${avgTime.toFixed(2)} µs`);
console.log(` Ops/sec: ${(1000000 / avgTime).toFixed(0)}`);
// Should be reasonably fast
assert.ok(avgTime < 1000, `getOutEdgesByRel should be < 1000µs, got ${avgTime.toFixed(2)}µs`);
});
perfTest(`read performance - ${size} edges - findEdge`, () => {
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 });
// Warm-up
for (let i = 0; i < 10; i++) {
graph.findEdge(`user:${i}`, 'owner');
}
// Benchmark
const iterations = 10000;
const start = performance.now();
for (let i = 0; i < iterations; i++) {
graph.findEdge(`user:${i % 100}`, ['owner', 'member', 'viewer', 'editor'][i % 4]);
}
const duration = performance.now() - start;
const avgTime = duration / iterations * 1000; // microseconds
console.log(`\nRead performance - ${size} edges - findEdge:`);
console.log(` Total time: ${duration.toFixed(2)} ms`);
console.log(` Avg time: ${avgTime.toFixed(2)} µs`);
console.log(` Ops/sec: ${(1000000 / avgTime).toFixed(0)}`);
assert.ok(avgTime < 500, `findEdge should be < 500µs, got ${avgTime.toFixed(2)}µs`);
});
perfTest(`read performance - ${size} edges - hasEdge`, () => {
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 });
// Warm-up
for (let i = 0; i < 10; i++) {
graph.hasEdge(`user:${i}`, 'owner', `doc:${i}`);
}
// Benchmark
const iterations = 10000;
const start = performance.now();
for (let i = 0; i < iterations; i++) {
graph.hasEdge(
`user:${i % 100}`,
['owner', 'member', 'viewer', 'editor'][i % 4],
`doc:${i % 500}`
);
}
const duration = performance.now() - start;
const avgTime = duration / iterations * 1000; // microseconds
console.log(`\nRead performance - ${size} edges - hasEdge:`);
console.log(` Total time: ${duration.toFixed(2)} ms`);
console.log(` Avg time: ${avgTime.toFixed(2)} µs`);
console.log(` Ops/sec: ${(1000000 / avgTime).toFixed(0)}`);
assert.ok(avgTime < 1000, `hasEdge should be < 1000µs, got ${avgTime.toFixed(2)}µs`);
});
perfTest(`iteration performance - ${size} edges - forEachOutEdgeByRel`, () => {
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 });
let edgeCount = 0;
// Warm-up
graph.forEachOutEdgeByRel('user:0', 'owner', (edge) => {
edgeCount++;
});
edgeCount = 0;
const start = performance.now();
for (let i = 0; i < 1000; i++) {
graph.forEachOutEdgeByRel(`user:${i % 100}`, 'owner', (edge) => {
edgeCount++;
});
}
const duration = performance.now() - start;
const avgTime = duration / 1000 * 1000; // microseconds per iteration
console.log(`\nIteration performance - ${size} edges - forEachOutEdgeByRel:`);
console.log(` Total time: ${duration.toFixed(2)} ms`);
console.log(` Avg time per iteration: ${avgTime.toFixed(2)} µs`);
console.log(` Total edges processed: ${edgeCount}`);
assert.ok(avgTime < 500, `forEachOutEdgeByRel should be < 500µs, got ${avgTime.toFixed(2)}µs`);
});
perfTest(`write performance - ${size} edges - addEdge`, () => {
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 avgTime = duration / size * 1000; // microseconds per edge
console.log(`\nWrite performance - ${size} edges - addEdge:`);
console.log(` Total time: ${duration.toFixed(2)} ms`);
console.log(` Avg time per edge: ${avgTime.toFixed(2)} µs`);
console.log(` Edges/sec: ${(size / duration * 1000).toFixed(0)}`);
assert.ok(avgTime < 100, `addEdge should be < 100µs, got ${avgTime.toFixed(2)}µs`);
});
});
perfTest('comparison with plain object array', () => {
const size = 50000;
// Test CondensedGraph
const cg = new CondensedGraph();
const cgStartMem = process.memoryUsage().heapUsed;
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 cgEndMem = process.memoryUsage().heapUsed;
const cgMemDelta = cgEndMem - cgStartMem;
// Test plain object array
const plainEdges = [];
const plainStartMem = process.memoryUsage().heapUsed;
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 plainEndMem = process.memoryUsage().heapUsed;
const plainMemDelta = plainEndMem - plainStartMem;
// Benchmark read operations
let cgTime = 0;
let plainTime = 0;
const iterations = 10000;
// CondensedGraph read
const cgReadStart = performance.now();
for (let i = 0; i < iterations; i++) {
cg.getOutEdgesByRel(`user:${i % 100}`, 'owner');
}
cgTime = performance.now() - cgReadStart;
// Plain array read
const plainReadStart = performance.now();
for (let i = 0; i < iterations; i++) {
const src = `user:${i % 100}`;
plainEdges.filter(e => e.src === src);
}
plainTime = performance.now() - plainReadStart;
console.log('\nComparison with plain object array:');
console.log(` CondensedGraph memory: ${(cgMemDelta / 1024 / 1024).toFixed(2)} MB`);
console.log(` Plain array memory: ${(plainMemDelta / 1024 / 1024).toFixed(2)} MB`);
console.log(` Memory savings: ${((plainMemDelta - cgMemDelta) / plainMemDelta * 100).toFixed(1)}%`);
console.log(` CondensedGraph bytes/edge: ${(cgMemDelta / size).toFixed(2)}`);
console.log(` Plain array bytes/edge: ${(plainMemDelta / size).toFixed(2)}`);
console.log(`\n CondensedGraph read time: ${cgTime.toFixed(2)} ms`);
console.log(` Plain array read time: ${plainTime.toFixed(2)} ms`);
console.log(` Read speedup: ${(plainTime / cgTime).toFixed(2)}x`);
const plainEstimateBytes = size * 120;
assert.ok(cgStats.bytesPerEdge < (plainEstimateBytes / size), 'CondensedGraph should use less memory');
});
perfTest('scalability summary', () => {
console.log('\n\n=== Scalability Summary ===');
console.log('Size | Memory (MB) | Bytes/Edge | Read (µs) | Write (µs)');
console.log('-----|-------------|------------|-----------|-----------');
results.forEach(r => {
console.log(
`${r.size.toString().padEnd(5)} | ` +
`${(r.memDelta / 1024 / 1024).toFixed(2).padEnd(11)} | ` +
`${r.bytesPerEdge.toFixed(2).padEnd(10)} | ` +
`${(r.stats.avgDegree * 10).toFixed(0).padEnd(9)} | ` +
`${(r.memDelta / r.size / 100).toFixed(2).padEnd(10)}`
);
});
});
});