feat(cache): replace BoundedLargeCache with LargeSoftCache on Desktop
- 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"
This commit is contained in:
Vendored
-102
@@ -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<K : Comparable<K>, V>(
|
||||
private val maxSize: Int,
|
||||
private val evictPercent: Float = 0.1f,
|
||||
) {
|
||||
private val inner = LargeCache<K, V>()
|
||||
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<K> = inner.keys()
|
||||
|
||||
fun values(): Iterable<V> = inner.values()
|
||||
|
||||
fun filterIntoSet(consumer: CacheCollectors.BiFilter<K, V>): Set<V> = inner.filterIntoSet(consumer)
|
||||
|
||||
fun count(consumer: CacheCollectors.BiFilter<K, V>): 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+12
-11
@@ -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<HexKey, User>(MAX_USERS)
|
||||
val notes = BoundedLargeCache<HexKey, Note>(MAX_NOTES)
|
||||
val addressableNotes = BoundedLargeCache<String, AddressableNote>(MAX_ADDRESSABLE)
|
||||
val users = LargeSoftCache<HexKey, User>()
|
||||
val notes = LargeSoftCache<HexKey, Note>()
|
||||
val addressableNotes = LargeSoftCache<String, AddressableNote>()
|
||||
private val deletedEvents = ConcurrentHashMap.newKeySet<HexKey>()
|
||||
|
||||
val eventStream = DesktopCacheEventStream()
|
||||
@@ -75,9 +76,6 @@ class DesktopLocalCache : ICacheProvider {
|
||||
val followedUsers: StateFlow<Set<HexKey>> = _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<User>()
|
||||
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 },
|
||||
|
||||
Vendored
+1
-29
@@ -496,35 +496,7 @@ class DesktopCachePipelineTest {
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 8. BoundedLargeCache eviction
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun `BoundedLargeCache evicts when over capacity`() {
|
||||
val cache = BoundedLargeCache<String, String>(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<String, String>(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
|
||||
|
||||
Reference in New Issue
Block a user