219 lines
7.1 KiB
JavaScript
219 lines
7.1 KiB
JavaScript
|
|
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.skip('RelationStore (SoA Memory Optimization)', () => {
|
||
|
|
test('basic add and retrieve', () => {
|
||
|
|
const store = new RelationStore();
|
||
|
|
|
||
|
|
const idx1 = store.add(1, 'owner', 2, { possibility: 1.0 });
|
||
|
|
const idx2 = store.add(1, 'member', 3, { possibility: 0.5 });
|
||
|
|
|
||
|
|
assert.strictEqual(store.size(), 2);
|
||
|
|
|
||
|
|
const rel1 = store.get(idx1);
|
||
|
|
assert.strictEqual(rel1.rel, 'owner');
|
||
|
|
assert.ok(Math.abs(rel1.possibility - 1.0) < 1e-6);
|
||
|
|
|
||
|
|
const rel2 = store.get(idx2);
|
||
|
|
assert.strictEqual(rel2.rel, 'member');
|
||
|
|
assert.ok(Math.abs(rel2.possibility - 0.5) < 1e-6);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('update relation', () => {
|
||
|
|
const store = new RelationStore();
|
||
|
|
|
||
|
|
const idx = store.add(1, 'owner', 2, { possibility: 0.5 });
|
||
|
|
assert.strictEqual(store.possibility[idx], 0.5);
|
||
|
|
|
||
|
|
store.update(idx, { possibility: 0.9 });
|
||
|
|
assert.ok(Math.abs(store.possibility[idx] - 0.9) < 1e-6, `possibility should be ~0.9, got ${store.possibility[idx]}`);
|
||
|
|
|
||
|
|
const rel = store.get(idx);
|
||
|
|
assert.ok(Math.abs(rel.possibility - 0.9) < 1e-6);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('remove relation', () => {
|
||
|
|
const store = new RelationStore();
|
||
|
|
|
||
|
|
const idx1 = store.add(1, 'owner', 2);
|
||
|
|
const idx2 = store.add(1, 'member', 3);
|
||
|
|
const idx3 = store.add(2, 'owner', 4);
|
||
|
|
|
||
|
|
assert.strictEqual(store.size(), 3);
|
||
|
|
|
||
|
|
store.remove(idx2);
|
||
|
|
assert.strictEqual(store.size(), 2);
|
||
|
|
|
||
|
|
const rel1 = store.get(idx1);
|
||
|
|
assert.strictEqual(rel1.rel, 'owner');
|
||
|
|
|
||
|
|
// After removal, idx2's data was moved from idx3
|
||
|
|
const rel3 = store.get(idx3);
|
||
|
|
assert.strictEqual(rel3, null);
|
||
|
|
|
||
|
|
const remaining = store.findMatches(null, null, null);
|
||
|
|
assert.strictEqual(remaining.length, 2);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('find matches', () => {
|
||
|
|
const store = new RelationStore();
|
||
|
|
|
||
|
|
store.add(1, 'owner', 2);
|
||
|
|
store.add(1, 'member', 3);
|
||
|
|
store.add(1, 'owner', 4);
|
||
|
|
store.add(2, 'owner', 3);
|
||
|
|
|
||
|
|
const ownerFrom1 = store.findMatches(1, store.getRelationId('owner'), null);
|
||
|
|
assert.strictEqual(ownerFrom1.length, 2);
|
||
|
|
|
||
|
|
const allOwner = store.findMatches(null, store.getRelationId('owner'), null);
|
||
|
|
assert.strictEqual(allOwner.length, 3);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('raw access for performance', () => {
|
||
|
|
const store = new RelationStore();
|
||
|
|
|
||
|
|
const idx = store.add(1, 'owner', 2, { possibility: 0.75 });
|
||
|
|
|
||
|
|
const raw = store.getRaw(idx);
|
||
|
|
assert.ok(raw !== null, 'raw should not be null');
|
||
|
|
assert.strictEqual(raw.src, 1);
|
||
|
|
const relId = store.getRelationId('owner');
|
||
|
|
assert.strictEqual(raw.relId, relId);
|
||
|
|
assert.strictEqual(raw.dst, 2);
|
||
|
|
assert.ok(Math.abs(raw.possibility - 0.75) < 1e-6);
|
||
|
|
assert.strictEqual(raw.reliability, 1.0);
|
||
|
|
assert.strictEqual('value' in raw, true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('metadata storage', () => {
|
||
|
|
const store = new RelationStore();
|
||
|
|
|
||
|
|
const customValue = { tier: 'premium', level: 5 };
|
||
|
|
const decay = { rate: 0.1, interval: 3600000 };
|
||
|
|
|
||
|
|
const idx = store.add(1, 'owner', 2, {
|
||
|
|
possibility: 1.0,
|
||
|
|
value: customValue,
|
||
|
|
decayConfig: decay,
|
||
|
|
stateId: 'test-state-123'
|
||
|
|
});
|
||
|
|
|
||
|
|
const rel = store.get(idx);
|
||
|
|
assert.deepStrictEqual(rel.value, customValue);
|
||
|
|
assert.deepStrictEqual(rel.decayConfig, decay);
|
||
|
|
assert.strictEqual(rel.stateId, 'test-state-123');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('capacity expansion', () => {
|
||
|
|
const store = new RelationStore(4);
|
||
|
|
|
||
|
|
assert.strictEqual(store.capacity(), 4);
|
||
|
|
|
||
|
|
for (let i = 0; i < 10; i++) {
|
||
|
|
store.add(i, 'rel', i + 1);
|
||
|
|
}
|
||
|
|
|
||
|
|
assert.strictEqual(store.size(), 10);
|
||
|
|
assert.ok(store.capacity() >= 10);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('forEach iteration', () => {
|
||
|
|
const store = new RelationStore();
|
||
|
|
|
||
|
|
store.add(1, 'owner', 2);
|
||
|
|
store.add(1, 'member', 3);
|
||
|
|
store.add(2, 'owner', 4);
|
||
|
|
|
||
|
|
let count = 0;
|
||
|
|
store.forEach((rel) => {
|
||
|
|
count++;
|
||
|
|
assert.ok(rel.src >= 1);
|
||
|
|
assert.ok(rel.dst >= 2);
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.strictEqual(count, 3);
|
||
|
|
});
|
||
|
|
|
||
|
|
perfTest('memory efficiency compared to AoS', () => {
|
||
|
|
const numRelations = 100000;
|
||
|
|
const store = new RelationStore(numRelations);
|
||
|
|
|
||
|
|
const startMem = process.memoryUsage().heapUsed;
|
||
|
|
|
||
|
|
for (let i = 0; i < numRelations; i++) {
|
||
|
|
store.add(i % 1000, 'relation', (i + 1) % 1000, {
|
||
|
|
possibility: Math.random(),
|
||
|
|
reliability: Math.random()
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const endMem = process.memoryUsage().heapUsed;
|
||
|
|
const memDelta = endMem - startMem;
|
||
|
|
const memMB = memDelta / (1024 * 1024);
|
||
|
|
|
||
|
|
const stats = store.getStats();
|
||
|
|
console.log(`RelationStore memory for ${numRelations} relations:`);
|
||
|
|
console.log(` - Typed arrays: ${(stats.memoryUsage.typedArrays / 1024 / 1024).toFixed(2)} MB`);
|
||
|
|
console.log(` - Metadata: ${(stats.memoryUsage.metadata / 1024 / 1024).toFixed(2)} MB`);
|
||
|
|
console.log(` - Total heap delta: ${memMB.toFixed(2)} MB`);
|
||
|
|
console.log(` - Bytes per relation: ${(memDelta / numRelations).toFixed(2)}`);
|
||
|
|
console.log(` - Capacity: ${stats.capacity}, Utilization: ${(stats.size / stats.capacity).toFixed(2)}`);
|
||
|
|
|
||
|
|
assert.strictEqual(store.size(), numRelations);
|
||
|
|
assert.ok(memMB < 50, `Memory usage should be efficient (< 50MB), got ${memMB.toFixed(2)} MB`);
|
||
|
|
});
|
||
|
|
|
||
|
|
perfTest('comparison with traditional object array', () => {
|
||
|
|
const numRelations = 50000;
|
||
|
|
|
||
|
|
const start1 = process.memoryUsage().heapUsed;
|
||
|
|
|
||
|
|
const traditional = [];
|
||
|
|
for (let i = 0; i < numRelations; i++) {
|
||
|
|
traditional.push({
|
||
|
|
src: i % 1000,
|
||
|
|
rel: 'relation',
|
||
|
|
dst: (i + 1) % 1000,
|
||
|
|
possibility: Math.random(),
|
||
|
|
reliability: Math.random(),
|
||
|
|
updated_last_at: Date.now(),
|
||
|
|
changed_last_at: Date.now(),
|
||
|
|
stateId: 'state-' + i
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const end1 = process.memoryUsage().heapUsed;
|
||
|
|
const traditionalMem = end1 - start1;
|
||
|
|
|
||
|
|
const start2 = process.memoryUsage().heapUsed;
|
||
|
|
|
||
|
|
const store = new RelationStore(numRelations);
|
||
|
|
for (let i = 0; i < numRelations; i++) {
|
||
|
|
store.add(i % 1000, 'relation', (i + 1) % 1000, {
|
||
|
|
possibility: Math.random(),
|
||
|
|
reliability: Math.random()
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const end2 = process.memoryUsage().heapUsed;
|
||
|
|
const soaMem = end2 - start2;
|
||
|
|
|
||
|
|
const savingsMB = (traditionalMem - soaMem) / (1024 * 1024);
|
||
|
|
const savingsPercent = ((traditionalMem - soaMem) / traditionalMem * 100);
|
||
|
|
|
||
|
|
console.log(`Memory comparison for ${numRelations} relations:`);
|
||
|
|
console.log(` - Traditional AoS: ${(traditionalMem / 1024 / 1024).toFixed(2)} MB`);
|
||
|
|
console.log(` - SoA RelationStore: ${(soaMem / 1024 / 1024).toFixed(2)} MB`);
|
||
|
|
console.log(` - Savings: ${savingsMB.toFixed(2)} MB (${savingsPercent.toFixed(1)}%)`);
|
||
|
|
console.log(` - Bytes per relation (AoS): ${(traditionalMem / numRelations).toFixed(2)}`);
|
||
|
|
console.log(` - Bytes per relation (SoA): ${(soaMem / numRelations).toFixed(2)}`);
|
||
|
|
|
||
|
|
assert.ok(soaMem <= traditionalMem, 'SoA should use less memory than AoS');
|
||
|
|
});
|
||
|
|
});
|