perf(playback): size the video cache adaptively instead of fixed 1 GB

With multi-rendition HLS renditions in the tens-to-low-hundreds of MB
each, the previous 1 GB fixed budget held only ~10-20 videos and
evicted everything not currently on screen. A Nostr timeline is
append-only so LRU degenerates to FIFO here, which is actually fine —
the bottleneck was the budget, not the policy.

Mirror the image cache pattern: target 20% of currently-available disk,
clamped between a 256 MB floor (low-storage devices) and a 4 GB cap.
That typically lets the evictor keep an order of magnitude more videos
without notably affecting disk pressure on modern devices.

https://claude.ai/code/session_01W8yGieuEyMKeKY9vU9zjKH
This commit is contained in:
Claude
2026-04-16 06:07:56 +00:00
committed by davotoula
parent 6afacfb747
commit 0218653a9c
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.playback.diskCache
import android.annotation.SuppressLint
import android.content.Context
import android.os.StatFs
import androidx.media3.database.StandaloneDatabaseProvider
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.cache.CacheDataSource
@@ -31,9 +32,14 @@ import java.io.File
@SuppressLint("UnsafeOptInUsageError")
class VideoCache {
var exoPlayerCacheSize: Long = 1000 * 1024 * 1024 // 1GB
var leastRecentlyUsedCacheEvictor = LeastRecentlyUsedCacheEvictor(exoPlayerCacheSize)
companion object {
// Target fraction of currently-available disk space.
private const val CACHE_SIZE_PERCENT = 0.20
// Hard cap so we never consume more than this even on large devices.
private const val CACHE_SIZE_MAX_BYTES = 4L * 1024 * 1024 * 1024 // 4 GB
// Floor so the cache is still useful on low-storage devices.
private const val CACHE_SIZE_MIN_BYTES = 256L * 1024 * 1024 // 256 MB
}
lateinit var exoDatabaseProvider: StandaloneDatabaseProvider
lateinit var simpleCache: SimpleCache
@@ -46,14 +52,38 @@ class VideoCache {
) {
exoDatabaseProvider = StandaloneDatabaseProvider(context)
val cacheSize = calculateCacheSize(cachePath)
simpleCache =
SimpleCache(
cachePath,
leastRecentlyUsedCacheEvictor,
LeastRecentlyUsedCacheEvictor(cacheSize),
exoDatabaseProvider,
)
}
/**
* Adaptive cache sizing: target [CACHE_SIZE_PERCENT] of currently-available
* disk, clamped between [CACHE_SIZE_MIN_BYTES] and [CACHE_SIZE_MAX_BYTES].
*
* A Nostr timeline is append-only — users rarely rewatch older videos — so
* LRU approximates FIFO here. That's actually fine as long as the budget is
* large enough to cover a useful scroll window. With multi-rendition HLS
* renditions at ~20-100 MB each, 1 GB held only a handful of videos and
* evicted anything not visible on screen. 4 GB keeps roughly an order of
* magnitude more without meaningfully impacting disk pressure on modern
* devices (Android can reclaim cache dirs when storage gets tight).
*/
private fun calculateCacheSize(cachePath: File): Long {
cachePath.mkdirs()
val availableBytes =
runCatching {
StatFs(cachePath.absolutePath).availableBytes
}.getOrDefault(0L)
val target = (availableBytes * CACHE_SIZE_PERCENT).toLong()
return target.coerceIn(CACHE_SIZE_MIN_BYTES, CACHE_SIZE_MAX_BYTES)
}
// This method should be called when proxy setting changes.
fun renewCacheFactory(dataSourceFactory: DataSource.Factory) {
cacheDataSourceFactory =