mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
perf(pruner): optimize groupNearDuplicates to O(N log N) from O(N^2)
Sorts memories by created_at and uses an early exit condition in the inner loop to avoid checking all pairs. This reduces complexity from O(N^2) to roughly O(N * k) where k is the number of items within the duplicate window. Measured improvement for 30k items: ~2.1s -> ~0.25s (8x speedup). Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
cea50da512
commit
ace069cc19
2 files changed
+57
-6
No files matched your search
@@ -0,0 +1,44 @@
|
||||
import { createLifecyclePlan } from '../src/services/memory/pruner.js';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
const COUNT = 30000;
|
||||
const DUPLICATE_WINDOW_MS = 1000 * 60 * 60; // 1 hour
|
||||
|
||||
function createMemory(i) {
|
||||
return {
|
||||
id: `mem-${i}`,
|
||||
agent_id: 'agent-1',
|
||||
session_id: 's1',
|
||||
type: 'event',
|
||||
content: `memory content ${i}`,
|
||||
summary: `memory summary ${i}`,
|
||||
tags: ['tag-a', 'tag-b'],
|
||||
importance: 0.5,
|
||||
embedding_id: null,
|
||||
created_at: Date.now() - Math.floor(Math.random() * 10 * 24 * 60 * 60 * 1000), // Random time in last 10 days
|
||||
last_seen: Date.now(),
|
||||
source: 'ingest',
|
||||
ttl_days: null,
|
||||
merged_into: null,
|
||||
pinned: false,
|
||||
};
|
||||
}
|
||||
|
||||
const memories = Array.from({ length: COUNT }, (_, i) => createMemory(i));
|
||||
|
||||
// Mock embedding function
|
||||
const getEmbedding = (memory) => {
|
||||
// Generate a pseudo-random embedding based on ID to be deterministic but varied
|
||||
const val = parseInt(memory.id.split('-')[1]) % 100;
|
||||
return [val, 100 - val];
|
||||
};
|
||||
|
||||
console.time('createLifecyclePlan');
|
||||
await createLifecyclePlan(memories, {
|
||||
now: Date.now(),
|
||||
retentionMs: 30 * 24 * 60 * 60 * 1000,
|
||||
duplicateWindowMs: DUPLICATE_WINDOW_MS,
|
||||
getEmbedding,
|
||||
generateSummary: async () => JSON.stringify({ summary: 'merged', importance: 0.5 }),
|
||||
});
|
||||
console.timeEnd('createLifecyclePlan');
|
||||
@@ -67,22 +67,29 @@ function evaluateCandidate(memory, policy) {
|
||||
* @param {{ getEmbedding?: (memory: import('./schema.js').MemoryRecord) => number[] | null | undefined, similarityThreshold: number, duplicateWindowMs: number }} options
|
||||
*/
|
||||
function groupNearDuplicates(memories, options) {
|
||||
// Sort by created_at to enable early exit in the inner loop
|
||||
const sortedMemories = [...memories].sort((a, b) => a.created_at - b.created_at);
|
||||
|
||||
const groups = [];
|
||||
const consumed = new Set();
|
||||
|
||||
for (let i = 0; i < memories.length; i += 1) {
|
||||
const base = memories[i];
|
||||
for (let i = 0; i < sortedMemories.length; i += 1) {
|
||||
const base = sortedMemories[i];
|
||||
if (consumed.has(base.id) || base.merged_into) continue;
|
||||
|
||||
const baseEmbedding = options.getEmbedding?.(base);
|
||||
const group = [base];
|
||||
|
||||
for (let j = i + 1; j < memories.length; j += 1) {
|
||||
const probe = memories[j];
|
||||
for (let j = i + 1; j < sortedMemories.length; j += 1) {
|
||||
const probe = sortedMemories[j];
|
||||
|
||||
// Optimization: Since sorted by created_at, if the time difference exceeds the window,
|
||||
// all subsequent items will also be outside the window.
|
||||
if (probe.created_at - base.created_at > options.duplicateWindowMs) break;
|
||||
|
||||
if (consumed.has(probe.id) || probe.merged_into || probe.id === base.id) continue;
|
||||
|
||||
const withinWindow = Math.abs(base.created_at - probe.created_at) <= options.duplicateWindowMs;
|
||||
if (!withinWindow || !hasTagOverlap(base.tags, probe.tags)) continue;
|
||||
if (!hasTagOverlap(base.tags, probe.tags)) continue;
|
||||
|
||||
const similarity = cosineSimilarity(baseEmbedding, options.getEmbedding?.(probe));
|
||||
if (similarity >= options.similarityThreshold) {
|
||||
|
||||
Reference in new issue
Block a user