diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..8f342ab --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,5 @@ +## 2024-05-24 - Started + +## 2024-05-24 - O(N) Iteration in Cache Size Calculation +**Learning:** Calculating dynamic capacity dynamically via `store.values.sumOf { it.sizeBytes }` effectively incurs an O(N) cost on every `put` because it runs when `trimToCapacity()` calls `dynamicCapacity`. When cache sizes get large, this causes O(N²) overall insertion performance and UI stutters. +**Action:** Use an `AtomicLong` (O(1)) to track the running sum of cache size instead of dynamically iterating through the entire collection. diff --git a/app/src/main/java/com/example/reddittube/util/AdaptiveCacheEngine.kt b/app/src/main/java/com/example/reddittube/util/AdaptiveCacheEngine.kt index d7ffe01..177860c 100644 --- a/app/src/main/java/com/example/reddittube/util/AdaptiveCacheEngine.kt +++ b/app/src/main/java/com/example/reddittube/util/AdaptiveCacheEngine.kt @@ -25,6 +25,7 @@ class AdaptiveCacheEngine( private val store = ConcurrentHashMap>() private val lambda = 0.005 // Time decay parameter + private val currentSizeBytes = java.util.concurrent.atomic.AtomicLong(0L) /** * Dynamically calculates target capacity C_max based on available JVM memory: @@ -35,7 +36,7 @@ class AdaptiveCacheEngine( val runtime = Runtime.getRuntime() val availableMemory = runtime.maxMemory() - (runtime.totalMemory() - runtime.freeMemory()) val allocatable = (availableMemory * memoryFraction).toLong() - val totalSize = store.values.sumOf { it.sizeBytes } + val totalSize = currentSizeBytes.get() val count = store.size.coerceAtLeast(1) val avgItemSize = (totalSize / count).coerceAtLeast(1024L) val calcCapacity = (allocatable / avgItemSize).toInt() @@ -70,7 +71,12 @@ class AdaptiveCacheEngine( fun put(key: K, value: V) { val now = SystemClock.elapsedRealtime() val weight = sizeEstimator(value) - store[key] = CacheNode(value = value, frequency = 1, lastAccessedMs = now, sizeBytes = weight) + val oldNode = store.put(key, CacheNode(value = value, frequency = 1, lastAccessedMs = now, sizeBytes = weight)) + if (oldNode != null) { + currentSizeBytes.addAndGet(weight - oldNode.sizeBytes) + } else { + currentSizeBytes.addAndGet(weight) + } trimToCapacity() } @@ -94,13 +100,19 @@ class AdaptiveCacheEngine( val candidate = store.filter { (k, n) -> !isProtectedKey(k) || (now - n.lastAccessedMs > protectedTtlMs) }.minByOrNull { computeScore(it.value, now) } ?: break - store.remove(candidate.key) + val removedNode = store.remove(candidate.key) + if (removedNode != null) { + currentSizeBytes.addAndGet(-removedNode.sizeBytes) + } } } fun containsKey(key: K): Boolean = store.containsKey(key) - fun clear() = store.clear() + fun clear() { + store.clear() + currentSizeBytes.set(0L) + } fun size(): Int = store.size }