Improve UI performance for no-op profile fetches
This commit is contained in:
parent
e859fcd4b4
commit
95d404c70b
3 changed files with 52 additions and 24 deletions
|
@ -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()!;
|
||||||
|
|
|
@ -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) &&
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue