From 829a8a61a23688aae1384b2844f1e75b1fd773d9 Mon Sep 17 00:00:00 2001 From: David Snelling Date: Mon, 13 Oct 2025 11:50:53 -0700 Subject: [PATCH] perf: more aggressive cache fairness to prevent thrashing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v3.40.1 fixed the eviction formula but fairness parameters were too gentle, allowing metadata to regenerate faster than eviction could keep up. This caused cache thrashing: evict 20% → regenerate → evict 20% → repeat. Changes to prevent thrashing: 1. Fairness interval: 60s → 30s (faster response to imbalances) 2. Size threshold: 90% → 70% (earlier intervention) 3. Access threshold: <10% → <15% (catch more imbalances) 4. Eviction amount: 20% → 50% (more aggressive cleanup) 5. Proactive checking: Added immediate fairness check during set() operations to prevent imbalance formation rather than just reacting to it This should eliminate the "still creating relationships" slowdown reported by Soulcraft Studio while maintaining the OOM crash prevention from v3.40.1. --- src/utils/unifiedCache.ts | 43 +++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/src/utils/unifiedCache.ts b/src/utils/unifiedCache.ts index 36531864..39a683ee 100644 --- a/src/utils/unifiedCache.ts +++ b/src/utils/unifiedCache.ts @@ -113,7 +113,7 @@ export class UnifiedCache { this.config = { enableRequestCoalescing: true, enableFairnessCheck: true, - fairnessCheckInterval: 60000, // Check fairness every minute + fairnessCheckInterval: 30000, // Check fairness every 30 seconds (v3.40.2: was 60s) persistPatterns: true, enableMemoryMonitoring: true, memoryCheckInterval: 30000, // Check memory every 30s @@ -231,6 +231,12 @@ export class UnifiedCache { this.currentSize += size this.typeAccessCounts[type]++ this.totalAccessCount++ + + // Proactive fairness check (v3.40.2): Check immediately if adding to a dominant type + // This prevents imbalance formation instead of reacting to it + if (this.config.enableFairnessCheck && this.cache.size > 10) { + this.checkProactiveFairness(type) + } } /** @@ -335,15 +341,40 @@ export class UnifiedCache { other: typeSizes.other / totalSize } - // Check for starvation (90% cache but <10% accesses) + // Check for starvation (v3.40.2: more aggressive - 70% cache with <15% accesses) + // Previous: 90% cache, <10% access (too lenient, caused thrashing) for (const type of ['hnsw', 'metadata', 'embedding', 'other'] as const) { - if (sizeRatios[type] > 0.9 && accessRatios[type] < 0.1) { + if (sizeRatios[type] > 0.7 && accessRatios[type] < 0.15) { prodLog.warn(`Type ${type} is hogging cache (${(sizeRatios[type] * 100).toFixed(1)}% size, ${(accessRatios[type] * 100).toFixed(1)}% access)`) this.evictType(type) } } } + /** + * Proactive fairness check (v3.40.2) + * Called immediately when adding items to prevent imbalance formation + * Uses same thresholds as periodic check but runs on-demand + */ + private checkProactiveFairness(addedType: 'hnsw' | 'metadata' | 'embedding' | 'other'): void { + // Quick check: only evaluate the type being added + let typeSize = 0 + for (const item of this.cache.values()) { + if (item.type === addedType) { + typeSize += item.size + } + } + + const sizeRatio = typeSize / (this.currentSize || 1) + const accessRatio = this.typeAccessCounts[addedType] / (this.totalAccessCount || 1) + + // Same threshold as periodic check: 70% size, <15% access + if (sizeRatio > 0.7 && accessRatio < 0.15) { + prodLog.debug(`Proactive fairness: ${addedType} reaching dominance (${(sizeRatio * 100).toFixed(1)}% size, ${(accessRatio * 100).toFixed(1)}% access)`) + this.evictType(addedType) + } + } + /** * Force evict items of a specific type */ @@ -360,9 +391,9 @@ export class UnifiedCache { // Sort by score (lower is worse) candidates.sort((a, b) => a[1] - b[1]) - // Evict bottom 20% of this type - const evictCount = Math.max(1, Math.floor(candidates.length * 0.2)) - + // Evict bottom 50% of this type (v3.40.2: was 20%, too slow to prevent thrashing) + const evictCount = Math.max(1, Math.floor(candidates.length * 0.5)) + for (let i = 0; i < evictCount && i < candidates.length; i++) { const [key, , item] = candidates[i] this.currentSize -= item.size