More conversation viewmodel perf improvements.

pull/1/head
Matthew Chen 6 years ago
parent 5087feb555
commit 19a2bfeaad

@ -26,6 +26,22 @@
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@interface ConversationProfileState : NSObject
@property (nonatomic) BOOL hasLocalProfile;
@property (nonatomic) BOOL isThreadInProfileWhitelist;
@property (nonatomic) BOOL hasUnwhitelistedMember;
@end
#pragma mark -
@implementation ConversationProfileState
@end
#pragma mark -
@implementation ConversationUpdateItem @implementation ConversationUpdateItem
- (instancetype)initWithUpdateItemType:(ConversationUpdateItemType)updateItemType - (instancetype)initWithUpdateItemType:(ConversationUpdateItemType)updateItemType
@ -146,6 +162,9 @@ static const int kYapDatabaseRangeMinLength = 0;
@property (nonatomic, nullable) NSDate *collapseCutoffDate; @property (nonatomic, nullable) NSDate *collapseCutoffDate;
@property (nonatomic, nullable) NSString *typingIndicatorsSender; @property (nonatomic, nullable) NSString *typingIndicatorsSender;
@property (nonatomic, nullable) ConversationProfileState *conversationProfileState;
@property (nonatomic) BOOL hasTooManyOutgoingMessagesToBlockCached;
@end @end
#pragma mark - #pragma mark -
@ -243,6 +262,10 @@ static const int kYapDatabaseRangeMinLength = 0;
selector:@selector(blockListDidChange:) selector:@selector(blockListDidChange:)
name:kNSNotificationName_BlockListDidChange name:kNSNotificationName_BlockListDidChange
object:nil]; object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(localProfileDidChange:)
name:kNSNotificationName_LocalProfileDidChange
object:nil];
} }
- (void)signalAccountsDidChange:(NSNotification *)notification - (void)signalAccountsDidChange:(NSNotification *)notification
@ -256,17 +279,16 @@ static const int kYapDatabaseRangeMinLength = 0;
{ {
OWSAssertIsOnMainThread(); OWSAssertIsOnMainThread();
// If profile whitelist just changed, we may want to hide a profile whitelist offer. self.conversationProfileState = nil;
NSString *_Nullable recipientId = notification.userInfo[kNSNotificationKey_ProfileRecipientId]; [self updateForTransientItems];
NSData *_Nullable groupId = notification.userInfo[kNSNotificationKey_ProfileGroupId]; }
if (recipientId.length > 0 && [self.thread.recipientIdentifiers containsObject:recipientId]) {
[self updateForTransientItems]; - (void)localProfileDidChange:(NSNotification *)notification
} else if (groupId.length > 0 && self.thread.isGroupThread) { {
TSGroupThread *groupThread = (TSGroupThread *)self.thread; OWSAssertIsOnMainThread();
if ([groupThread.groupModel.groupId isEqualToData:groupId]) {
[self updateForTransientItems]; self.conversationProfileState = nil;
} [self updateForTransientItems];
}
} }
- (void)blockListDidChange:(id)notification - (void)blockListDidChange:(id)notification
@ -929,8 +951,11 @@ static const int kYapDatabaseRangeMinLength = 0;
#pragma mark - View Items #pragma mark - View Items
- (nullable OWSContactOffersInteraction *)tryToBuildContactOffersInteraction - (void)ensureConversationProfileState
{ {
if (self.conversationProfileState) {
return;
}
// Many OWSProfileManager methods aren't safe to call from inside a database // Many OWSProfileManager methods aren't safe to call from inside a database
// transaction, so do this work now. // transaction, so do this work now.
@ -946,23 +971,46 @@ static const int kYapDatabaseRangeMinLength = 0;
} }
} }
__block OWSContactOffersInteraction *_Nullable offers = nil; ConversationProfileState *conversationProfileState = [ConversationProfileState new];
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { conversationProfileState.hasLocalProfile = hasLocalProfile;
offers = [self tryToBuildContactOffersInteractionWithTransaction:transaction conversationProfileState.isThreadInProfileWhitelist = isThreadInProfileWhitelist;
hasLocalProfile:hasLocalProfile conversationProfileState.hasUnwhitelistedMember = hasUnwhitelistedMember;
isThreadInProfileWhitelist:isThreadInProfileWhitelist self.conversationProfileState = conversationProfileState;
hasUnwhitelistedMember:hasUnwhitelistedMember]; }
}];
return offers; - (nullable TSInteraction *)firstCallOrMessageForLoadedInteractions:(NSArray<TSInteraction *> *)loadedInteractions
{
for (TSInteraction *interaction in loadedInteractions) {
switch (interaction.interactionType) {
case OWSInteractionType_Unknown:
OWSFailDebug(@"Unknown interaction type.");
return nil;
case OWSInteractionType_IncomingMessage:
case OWSInteractionType_OutgoingMessage:
return interaction;
case OWSInteractionType_Error:
case OWSInteractionType_Info:
break;
case OWSInteractionType_Call:
case OWSInteractionType_Offer:
case OWSInteractionType_TypingIndicator:
break;
}
}
return nil;
} }
- (nullable OWSContactOffersInteraction *) - (nullable OWSContactOffersInteraction *)
tryToBuildContactOffersInteractionWithTransaction:(YapDatabaseReadTransaction *)transaction tryToBuildContactOffersInteractionWithTransaction:(YapDatabaseReadTransaction *)transaction
hasLocalProfile:(BOOL)hasLocalProfile loadedInteractions:(NSArray<TSInteraction *> *)loadedInteractions
isThreadInProfileWhitelist:(BOOL)isThreadInProfileWhitelist
hasUnwhitelistedMember:(BOOL)hasUnwhitelistedMember
{ {
OWSAssertDebug(transaction); OWSAssertDebug(transaction);
OWSAssertDebug(self.conversationProfileState);
BOOL hasLocalProfile = self.conversationProfileState.hasLocalProfile;
BOOL isThreadInProfileWhitelist = self.conversationProfileState.isThreadInProfileWhitelist;
BOOL hasUnwhitelistedMember = self.conversationProfileState.hasUnwhitelistedMember;
TSThread *thread = self.thread; TSThread *thread = self.thread;
BOOL isContactThread = [thread isKindOfClass:[TSContactThread class]]; BOOL isContactThread = [thread isKindOfClass:[TSContactThread class]];
@ -977,27 +1025,22 @@ static const int kYapDatabaseRangeMinLength = 0;
NSString *localNumber = [self.tsAccountManager localNumber]; NSString *localNumber = [self.tsAccountManager localNumber];
OWSAssertDebug(localNumber.length > 0); OWSAssertDebug(localNumber.length > 0);
const int kMaxBlockOfferOutgoingMessageCount = 10; TSInteraction *firstCallOrMessage = [self firstCallOrMessageForLoadedInteractions:loadedInteractions];
__block TSInteraction *firstCallOrMessage = nil;
[[transaction ext:TSMessageDatabaseViewExtensionName]
enumerateRowsInGroup:thread.uniqueId
usingBlock:^(
NSString *collection, NSString *key, id object, id metadata, NSUInteger index, BOOL *stop) {
OWSAssertDebug([object isKindOfClass:[TSInteraction class]]);
if ([object isKindOfClass:[TSIncomingMessage class]] ||
[object isKindOfClass:[TSOutgoingMessage class]] || [object isKindOfClass:[TSCall class]]) {
firstCallOrMessage = object;
*stop = YES;
}
}];
if (!firstCallOrMessage) { if (!firstCallOrMessage) {
return nil; return nil;
} }
NSUInteger outgoingMessageCount = BOOL hasTooManyOutgoingMessagesToBlock;
[[TSDatabaseView threadOutgoingMessageDatabaseView:transaction] numberOfItemsInGroup:thread.uniqueId]; if (self.hasTooManyOutgoingMessagesToBlockCached) {
hasTooManyOutgoingMessagesToBlock = YES;
} else {
NSUInteger outgoingMessageCount =
[[TSDatabaseView threadOutgoingMessageDatabaseView:transaction] numberOfItemsInGroup:thread.uniqueId];
const int kMaxBlockOfferOutgoingMessageCount = 10;
hasTooManyOutgoingMessagesToBlock = (outgoingMessageCount > kMaxBlockOfferOutgoingMessageCount);
self.hasTooManyOutgoingMessagesToBlockCached = hasTooManyOutgoingMessagesToBlock;
}
BOOL shouldHaveBlockOffer = YES; BOOL shouldHaveBlockOffer = YES;
BOOL shouldHaveAddToContactsOffer = YES; BOOL shouldHaveAddToContactsOffer = YES;
@ -1032,7 +1075,7 @@ static const int kYapDatabaseRangeMinLength = 0;
} }
} }
if (outgoingMessageCount > kMaxBlockOfferOutgoingMessageCount) { if (hasTooManyOutgoingMessagesToBlock) {
// If the user has sent more than N messages, don't show a block offer. // If the user has sent more than N messages, don't show a block offer.
shouldHaveBlockOffer = NO; shouldHaveBlockOffer = NO;
} }
@ -1101,7 +1144,7 @@ static const int kYapDatabaseRangeMinLength = 0;
BOOL isGroupThread = self.thread.isGroupThread; BOOL isGroupThread = self.thread.isGroupThread;
ConversationStyle *conversationStyle = self.delegate.conversationStyle; ConversationStyle *conversationStyle = self.delegate.conversationStyle;
OWSContactOffersInteraction *_Nullable offers = [self tryToBuildContactOffersInteraction]; [self ensureConversationProfileState];
__block BOOL hasError = NO; __block BOOL hasError = NO;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
@ -1149,6 +1192,8 @@ static const int kYapDatabaseRangeMinLength = 0;
[interactionIds addObject:interaction.uniqueId]; [interactionIds addObject:interaction.uniqueId];
} }
OWSContactOffersInteraction *_Nullable offers =
[self tryToBuildContactOffersInteractionWithTransaction:transaction loadedInteractions:interactions];
if (offers && [interactionIds containsObject:offers.beforeInteractionId]) { if (offers && [interactionIds containsObject:offers.beforeInteractionId]) {
id<ConversationViewItem> offersItem = tryToAddViewItem(offers); id<ConversationViewItem> offersItem = tryToAddViewItem(offers);
if ([offersItem.interaction isKindOfClass:[OWSContactOffersInteraction class]]) { if ([offersItem.interaction isKindOfClass:[OWSContactOffersInteraction class]]) {

@ -347,38 +347,38 @@ NS_ASSUME_NONNULL_BEGIN
// We want to delete legacy and duplicate interactions. // We want to delete legacy and duplicate interactions.
NSMutableArray<TSInteraction *> *interactionsToDelete = [NSMutableArray new]; NSMutableArray<TSInteraction *> *interactionsToDelete = [NSMutableArray new];
[[TSDatabaseView threadSpecialMessagesDatabaseView:transaction] [[TSDatabaseView threadSpecialMessagesDatabaseView:transaction]
enumerateRowsInGroup:thread.uniqueId enumerateKeysAndObjectsInGroup:thread.uniqueId
usingBlock:^( usingBlock:^(
NSString *collection, NSString *key, id object, id metadata, NSUInteger index, BOOL *stop) { NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
if ([object isKindOfClass:[OWSUnknownContactBlockOfferMessage class]]) {
if ([object isKindOfClass:[OWSUnknownContactBlockOfferMessage class]]) { // Delete this legacy interactions, which has been superseded by
// Delete this legacy interactions, which has been superseded by // the OWSContactOffersInteraction.
// the OWSContactOffersInteraction. [interactionsToDelete addObject:object];
[interactionsToDelete addObject:object]; } else if ([object isKindOfClass:[OWSAddToContactsOfferMessage class]]) {
} else if ([object isKindOfClass:[OWSAddToContactsOfferMessage class]]) { // Delete this legacy interactions, which has been superseded by
// Delete this legacy interactions, which has been superseded by // the OWSContactOffersInteraction.
// the OWSContactOffersInteraction. [interactionsToDelete addObject:object];
[interactionsToDelete addObject:object]; } else if ([object isKindOfClass:[OWSAddToProfileWhitelistOfferMessage class]]) {
} else if ([object isKindOfClass:[OWSAddToProfileWhitelistOfferMessage class]]) { // Delete this legacy interactions, which has been superseded by
// Delete this legacy interactions, which has been superseded by // the OWSContactOffersInteraction.
// the OWSContactOffersInteraction. [interactionsToDelete addObject:object];
[interactionsToDelete addObject:object]; } else if ([object isKindOfClass:[TSUnreadIndicatorInteraction class]]) {
} else if ([object isKindOfClass:[TSUnreadIndicatorInteraction class]]) { // Remove obsolete unread indicator interactions.
// Remove obsolete unread indicator interactions. [interactionsToDelete addObject:object];
[interactionsToDelete addObject:object]; } else if ([object isKindOfClass:[OWSContactOffersInteraction class]]) {
} else if ([object isKindOfClass:[OWSContactOffersInteraction class]]) { // Remove obsolete contact offers.
// Remove obsolete contact offers. [interactionsToDelete addObject:object];
[interactionsToDelete addObject:object]; } else if ([object isKindOfClass:[TSInvalidIdentityKeyErrorMessage class]]) {
} else if ([object isKindOfClass:[TSInvalidIdentityKeyErrorMessage class]]) { [blockingSafetyNumberChanges addObject:object];
[blockingSafetyNumberChanges addObject:object]; } else if ([object isKindOfClass:[TSErrorMessage class]]) {
} else if ([object isKindOfClass:[TSErrorMessage class]]) { TSErrorMessage *errorMessage = (TSErrorMessage *)object;
TSErrorMessage *errorMessage = (TSErrorMessage *)object; OWSAssertDebug(
OWSAssertDebug(errorMessage.errorType == TSErrorMessageNonBlockingIdentityChange); errorMessage.errorType == TSErrorMessageNonBlockingIdentityChange);
[nonBlockingSafetyNumberChanges addObject:errorMessage]; [nonBlockingSafetyNumberChanges addObject:errorMessage];
} else { } else {
OWSFailDebug(@"Unexpected dynamic interaction type: %@", [object class]); OWSFailDebug(@"Unexpected dynamic interaction type: %@", [object class]);
} }
}]; }];
for (TSInteraction *interaction in interactionsToDelete) { for (TSInteraction *interaction in interactionsToDelete) {
OWSLogDebug(@"Cleaning up interaction: %@", [interaction class]); OWSLogDebug(@"Cleaning up interaction: %@", [interaction class]);
@ -461,41 +461,41 @@ NS_ASSUME_NONNULL_BEGIN
__block TSInteraction *interactionAfterUnreadIndicator = nil; __block TSInteraction *interactionAfterUnreadIndicator = nil;
__block BOOL hasMoreUnseenMessages = NO; __block BOOL hasMoreUnseenMessages = NO;
[threadMessagesTransaction [threadMessagesTransaction
enumerateRowsInGroup:thread.uniqueId enumerateKeysAndObjectsInGroup:thread.uniqueId
withOptions:NSEnumerationReverse withOptions:NSEnumerationReverse
usingBlock:^( usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
NSString *collection, NSString *key, id object, id metadata, NSUInteger index, BOOL *stop) { if (![object isKindOfClass:[TSInteraction class]]) {
if (![object isKindOfClass:[TSInteraction class]]) { OWSFailDebug(@"Expected a TSInteraction: %@", [object class]);
OWSFailDebug(@"Expected a TSInteraction: %@", [object class]); return;
return; }
}
TSInteraction *interaction = (TSInteraction *)object;
TSInteraction *interaction = (TSInteraction *)object;
if (interaction.isDynamicInteraction) {
if (interaction.isDynamicInteraction) { // Ignore dynamic interactions, if any.
// Ignore dynamic interactions, if any. return;
return; }
}
if (interaction.timestampForSorting
if (interaction.timestampForSorting < firstUnseenInteractionTimestamp.unsignedLongLongValue) { < firstUnseenInteractionTimestamp.unsignedLongLongValue) {
// By default we want the unread indicator to appear just before // By default we want the unread indicator to appear just before
// the first unread message. // the first unread message.
*stop = YES; *stop = YES;
return; return;
} }
visibleUnseenMessageCount++; visibleUnseenMessageCount++;
interactionAfterUnreadIndicator = interaction; interactionAfterUnreadIndicator = interaction;
if (visibleUnseenMessageCount + 1 >= maxRangeSize) { if (visibleUnseenMessageCount + 1 >= maxRangeSize) {
// If there are more unseen messages than can be displayed in the // If there are more unseen messages than can be displayed in the
// messages view, show the unread indicator at the top of the // messages view, show the unread indicator at the top of the
// displayed messages. // displayed messages.
*stop = YES; *stop = YES;
hasMoreUnseenMessages = YES; hasMoreUnseenMessages = YES;
} }
}]; }];
if (!interactionAfterUnreadIndicator) { if (!interactionAfterUnreadIndicator) {
// If we can't find an interaction after the unread indicator, // If we can't find an interaction after the unread indicator,

@ -212,14 +212,13 @@ ConversationColorName const kConversationColorName_Default = ConversationColorNa
usingBlock:(void (^)(TSInteraction *interaction, usingBlock:(void (^)(TSInteraction *interaction,
YapDatabaseReadTransaction *transaction))block YapDatabaseReadTransaction *transaction))block
{ {
void (^interactionBlock)(NSString *, NSString *, id, id, NSUInteger, BOOL *) = ^void(
NSString *collection, NSString *key, id _Nonnull object, id _Nonnull metadata, NSUInteger index, BOOL *stop) {
TSInteraction *interaction = object;
block(interaction, transaction);
};
YapDatabaseViewTransaction *interactionsByThread = [transaction ext:TSMessageDatabaseViewExtensionName]; YapDatabaseViewTransaction *interactionsByThread = [transaction ext:TSMessageDatabaseViewExtensionName];
[interactionsByThread enumerateRowsInGroup:self.uniqueId usingBlock:interactionBlock]; [interactionsByThread
enumerateKeysAndObjectsInGroup:self.uniqueId
usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
TSInteraction *interaction = object;
block(interaction, transaction);
}];
} }
/** /**
@ -284,16 +283,14 @@ ConversationColorName const kConversationColorName_Default = ConversationColorNa
{ {
NSMutableArray<id<OWSReadTracking>> *messages = [NSMutableArray new]; NSMutableArray<id<OWSReadTracking>> *messages = [NSMutableArray new];
[[TSDatabaseView unseenDatabaseViewExtension:transaction] [[TSDatabaseView unseenDatabaseViewExtension:transaction]
enumerateRowsInGroup:self.uniqueId enumerateKeysAndObjectsInGroup:self.uniqueId
usingBlock:^( usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
NSString *collection, NSString *key, id object, id metadata, NSUInteger index, BOOL *stop) { if (![object conformsToProtocol:@protocol(OWSReadTracking)]) {
OWSFailDebug(@"Unexpected object in unseen messages: %@", [object class]);
if (![object conformsToProtocol:@protocol(OWSReadTracking)]) { return;
OWSFailDebug(@"Unexpected object in unseen messages: %@", [object class]); }
return; [messages addObject:(id<OWSReadTracking>)object];
} }];
[messages addObject:(id<OWSReadTracking>)object];
}];
return [messages copy]; return [messages copy];
} }
@ -320,29 +317,29 @@ ConversationColorName const kConversationColorName_Default = ConversationColorNa
__block NSUInteger missedCount = 0; __block NSUInteger missedCount = 0;
__block TSInteraction *last = nil; __block TSInteraction *last = nil;
[[transaction ext:TSMessageDatabaseViewExtensionName] [[transaction ext:TSMessageDatabaseViewExtensionName]
enumerateRowsInGroup:self.uniqueId enumerateKeysAndObjectsInGroup:self.uniqueId
withOptions:NSEnumerationReverse withOptions:NSEnumerationReverse
usingBlock:^( usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
NSString *collection, NSString *key, id object, id metadata, NSUInteger index, BOOL *stop) { OWSAssertDebug([object isKindOfClass:[TSInteraction class]]);
OWSAssertDebug([object isKindOfClass:[TSInteraction class]]); missedCount++;
TSInteraction *interaction = (TSInteraction *)object;
missedCount++;
TSInteraction *interaction = (TSInteraction *)object; if ([TSThread shouldInteractionAppearInInbox:interaction]) {
last = interaction;
if ([TSThread shouldInteractionAppearInInbox:interaction]) {
last = interaction; // For long ignored threads, with lots of SN changes this can get really slow.
// I see this in development because I have a lot of long forgotten threads with
// For long ignored threads, with lots of SN changes this can get really slow. // members who's test devices are constantly reinstalled. We could add a
// I see this in development because I have a lot of long forgotten threads with members // purpose-built DB view, but I think in the real world this is rare to be a
// who's test devices are constantly reinstalled. We could add a purpose-built DB view, // hotspot.
// but I think in the real world this is rare to be a hotspot. if (missedCount > 50) {
if (missedCount > 50) { OWSLogWarn(@"found last interaction for inbox after skipping %lu items",
OWSLogWarn(@"found last interaction for inbox after skipping %lu items", (unsigned long)missedCount); (unsigned long)missedCount);
} }
*stop = YES; *stop = YES;
} }
}]; }];
return last; return last;
} }

@ -478,35 +478,30 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE
NSMutableArray<id<OWSReadTracking>> *newlyReadList = [NSMutableArray new]; NSMutableArray<id<OWSReadTracking>> *newlyReadList = [NSMutableArray new];
[[TSDatabaseView unseenDatabaseViewExtension:transaction] [[TSDatabaseView unseenDatabaseViewExtension:transaction]
enumerateRowsInGroup:thread.uniqueId enumerateKeysAndObjectsInGroup:thread.uniqueId
usingBlock:^(NSString *collection, usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
NSString *key, if (![object conformsToProtocol:@protocol(OWSReadTracking)]) {
id object, OWSFailDebug(
id metadata, @"Expected to conform to OWSReadTracking: object with class: %@ collection: %@ "
NSUInteger index, @"key: %@",
BOOL *stop) { [object class],
collection,
if (![object conformsToProtocol:@protocol(OWSReadTracking)]) { key);
OWSFailDebug(@"Expected to conform to OWSReadTracking: object with class: %@ collection: %@ " return;
@"key: %@", }
[object class], id<OWSReadTracking> possiblyRead = (id<OWSReadTracking>)object;
collection,
key); if (possiblyRead.timestampForSorting > timestamp) {
return; *stop = YES;
} return;
id<OWSReadTracking> possiblyRead = (id<OWSReadTracking>)object; }
if (possiblyRead.timestampForSorting > timestamp) { OWSAssertDebug(!possiblyRead.read);
*stop = YES; OWSAssertDebug(possiblyRead.expireStartedAt == 0);
return; if (!possiblyRead.read) {
} [newlyReadList addObject:possiblyRead];
}
OWSAssertDebug(!possiblyRead.read); }];
OWSAssertDebug(possiblyRead.expireStartedAt == 0);
if (!possiblyRead.read) {
[newlyReadList addObject:possiblyRead];
}
}];
if (newlyReadList.count < 1) { if (newlyReadList.count < 1) {
return; return;

Loading…
Cancel
Save