From 3dbbc039b3ab3188ae33fa8b0539b681ee333cbc Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Tue, 24 Mar 2026 09:14:02 +0200 Subject: [PATCH] feat(cache): replace BoundedLargeCache with LargeSoftCache on Desktop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DesktopLocalCache now uses LargeSoftCache (WeakReference-based, GC-driven) instead of BoundedLargeCache (strong refs, 50k cap, arbitrary eviction) - Delete BoundedLargeCache.kt — no longer needed - Rewrite findUsersStartingWith to use forEach instead of values() - Remove BoundedLargeCache eviction tests (no longer applicable) - Notes now only disappear when nothing references them, not arbitrarily Per Vitor's feedback: "this maximum size approach might not work well, as things will just disappear" --- .../desktop/cache/BoundedLargeCache.kt | 102 ------------------ .../desktop/cache/DesktopLocalCache.kt | 23 ++-- .../desktop/cache/DesktopCachePipelineTest.kt | 30 +----- 3 files changed, 13 insertions(+), 142 deletions(-) delete mode 100644 desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/BoundedLargeCache.kt diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/BoundedLargeCache.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/BoundedLargeCache.kt deleted file mode 100644 index 4b71abe10..000000000 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/BoundedLargeCache.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.desktop.cache - -import com.vitorpamplona.quartz.utils.cache.CacheCollectors -import com.vitorpamplona.quartz.utils.cache.LargeCache -import java.util.concurrent.atomic.AtomicInteger - -/** - * A bounded wrapper around [LargeCache] that enforces a maximum size. - * - * When the cache exceeds [maxSize], entries are evicted by key order. - * Uses [LargeCache] (ConcurrentSkipListMap) for lock-free reads and rich query APIs. - * - * Size tracking uses AtomicInteger (O(1)) instead of ConcurrentSkipListMap.size() (O(n)). - */ -class BoundedLargeCache, V>( - private val maxSize: Int, - private val evictPercent: Float = 0.1f, -) { - private val inner = LargeCache() - private val sizeCounter = AtomicInteger(0) - - fun get(key: K): V? = inner.get(key) - - fun put( - key: K, - value: V, - ) { - val existing = inner.get(key) - inner.put(key, value) - if (existing == null) sizeCounter.incrementAndGet() - enforceSize() - } - - fun getOrCreate( - key: K, - builder: (K) -> V, - ): V { - val existing = inner.get(key) - if (existing != null) return existing - val result = inner.getOrCreate(key, builder) - // Increment if we were the ones who created it (not a concurrent insert) - if (inner.get(key) === result) { - sizeCounter.incrementAndGet() - } - enforceSize() - return result - } - - fun remove(key: K): V? { - val removed = inner.remove(key) - if (removed != null) sizeCounter.decrementAndGet() - return removed - } - - fun size(): Int = sizeCounter.get() - - fun clear() { - inner.clear() - sizeCounter.set(0) - } - - fun keys(): Set = inner.keys() - - fun values(): Iterable = inner.values() - - fun filterIntoSet(consumer: CacheCollectors.BiFilter): Set = inner.filterIntoSet(consumer) - - fun count(consumer: CacheCollectors.BiFilter): Int = inner.count(consumer) - - private fun enforceSize() { - val currentSize = sizeCounter.get() - if (currentSize > maxSize) { - val toRemove = (maxSize * evictPercent).toInt().coerceAtLeast(1) - val keys = inner.keys().take(toRemove) - keys.forEach { - if (inner.remove(it) != null) { - sizeCounter.decrementAndGet() - } - } - } - } -} diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt index 7952ff5c2..ca08c471c 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.ICacheEventStream import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider +import com.vitorpamplona.amethyst.commons.model.cache.LargeSoftCache import com.vitorpamplona.amethyst.commons.services.nwc.NwcPaymentTracker import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.Event @@ -63,9 +64,9 @@ import java.util.concurrent.ConcurrentHashMap * Supports searching users by name prefix for the search functionality. */ class DesktopLocalCache : ICacheProvider { - val users = BoundedLargeCache(MAX_USERS) - val notes = BoundedLargeCache(MAX_NOTES) - val addressableNotes = BoundedLargeCache(MAX_ADDRESSABLE) + val users = LargeSoftCache() + val notes = LargeSoftCache() + val addressableNotes = LargeSoftCache() private val deletedEvents = ConcurrentHashMap.newKeySet() val eventStream = DesktopCacheEventStream() @@ -75,9 +76,6 @@ class DesktopLocalCache : ICacheProvider { val followedUsers: StateFlow> = _followedUsers.asStateFlow() companion object { - const val MAX_NOTES = 50_000 - const val MAX_USERS = 25_000 - const val MAX_ADDRESSABLE = 10_000 } val paymentTracker = NwcPaymentTracker() @@ -114,10 +112,10 @@ class DesktopLocalCache : ICacheProvider { ) // Search by name/displayName/nip05/lud16 - return users - .values() - .filter { user -> - val metadata = user.metadataOrNull() + val results = mutableListOf() + users.forEach { _, user -> + val metadata = user.metadataOrNull() + val matches = if (metadata == null) { user.pubkeyHex.startsWith(prefix, true) || user.pubkeyNpub().startsWith(prefix, true) @@ -126,7 +124,10 @@ class DesktopLocalCache : ICacheProvider { user.pubkeyHex.startsWith(prefix, true) || user.pubkeyNpub().startsWith(prefix, true) } - }.sortedWith( + if (matches) results.add(user) + } + return results + .sortedWith( compareBy( { it.metadataOrNull()?.anyNameStartsWith(dualCase) == false }, { it.metadataOrNull()?.anyAddressStartsWith(dualCase) == false }, diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopCachePipelineTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopCachePipelineTest.kt index e3f2e77ea..20c1b2bcd 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopCachePipelineTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopCachePipelineTest.kt @@ -496,35 +496,7 @@ class DesktopCachePipelineTest { } // ----------------------------------------------------------------------- - // 8. BoundedLargeCache eviction - // ----------------------------------------------------------------------- - - @Test - fun `BoundedLargeCache evicts when over capacity`() { - val cache = BoundedLargeCache(10, evictPercent = 0.5f) - - repeat(15) { i -> - cache.put("key_${i.toString().padStart(3, '0')}", "value_$i") - } - - assertTrue(cache.size() <= 10, "Cache should not exceed max size, got ${cache.size()}") - } - - @Test - fun `BoundedLargeCache get returns null for evicted entries`() { - val cache = BoundedLargeCache(5, evictPercent = 0.5f) - - repeat(10) { i -> - cache.put("key_${i.toString().padStart(3, '0')}", "value_$i") - } - - // Some early entries should have been evicted - val size = cache.size() - assertTrue(size <= 5, "Cache should be at or below max size") - } - - // ----------------------------------------------------------------------- - // 9. Additive filter incremental updates + // 8. Additive filter incremental updates // ----------------------------------------------------------------------- @Test