Update group update messages faster.

This commit is contained in:
Greyson Parrelli 2022-03-17 14:24:17 -04:00 committed by Cody Henthorne
parent f91494f813
commit 945c308cf5
7 changed files with 50 additions and 55 deletions

View file

@ -22,17 +22,23 @@ import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.ReactionRecord; import org.thoughtcrime.securesms.database.model.ReactionRecord;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.database.model.UpdateDescription;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.Stopwatch; import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.push.ServiceId;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -90,6 +96,7 @@ class ConversationDataSource implements PagedDataSource<MessageId, ConversationM
MentionHelper mentionHelper = new MentionHelper(); MentionHelper mentionHelper = new MentionHelper();
AttachmentHelper attachmentHelper = new AttachmentHelper(); AttachmentHelper attachmentHelper = new AttachmentHelper();
ReactionHelper reactionHelper = new ReactionHelper(); ReactionHelper reactionHelper = new ReactionHelper();
Set<ServiceId> referencedIds = new HashSet<>();
try (MmsSmsDatabase.Reader reader = MmsSmsDatabase.readerFor(db.getConversation(threadId, start, length))) { try (MmsSmsDatabase.Reader reader = MmsSmsDatabase.readerFor(db.getConversation(threadId, start, length))) {
MessageRecord record; MessageRecord record;
@ -98,6 +105,11 @@ class ConversationDataSource implements PagedDataSource<MessageId, ConversationM
mentionHelper.add(record); mentionHelper.add(record);
reactionHelper.add(record); reactionHelper.add(record);
attachmentHelper.add(record); attachmentHelper.add(record);
UpdateDescription description = record.getUpdateDisplayBody(context);
if (description != null) {
referencedIds.addAll(description.getMentioned());
}
} }
} }
@ -126,6 +138,11 @@ class ConversationDataSource implements PagedDataSource<MessageId, ConversationM
records = attachmentHelper.buildUpdatedModels(context, records); records = attachmentHelper.buildUpdatedModels(context, records);
stopwatch.split("attachment-models"); stopwatch.split("attachment-models");
for (ServiceId serviceId : referencedIds) {
Recipient.resolved(RecipientId.from(serviceId, null));
}
stopwatch.split("recipient-resolves");
List<ConversationMessage> messages = Stream.of(records) List<ConversationMessage> messages = Stream.of(records)
.map(m -> ConversationMessageFactory.createWithUnresolvedData(context, m, mentionHelper.getMentions(m.getId()))) .map(m -> ConversationMessageFactory.createWithUnresolvedData(context, m, mentionHelper.getMentions(m.getId())))
.toList(); .toList();

View file

