Browse Source

[resources] Check cached deferreds and drop them if they are cancelled. (#4819)

Before the fix we could cancel a coroutine and the cancelled deferred
was saved in cache.

## Release Notes
### Fixes - Resources
- _(prerelease fix)_ Fix a cached empty resource on a Compose for Web if
the resource loading was canceled during progress
pull/4820/head
Konstantin 6 months ago committed by GitHub
parent
commit
dbab89354a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 31
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/AsyncCache.kt
  2. 24
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt
  3. 44
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResourcesUtils.kt
  4. 22
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/plural/PluralRuleList.kt

31
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/AsyncCache.kt

@ -0,0 +1,31 @@
package org.jetbrains.compose.resources
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
internal class AsyncCache<K, V> {
private val mutex = Mutex()
private val cache = mutableMapOf<K, Deferred<V>>()
suspend fun getOrLoad(key: K, load: suspend () -> V): V = coroutineScope {
val deferred = mutex.withLock {
var cached = cache[key]
if (cached == null || cached.isCancelled) {
//LAZY - to free the mutex lock as fast as possible
cached = async(start = CoroutineStart.LAZY) { load() }
cache[key] = cached
}
cached
}
deferred.await()
}
//@TestOnly
fun clear() {
cache.clear()
}
}

24
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt

@ -1,6 +1,9 @@
package org.jetbrains.compose.resources
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
@ -9,9 +12,6 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.jetbrains.compose.resources.vector.toImageVector
import org.jetbrains.compose.resources.vector.xmldom.Element
@ -91,6 +91,7 @@ fun vectorResource(resource: DrawableResource): ImageVector {
}
internal expect class SvgElement
internal expect fun SvgElement.toSvgPainter(density: Density): Painter
private val emptySvgPainter: Painter by lazy { BitmapPainter(emptyImageBitmap) }
@ -135,8 +136,7 @@ private sealed interface ImageCache {
class Svg(val painter: Painter) : ImageCache
}
private val imageCacheMutex = Mutex()
private val imageCache = mutableMapOf<String, Deferred<ImageCache>>()
private val imageCache = AsyncCache<String, ImageCache>()
//@TestOnly
internal fun dropImageCache() {
@ -147,14 +147,4 @@ private suspend fun loadImage(
path: String,
resourceReader: ResourceReader,
decode: (ByteArray) -> ImageCache
): ImageCache = coroutineScope {
val deferred = imageCacheMutex.withLock {
imageCache.getOrPut(path) {
//LAZY - to free the mutex lock as fast as possible
async(start = CoroutineStart.LAZY) {
decode(resourceReader.read(path))
}
}
}
deferred.await()
}
): ImageCache = imageCache.getOrLoad(path) { decode(resourceReader.read(path)) }

44
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResourcesUtils.kt

@ -1,11 +1,6 @@
package org.jetbrains.compose.resources
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.jetbrains.compose.resources.plural.PluralCategory
import org.jetbrains.compose.resources.vector.xmldom.Element
import org.jetbrains.compose.resources.vector.xmldom.NodeList
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
@ -20,8 +15,7 @@ internal sealed interface StringItem {
data class Array(val items: List<String>) : StringItem
}
private val stringsCacheMutex = Mutex()
private val stringItemsCache = mutableMapOf<String, Deferred<StringItem>>()
private val stringItemsCache = AsyncCache<String, StringItem>()
//@TestOnly
internal fun dropStringItemsCache() {
stringItemsCache.clear()
@ -30,28 +24,22 @@ internal fun dropStringItemsCache() {
internal suspend fun getStringItem(
resourceItem: ResourceItem,
resourceReader: ResourceReader
): StringItem = coroutineScope {
val deferred = stringsCacheMutex.withLock {
stringItemsCache.getOrPut("${resourceItem.path}/${resourceItem.offset}-${resourceItem.size}") {
//LAZY - to free the mutex lock as fast as possible
async(start = CoroutineStart.LAZY) {
val record = resourceReader.readPart(
resourceItem.path,
resourceItem.offset,
resourceItem.size
).decodeToString()
val recordItems = record.split('|')
val recordType = recordItems.first()
val recordData = recordItems.last()
when (recordType) {
"plurals" -> recordData.decodeAsPlural()
"string-array" -> recordData.decodeAsArray()
else -> recordData.decodeAsString()
}
}
}
): StringItem = stringItemsCache.getOrLoad(
key = "${resourceItem.path}/${resourceItem.offset}-${resourceItem.size}"
) {
val record = resourceReader.readPart(
resourceItem.path,
resourceItem.offset,
resourceItem.size
).decodeToString()
val recordItems = record.split('|')
val recordType = recordItems.first()
val recordData = recordItems.last()
when (recordType) {
"plurals" -> recordData.decodeAsPlural()
"string-array" -> recordData.decodeAsArray()
else -> recordData.decodeAsString()
}
deferred.await()
}
@OptIn(ExperimentalEncodingApi::class)

22
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/plural/PluralRuleList.kt

@ -5,12 +5,7 @@
package org.jetbrains.compose.resources.plural
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.jetbrains.compose.resources.AsyncCache
import org.jetbrains.compose.resources.InternalResourceApi
import org.jetbrains.compose.resources.LanguageQualifier
import org.jetbrains.compose.resources.RegionQualifier
@ -21,8 +16,7 @@ internal class PluralRuleList(private val rules: Array<PluralRule>) {
}
companion object {
private val cacheMutex = Mutex()
private val cache = Array<Deferred<PluralRuleList>?>(cldrPluralRuleLists.size) { null }
private val cache = AsyncCache<Int, PluralRuleList>()
private val emptyList = PluralRuleList(emptyArray())
@OptIn(InternalResourceApi::class)
@ -36,17 +30,7 @@ internal class PluralRuleList(private val rules: Array<PluralRule>) {
suspend fun getInstance(cldrLocaleName: String): PluralRuleList {
val listIndex = cldrPluralRuleListIndexByLocale[cldrLocaleName]!!
return coroutineScope {
val deferred = cacheMutex.withLock {
if (cache[listIndex] == null) {
cache[listIndex] = async(start = CoroutineStart.LAZY) {
createInstance(listIndex)
}
}
cache[listIndex]!!
}
deferred.await()
}
return cache.getOrLoad(listIndex) { createInstance(listIndex) }
}
@OptIn(InternalResourceApi::class)

Loading…
Cancel
Save