diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt index f32cdf0517..31b4ea27e8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt @@ -155,13 +155,11 @@ class Amethyst : Application() { if (isNappletSandbox) return instance.trim(level) // Drop warm embedded tab sessions under genuine memory pressure (decision: keep warm until the - // user or Android reclaims them). Deliberately NOT on UI_HIDDEN/BACKGROUND — those fire on every - // backgrounding, and a pinned tab should survive that. Only on real pressure levels, R+ only. - val pressure = - level == ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW || - level == ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL || - level == ComponentCallbacks2.TRIM_MEMORY_MODERATE || - level == ComponentCallbacks2.TRIM_MEMORY_COMPLETE + // user or Android reclaims them). Since API 34 the OS only delivers UI_HIDDEN and BACKGROUND: + // BACKGROUND means the process is on the system LRU list (real reclaim pressure), while UI_HIDDEN + // fires on every app switch — so evict only at BACKGROUND and above, letting a pinned tab survive + // a plain backgrounding. R+ only. + val pressure = level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND if (pressure && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { EmbeddedTabHost.evictAll() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt index 34efbe282e..3dc465b555 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt @@ -912,61 +912,36 @@ class AppModules( trimmingService.run(loggedIn, LocalPreferences.allSavedAccounts(), level) // Trim in-process caches proportional to OS memory pressure. // - // Background levels (app not visible, ordered highest-first so the when - // chain short-circuits at the right tier): - // COMPLETE (80) — at the bottom of the LRU list, kill imminent - // MODERATE (60) — system is hurting, neighbouring apps being killed - // BACKGROUND(40) — backgrounded, mild system pressure - // UI_HIDDEN (20) — just backgrounded, no pressure yet + // Since API 34 the OS only ever delivers two trim levels (the foreground + // RUNNING_* levels and the deeper MODERATE/COMPLETE background tiers were + // deprecated because apps are no longer notified of them): + // BACKGROUND(40) — process is on the system LRU list: real reclaim + // pressure, and the strongest signal we still get. + // UI_HIDDEN (20) — just backgrounded, no pressure yet. Fires on EVERY + // app switch. // - // Foreground levels (app is active but system is low): - // RUNNING_CRITICAL (15), RUNNING_LOW (10) - // - // UI_HIDDEN fires on EVERY app switch. Don't clear CPU-heavy caches - // (Robohash SVG assembly, rich-text parsing) there — clearing them - // forces a full rebuild on every resume and causes visible jank. + // So we key off exactly those two. UI_HIDDEN is frequent, so it only trims + // images (bitmaps are the largest allocations) and keeps the CPU-heavy + // caches (Robohash SVG assembly, rich-text parsing) warm — clearing them + // would force a full rebuild on every resume and cause visible jank. + // BACKGROUND trims hard but keeps a small working set: it means "on the LRU + // list" (real reclaim pressure), not the imminent kill that COMPLETE used to + // signal — so leave just enough warm to redraw the screen the user left on. when { - level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> { - // Kill imminent: free everything. - memoryCache.trimToSize(0) - CachedRichTextParser.trimToSize(0) - CachedRobohash.trimToSize(0) - nip11Cache.trimToSize(0) - } - level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE -> { - // System under real pressure: clear images and most parsed state. - memoryCache.trimToSize(0) - CachedRichTextParser.trimToSize(50) - CachedRobohash.trimToSize(10) - nip11Cache.trimToSize(100) - } level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND -> { - // Backgrounded with mild pressure: trim significantly. - memoryCache.trimToSize(memoryCache.maxSize / 4) - CachedRichTextParser.trimToSize(100) + // On the LRU list under real pressure: trim hard, but keep a small + // working set so a returning user doesn't rebuild the visible screen + // from scratch. memoryCache is byte-sized (Coil), the rest are entry counts. + memoryCache.trimToSize(memoryCache.maxSize / 10) + CachedRichTextParser.trimToSize(10) CachedRobohash.trimToSize(20) - nip11Cache.trimToSize(200) + nip11Cache.trimToSize(10) } level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> { - // Just backgrounded, no pressure yet: trim images (bitmaps are the - // largest allocations) but keep parsed-text and avatar caches warm - // so resuming is instant. + // Just backgrounded, no pressure yet: trim images but keep the + // parsed-text and avatar caches warm so resuming is instant. memoryCache.trimToSize(memoryCache.maxSize / 2) } - level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> { - // Foreground, critically low memory. - memoryCache.trimToSize(memoryCache.maxSize / 4) - CachedRichTextParser.trimToSize(100) - CachedRobohash.trimToSize(20) - nip11Cache.trimToSize(200) - } - level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW -> { - // Foreground, low memory. - memoryCache.trimToSize(memoryCache.maxSize / 2) - CachedRichTextParser.trimToSize(250) - CachedRobohash.trimToSize(50) - nip11Cache.trimToSize(500) - } } } } 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 3c29d78546..0a70fd4923 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 @@ -34,19 +34,18 @@ class MemoryTrimmingService( var isTrimmingMemoryMutex = AtomicBoolean(false) /** - * Tiered pruning scaled to the OS memory-pressure level. + * Two-tier pruning keyed to the OS trim levels still delivered since API 34 + * (the foreground RUNNING_* and deeper MODERATE/COMPLETE levels were deprecated + * because apps are no longer notified of them). * - * Tier 1 — mild pressure (UI hidden, running-moderate): + * Tier 1 — UI hidden (fires on every app switch): * 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. + * Tier 2 — background / real reclaim pressure (process on the LRU list): + * Tier 1 + drop events from muted/blocked users + old chat messages + + * unobserved thread replies / reactions. May cause feeds to re-fetch content + * that was scrolled past; triggers recomposition wherever StateFlows cleared. */ private fun doTrim( account: Collection, @@ -60,16 +59,13 @@ class MemoryTrimmingService( cache.pruneExpiredEvents() cache.prunePastVersionsOfReplaceables() - if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) { - // Tier 2: medium pressure — drop events from muted/blocked users + if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { + // Tier 2: real reclaim pressure — drop events from muted/blocked users, old + // messages, and unobserved reactions. account.forEach { cache.pruneHiddenEvents(it) cache.pruneHiddenMessages(it) } - } - - if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) { - // Tier 3: critical pressure — drop old messages and unobserved reactions val accounts = otherAccounts.mapNotNull { decodePublicKeyAsHexOrNull(it.npub) }.toSet() cache.pruneOldMessages() cache.pruneRepliesAndReactions(accounts) @@ -79,7 +75,7 @@ class MemoryTrimmingService( suspend fun run( account: Collection, otherAccounts: List, - level: Int = ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL, + level: Int = ComponentCallbacks2.TRIM_MEMORY_BACKGROUND, ) { if (isTrimmingMemoryMutex.compareAndSet(false, true)) { Log.d("ServiceManager", "Trimming Memory (level=$level)") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt index 4b818f1c8b..59fca51fdd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt @@ -166,7 +166,9 @@ class PlaybackService : MediaSessionService() { override fun onTrimMemory(level: Int) { super.onTrimMemory(level) - if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) { + // Since API 34 the OS only delivers UI_HIDDEN and BACKGROUND; BACKGROUND (process on + // the system LRU list) is the real reclaim-pressure signal, so release the warm pool then. + if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { poolNoProxy?.exoPlayerPool?.releaseWarmPool() poolWithProxy?.exoPlayerPool?.releaseWarmPool() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt index 50cdfd204e..96ffc4ec7e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt @@ -143,11 +143,12 @@ class AccountFeedContentStates( val webBookmarks = FeedContentState(WebBookmarkFeedFilter(account), scope, LocalCache) init { - // Under critical memory pressure, trim every feed down to 50 items to release - // the strong Note references that would otherwise keep pruned cache objects alive. + // Under real memory pressure (process on the system LRU list — the strongest trim + // level the OS still delivers since API 34), trim every feed down to release the + // strong Note references that would otherwise keep pruned cache objects alive. scope.launch(Dispatchers.IO) { Amethyst.instance.trimLevelEvents.collect { level -> - if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) { + if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { trimFeedsToSize(200) } }