@ -73,13 +73,12 @@ public final class ConversationUpdateItem extends FrameLayout
private Stub<CardView> donateButtonStub; private Stub<CardView> donateButtonStub;
private View background; private View background;
private ConversationMessage conversationMessage; private ConversationMessage conversationMessage;
private Recipient conversationRecipient; private Recipient conversationRecipient;
private Optional<MessageRecord> nextMessageRecord; private Optional<MessageRecord> nextMessageRecord;
private MessageRecord messageRecord; private MessageRecord messageRecord;
private boolean isMessageRequestAccepted; private boolean isMessageRequestAccepted;
private LiveData<SpannableString> displayBody; private LiveData<SpannableString> displayBody;
private EventListener eventListener; private EventListener eventListener;
private boolean hasWallpaper;
private final UpdateObserver updateObserver = new UpdateObserver(); private final UpdateObserver updateObserver = new UpdateObserver();
@ -146,7 +145,6 @@ public final class ConversationUpdateItem extends FrameLayout
boolean hasWallpaper, boolean hasWallpaper,
boolean isMessageRequestAccepted) boolean isMessageRequestAccepted)
{ {
this.hasWallpaper = hasWallpaper;
this.conversationMessage = conversationMessage; this.conversationMessage = conversationMessage;
this.messageRecord = conversationMessage.getMessageRecord(); this.messageRecord = conversationMessage.getMessageRecord();
this.nextMessageRecord = nextMessageRecord; this.nextMessageRecord = nextMessageRecord;

View file

@ -14,15 +14,22 @@ import org.signal.paging.PagedDataSource;
import org.thoughtcrime.securesms.conversationlist.model.Conversation; import org.thoughtcrime.securesms.conversationlist.model.Conversation;
import org.thoughtcrime.securesms.conversationlist.model.ConversationReader; import org.thoughtcrime.securesms.conversationlist.model.ConversationReader;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.database.model.UpdateDescription;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.Stopwatch; import org.thoughtcrime.securesms.util.Stopwatch;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
abstract class ConversationListDataSource implements PagedDataSource<Long, Conversation> { abstract class ConversationListDataSource implements PagedDataSource<Long, Conversation> {
@ -53,22 +60,32 @@ abstract class ConversationListDataSource implements PagedDataSource<Long, Conve
Stopwatch stopwatch = new Stopwatch("load(" + start + ", " + length + "), " + getClass().getSimpleName()); Stopwatch stopwatch = new Stopwatch("load(" + start + ", " + length + "), " + getClass().getSimpleName());
List<Conversation> conversations = new ArrayList<>(length); List<Conversation> conversations = new ArrayList<>(length);
List<Recipient> recipients = new LinkedList<>(); List<Recipient> recipients = new LinkedList<>();
Set<RecipientId> needsResolve = new HashSet<>();
try (ConversationReader reader = new ConversationReader(getCursor(start, length))) { try (ConversationReader reader = new ConversationReader(getCursor(start, length))) {
ThreadRecord record; ThreadRecord record;
while ((record = reader.getNext()) != null && !cancellationSignal.isCanceled()) { while ((record = reader.getNext()) != null && !cancellationSignal.isCanceled()) {
conversations.add(new Conversation(record)); conversations.add(new Conversation(record));
recipients.add(record.getRecipient()); recipients.add(record.getRecipient());
if (!record.getRecipient().isPushV2Group()) {
needsResolve.add(record.getRecipient().getId());
} else if (SmsDatabase.Types.isGroupUpdate(record.getType())) {
UpdateDescription description = MessageRecord.getGv2ChangeDescription(ApplicationDependencies.getApplication(), record.getBody());
needsResolve.addAll(description.getMentioned().stream().map(sid -> RecipientId.from(sid, null)).collect(Collectors.toList()));
}
} }
} }
stopwatch.split("cursor"); stopwatch.split("cursor");
ApplicationDependencies.getRecipientCache().addToCache(recipients); ApplicationDependencies.getRecipientCache().addToCache(recipients);
stopwatch.split("cache-recipients"); stopwatch.split("cache-recipients");
Recipient.resolvedList(needsResolve);
stopwatch.split("recipient-resolve");
stopwatch.stop(TAG); stopwatch.stop(TAG);
return conversations; return conversations;

View file

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.database.model;
import android.content.Context; import android.content.Context;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
@ -763,11 +764,9 @@ final class GroupsV2UpdateMessageProducer {
interface DescribeMemberStrategy { interface DescribeMemberStrategy {
/** /**
* Map an ACI to a string that describes the group member. * Map a ServiceId to a string that describes the group member.
* @param serviceId
*/ */
@NonNull @NonNull
@WorkerThread
String describe(@NonNull ServiceId serviceId); String describe(@NonNull ServiceId serviceId);
} }

View file

@ -12,8 +12,10 @@ import android.text.SpannableStringBuilder;
import androidx.annotation.AnyThread; import androidx.annotation.AnyThread;
import androidx.annotation.ColorInt; import androidx.annotation.ColorInt;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.Transformations;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
@ -34,11 +36,12 @@ public final class LiveUpdateMessage {
* Creates a live data that observes the recipients mentioned in the {@link UpdateDescription} and * Creates a live data that observes the recipients mentioned in the {@link UpdateDescription} and
* recreates the string asynchronously when they change. * recreates the string asynchronously when they change.
*/ */
@AnyThread @MainThread
public static LiveData<SpannableString> fromMessageDescription(@NonNull Context context, public static LiveData<SpannableString> fromMessageDescription(@NonNull Context context,
@NonNull UpdateDescription updateDescription, @NonNull UpdateDescription updateDescription,
@ColorInt int defaultTint, @ColorInt int defaultTint,
boolean adjustPosition) { boolean adjustPosition)
{
if (updateDescription.isStringStatic()) { if (updateDescription.isStringStatic()) {
return LiveDataUtil.just(toSpannable(context, updateDescription, updateDescription.getStaticString(), defaultTint, adjustPosition)); return LiveDataUtil.just(toSpannable(context, updateDescription, updateDescription.getStaticString(), defaultTint, adjustPosition));
} }
@ -50,16 +53,17 @@ public final class LiveUpdateMessage {
LiveData<?> mentionedRecipientChangeStream = allMentionedRecipients.isEmpty() ? LiveDataUtil.just(new Object()) LiveData<?> mentionedRecipientChangeStream = allMentionedRecipients.isEmpty() ? LiveDataUtil.just(new Object())
: LiveDataUtil.merge(allMentionedRecipients); : LiveDataUtil.merge(allMentionedRecipients);
return LiveDataUtil.mapAsync(mentionedRecipientChangeStream, event -> toSpannable(context, updateDescription, updateDescription.getString(), defaultTint, adjustPosition)); return Transformations.map(mentionedRecipientChangeStream, event -> toSpannable(context, updateDescription, updateDescription.getString(), defaultTint, adjustPosition));
} }
/** /**
* Observes a single recipient and recreates the string asynchronously when they change. * Observes a single recipient and recreates the string asynchronously when they change.
*/ */
@MainThread
public static LiveData<SpannableString> recipientToStringAsync(@NonNull RecipientId recipientId, public static LiveData<SpannableString> recipientToStringAsync(@NonNull RecipientId recipientId,
@NonNull Function<Recipient, SpannableString> createStringInBackground) @NonNull Function<Recipient, SpannableString> createStringInBackground)
{ {
return LiveDataUtil.mapAsync(Recipient.live(recipientId).getLiveDataResolved(), createStringInBackground); return Transformations.map(Recipient.live(recipientId).getLiveDataResolved(), createStringInBackground::apply);
} }
private static @NonNull SpannableString toSpannable(@NonNull Context context, @NonNull UpdateDescription updateDescription, @NonNull String string, @ColorInt int defaultTint, boolean adjustPosition) { private static @NonNull SpannableString toSpannable(@NonNull Context context, @NonNull UpdateDescription updateDescription, @NonNull String string, @ColorInt int defaultTint, boolean adjustPosition) {

View file

@ -22,7 +22,6 @@ import java.util.Set;
public final class UpdateDescription { public final class UpdateDescription {
public interface StringFactory { public interface StringFactory {
@WorkerThread
String create(); String create();
} }
@ -109,14 +108,12 @@ public final class UpdateDescription {
return staticString; return staticString;
} }
ThreadUtil.assertNotMainThread();
//noinspection ConstantConditions //noinspection ConstantConditions
return stringFactory.create(); return stringFactory.create();
} }
@AnyThread @AnyThread
public Collection<ServiceId> getMentioned() { public @NonNull Collection<ServiceId> getMentioned() {
return mentioned; return mentioned;
} }

View file

@ -1,13 +1,6 @@
package org.thoughtcrime.securesms.database.model; package org.thoughtcrime.securesms.database.model;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.signal.core.util.ThreadUtil;
import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.ServiceId;
import java.util.Arrays; import java.util.Arrays;
@ -22,19 +15,6 @@ import static org.junit.Assert.assertTrue;
public final class UpdateDescriptionTest { public final class UpdateDescriptionTest {
@Rule
public MockitoRule rule = MockitoJUnit.rule();
@Mock
private MockedStatic<ThreadUtil> threadUtilMockedStatic;
@Before
public void setup() {
threadUtilMockedStatic.when(ThreadUtil::isMainThread).thenReturn(true);
threadUtilMockedStatic.when(ThreadUtil::assertMainThread).thenCallRealMethod();
threadUtilMockedStatic.when(ThreadUtil::assertNotMainThread).thenCallRealMethod();
}
@Test @Test
public void staticDescription_byGetStaticString() { public void staticDescription_byGetStaticString() {
UpdateDescription description = UpdateDescription.staticDescription("update", 0); UpdateDescription description = UpdateDescription.staticDescription("update", 0);
@ -56,15 +36,6 @@ public final class UpdateDescriptionTest {
assertEquals("update", description.getString()); assertEquals("update", description.getString());
} }
@Test(expected = AssertionError.class)
public void stringFactory_cannot_run_on_main_thread() {
UpdateDescription description = UpdateDescription.mentioning(Collections.singletonList(ServiceId.from(UUID.randomUUID())), () -> "update", 0);
threadUtilMockedStatic.when(ThreadUtil::isMainThread).thenReturn(true);
description.getString();
}
@Test(expected = UnsupportedOperationException.class) @Test(expected = UnsupportedOperationException.class)
public void stringFactory_cannot_call_static_string() { public void stringFactory_cannot_call_static_string() {
UpdateDescription description = UpdateDescription.mentioning(Collections.singletonList(ServiceId.from(UUID.randomUUID())), () -> "update", 0); UpdateDescription description = UpdateDescription.mentioning(Collections.singletonList(ServiceId.from(UUID.randomUUID())), () -> "update", 0);
@ -85,8 +56,6 @@ public final class UpdateDescriptionTest {
assertEquals(0, factoryCalls.get()); assertEquals(0, factoryCalls.get());
threadUtilMockedStatic.when(ThreadUtil::isMainThread).thenReturn(false);
String string = description.getString(); String string = description.getString();
assertEquals("update", string); assertEquals("update", string);
@ -99,8 +68,6 @@ public final class UpdateDescriptionTest {
UpdateDescription.StringFactory stringFactory = () -> "call" + factoryCalls.incrementAndGet(); UpdateDescription.StringFactory stringFactory = () -> "call" + factoryCalls.incrementAndGet();
UpdateDescription description = UpdateDescription.mentioning(Collections.singletonList(ServiceId.from(UUID.randomUUID())), stringFactory, 0); UpdateDescription description = UpdateDescription.mentioning(Collections.singletonList(ServiceId.from(UUID.randomUUID())), stringFactory, 0);
threadUtilMockedStatic.when(ThreadUtil::isMainThread).thenReturn(false);
assertEquals("call1", description.getString()); assertEquals("call1", description.getString());
assertEquals("call2", description.getString()); assertEquals("call2", description.getString());
assertEquals("call3", description.getString()); assertEquals("call3", description.getString());
@ -143,8 +110,6 @@ public final class UpdateDescriptionTest {
assertFalse(description.isStringStatic()); assertFalse(description.isStringStatic());
threadUtilMockedStatic.when(ThreadUtil::isMainThread).thenReturn(false);
assertEquals("update.11\nupdate.21", description.getString()); assertEquals("update.11\nupdate.21", description.getString());
assertEquals("update.12\nupdate.22", description.getString()); assertEquals("update.12\nupdate.22", description.getString());
assertEquals("update.13\nupdate.23", description.getString()); assertEquals("update.13\nupdate.23", description.getString());
@ -167,8 +132,6 @@ public final class UpdateDescriptionTest {
assertFalse(description.isStringStatic()); assertFalse(description.isStringStatic());
threadUtilMockedStatic.when(ThreadUtil::isMainThread).thenReturn(false);
assertEquals("update.101\nstatic\nupdate.201", description.getString()); assertEquals("update.101\nstatic\nupdate.201", description.getString());
assertEquals("update.102\nstatic\nupdate.202", description.getString()); assertEquals("update.102\nstatic\nupdate.202", description.getString());
assertEquals("update.103\nstatic\nupdate.203", description.getString()); assertEquals("update.103\nstatic\nupdate.203", description.getString());