277 lines
13 KiB
JavaScript
277 lines
13 KiB
JavaScript
import { describe, it, expect, vi } from 'vitest';
|
|
import { MemoryGraph } from './memory-graph.js';
|
|
import { LearningBridge } from './learning-bridge.js';
|
|
import { AutoMemoryBridge } from './auto-memory-bridge.js';
|
|
import { resolveAgentMemoryDir, transferKnowledge } from './agent-memory-scope.js';
|
|
import { createDefaultEntry } from './types.js';
|
|
function createMockBackend(entries = []) {
|
|
const stored = new Map();
|
|
entries.forEach(e => stored.set(e.id, e));
|
|
return {
|
|
initialize: vi.fn(async () => { }),
|
|
shutdown: vi.fn(async () => { }),
|
|
store: vi.fn(async (e) => { stored.set(e.id, e); }),
|
|
get: vi.fn(async (id) => stored.get(id) ?? null),
|
|
getByKey: vi.fn(async () => null),
|
|
update: vi.fn(async (id, u) => { const e = stored.get(id); if (e) {
|
|
Object.assign(e.metadata, u.metadata || {});
|
|
return e;
|
|
} return null; }),
|
|
delete: vi.fn(async (id) => stored.delete(id)),
|
|
query: vi.fn(async () => [...stored.values()]),
|
|
search: vi.fn(async () => []),
|
|
bulkInsert: vi.fn(async (es) => es.forEach(e => stored.set(e.id, e))),
|
|
bulkDelete: vi.fn(async (ids) => { ids.forEach(id => stored.delete(id)); return ids.length; }),
|
|
count: vi.fn(async () => stored.size),
|
|
listNamespaces: vi.fn(async () => ['default']),
|
|
clearNamespace: vi.fn(async () => 0),
|
|
getStats: vi.fn(async () => ({ totalEntries: stored.size, entriesByNamespace: {}, entriesByType: { semantic: 0, episodic: 0, procedural: 0, working: 0, cache: 0 }, memoryUsage: 0, avgQueryTime: 0, avgSearchTime: 0 })),
|
|
healthCheck: vi.fn(async () => ({ status: 'healthy', components: { storage: { status: 'healthy', latency: 0 }, index: { status: 'healthy', latency: 0 }, cache: { status: 'healthy', latency: 0 } }, timestamp: Date.now(), issues: [], recommendations: [] })),
|
|
};
|
|
}
|
|
function makeEntry(id, refs = []) {
|
|
return { ...createDefaultEntry({ key: id, content: `Content for ${id}`, references: refs, metadata: { confidence: 0.7 + Math.random() * 0.3 } }), id };
|
|
}
|
|
function createMockNeural() {
|
|
let trajId = 0;
|
|
return {
|
|
initialize: async () => { },
|
|
beginTask: () => `traj-${++trajId}`,
|
|
recordStep: () => { },
|
|
completeTask: async () => { },
|
|
findPatterns: async () => [],
|
|
retrieveMemories: async () => [],
|
|
triggerLearning: async () => { },
|
|
getStats: () => ({}),
|
|
cleanup: async () => { },
|
|
getSONAManager: () => ({ getMode: () => 'balanced' }),
|
|
getReasoningBank: () => ({ consolidate: async () => ({}) }),
|
|
getPatternLearner: () => ({}),
|
|
};
|
|
}
|
|
describe('ADR-049 Performance Benchmarks', () => {
|
|
const targets = [];
|
|
function buildGraphEntries(n) {
|
|
const entries = [];
|
|
for (let i = 0; i < n; i++) {
|
|
const refs = [];
|
|
for (let j = 0; j < 3; j++) {
|
|
const r = Math.floor(Math.random() * n);
|
|
if (r !== i)
|
|
refs.push(`entry-${r}`);
|
|
}
|
|
entries.push(makeEntry(`entry-${i}`, refs));
|
|
}
|
|
return entries;
|
|
}
|
|
function buildGraph(entries, maxNodes) {
|
|
const g = new MemoryGraph({ maxNodes });
|
|
for (const e of entries)
|
|
g.addNode(e);
|
|
for (const e of entries) {
|
|
for (const r of e.references)
|
|
g.addEdge(e.id, r, 'reference');
|
|
}
|
|
return g;
|
|
}
|
|
it('MemoryGraph: build 100 nodes', () => {
|
|
const entries = buildGraphEntries(100);
|
|
const t0 = performance.now();
|
|
buildGraph(entries, 200);
|
|
const dt = performance.now() - t0;
|
|
console.log(` Graph build (100): ${dt.toFixed(2)}ms`);
|
|
expect(dt).toBeLessThan(50);
|
|
});
|
|
it('MemoryGraph: build 1000 nodes', () => {
|
|
const entries = buildGraphEntries(1000);
|
|
const t0 = performance.now();
|
|
buildGraph(entries, 1100);
|
|
const dt = performance.now() - t0;
|
|
console.log(` Graph build (1k): ${dt.toFixed(2)}ms [target: <200ms]`);
|
|
targets.push({ name: 'Graph build (1k nodes)', actual: dt, target: 200, unit: 'ms' });
|
|
expect(dt).toBeLessThan(200);
|
|
});
|
|
it('MemoryGraph: build 2000 nodes', () => {
|
|
const entries = buildGraphEntries(2000);
|
|
const t0 = performance.now();
|
|
buildGraph(entries, 2100);
|
|
const dt = performance.now() - t0;
|
|
console.log(` Graph build (2k): ${dt.toFixed(2)}ms`);
|
|
expect(dt).toBeLessThan(500);
|
|
});
|
|
it('MemoryGraph: PageRank 1000 nodes', () => {
|
|
const entries = buildGraphEntries(1000);
|
|
const g = buildGraph(entries, 1100);
|
|
const t0 = performance.now();
|
|
g.computePageRank();
|
|
const dt = performance.now() - t0;
|
|
console.log(` PageRank (1k): ${dt.toFixed(2)}ms [target: <100ms]`);
|
|
targets.push({ name: 'PageRank (1k nodes)', actual: dt, target: 100, unit: 'ms' });
|
|
expect(dt).toBeLessThan(100);
|
|
});
|
|
it('MemoryGraph: PageRank 2000 nodes', () => {
|
|
const entries = buildGraphEntries(2000);
|
|
const g = buildGraph(entries, 2100);
|
|
const t0 = performance.now();
|
|
g.computePageRank();
|
|
const dt = performance.now() - t0;
|
|
console.log(` PageRank (2k): ${dt.toFixed(2)}ms`);
|
|
expect(dt).toBeLessThan(300);
|
|
});
|
|
it('MemoryGraph: community detection 1000 nodes', () => {
|
|
const entries = buildGraphEntries(1000);
|
|
const g = buildGraph(entries, 1100);
|
|
g.computePageRank();
|
|
const t0 = performance.now();
|
|
g.detectCommunities();
|
|
const dt = performance.now() - t0;
|
|
const stats = g.getStats();
|
|
console.log(` Communities (1k): ${dt.toFixed(2)}ms (${stats.communityCount} found)`);
|
|
expect(dt).toBeLessThan(200);
|
|
});
|
|
it('MemoryGraph: rankWithGraph 10 results', () => {
|
|
const entries = buildGraphEntries(1000);
|
|
const g = buildGraph(entries, 1100);
|
|
g.computePageRank();
|
|
const fakeResults = entries.slice(0, 10).map(e => ({ entry: e, score: Math.random(), distance: Math.random() }));
|
|
const t0 = performance.now();
|
|
for (let i = 0; i < 100; i++)
|
|
g.rankWithGraph(fakeResults);
|
|
const dt = (performance.now() - t0) / 100;
|
|
console.log(` rankWithGraph(10): ${dt.toFixed(3)}ms (avg of 100 runs)`);
|
|
expect(dt).toBeLessThan(1);
|
|
});
|
|
it('MemoryGraph: getTopNodes(20)', () => {
|
|
const entries = buildGraphEntries(1000);
|
|
const g = buildGraph(entries, 1100);
|
|
g.computePageRank();
|
|
g.detectCommunities();
|
|
const t0 = performance.now();
|
|
for (let i = 0; i < 100; i++)
|
|
g.getTopNodes(20);
|
|
const dt = (performance.now() - t0) / 100;
|
|
console.log(` getTopNodes(20): ${dt.toFixed(3)}ms (avg of 100 runs)`);
|
|
expect(dt).toBeLessThan(5);
|
|
});
|
|
it('MemoryGraph: getNeighbors depth=2', () => {
|
|
const entries = buildGraphEntries(1000);
|
|
const g = buildGraph(entries, 1100);
|
|
const t0 = performance.now();
|
|
for (let i = 0; i < 100; i++)
|
|
g.getNeighbors(entries[i % entries.length].id, 2);
|
|
const dt = (performance.now() - t0) / 100;
|
|
console.log(` getNeighbors(d=2): ${dt.toFixed(3)}ms (avg of 100 runs)`);
|
|
expect(dt).toBeLessThan(5);
|
|
});
|
|
it('LearningBridge: record 1000 insights', async () => {
|
|
const mockNeural = createMockNeural();
|
|
const backend = createMockBackend();
|
|
const lb = new LearningBridge(backend, { neuralLoader: async () => mockNeural, consolidationThreshold: 99999 });
|
|
const t0 = performance.now();
|
|
for (let i = 0; i < 1000; i++) {
|
|
await lb.onInsightRecorded({ category: 'debugging', summary: `Insight ${i}`, source: 'bench', confidence: 0.8 }, `entry-${i}`);
|
|
}
|
|
const dt = performance.now() - t0;
|
|
const per = dt / 1000;
|
|
console.log(` Record 1000: ${dt.toFixed(2)}ms (${per.toFixed(3)}ms/each) [target: <5ms/each]`);
|
|
targets.push({ name: 'Insight recording', actual: per, target: 5, unit: 'ms/each' });
|
|
expect(per).toBeLessThan(5);
|
|
lb.destroy();
|
|
});
|
|
it('LearningBridge: access 1000 insights', async () => {
|
|
const mockNeural = createMockNeural();
|
|
const entries = [];
|
|
for (let i = 0; i < 1000; i++) {
|
|
const e = makeEntry(`entry-${i}`);
|
|
e.metadata.confidence = 0.5;
|
|
entries.push(e);
|
|
}
|
|
const backend = createMockBackend(entries);
|
|
const lb = new LearningBridge(backend, { neuralLoader: async () => mockNeural, consolidationThreshold: 99999 });
|
|
// warm up trajectories
|
|
for (let i = 0; i < 1000; i++) {
|
|
await lb.onInsightRecorded({ category: 'debugging', summary: `I-${i}`, source: 'bench', confidence: 0.8 }, `entry-${i}`);
|
|
}
|
|
const t0 = performance.now();
|
|
for (let i = 0; i < 1000; i++)
|
|
await lb.onInsightAccessed(`entry-${i}`);
|
|
const dt = performance.now() - t0;
|
|
console.log(` Access 1000: ${dt.toFixed(2)}ms (${(dt / 1000).toFixed(3)}ms/each)`);
|
|
expect(dt / 1000).toBeLessThan(5);
|
|
lb.destroy();
|
|
});
|
|
it('LearningBridge: consolidation', async () => {
|
|
const mockNeural = createMockNeural();
|
|
const backend = createMockBackend();
|
|
const lb = new LearningBridge(backend, { neuralLoader: async () => mockNeural, consolidationThreshold: 1 });
|
|
for (let i = 0; i < 100; i++) {
|
|
await lb.onInsightRecorded({ category: 'debugging', summary: `I-${i}`, source: 'bench', confidence: 0.8 }, `entry-${i}`);
|
|
}
|
|
const t0 = performance.now();
|
|
const result = await lb.consolidate();
|
|
const dt = performance.now() - t0;
|
|
console.log(` Consolidation: ${dt.toFixed(2)}ms (${result.trajectoriesCompleted} trajectories) [target: <500ms]`);
|
|
targets.push({ name: 'Consolidation', actual: dt, target: 500, unit: 'ms' });
|
|
expect(dt).toBeLessThan(500);
|
|
lb.destroy();
|
|
});
|
|
it('LearningBridge: decay 1000 entries', async () => {
|
|
const mockNeural = createMockNeural();
|
|
const entries = [];
|
|
for (let i = 0; i < 1000; i++) {
|
|
const e = makeEntry(`decay-${i}`);
|
|
e.metadata.confidence = 0.9;
|
|
e.lastAccessedAt = Date.now() - 7200000;
|
|
entries.push(e);
|
|
}
|
|
const backend = createMockBackend(entries);
|
|
const lb = new LearningBridge(backend, { neuralLoader: async () => mockNeural });
|
|
const t0 = performance.now();
|
|
const decayed = await lb.decayConfidences('default');
|
|
const dt = performance.now() - t0;
|
|
console.log(` Decay 1k entries: ${dt.toFixed(2)}ms (${decayed} decayed) [target: <50ms]`);
|
|
targets.push({ name: 'Confidence decay (1k)', actual: dt, target: 50, unit: 'ms' });
|
|
expect(dt).toBeLessThan(50);
|
|
lb.destroy();
|
|
});
|
|
it('AgentMemoryScope: resolve 10k paths', () => {
|
|
const scopes = ['project', 'local', 'user'];
|
|
const t0 = performance.now();
|
|
for (let i = 0; i < 10000; i++)
|
|
resolveAgentMemoryDir(`agent-${i % 100}`, scopes[i % 3]);
|
|
const dt = performance.now() - t0;
|
|
console.log(` Resolve 10k paths: ${dt.toFixed(2)}ms (${(dt / 10).toFixed(1)}us/each)`);
|
|
expect(dt).toBeLessThan(200); // Not an ADR-049 target; relaxed for CI variance
|
|
});
|
|
it('AgentMemoryScope: transfer knowledge', async () => {
|
|
const sourceEntries = [];
|
|
for (let i = 0; i < 50; i++) {
|
|
const e = makeEntry(`src-${i}`, []);
|
|
e.tags = ['debugging'];
|
|
e.metadata.confidence = 0.5 + Math.random() * 0.5;
|
|
e.namespace = 'learnings';
|
|
sourceEntries.push(e);
|
|
}
|
|
const sourceBackend = createMockBackend(sourceEntries);
|
|
const targetBackend = createMockBackend();
|
|
const targetBridge = new AutoMemoryBridge(targetBackend, { memoryDir: '/tmp/bench-target' });
|
|
const t0 = performance.now();
|
|
const result = await transferKnowledge(sourceBackend, targetBridge, { sourceNamespace: 'learnings', minConfidence: 0.8, maxEntries: 20 });
|
|
const dt = performance.now() - t0;
|
|
console.log(` Transfer knowledge: ${dt.toFixed(2)}ms (${result.transferred} transferred) [target: <100ms]`);
|
|
targets.push({ name: 'Knowledge transfer', actual: dt, target: 100, unit: 'ms' });
|
|
expect(dt).toBeLessThan(100);
|
|
});
|
|
it('SUMMARY: all targets met', () => {
|
|
console.log('\n=== ADR-049 Performance Summary ===\n');
|
|
for (const t of targets) {
|
|
const pass = t.actual <= t.target;
|
|
const ratio = (t.target / t.actual).toFixed(1);
|
|
console.log(` ${pass ? 'PASS' : 'FAIL'} ${t.name.padEnd(28)} ${t.actual.toFixed(2).padStart(8)} ${t.unit.padEnd(8)} target: <${t.target}${t.unit} (${ratio}x headroom)`);
|
|
}
|
|
console.log('');
|
|
const allPass = targets.every(t => t.actual <= t.target);
|
|
expect(allPass).toBe(true);
|
|
});
|
|
});
|
|
//# sourceMappingURL=benchmark.test.js.map
|