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. 20
      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 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.ImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter 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.platform.LocalDensity
import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp 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.toImageVector
import org.jetbrains.compose.resources.vector.xmldom.Element import org.jetbrains.compose.resources.vector.xmldom.Element
@ -91,6 +91,7 @@ fun vectorResource(resource: DrawableResource): ImageVector {
} }
internal expect class SvgElement internal expect class SvgElement
internal expect fun SvgElement.toSvgPainter(density: Density): Painter internal expect fun SvgElement.toSvgPainter(density: Density): Painter
private val emptySvgPainter: Painter by lazy { BitmapPainter(emptyImageBitmap) } private val emptySvgPainter: Painter by lazy { BitmapPainter(emptyImageBitmap) }
@ -135,8 +136,7 @@ private sealed interface ImageCache {
class Svg(val painter: Painter) : ImageCache class Svg(val painter: Painter) : ImageCache
} }
private val imageCacheMutex = Mutex() private val imageCache = AsyncCache<String, ImageCache>()
private val imageCache = mutableMapOf<String, Deferred<ImageCache>>()
//@TestOnly //@TestOnly
internal fun dropImageCache() { internal fun dropImageCache() {
@ -147,14 +147,4 @@ private suspend fun loadImage(
path: String, path: String,
resourceReader: ResourceReader, resourceReader: ResourceReader,
decode: (ByteArray) -> ImageCache decode: (ByteArray) -> ImageCache
): ImageCache = coroutineScope { ): ImageCache = imageCache.getOrLoad(path) { decode(resourceReader.read(path)) }
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()
}

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

@ -1,11 +1,6 @@
package org.jetbrains.compose.resources 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.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.Base64
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
@ -20,8 +15,7 @@ internal sealed interface StringItem {
data class Array(val items: List<String>) : StringItem data class Array(val items: List<String>) : StringItem
} }
private val stringsCacheMutex = Mutex() private val stringItemsCache = AsyncCache<String, StringItem>()
private val stringItemsCache = mutableMapOf<String, Deferred<StringItem>>()
//@TestOnly //@TestOnly
internal fun dropStringItemsCache() { internal fun dropStringItemsCache() {
stringItemsCache.clear() stringItemsCache.clear()
@ -30,11 +24,9 @@ internal fun dropStringItemsCache() {
internal suspend fun getStringItem( internal suspend fun getStringItem(
resourceItem: ResourceItem, resourceItem: ResourceItem,
resourceReader: ResourceReader resourceReader: ResourceReader
): StringItem = coroutineScope { ): StringItem = stringItemsCache.getOrLoad(
val deferred = stringsCacheMutex.withLock { key = "${resourceItem.path}/${resourceItem.offset}-${resourceItem.size}"
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( val record = resourceReader.readPart(
resourceItem.path, resourceItem.path,
resourceItem.offset, resourceItem.offset,
@ -49,10 +41,6 @@ internal suspend fun getStringItem(
else -> recordData.decodeAsString() else -> recordData.decodeAsString()
} }
} }
}
}
deferred.await()
}
@OptIn(ExperimentalEncodingApi::class) @OptIn(ExperimentalEncodingApi::class)
private fun String.decodeAsString(): StringItem.Value = StringItem.Value( private fun String.decodeAsString(): StringItem.Value = StringItem.Value(

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

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

Loading…
Cancel
Save