Improve conversation list cold start performance.

This commit is contained in:
Clark
2023-02-28 11:39:30 -05:00
committed by Greyson Parrelli
parent 10e8c6d795
commit f3693c966a
7 changed files with 92 additions and 18 deletions

View File

@@ -73,6 +73,7 @@ import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
import org.thoughtcrime.securesms.logging.PersistentLogger; import org.thoughtcrime.securesms.logging.PersistentLogger;
import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver; import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations; import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.SignalGlideComponents; import org.thoughtcrime.securesms.mms.SignalGlideComponents;
import org.thoughtcrime.securesms.mms.SignalGlideModule; import org.thoughtcrime.securesms.mms.SignalGlideModule;
import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.providers.BlobProvider;
@@ -176,6 +177,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addBlocking("feature-flags", FeatureFlags::init) .addBlocking("feature-flags", FeatureFlags::init)
.addBlocking("ring-rtc", this::initializeRingRtc) .addBlocking("ring-rtc", this::initializeRingRtc)
.addBlocking("glide", () -> SignalGlideModule.setRegisterGlideComponents(new SignalGlideComponents())) .addBlocking("glide", () -> SignalGlideModule.setRegisterGlideComponents(new SignalGlideComponents()))
.addNonBlocking(() -> GlideApp.get(this))
.addNonBlocking(this::checkIsGooglePayReady) .addNonBlocking(this::checkIsGooglePayReady)
.addNonBlocking(this::cleanAvatarStorage) .addNonBlocking(this::cleanAvatarStorage)
.addNonBlocking(this::initializeRevealableMessageManager) .addNonBlocking(this::initializeRevealableMessageManager)
@@ -212,6 +214,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addPostRender(PnpInitializeDevicesJob::enqueueIfNecessary) .addPostRender(PnpInitializeDevicesJob::enqueueIfNecessary)
.addPostRender(() -> ApplicationDependencies.getExoPlayerPool().getPoolStats().getMaxUnreserved()) .addPostRender(() -> ApplicationDependencies.getExoPlayerPool().getPoolStats().getMaxUnreserved())
.addPostRender(() -> SignalDatabase.groupCallRings().removeOldRings()) .addPostRender(() -> SignalDatabase.groupCallRings().removeOldRings())
.addPostRender(() -> ApplicationDependencies.getRecipientCache().warmUp())
.execute(); .execute();
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms"); Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
@@ -231,7 +234,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
SignalExecutors.BOUNDED.execute(() -> { SignalExecutors.BOUNDED.execute(() -> {
FeatureFlags.refreshIfNecessary(); FeatureFlags.refreshIfNecessary();
ApplicationDependencies.getRecipientCache().warmUp();
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this); RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
executePendingContactSync(); executePendingContactSync();
KeyCachingService.onAppForegrounded(this); KeyCachingService.onAppForegrounded(this);

View File

@@ -6,6 +6,7 @@ import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
import android.view.ViewTreeObserver;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -37,6 +38,8 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
private VoiceNoteMediaController mediaController; private VoiceNoteMediaController mediaController;
private ConversationListTabsViewModel conversationListTabsViewModel; private ConversationListTabsViewModel conversationListTabsViewModel;
private boolean onFirstRender = false;
public static @NonNull Intent clearTop(@NonNull Context context) { public static @NonNull Intent clearTop(@NonNull Context context) {
Intent intent = new Intent(context, MainActivity.class); Intent intent = new Intent(context, MainActivity.class);
@@ -53,6 +56,21 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
super.onCreate(savedInstanceState, ready); super.onCreate(savedInstanceState, ready);
setContentView(R.layout.main_activity); setContentView(R.layout.main_activity);
final View content = findViewById(android.R.id.content);
content.getViewTreeObserver().addOnPreDrawListener(
new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
// Use pre draw listener to delay drawing frames till conversation list is ready
if (onFirstRender) {
content.getViewTreeObserver().removeOnPreDrawListener(this);
return true;
} else {
return false;
}
}
});
mediaController = new VoiceNoteMediaController(this, true); mediaController = new VoiceNoteMediaController(this, true);
@@ -158,6 +176,10 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
} }
} }
public void onFirstRender() {
onFirstRender = true;
}
@Override @Override
public @NonNull VoiceNoteMediaController getVoiceNoteMediaController() { public @NonNull VoiceNoteMediaController getVoiceNoteMediaController() {
return mediaController; return mediaController;

View File

@@ -82,6 +82,7 @@ import org.signal.core.util.Stopwatch;
import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.concurrent.SimpleTask; import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.MainFragment; import org.thoughtcrime.securesms.MainFragment;
import org.thoughtcrime.securesms.MainNavigator; import org.thoughtcrime.securesms.MainNavigator;
import org.thoughtcrime.securesms.MuteDialog; import org.thoughtcrime.securesms.MuteDialog;
@@ -850,7 +851,11 @@ public class ConversationListFragment extends MainFragment implements ActionMode
} }
private void updateSearchToolbarHint(@NonNull ConversationFilterRequest conversationFilterRequest) { private void updateSearchToolbarHint(@NonNull ConversationFilterRequest conversationFilterRequest) {
requireCallback().getSearchToolbar().get().setSearchInputHint( Stub<Material3SearchToolbar> searchToolbar = requireCallback().getSearchToolbar();
if (!searchToolbar.resolved()) {
return;
}
searchToolbar.get().setSearchInputHint(
conversationFilterRequest.getFilter() == ConversationFilter.OFF ? R.string.SearchToolbar_search : R.string.SearchToolbar_search_unread_chats conversationFilterRequest.getFilter() == ConversationFilter.OFF ? R.string.SearchToolbar_search : R.string.SearchToolbar_search_unread_chats
); );
} }
@@ -887,13 +892,14 @@ public class ConversationListFragment extends MainFragment implements ActionMode
startupStopwatch.split("data-set"); startupStopwatch.split("data-set");
SignalLocalMetrics.ColdStart.onConversationListDataLoaded(); SignalLocalMetrics.ColdStart.onConversationListDataLoaded();
defaultAdapter.unregisterAdapterDataObserver(this); defaultAdapter.unregisterAdapterDataObserver(this);
list.post(() -> { if (requireActivity() instanceof MainActivity) {
AppStartup.getInstance().onCriticalRenderEventEnd(); ((MainActivity) requireActivity()).onFirstRender();
startupStopwatch.split("first-render"); }
startupStopwatch.stop(TAG); list.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
mediaControllerOwner.getVoiceNoteMediaController().finishPostpone(); @Override
if (getContext() != null) { public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
ConversationFragment.prepare(getContext()); list.removeOnLayoutChangeListener(this);
list.post(ConversationListFragment.this::onFirstRender);
} }
}); });
} }
@@ -968,6 +974,17 @@ public class ConversationListFragment extends MainFragment implements ActionMode
}); });
} }
private void onFirstRender() {
AppStartup.getInstance().onCriticalRenderEventEnd();
startupStopwatch.split("first-render");
startupStopwatch.stop(TAG);
mediaControllerOwner.getVoiceNoteMediaController().finishPostpone();
requireCallback().getSearchToolbar().get();
if (getContext() != null) {
ConversationFragment.prepare(getContext());
}
}
private void onConversationListChanged(@NonNull List<Conversation> conversations) { private void onConversationListChanged(@NonNull List<Conversation> conversations) {
LinearLayoutManager layoutManager = (LinearLayoutManager) list.getLayoutManager(); LinearLayoutManager layoutManager = (LinearLayoutManager) list.getLayoutManager();
int firstVisibleItem = layoutManager != null ? layoutManager.findFirstCompletelyVisibleItemPosition() : -1; int firstVisibleItem = layoutManager != null ? layoutManager.findFirstCompletelyVisibleItemPosition() : -1;

View File

@@ -8,6 +8,7 @@ import android.database.MergeCursor
import android.net.Uri import android.net.Uri
import androidx.core.content.contentValuesOf import androidx.core.content.contentValuesOf
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import org.json.JSONObject
import org.jsoup.helper.StringUtil import org.jsoup.helper.StringUtil
import org.signal.core.util.CursorUtil import org.signal.core.util.CursorUtil
import org.signal.core.util.SqlUtil import org.signal.core.util.SqlUtil
@@ -59,6 +60,7 @@ import org.thoughtcrime.securesms.recipients.RecipientUtil
import org.thoughtcrime.securesms.storage.StorageSyncHelper import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.ConversationUtil import org.thoughtcrime.securesms.util.ConversationUtil
import org.thoughtcrime.securesms.util.JsonUtils import org.thoughtcrime.securesms.util.JsonUtils
import org.thoughtcrime.securesms.util.JsonUtils.SaneJSONObject
import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.isScheduled import org.thoughtcrime.securesms.util.isScheduled
import org.whispersystems.signalservice.api.push.ServiceId import org.whispersystems.signalservice.api.push.ServiceId
@@ -1743,9 +1745,21 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
val extraString = cursor.getString(cursor.getColumnIndexOrThrow(SNIPPET_EXTRAS)) val extraString = cursor.getString(cursor.getColumnIndexOrThrow(SNIPPET_EXTRAS))
val extra: Extra? = if (extraString != null) { val extra: Extra? = if (extraString != null) {
try { try {
JsonUtils.fromJson(extraString, Extra::class.java) val jsonObject = SaneJSONObject(JSONObject(extraString))
} catch (e: IOException) { Extra(
Log.w(TAG, "Failed to decode extras!") isViewOnce = jsonObject.getBoolean("isRevealable"),
isSticker = jsonObject.getBoolean("isSticker"),
stickerEmoji = jsonObject.getString("stickerEmoji"),
isAlbum = jsonObject.getBoolean("isAlbum"),
isRemoteDelete = jsonObject.getBoolean("isRemoteDelete"),
isMessageRequestAccepted = jsonObject.getBoolean("isMessageRequestAccepted"),
isGv2Invite = jsonObject.getBoolean("isGv2Invite"),
groupAddedBy = jsonObject.getString("groupAddedBy"),
individualRecipientId = jsonObject.getString("individualRecipientId")!!,
bodyRanges = jsonObject.getString("bodyRanges"),
isScheduled = jsonObject.getBoolean("isScheduled")
)
} catch (exception: Exception) {
null null
} }
} else { } else {

View File

@@ -87,6 +87,7 @@ public class ApplicationDependencies {
private static final Object LOCK = new Object(); private static final Object LOCK = new Object();
private static final Object FRAME_RATE_TRACKER_LOCK = new Object(); private static final Object FRAME_RATE_TRACKER_LOCK = new Object();
private static final Object JOB_MANAGER_LOCK = new Object(); private static final Object JOB_MANAGER_LOCK = new Object();
private static final Object SIGNAL_HTTP_CLIENT_LOCK = new Object();
private static Application application; private static Application application;
private static Provider provider; private static Provider provider;
@@ -544,7 +545,7 @@ public class ApplicationDependencies {
public static @NonNull OkHttpClient getSignalOkHttpClient() { public static @NonNull OkHttpClient getSignalOkHttpClient() {
if (signalOkHttpClient == null) { if (signalOkHttpClient == null) {
synchronized (LOCK) { synchronized (SIGNAL_HTTP_CLIENT_LOCK) {
if (signalOkHttpClient == null) { if (signalOkHttpClient == null) {
try { try {
OkHttpClient baseClient = ApplicationDependencies.getOkHttpClient(); OkHttpClient baseClient = ApplicationDependencies.getOkHttpClient();

View File

@@ -4,6 +4,7 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import com.fasterxml.jackson.module.kotlin.registerKotlinModule import com.fasterxml.jackson.module.kotlin.registerKotlinModule
@@ -142,7 +143,6 @@ object EmojiFiles {
private fun getDirectory(context: Context): File = File(context.getEmojiDirectory(), this.uuid.toString()).apply { mkdir() } private fun getDirectory(context: Context): File = File(context.getEmojiDirectory(), this.uuid.toString()).apply { mkdir() }
companion object { companion object {
private val objectMapper = ObjectMapper().registerKotlinModule() private val objectMapper = ObjectMapper().registerKotlinModule()
@JvmStatic @JvmStatic
@@ -150,7 +150,12 @@ object EmojiFiles {
fun readVersion(context: Context, skipValidation: Boolean = false): Version? { fun readVersion(context: Context, skipValidation: Boolean = false): Version? {
val version = try { val version = try {
getInputStream(context, context.getVersionFile()).use { getInputStream(context, context.getVersionFile()).use {
objectMapper.readValue(it, Version::class.java) val tree: JsonNode = objectMapper.readTree(it)
Version(
version = tree["version"].asInt(),
uuid = objectMapper.convertValue(tree["uuid"], UUID::class.java),
density = tree["density"].asText()
)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "Could not read current emoji version from disk.", e) Log.w(TAG, "Could not read current emoji version from disk.", e)
@@ -222,8 +227,15 @@ object EmojiFiles {
@JvmStatic @JvmStatic
fun read(context: Context, version: Version): NameCollection { fun read(context: Context, version: Version): NameCollection {
try { try {
getInputStream(context, context.getNameFile(version.uuid)).use { getInputStream(context, context.getNameFile(version.uuid)).use { inputStream ->
return objectMapper.readValue(it) val tree: JsonNode = objectMapper.readTree(inputStream)
val elements = tree["names"].elements().asSequence().map {
Name(
name = it["name"].asText(),
uuid = objectMapper.convertValue(it["uuid"], UUID::class.java)
)
}.toList()
return NameCollection(objectMapper.convertValue(tree["versionUuid"], UUID::class.java), elements)
} }
} catch (e: Exception) { } catch (e: Exception) {
return NameCollection(version.uuid, listOf()) return NameCollection(version.uuid, listOf())

View File

@@ -11,6 +11,8 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.Reader; import java.io.Reader;
import javax.annotation.Nullable;
public class JsonUtils { public class JsonUtils {
private static final ObjectMapper objectMapper = new ObjectMapper(); private static final ObjectMapper objectMapper = new ObjectMapper();
@@ -54,7 +56,7 @@ public class JsonUtils {
this.delegate = delegate; this.delegate = delegate;
} }
public String getString(String name) throws JSONException { public @Nullable String getString(String name) throws JSONException {
if (delegate.isNull(name)) return null; if (delegate.isNull(name)) return null;
else return delegate.getString(name); else return delegate.getString(name);
} }
@@ -63,6 +65,10 @@ public class JsonUtils {
return delegate.getLong(name); return delegate.getLong(name);
} }
public boolean getBoolean(String name) throws JSONException {
return delegate.getBoolean(name);
}
public boolean isNull(String name) { public boolean isNull(String name) {
return delegate.isNull(name); return delegate.isNull(name);
} }