import assert from 'node:assert/strict'; import { describe, test } from 'node:test'; import { RelationStore } from '../../src/core/RelationStore.js'; const runPerf = process.env.RUN_PERF_TESTS === '1'; const perfTest = runPerf ? test : test.skip; describe('RelationStore Read Performance', () => { perfTest('sequential read speed comparison', () => { const numRelations = 100000; const store = new RelationStore(numRelations); // Add relations for (let i = 0; i < numRelations; i++) { store.add(i, 'relation', (i + numRelations) % numRelations); } // Benchmark sequential reads const iterations = 10000; const start1 = Date.now(); let sum1 = 0; for (let i = 0; i < iterations; i++) { const idx = i % store.size(); const rel = store.get(idx); sum1 += rel.possibility; } const time1 = Date.now() - start1; const start2 = Date.now(); let sum2 = 0; for (let i = 0; i < iterations; i++) { const idx = i % store.size(); const rel = store.getRaw(idx); sum2 += rel.possibility; } const time2 = Date.now() - start2; console.log(`Sequential read speed (${iterations} iterations):`); console.log(` - get() (full object): ${time1}ms (${(time1/iterations*1000).toFixed(4)} µs/op)`); console.log(` - getRaw() (typed arrays): ${time2}ms (${(time2/iterations*1000).toFixed(4)} µs/op)`); console.log(` - Speedup: ${(time1/time2).toFixed(2)}x ${time2 < time1 ? '(faster)' : '(slower)'}`); // Both should give same result assert.ok(Math.abs(sum1 - sum2) < 1e-6); }); perfTest('random access speed', () => { const numRelations = 100000; const store = new RelationStore(numRelations); for (let i = 0; i < numRelations; i++) { store.add(i, 'relation', (i + numRelations) % numRelations); } const iterations = 10000; const indices = Array.from({length: iterations}, () => Math.floor(Math.random() * store.size()) ); const start1 = Date.now(); let count1 = 0; for (const idx of indices) { const rel = store.get(idx); if (rel.possibility > 0.5) count1++; } const time1 = Date.now() - start1; const start2 = Date.now(); let count2 = 0; for (const idx of indices) { const rel = store.getRaw(idx); if (rel.possibility > 0.5) count2++; } const time2 = Date.now() - start2; console.log(`Random access speed (${iterations} iterations):`); console.log(` - get() (full object): ${time1}ms (${(time1/iterations*1000).toFixed(4)} µs/op)`); console.log(` - getRaw() (typed arrays): ${time2}ms (${(time2/iterations*1000).toFixed(4)} µs/op)`); console.log(` - Speedup: ${(time1/time2).toFixed(2)}x ${time2 < time1 ? '(faster)' : '(slower)'}`); assert.strictEqual(count1, count2); }); perfTest('findMatches performance', () => { const numRelations = 50000; const store = new RelationStore(numRelations); for (let i = 0; i < numRelations; i++) { store.add(i % 1000, 'rel', (i + 1) % 1000); } const srcId = 500; const relId = store.getRelationId('rel'); const iterations = 1000; const start = Date.now(); for (let i = 0; i < iterations; i++) { const matches = store.findMatches(srcId, relId, null); } const time = Date.now() - start; console.log(`findMatches performance (${iterations} iterations):`); console.log(` - Time: ${time}ms (${(time/iterations).toFixed(4)} ms/op)`); console.log(` - Matches found: ${store.findMatches(srcId, relId, null).length}`); console.log(` - Avg matches/query: ${(time/iterations).toFixed(4)}ms`); assert.ok(time < iterations * 10, 'findMatches should be reasonably fast'); }); perfTest('forEach iteration speed', () => { const numRelations = 100000; const store = new RelationStore(numRelations); for (let i = 0; i < numRelations; i++) { store.add(i, 'relation', (i + numRelations) % numRelations, { possibility: Math.random() }); } const start1 = Date.now(); let sum1 = 0; store.forEach((rel) => { sum1 += rel.possibility; }); const time1 = Date.now() - start1; const start2 = Date.now(); let sum2 = 0; store.forEachRaw((rel) => { sum2 += rel.possibility; }); const time2 = Date.now() - start2; console.log(`forEach iteration speed (${store.size()} relations):`); console.log(` - forEach() (full object): ${time1}ms`); console.log(` - forEachRaw() (typed arrays): ${time2}ms`); console.log(` - Speedup: ${(time1/time2).toFixed(2)}x ${time2 < time1 ? '(faster)' : '(slower)'}`); assert.ok(Math.abs(sum1 - sum2) < 1e-6); }); perfTest('cache-friendly access pattern', () => { const numRelations = 100000; const store = new RelationStore(numRelations); for (let i = 0; i < numRelations; i++) { store.add(i, 'relation', (i + numRelations) % numRelations, { possibility: Math.random(), reliability: Math.random() }); } // Access all src fields first (cache-friendly) const start1 = Date.now(); let totalSrc = 0; for (let i = 0; i < store.size(); i++) { totalSrc += store.src[i]; } const time1 = Date.now() - start1; // Access possibility fields (different typed array) const start2 = Date.now(); let totalPoss = 0; for (let i = 0; i < store.size(); i++) { totalPoss += store.possibility[i]; } const time2 = Date.now() - start2; console.log(`Cache-friendly field access (${store.size()} relations):`); console.log(` - src field access: ${time1}ms`); console.log(` - possibility field access: ${time2}ms`); console.log(` - Each operation: ${((time1+time2)/(store.size()*1000)).toFixed(4)} µs/element`); }); perfTest('pattern matching queries', () => { const numRelations = 50000; const store = new RelationStore(numRelations); // Create different relation types const relationTypes = ['owner', 'member', 'viewer', 'editor']; relationTypes.forEach(rel => store.getRelationId(rel)); for (let i = 0; i < numRelations; i++) { const relType = relationTypes[i % relationTypes.length]; store.add(i, relType, (i + 1) % 1000, { possibility: Math.random() }); } const queries = 1000; // Query: Find all relations from a source const srcId = 100; const start1 = Date.now(); let count1 = 0; for (let i = 0; i < queries; i++) { const matches = store.findMatches(srcId, null, null); count1 += matches.length; } const time1 = Date.now() - start1; // Query: Find all relations of a specific type const ownerRelId = store.getRelationId('owner'); const start2 = Date.now(); let count2 = 0; for (let i = 0; i < queries; i++) { const matches = store.findMatches(null, ownerRelId, null); count2 += matches.length; } const time2 = Date.now() - start2; console.log(`Pattern matching queries (${queries} queries):`); console.log(` - Find by source: ${time1}ms (${(time1/queries).toFixed(4)} ms/query)`); console.log(` - Find by relation type: ${time2}ms (${(time2/queries).toFixed(4)} ms/query)`); console.log(` - Results: source=${count1/queries} avg, type=${count2/queries} avg`); assert.ok(count1 > 0, 'Should find relations by source'); assert.ok(count2 > 0, 'Should find relations by type'); }); perfTest('update operation speed', () => { const numRelations = 50000; const store = new RelationStore(numRelations); for (let i = 0; i < numRelations; i++) { store.add(i, 'relation', (i + numRelations) % numRelations, { possibility: 0.5, reliability: 0.7 }); } const updates = 10000; const start = Date.now(); for (let i = 0; i < updates; i++) { const idx = i % store.size(); store.update(idx, { possibility: 0.9 }); } const time = Date.now() - start; console.log(`Update operation speed (${updates} updates):`); console.log(` - Time: ${time}ms`); console.log(` - Avg per update: ${(time/updates).toFixed(4)} ms`); console.log(` - Updates/sec: ${(updates/time*1000).toFixed(0)}`); // Verify updates worked - check that value changed const checkIdx = Math.floor(store.size() / 2); const rel = store.get(checkIdx); const beforeUpdate = store.get(0); assert.ok(rel.possibility !== beforeUpdate.possibility || store.size() < 2, 'Update should change value'); }); perfTest('remove operation speed', () => { const numRelations = 50000; const store = new RelationStore(numRelations); for (let i = 0; i < numRelations; i++) { store.add(i, 'relation', (i + numRelations) % numRelations); } const initialSize = store.size(); const removes = 10000; const start = Date.now(); for (let i = 0; i < removes; i++) { const idx = Math.floor(Math.random() * store.size()); store.remove(idx); } const time = Date.now() - start; const finalSize = store.size(); const actualRemoves = initialSize - finalSize; console.log(`Remove operation speed (${removes} attempted):`); console.log(` - Time: ${time}ms`); console.log(` - Avg per remove: ${(time/removes).toFixed(4)} ms`); console.log(` - Actual removes: ${actualRemoves} (deduplication)`); console.log(` - Removes/sec: ${(actualRemoves/time*1000).toFixed(0)}`); assert.strictEqual(finalSize, initialSize - removes); }); });