Skip to content

Commit

Permalink
[resources] Check cached deferreds and drop them if they are cancelle…
Browse files Browse the repository at this point in the history
…d. (#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

(cherry picked from commit dbab893)
  • Loading branch information
terrakok committed May 16, 2024
1 parent abfd6c9 commit b5daa4d
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 64 deletions.
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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) }
Expand Down Expand Up @@ -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() {
Expand All @@ -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)) }
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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()
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand Down

0 comments on commit b5daa4d

Please sign in to comment.