mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(android): bound link preview image cache (#101560)
This commit is contained in:
committed by
GitHub
parent
42e2390b31
commit
903a09ab85
@@ -38,6 +38,7 @@ internal const val LINK_PREVIEW_IMAGE_MAX_DIMENSION = 600
|
||||
private const val LINK_PREVIEW_MAX_REDIRECTS = 3
|
||||
private const val LINK_PREVIEW_TIMEOUT_MILLIS = 6_000L
|
||||
private const val LINK_PREVIEW_CACHE_ENTRIES = 64
|
||||
private const val LINK_PREVIEW_IMAGE_CACHE_MAX_BYTES = 8 * 1024 * 1024
|
||||
private const val LINK_PREVIEW_IMAGE_CACHE_ENTRIES = 32
|
||||
private const val LINK_PREVIEW_ACCEPT = "text/html, application/xhtml+xml;q=0.9"
|
||||
private const val LINK_PREVIEW_IMAGE_ACCEPT = "image/*"
|
||||
@@ -324,9 +325,22 @@ internal class LinkPreviewStore(
|
||||
|
||||
internal class LinkPreviewImageStore(
|
||||
private val fetcher: suspend (String) -> LinkPreviewImageResult,
|
||||
maxEntries: Int = LINK_PREVIEW_IMAGE_CACHE_ENTRIES,
|
||||
maxBytes: Int = LINK_PREVIEW_IMAGE_CACHE_MAX_BYTES,
|
||||
) {
|
||||
private val cache = LruCache<String, LinkPreviewImageResult>(maxEntries)
|
||||
// Every result pays at least one entry share, preserving the entry cap while loaded bitmaps
|
||||
// also pay their full backing allocation.
|
||||
private val minimumResultBytes = max(1, maxBytes / LINK_PREVIEW_IMAGE_CACHE_ENTRIES)
|
||||
private val cache =
|
||||
object : LruCache<String, LinkPreviewImageResult>(maxBytes) {
|
||||
override fun sizeOf(
|
||||
key: String,
|
||||
value: LinkPreviewImageResult,
|
||||
): Int =
|
||||
when (value) {
|
||||
is LinkPreviewImageResult.Loaded -> value.bitmap.allocationByteCount.coerceAtLeast(minimumResultBytes)
|
||||
LinkPreviewImageResult.Failed -> minimumResultBytes
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun get(url: String): LinkPreviewImageResult {
|
||||
cache.get(url)?.let { return it }
|
||||
|
||||
@@ -318,6 +318,84 @@ class ChatLinkPreviewTest {
|
||||
assertEquals("image/*", server.takeRequest().getHeader("Accept"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun imageCacheEvictsLeastRecentlyUsedBitmapByAllocatedBytes() =
|
||||
runBlocking {
|
||||
val first = Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_8888)
|
||||
val second = Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_8888)
|
||||
val fetchCounts = mutableMapOf<String, Int>()
|
||||
val store =
|
||||
LinkPreviewImageStore(
|
||||
fetcher = { url ->
|
||||
fetchCounts[url] = fetchCounts.getOrDefault(url, 0) + 1
|
||||
LinkPreviewImageResult.Loaded(if (url == "first") first else second)
|
||||
},
|
||||
maxBytes = first.allocationByteCount,
|
||||
)
|
||||
|
||||
try {
|
||||
assertTrue(store.get("first") is LinkPreviewImageResult.Loaded)
|
||||
assertTrue(store.get("second") is LinkPreviewImageResult.Loaded)
|
||||
assertTrue(store.get("first") is LinkPreviewImageResult.Loaded)
|
||||
|
||||
assertEquals(2, fetchCounts["first"])
|
||||
assertEquals(1, fetchCounts["second"])
|
||||
} finally {
|
||||
first.recycle()
|
||||
second.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun imageCacheBoundsNegativeResults() =
|
||||
runBlocking {
|
||||
val fetchCounts = mutableMapOf<String, Int>()
|
||||
val store =
|
||||
LinkPreviewImageStore(
|
||||
fetcher = { url ->
|
||||
fetchCounts[url] = fetchCounts.getOrDefault(url, 0) + 1
|
||||
LinkPreviewImageResult.Failed
|
||||
},
|
||||
maxBytes = 2,
|
||||
)
|
||||
|
||||
assertSame(LinkPreviewImageResult.Failed, store.get("first"))
|
||||
assertSame(LinkPreviewImageResult.Failed, store.get("second"))
|
||||
assertSame(LinkPreviewImageResult.Failed, store.get("third"))
|
||||
assertSame(LinkPreviewImageResult.Failed, store.get("first"))
|
||||
|
||||
assertEquals(2, fetchCounts["first"])
|
||||
assertEquals(1, fetchCounts["second"])
|
||||
assertEquals(1, fetchCounts["third"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun imageCacheBoundsTinyLoadedResultsByEntryCount() =
|
||||
runBlocking {
|
||||
val tiny = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
|
||||
val maxEntries = 32
|
||||
val fetchCounts = mutableMapOf<String, Int>()
|
||||
val store =
|
||||
LinkPreviewImageStore(
|
||||
fetcher = { url ->
|
||||
fetchCounts[url] = fetchCounts.getOrDefault(url, 0) + 1
|
||||
LinkPreviewImageResult.Loaded(tiny)
|
||||
},
|
||||
maxBytes = tiny.allocationByteCount * maxEntries * 2,
|
||||
)
|
||||
|
||||
try {
|
||||
repeat(maxEntries + 1) { index ->
|
||||
assertTrue(store.get("image-$index") is LinkPreviewImageResult.Loaded)
|
||||
}
|
||||
assertTrue(store.get("image-0") is LinkPreviewImageResult.Loaded)
|
||||
|
||||
assertEquals(2, fetchCounts["image-0"])
|
||||
} finally {
|
||||
tiny.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun imageContentTypeAllowlistAndBodyCapAreEnforced() =
|
||||
withServer { server ->
|
||||
|
||||
Reference in New Issue
Block a user