chore: fix some crash and performance issues #45

Merged
reya merged 5 commits from perf-patch into master 2026-07-15 12:44:29 +00:00
3 changed files with 86 additions and 43 deletions
Showing only changes of commit 43618533a5 - Show all commits

View File

@@ -98,9 +98,9 @@ import su.reya.coop.RoomKind
import su.reya.coop.RoomUiState import su.reya.coop.RoomUiState
import su.reya.coop.Screen import su.reya.coop.Screen
import su.reya.coop.ago import su.reya.coop.ago
import su.reya.coop.flow
import su.reya.coop.shared.Avatar import su.reya.coop.shared.Avatar
import su.reya.coop.shared.getExpressiveFontFamily import su.reya.coop.shared.getExpressiveFontFamily
import su.reya.coop.uiStateFlow
import su.reya.coop.viewmodel.AccountViewModel import su.reya.coop.viewmodel.AccountViewModel
import su.reya.coop.viewmodel.ChatViewModel import su.reya.coop.viewmodel.ChatViewModel
@@ -620,9 +620,9 @@ fun NewRequests(requests: List<Room>) {
val firstRoom = requests.getOrNull(0) val firstRoom = requests.getOrNull(0)
val secondRoom = requests.getOrNull(1) val secondRoom = requests.getOrNull(1)
val firstRoomState by (firstRoom as Room).uiStateFlow(profileCache) val firstRoomState by (firstRoom as Room).flow(profileCache)
.collectAsStateWithLifecycle(RoomUiState()) .collectAsStateWithLifecycle(RoomUiState())
val secondRoomState by (secondRoom ?: firstRoom).uiStateFlow(profileCache) val secondRoomState by (secondRoom ?: firstRoom).flow(profileCache)
.collectAsStateWithLifecycle(RoomUiState()) .collectAsStateWithLifecycle(RoomUiState())
val supportingText = when { val supportingText = when {
@@ -695,8 +695,7 @@ fun NewRequests(requests: List<Room>) {
@Composable @Composable
fun ChatRoom(room: Room, onClick: () -> Unit) { fun ChatRoom(room: Room, onClick: () -> Unit) {
val profileCache = LocalProfileCache.current val profileCache = LocalProfileCache.current
val roomState by room.uiStateFlow(profileCache) val roomState by room.flow(profileCache).collectAsStateWithLifecycle(RoomUiState())
.collectAsStateWithLifecycle(RoomUiState())
ListItem( ListItem(
modifier = Modifier.clickable(onClick = onClick), modifier = Modifier.clickable(onClick = onClick),

View File

@@ -13,7 +13,6 @@ import su.reya.coop.repository.AccountState
class AccountViewModel( class AccountViewModel(
private val repository: AccountRepository, private val repository: AccountRepository,
) : ViewModel(), ErrorHost by repository { ) : ViewModel(), ErrorHost by repository {
val state: StateFlow<AccountState> = repository.state val state: StateFlow<AccountState> = repository.state
val isUpdatingProfile: StateFlow<Boolean> = repository.isUpdatingProfile val isUpdatingProfile: StateFlow<Boolean> = repository.isUpdatingProfile
val currentUserProfile: StateFlow<Profile?> = repository.currentUserProfile val currentUserProfile: StateFlow<Profile?> = repository.currentUserProfile

View File

@@ -1,16 +1,15 @@
package su.reya.coop.viewmodel package su.reya.coop.viewmodel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import rust.nostr.sdk.PublicKey import rust.nostr.sdk.PublicKey
import su.reya.coop.Profile import su.reya.coop.Profile
@@ -19,27 +18,41 @@ import kotlin.time.Duration.Companion.milliseconds
class ProfileCache( class ProfileCache(
private val nostr: Nostr, private val nostr: Nostr,
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
) : ErrorHost by createErrorHost() { ) : ErrorHost by createErrorHost() {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private val scope = CoroutineScope(SupervisorJob() + defaultDispatcher)
private val profilesMutex = Mutex() private val profiles = MutableStateFlow<Map<PublicKey, MutableStateFlow<Profile?>>>(emptyMap())
private val profiles = mutableMapOf<PublicKey, MutableStateFlow<Profile?>>() private val seenPublicKeys = MutableStateFlow<Set<PublicKey>>(emptySet())
private val metadataRequestChannel = Channel<PublicKey>(Channel.UNLIMITED) private val metadataRequestChannel = Channel<PublicKey>(Channel.UNLIMITED)
private val seenPublicKeys = mutableSetOf<PublicKey>()
init { init {
scope.launch { runObserver() }
scope.launch { runMetadataBatching() }
scope.launch { scope.launch {
nostr.waitUntilInitialized() try {
loadCacheMetadata() runObserver()
} catch (e: Exception) {
showError("Metadata observer failed: ${e.message}")
}
}
scope.launch {
try {
runMetadataBatching()
} catch (e: Exception) {
showError("Metadata batching failed: ${e.message}")
}
}
scope.launch {
try {
nostr.waitUntilInitialized()
getCachedMetadata()
} catch (e: Exception) {
showError("Failed to load initial cache: ${e.message}")
}
} }
} }
private suspend fun runObserver() = coroutineScope { private suspend fun runObserver() {
launch { nostr.profiles.metadataUpdates.collect { (pubkey, metadata) ->
nostr.profiles.metadataUpdates.collect { (pubkey, metadata) -> updateMetadata(pubkey, Profile(pubkey, metadata))
updateMetadata(pubkey, Profile(pubkey, metadata))
}
} }
} }
@@ -57,37 +70,69 @@ class ProfileCache(
batch.add(nextKey) batch.add(nextKey)
} }
nostr.profiles.fetchMetadataBatch(batch.toList()) try {
} nostr.profiles.fetchMetadataBatch(batch.toList())
} } catch (e: Exception) {
// Allow these keys to be requested again since the fetch failed
private suspend fun loadCacheMetadata() { seenPublicKeys.update { it - batch }
val cache = nostr.profiles.getAllCacheMetadata() println("Failed to fetch metadata batch: ${e.message}")
profilesMutex.withLock {
cache.forEach { (pubkey, metadata) ->
val profile = Profile(pubkey, metadata)
profiles.getOrPut(pubkey) { MutableStateFlow(null) }.value = profile
seenPublicKeys.add(pubkey)
} }
} }
} }
private fun requestMetadata(pubkey: PublicKey) { private suspend fun getCachedMetadata() {
if (seenPublicKeys.add(pubkey)) { val cache = nostr.profiles.getAllCacheMetadata()
metadataRequestChannel.trySend(pubkey) cache.forEach { (pubkey, metadata) ->
updateMetadata(pubkey, Profile(pubkey, metadata))
} }
} }
private suspend fun updateMetadata(pubkey: PublicKey, profile: Profile) { private fun requestMetadata(pubkey: PublicKey) {
profilesMutex.withLock { var added = false
profiles.getOrPut(pubkey) { MutableStateFlow(null) }.value = profile seenPublicKeys.update { current ->
if (current.contains(pubkey)) {
added = false
current
} else {
added = true
current + pubkey
}
} }
if (added) metadataRequestChannel.trySend(pubkey)
}
private fun updateMetadata(pubkey: PublicKey, profile: Profile) {
profiles.update { current ->
val flow = current[pubkey] ?: MutableStateFlow<Profile?>(null)
flow.value = profile
if (current.containsKey(pubkey)) current else current + (pubkey to flow)
}
seenPublicKeys.update { it + pubkey }
} }
fun getMetadata(pubkey: PublicKey): StateFlow<Profile?> { fun getMetadata(pubkey: PublicKey): StateFlow<Profile?> {
val flow = profiles.getOrPut(pubkey) { MutableStateFlow(null) } val currentMap = profiles.value
if (flow.value == null) requestMetadata(pubkey) val existingFlow = currentMap[pubkey]
return flow.asStateFlow()
if (existingFlow != null) {
if (existingFlow.value == null) requestMetadata(pubkey)
return existingFlow.asStateFlow()
}
val newFlow = MutableStateFlow<Profile?>(null)
var resultFlow = newFlow
profiles.update { prev ->
if (prev.containsKey(pubkey)) {
resultFlow = prev[pubkey]!!
prev
} else {
prev + (pubkey to newFlow)
}
}
if (resultFlow.value == null) requestMetadata(pubkey)
return resultFlow.asStateFlow()
} }
} }