perf: more aggressive cache fairness to prevent thrashing

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.
This commit is contained in:
David Snelling 2025-10-13 11:50:53 -07:00
parent 76466f7f24
commit 829a8a61a2

View file

@ -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