refactor(cache): move LargeSoftCache to commons/jvmAndroid, fix ICacheProvider types
- Move LargeSoftCache from amethyst/model to commons/jvmAndroid/model/cache so both Android and Desktop share the same WeakReference cache implementation - Change ICacheProvider return types from Any? to proper types (User?, Note?) since these model classes already live in commons - Remove unnecessary casts in ThreadAssembler, ChatNewMessageState, SearchBarState - Improve LargeSoftCache.cleanUp() to use single-pass iterator (zero allocation) - Update Android imports in LocalCache, LargeSoftCacheAddressExt, and test Per Vitor's feedback on PR #1905: BoundedLargeCache evicts arbitrarily. LargeSoftCache uses WeakReferences so GC respects the reference graph.
This commit is contained in:
@@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.model
|
package com.vitorpamplona.amethyst.model
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.commons.model.cache.LargeSoftCache
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.utils.cache.CacheCollectors
|
import com.vitorpamplona.quartz.utils.cache.CacheCollectors
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import androidx.compose.runtime.Stable
|
|||||||
import com.vitorpamplona.amethyst.Amethyst
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.commons.model.Channel
|
import com.vitorpamplona.amethyst.commons.model.Channel
|
||||||
import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
|
import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
|
||||||
|
import com.vitorpamplona.amethyst.commons.model.cache.LargeSoftCache
|
||||||
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
|
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
|
||||||
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
|
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
|
||||||
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
|
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
|
||||||
|
|||||||
+1
@@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.model
|
package com.vitorpamplona.amethyst.model
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.commons.model.cache.LargeSoftCache
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import junit.framework.TestCase.assertEquals
|
import junit.framework.TestCase.assertEquals
|
||||||
|
|||||||
+1
-1
@@ -52,7 +52,7 @@ class ThreadAssembler(
|
|||||||
?.getOrNull(1)
|
?.getOrNull(1)
|
||||||
if (markedAsRoot != null) {
|
if (markedAsRoot != null) {
|
||||||
// Check to see if there is an error in the tag and the root has replies
|
// Check to see if there is an error in the tag and the root has replies
|
||||||
val rootNote = cache.getNoteIfExists(markedAsRoot) as? Note
|
val rootNote = cache.getNoteIfExists(markedAsRoot)
|
||||||
if (rootNote?.replyTo?.isEmpty() == true) {
|
if (rootNote?.replyTo?.isEmpty() == true) {
|
||||||
return cache.checkGetOrCreateNote(markedAsRoot)
|
return cache.checkGetOrCreateNote(markedAsRoot)
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+4
-4
@@ -57,7 +57,7 @@ interface ICacheProvider {
|
|||||||
* @param pubkey The user's public key in hex format
|
* @param pubkey The user's public key in hex format
|
||||||
* @return The User if exists in cache, null otherwise
|
* @return The User if exists in cache, null otherwise
|
||||||
*/
|
*/
|
||||||
fun getUserIfExists(pubkey: HexKey): Any?
|
fun getUserIfExists(pubkey: HexKey): User?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Counts users matching a predicate.
|
* Counts users matching a predicate.
|
||||||
@@ -75,7 +75,7 @@ interface ICacheProvider {
|
|||||||
* @param hexKey The note's ID in hex format
|
* @param hexKey The note's ID in hex format
|
||||||
* @return The Note if exists in cache, null otherwise
|
* @return The Note if exists in cache, null otherwise
|
||||||
*/
|
*/
|
||||||
fun getNoteIfExists(hexKey: HexKey): Any?
|
fun getNoteIfExists(hexKey: HexKey): Note?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets an existing Note or creates a new one if it doesn't exist.
|
* Gets an existing Note or creates a new one if it doesn't exist.
|
||||||
@@ -123,7 +123,7 @@ interface ICacheProvider {
|
|||||||
fun findUsersStartingWith(
|
fun findUsersStartingWith(
|
||||||
prefix: String,
|
prefix: String,
|
||||||
limit: Int = 50,
|
limit: Int = 50,
|
||||||
): List<Any> = emptyList()
|
): List<User> = emptyList()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets or creates a User by public key hex.
|
* Gets or creates a User by public key hex.
|
||||||
@@ -132,7 +132,7 @@ interface ICacheProvider {
|
|||||||
* @param pubkey The user's public key in hex format
|
* @param pubkey The user's public key in hex format
|
||||||
* @return The User (existing or newly created)
|
* @return The User (existing or newly created)
|
||||||
*/
|
*/
|
||||||
fun getOrCreateUser(pubkey: HexKey): Any?
|
fun getOrCreateUser(pubkey: HexKey): User?
|
||||||
|
|
||||||
fun justConsumeMyOwnEvent(event: Event): Boolean
|
fun justConsumeMyOwnEvent(event: Event): Boolean
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-3
@@ -24,7 +24,6 @@ import androidx.compose.runtime.Stable
|
|||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import com.vitorpamplona.amethyst.commons.model.IAccount
|
import com.vitorpamplona.amethyst.commons.model.IAccount
|
||||||
import com.vitorpamplona.amethyst.commons.model.Note
|
import com.vitorpamplona.amethyst.commons.model.Note
|
||||||
import com.vitorpamplona.amethyst.commons.model.User
|
|
||||||
import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
|
import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.references.references
|
import com.vitorpamplona.quartz.nip01Core.tags.references.references
|
||||||
@@ -95,7 +94,7 @@ class ChatNewMessageState(
|
|||||||
if (currentRoom != null) {
|
if (currentRoom != null) {
|
||||||
_recipientsMissingDmRelays.value =
|
_recipientsMissingDmRelays.value =
|
||||||
currentRoom.users.any { hexKey ->
|
currentRoom.users.any { hexKey ->
|
||||||
val user = cache.getOrCreateUser(hexKey) as? User
|
val user = cache.getOrCreateUser(hexKey)
|
||||||
user?.dmInboxRelays().isNullOrEmpty()
|
user?.dmInboxRelays().isNullOrEmpty()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -141,7 +140,7 @@ class ChatNewMessageState(
|
|||||||
) {
|
) {
|
||||||
val pTags =
|
val pTags =
|
||||||
room.users.mapNotNull { hexKey ->
|
room.users.mapNotNull { hexKey ->
|
||||||
(cache.getOrCreateUser(hexKey) as? User)?.toPTag()
|
cache.getOrCreateUser(hexKey)?.toPTag()
|
||||||
}
|
}
|
||||||
|
|
||||||
val replyHint = _replyTo.value?.toEventHint<BaseDMGroupEvent>()
|
val replyHint = _replyTo.value?.toEventHint<BaseDMGroupEvent>()
|
||||||
|
|||||||
+1
-2
@@ -88,8 +88,7 @@ class SearchBarState(
|
|||||||
.debounce(debounceMs)
|
.debounce(debounceMs)
|
||||||
.onEach { query ->
|
.onEach { query ->
|
||||||
if (query.length >= 2 && _bech32Results.value.isEmpty()) {
|
if (query.length >= 2 && _bech32Results.value.isEmpty()) {
|
||||||
@Suppress("UNCHECKED_CAST")
|
_cachedUserResults.value = cache.findUsersStartingWith(query, 20)
|
||||||
_cachedUserResults.value = cache.findUsersStartingWith(query, 20) as List<User>
|
|
||||||
} else {
|
} else {
|
||||||
_cachedUserResults.value = emptyList()
|
_cachedUserResults.value = emptyList()
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-11
@@ -18,7 +18,7 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* 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.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.model
|
package com.vitorpamplona.amethyst.commons.model.cache
|
||||||
|
|
||||||
import com.vitorpamplona.quartz.utils.cache.CacheOperations
|
import com.vitorpamplona.quartz.utils.cache.CacheOperations
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
@@ -59,7 +59,7 @@ class LargeSoftCache<K : Any, V : Any> : CacheOperations<K, V> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Puts an object into the cache with a specified key.
|
* Puts an object into the cache with a specified key.
|
||||||
* The object is stored as a SoftReference.
|
* The object is stored as a WeakReference.
|
||||||
*
|
*
|
||||||
* @param key The key to associate with the object.
|
* @param key The key to associate with the object.
|
||||||
* @param value The object to cache.
|
* @param value The object to cache.
|
||||||
@@ -105,19 +105,16 @@ class LargeSoftCache<K : Any, V : Any> : CacheOperations<K, V> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Proactively cleans up the cache by removing entries whose weakly referenced
|
* Proactively cleans up the cache by removing entries whose weakly referenced
|
||||||
* objects have been garbage collected. While `get` handles cleanup on access,
|
* objects have been garbage collected. Single-pass iterator for efficiency.
|
||||||
* this method can be called periodically or when memory pressure is high.
|
|
||||||
*/
|
*/
|
||||||
fun cleanUp() {
|
fun cleanUp() {
|
||||||
val keysToRemove = mutableMapOf<K, WeakReference<V>>()
|
val iter = cache.entries.iterator()
|
||||||
cache.forEach { key, softRef ->
|
while (iter.hasNext()) {
|
||||||
if (softRef.get() == null) {
|
val entry = iter.next()
|
||||||
keysToRemove.put(key, softRef)
|
if (entry.value.get() == null) {
|
||||||
|
iter.remove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keysToRemove.forEach { key, value ->
|
|
||||||
cache.remove(key, value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun forEach(consumer: BiConsumer<K, V>) {
|
override fun forEach(consumer: BiConsumer<K, V>) {
|
||||||
Reference in New Issue
Block a user