perf(NoteCompose): cut per-item allocations during feed scroll
Three targeted fixes for the jitter that shows up as soon as a feed contains nested NoteCompose (reposts, quotes, BechLink previews): * `calculateBackgroundColor`: only schedule the 5s "new item" fade LaunchedEffect when the item is actually new and tracks read state. Inner notes pass `routeForLastRead = null` and were parking a coroutine for 5s per item, every scroll. Also drop a per-call `Color.copy(alpha = 0f)` allocation in favor of `Color.Transparent`. * `EventObservers` + `WatchBlockAndReport`: wrap the `StateFlow` lookups in `remember(note)` so each recomposition of the same note doesn't re-resolve `note.flow().…stateFlow` (and, in `WatchBlockAndReport`, hit the synchronized LRU lookup) on every pass. Affects observeNote / Replies / Reactions / Zaps / Reposts / Ots / Edits and the per-item hidden-flow check. * `produceCachedState` / `produceCachedStateAsync`: short-circuit on cache hits. The previous `produceState` body always launched a coroutine even when the LRU already had the value; this is the common path for BechLink previews and draft notes during scroll. Now we read the cache synchronously inside `remember`, and only launch a `LaunchedEffect` for the actual miss. No behavior change.
This commit is contained in:
+25
-13
@@ -22,35 +22,47 @@ package com.vitorpamplona.amethyst.commons.compose
|
||||
|
||||
import androidx.collection.LruCache
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
|
||||
// On a cache hit, short-circuit with a remembered State<V?> and skip the produceState coroutine.
|
||||
// Only on a miss do we launch the suspending update.
|
||||
@Composable
|
||||
fun <K : Any, V : Any> produceCachedStateAsync(
|
||||
cache: AsyncCachedState<K, V>,
|
||||
key: K,
|
||||
): State<V?> =
|
||||
@Suppress("ProduceStateDoesNotAssignValue")
|
||||
produceState(initialValue = cache.cached(key), key1 = key) {
|
||||
val newValue = cache.update(key)
|
||||
if (newValue != value) {
|
||||
value = newValue
|
||||
): State<V?> {
|
||||
val state = remember(key) { mutableStateOf(cache.cached(key)) }
|
||||
if (state.value == null) {
|
||||
LaunchedEffect(key) {
|
||||
val newValue = cache.update(key)
|
||||
if (state.value != newValue) {
|
||||
state.value = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <K : Any, V : Any> produceCachedStateAsync(
|
||||
cache: AsyncCachedState<K, V>,
|
||||
key: String,
|
||||
updateValue: K,
|
||||
): State<V?> =
|
||||
@Suppress("ProduceStateDoesNotAssignValue")
|
||||
produceState(initialValue = cache.cached(updateValue), key1 = key) {
|
||||
val newValue = cache.update(updateValue)
|
||||
if (newValue != value) {
|
||||
value = newValue
|
||||
): State<V?> {
|
||||
val state = remember(key) { mutableStateOf(cache.cached(updateValue)) }
|
||||
if (state.value == null) {
|
||||
LaunchedEffect(key) {
|
||||
val newValue = cache.update(updateValue)
|
||||
if (state.value != newValue) {
|
||||
state.value = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
interface AsyncCachedState<K : Any, V : Any> {
|
||||
fun cached(k: K): V?
|
||||
|
||||
+26
-13
@@ -22,35 +22,48 @@ package com.vitorpamplona.amethyst.commons.compose
|
||||
|
||||
import androidx.collection.LruCache
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
|
||||
// On a cache hit (the common case during scroll for things like Bech32 link previews),
|
||||
// short-circuit with a remembered State<V?> and skip the produceState coroutine entirely.
|
||||
// Only on a miss do we launch the suspending update.
|
||||
@Composable
|
||||
fun <K : Any, V : Any> produceCachedState(
|
||||
cache: CachedState<K, V>,
|
||||
key: K,
|
||||
): State<V?> =
|
||||
@Suppress("ProduceStateDoesNotAssignValue")
|
||||
produceState(initialValue = cache.cached(key), key1 = key) {
|
||||
val newValue = cache.update(key)
|
||||
if (value != newValue) {
|
||||
value = newValue
|
||||
): State<V?> {
|
||||
val state = remember(key) { mutableStateOf(cache.cached(key)) }
|
||||
if (state.value == null) {
|
||||
LaunchedEffect(key) {
|
||||
val newValue = cache.update(key)
|
||||
if (state.value != newValue) {
|
||||
state.value = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <K : Any, V : Any> produceCachedState(
|
||||
cache: CachedState<K, V>,
|
||||
key: String,
|
||||
updateValue: K,
|
||||
): State<V?> =
|
||||
@Suppress("ProduceStateDoesNotAssignValue")
|
||||
produceState(initialValue = cache.cached(updateValue), key1 = key) {
|
||||
val newValue = cache.update(updateValue)
|
||||
if (value != newValue) {
|
||||
value = newValue
|
||||
): State<V?> {
|
||||
val state = remember(key) { mutableStateOf(cache.cached(updateValue)) }
|
||||
if (state.value == null) {
|
||||
LaunchedEffect(key) {
|
||||
val newValue = cache.update(updateValue)
|
||||
if (state.value != newValue) {
|
||||
state.value = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
interface CachedState<K : Any, V : Any> {
|
||||
fun cached(k: K): V?
|
||||
|
||||
Reference in New Issue
Block a user