mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-07-22 07:48:27 +00:00
Compare commits
1 Commits
a3e239d33f
...
claude/pea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d17206c63d |
@@ -48,6 +48,7 @@ import com.vitorpamplona.amethyst.service.connectivity.ConnectivityManager
|
||||
import com.vitorpamplona.amethyst.service.connectivity.ConnectivityStatus
|
||||
import com.vitorpamplona.amethyst.service.crashreports.CrashReportCache
|
||||
import com.vitorpamplona.amethyst.service.crashreports.UnexpectedCrashSaver
|
||||
import com.vitorpamplona.amethyst.service.eventCache.MemoryPressureMonitor
|
||||
import com.vitorpamplona.amethyst.service.eventCache.MemoryTrimmingService
|
||||
import com.vitorpamplona.amethyst.service.images.ImageCacheFactory
|
||||
import com.vitorpamplona.amethyst.service.images.ImageLoaderSetup
|
||||
@@ -651,6 +652,20 @@ class AppModules(
|
||||
MemoryTrimmingService(cache)
|
||||
}
|
||||
|
||||
// Foreground heap watchdog. Android 15+ (API 35) deprecated the foreground onTrimMemory levels
|
||||
// (TRIM_MEMORY_RUNNING_* / UI_HIDDEN), so a foreground app scrolling its feed no longer receives
|
||||
// a system trim callback and the LocalCache can grow until the process OOMs. This restores a
|
||||
// foreground trim trigger driven by actual heap usage, reusing the same trimmingService.
|
||||
val memoryPressureMonitor by
|
||||
lazy {
|
||||
MemoryPressureMonitor(
|
||||
onPressure = {
|
||||
val loggedIn = accountsCache.accounts.value.values
|
||||
trimmingService.run(loggedIn, LocalPreferences.allSavedAccounts())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// as new accounts are loaded, updates the state of the TorRelaySettings, which produces new TorRelayEvaluator
|
||||
// and reconnects relays if the configuration has been changed.
|
||||
val accountsTorStateConnector = AccountsTorStateConnector(accountsCache, torEvaluatorFlow, applicationIOScope)
|
||||
@@ -738,6 +753,12 @@ class AppModules(
|
||||
}
|
||||
}
|
||||
|
||||
// Trim the event cache before the heap fills up. Needed because Android 15+ no longer
|
||||
// delivers foreground onTrimMemory callbacks (see MemoryPressureMonitor).
|
||||
applicationIOScope.launch {
|
||||
memoryPressureMonitor.run()
|
||||
}
|
||||
|
||||
applicationIOScope.launch {
|
||||
// loads main account quickly.
|
||||
LocalPreferences.loadAccountConfigFromEncryptedStorage()
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.service.eventCache
|
||||
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
|
||||
/**
|
||||
* Watches the JVM heap and proactively triggers a cache trim before the process runs out of memory.
|
||||
*
|
||||
* The app already trims [LocalCache][com.vitorpamplona.amethyst.model.LocalCache] from
|
||||
* `Application.onTrimMemory`, but Android 15 (API 35) deprecated the foreground trim levels
|
||||
* (`TRIM_MEMORY_RUNNING_*`, `TRIM_MEMORY_UI_HIDDEN`). A foreground app that keeps scrolling its feed
|
||||
* therefore receives no system trim callback, and the in-memory event store grows until the heap hits
|
||||
* its hard cap and the process OOMs (often surfacing as a `CoroutinesInternalError` thrown from
|
||||
* whichever coroutine happens to allocate next). This monitor restores a foreground trigger that is
|
||||
* independent of the system callback.
|
||||
*/
|
||||
class MemoryPressureMonitor(
|
||||
private val checkIntervalMs: Long = DEFAULT_CHECK_INTERVAL_MS,
|
||||
private val highWatermark: Double = DEFAULT_HIGH_WATERMARK,
|
||||
private val usedHeapBytes: () -> Long = { Runtime.getRuntime().let { it.totalMemory() - it.freeMemory() } },
|
||||
private val maxHeapBytes: () -> Long = { Runtime.getRuntime().maxMemory() },
|
||||
private val onPressure: suspend () -> Unit,
|
||||
) {
|
||||
suspend fun run() {
|
||||
while (currentCoroutineContext().isActive) {
|
||||
delay(checkIntervalMs)
|
||||
|
||||
val used = usedHeapBytes()
|
||||
val max = maxHeapBytes()
|
||||
if (isUnderPressure(used, max, highWatermark)) {
|
||||
Log.w("MemoryPressureMonitor") {
|
||||
"Heap at ${used / (1024 * 1024)}/${max / (1024 * 1024)} MB " +
|
||||
"(>= ${(highWatermark * 100).toInt()}%). Trimming the event cache."
|
||||
}
|
||||
onPressure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Checked frequently enough to react before a fast scroll fills the heap, but cheap: the
|
||||
// probe is a couple of Runtime reads and a trim only fires when actually over the watermark.
|
||||
const val DEFAULT_CHECK_INTERVAL_MS = 30_000L
|
||||
|
||||
// Leaves headroom below the hard cap so the trim (and the GC it enables) has room to work
|
||||
// before any single allocation is forced to fail.
|
||||
const val DEFAULT_HIGH_WATERMARK = 0.85
|
||||
|
||||
fun isUnderPressure(
|
||||
usedBytes: Long,
|
||||
maxBytes: Long,
|
||||
watermark: Double,
|
||||
): Boolean {
|
||||
if (maxBytes <= 0L) return false
|
||||
return usedBytes.toDouble() / maxBytes.toDouble() >= watermark
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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.service.eventCache
|
||||
|
||||
import com.vitorpamplona.amethyst.service.eventCache.MemoryPressureMonitor.Companion.isUnderPressure
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.advanceTimeBy
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class MemoryPressureMonitorTest {
|
||||
private val mb = 1024L * 1024L
|
||||
|
||||
@Test
|
||||
fun belowWatermarkIsNotUnderPressure() {
|
||||
assertFalse(isUnderPressure(usedBytes = 400 * mb, maxBytes = 512 * mb, watermark = 0.85))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun atWatermarkIsUnderPressure() {
|
||||
// 0.85 * 512 = 435.2 MB
|
||||
assertTrue(isUnderPressure(usedBytes = 436 * mb, maxBytes = 512 * mb, watermark = 0.85))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun wellAboveWatermarkIsUnderPressure() {
|
||||
assertTrue(isUnderPressure(usedBytes = 510 * mb, maxBytes = 512 * mb, watermark = 0.85))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unknownMaxHeapNeverReportsPressure() {
|
||||
// Runtime.maxMemory() returns Long.MAX_VALUE / 0 / -1 on platforms that can't report a cap.
|
||||
assertFalse(isUnderPressure(usedBytes = 1_000 * mb, maxBytes = 0L, watermark = 0.85))
|
||||
assertFalse(isUnderPressure(usedBytes = 1_000 * mb, maxBytes = -1L, watermark = 0.85))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun firesOnPressureOncePerProbeWhenHeapIsHigh() =
|
||||
runTest {
|
||||
var trims = 0
|
||||
val monitor =
|
||||
MemoryPressureMonitor(
|
||||
checkIntervalMs = 10L,
|
||||
highWatermark = 0.85,
|
||||
usedHeapBytes = { 500L * mb },
|
||||
maxHeapBytes = { 512L * mb },
|
||||
onPressure = { trims++ },
|
||||
)
|
||||
|
||||
// run() loops forever; advance exactly one interval then stop it.
|
||||
val job = launch { monitor.run() }
|
||||
advanceTimeBy(11L)
|
||||
job.cancel()
|
||||
|
||||
assertEquals(1, trims)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun doesNotTrimWhenHeapIsBelowWatermark() =
|
||||
runTest {
|
||||
var trims = 0
|
||||
val monitor =
|
||||
MemoryPressureMonitor(
|
||||
checkIntervalMs = 10L,
|
||||
highWatermark = 0.85,
|
||||
usedHeapBytes = { 100L * mb },
|
||||
maxHeapBytes = { 512L * mb },
|
||||
onPressure = { trims++ },
|
||||
)
|
||||
|
||||
val job = launch { monitor.run() }
|
||||
advanceTimeBy(11L)
|
||||
job.cancel()
|
||||
|
||||
assertEquals(0, trims)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user