diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt index 11c2371792..ca1de8490b 100644 Binary files a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt and b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt differ diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt index 0a57cee574..f6c4ec3145 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt @@ -852,12 +852,12 @@ class AppModules( accountsCache.clear() } - fun trim() { + fun trim(level: Int) { applicationIOScope.launch { // Backgrounding is a natural moment to flush the DNS cache. dnsStore.save() val loggedIn = accountsCache.accounts.value.values - trimmingService.run(loggedIn, LocalPreferences.allSavedAccounts()) + trimmingService.run(loggedIn, LocalPreferences.allSavedAccounts(), level) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/eventCache/MemoryTrimmingService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/eventCache/MemoryTrimmingService.kt index c2ebbb38ac..9aa4dfab76 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/eventCache/MemoryTrimmingService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/eventCache/MemoryTrimmingService.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.service.eventCache +import android.content.ComponentCallbacks2 import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache @@ -32,33 +33,57 @@ class MemoryTrimmingService( ) { var isTrimmingMemoryMutex = AtomicBoolean(false) + /** + * Tiered pruning scaled to the OS memory-pressure level. + * + * Tier 1 — mild pressure (UI hidden, running-moderate): + * Sweep stale WeakRefs, drop expired and superseded-replaceable events. + * Safe to run frequently; no UI-visible side effects. + * + * Tier 2 — low memory (running-low, background): + * Tier 1 + old chat messages + unobserved thread replies / reactions. + * May cause feeds to re-fetch content that was scrolled past. + * + * Tier 3 — critical / imminent kill (running-critical, moderate, complete): + * Tier 2 + sever all observer links + drop every event from muted/blocked users. + * Aggressive; triggers recomposition wherever StateFlows were cleared. + */ private fun doTrim( account: Collection, otherAccounts: List, + level: Int, ) { + // Tier 1: always run — cheap housekeeping cache.cleanMemory() - cache.cleanObservers() + cache.pruneExpiredEvents() + cache.prunePastVersionsOfReplaceables() - account.forEach { - cache.pruneHiddenEvents(it) - cache.pruneHiddenMessages(it) + if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) { + // Tier 2: medium pressure — drop messages and unobserved reactions + val accounts = otherAccounts.mapNotNull { decodePublicKeyAsHexOrNull(it.npub) }.toSet() + cache.pruneOldMessages() + cache.pruneRepliesAndReactions(accounts) } - val accounts = otherAccounts.mapNotNull { decodePublicKeyAsHexOrNull(it.npub) }.toSet() - cache.pruneOldMessages() - cache.pruneRepliesAndReactions(accounts) - cache.prunePastVersionsOfReplaceables() - cache.pruneExpiredEvents() + if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) { + // Tier 3: critical pressure — sever observer links and drop muted content + cache.cleanObservers() + account.forEach { + cache.pruneHiddenEvents(it) + cache.pruneHiddenMessages(it) + } + } } suspend fun run( account: Collection, otherAccounts: List, + level: Int = ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL, ) { if (isTrimmingMemoryMutex.compareAndSet(false, true)) { - Log.d("ServiceManager", "Trimming Memory") + Log.d("ServiceManager", "Trimming Memory (level=$level)") try { - doTrim(account, otherAccounts) + doTrim(account, otherAccounts, level) } finally { isTrimmingMemoryMutex.getAndSet(false) }