From 638d4997d159bd7988ae7e2692f0a8691b175b03 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Mon, 4 May 2026 11:03:09 -0400 Subject: [PATCH] Improve chat open performance when thread pool is saturated. Co-authored-by: Greyson Parrelli --- .../securesms/main/MainNavigationViewModel.kt | 24 +++++++++++++++---- .../securesms/wallpaper/ChatWallpaper.java | 4 ++++ .../securesms/wallpaper/UriChatWallpaper.java | 6 +++++ .../api/profiles/ProfileRepository.kt | 9 ++++++- 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/main/MainNavigationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/main/MainNavigationViewModel.kt index 583350f2d5..a7d5eb7687 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/main/MainNavigationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/main/MainNavigationViewModel.kt @@ -31,6 +31,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.reactive.asFlow import kotlinx.coroutines.rx3.asObservable import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.calls.log.CallLogRow import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfilesRepository @@ -56,6 +57,7 @@ class MainNavigationViewModel( companion object { private val TAG = Log.tag(MainNavigationViewModel::class) private const val LOCK_PANE_TO_SECONDARY = "lock_pane_to_secondary" + private const val NAV_PREFETCH_TIMEOUT_MS = 250L } class Factory( @@ -253,13 +255,27 @@ class MainNavigationViewModel( } private fun goToConversation(args: ConversationArgs) = viewModelScope.launch { - val updatedArgs = withContext(Dispatchers.IO) { - val wallpaper = Recipient.resolved(args.recipientId).wallpaper - if (wallpaper?.prefetch(AppDependencies.application, 250) == false) { - Log.w(TAG, "goToConversation: Failed to prefetch wallpaper.") + val liveRecipient = Recipient.live(args.recipientId) + val recipientSnapshot = liveRecipient.get() + val wallpaper = recipientSnapshot.wallpaper + + val updatedArgs = if (recipientSnapshot.isResolving || (wallpaper?.isPhoto == true && !wallpaper.isPrefetched)) { + withTimeoutOrNull(NAV_PREFETCH_TIMEOUT_MS) { + withContext(Dispatchers.Default) { + val freshWallpaper = liveRecipient.resolve().wallpaper + if (freshWallpaper?.prefetch(AppDependencies.application, NAV_PREFETCH_TIMEOUT_MS) == false) { + Log.w(TAG, "[goToConversation] Failed to prefetch wallpaper.") + } + args.copy(hasWallpaper = freshWallpaper != null) + } + } ?: run { + Log.w(TAG, "[goToConversation] Timed out resolving recipient/wallpaper. Navigating without prefetch.") + args } + } else { args.copy(hasWallpaper = wallpaper != null) } + internalDetailLocation.emit(MainNavigationDetailLocation.Chats.Conversation(updatedArgs)) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaper.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaper.java index 9d5336b921..f6b56dd00a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaper.java @@ -33,6 +33,10 @@ public interface ChatWallpaper extends Parcelable { return true; } + default boolean isPrefetched() { + return true; + } + default boolean isPhoto() { return false; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/UriChatWallpaper.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/UriChatWallpaper.java index 3590f0db47..521b8428e4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/UriChatWallpaper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/UriChatWallpaper.java @@ -118,6 +118,12 @@ public final class UriChatWallpaper implements ChatWallpaper, Parcelable { return false; } + @Override + public boolean isPrefetched() { + Bitmap cached = CACHE.get(uri); + return cached != null && !cached.isRecycled(); + } + public @NonNull Uri getUri() { return uri; } diff --git a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/profiles/ProfileRepository.kt b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/profiles/ProfileRepository.kt index e7004ea00e..32b4e17206 100644 --- a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/profiles/ProfileRepository.kt +++ b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/profiles/ProfileRepository.kt @@ -6,6 +6,8 @@ package org.whispersystems.signalservice.api.profiles import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -28,6 +30,11 @@ class ProfileRepository(private val profileApi: ProfileApi) { companion object { private val TAG = Log.tag(ProfileRepository::class) + + private const val MAX_PARALLEL_FETCHES = 32 + + @OptIn(ExperimentalCoroutinesApi::class) + private val fetchDispatcher = Dispatchers.IO.limitedParallelism(MAX_PARALLEL_FETCHES, "ProfileRepository") } /** @@ -44,7 +51,7 @@ class ProfileRepository(private val profileApi: ProfileApi) { val mutex = Mutex() val tasks: List> = requests.map { request -> - async { + async(fetchDispatcher) { val response: NetworkResult = if (request.serviceId is ServiceId.ACI && request.profileKey != null && request.fetchExpiringCredential) { profileApi .getVersionedProfileAndCredential(request.serviceId, request.profileKey, request.sealedSenderAccess)