Improve UI performance for no-op profile fetches

This commit is contained in:
Evan Hahn 2021-05-21 14:53:05 -05:00 committed by GitHub
parent e859fcd4b4
commit 95d404c70b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 52 additions and 24 deletions

View file

@ -93,6 +93,10 @@ const COLORS = [
const THREE_HOURS = 3 * 60 * 60 * 1000; const THREE_HOURS = 3 * 60 * 60 * 1000;
const FIVE_MINUTES = 1000 * 60 * 5; const FIVE_MINUTES = 1000 * 60 * 5;
const ATTRIBUTES_THAT_DONT_INVALIDATE_PROPS_CACHE = new Set([
'profileLastFetchedAt',
]);
type CustomError = Error & { type CustomError = Error & {
identifier?: string; identifier?: string;
number?: string; number?: string;
@ -289,6 +293,13 @@ export class ConversationModel extends window.Backbone
// We clear our cached props whenever we change so that the next call to format() will // We clear our cached props whenever we change so that the next call to format() will
// result in refresh via a getProps() call. See format() below. // result in refresh via a getProps() call. See format() below.
this.on('change', () => { this.on('change', () => {
const isPropsCacheStillValid = Object.keys(this.changed).every(key =>
ATTRIBUTES_THAT_DONT_INVALIDATE_PROPS_CACHE.has(key)
);
if (isPropsCacheStillValid) {
return;
}
if (this.cachedProps) { if (this.cachedProps) {
this.oldCachedProps = this.cachedProps; this.oldCachedProps = this.cachedProps;
} }
@ -1342,6 +1353,8 @@ export class ConversationModel extends window.Backbone
// maintains a cache, and protects against reentrant calls. // maintains a cache, and protects against reentrant calls.
// Note: When writing code inside this function, do not call .format() on a conversation // Note: When writing code inside this function, do not call .format() on a conversation
// unless you are sure that it's not this very same conversation. // unless you are sure that it's not this very same conversation.
// Note: If you start relying on an attribute that is in
// `ATTRIBUTES_THAT_DONT_INVALIDATE_PROPS_CACHE`, remove it from that list.
private getProps(): ConversationType { private getProps(): ConversationType {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const color = this.getColor()!; const color = this.getColor()!;

View file

@ -1,13 +1,17 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// We use `for ... of` to deal with iterables in several places in this file.
/* eslint-disable no-restricted-syntax */
import { isNil, sortBy } from 'lodash'; import { isNil, sortBy } from 'lodash';
import PQueue from 'p-queue';
import * as log from './logging/log'; import * as log from './logging/log';
import { assert } from './util/assert'; import { assert } from './util/assert';
import { missingCaseError } from './util/missingCaseError'; import { missingCaseError } from './util/missingCaseError';
import { isNormalNumber } from './util/isNormalNumber'; import { isNormalNumber } from './util/isNormalNumber';
import { map, take } from './util/iterables'; import { take } from './util/iterables';
import { isOlderThan } from './util/timestamp'; import { isOlderThan } from './util/timestamp';
import { ConversationModel } from './models/conversations'; import { ConversationModel } from './models/conversations';
@ -52,23 +56,37 @@ export async function routineProfileRefresh({
let totalCount = 0; let totalCount = 0;
let successCount = 0; let successCount = 0;
await Promise.all(
map(conversationsToRefresh, async (conversation: ConversationModel) => { async function refreshConversation(
totalCount += 1; conversation: ConversationModel
try { ): Promise<void> {
await conversation.getProfile( window.log.info(
conversation.get('uuid'), `routineProfileRefresh: refreshing profile for ${conversation.idForLogging()}`
conversation.get('e164') );
);
successCount += 1; totalCount += 1;
} catch (err) { try {
window.log.error( await conversation.getProfile(
'routineProfileRefresh: failed to fetch a profile', conversation.get('uuid'),
err?.stack || err conversation.get('e164')
); );
} window.log.info(
}) `routineProfileRefresh: refreshed profile for ${conversation.idForLogging()}`
); );
successCount += 1;
} catch (err) {
window.log.error(
`routineProfileRefresh: refreshed profile for ${conversation.idForLogging()}`,
err?.stack || err
);
}
}
const refreshQueue = new PQueue({ concurrency: 5, timeout: 1000 * 60 * 2 });
for (const conversation of conversationsToRefresh) {
refreshQueue.add(() => refreshConversation(conversation));
}
await refreshQueue.onIdle();
log.info( log.info(
`routineProfileRefresh: successfully refreshed ${successCount} out of ${totalCount} conversation(s)` `routineProfileRefresh: successfully refreshed ${successCount} out of ${totalCount} conversation(s)`
@ -112,9 +130,6 @@ function* getFilteredConversations(
const conversationIdsSeen = new Set<string>([ourConversationId]); const conversationIdsSeen = new Set<string>([ourConversationId]);
// We use a `for` loop (instead of something like `forEach`) because we want to be able
// to yield. We use `for ... of` for readability.
// eslint-disable-next-line no-restricted-syntax
for (const conversation of sorted) { for (const conversation of sorted) {
const type = conversation.get('type'); const type = conversation.get('type');
switch (type) { switch (type) {
@ -129,7 +144,6 @@ function* getFilteredConversations(
} }
break; break;
case 'group': case 'group':
// eslint-disable-next-line no-restricted-syntax
for (const member of conversation.getMembers()) { for (const member of conversation.getMembers()) {
if ( if (
!conversationIdsSeen.has(member.id) && !conversationIdsSeen.has(member.id) &&

View file

@ -1410,8 +1410,9 @@ export function reducer(
let { showArchived } = state; let { showArchived } = state;
const existing = conversationLookup[id]; const existing = conversationLookup[id];
// In the change case we only modify the lookup if we already had that conversation // We only modify the lookup if we already had that conversation and the conversation
if (!existing) { // changed.
if (!existing || data === existing) {
return state; return state;
} }