Fix CallsTab infinite scroll loading and add benchmark

This commit is contained in:
Jamie Kyle 2024-09-09 11:59:43 -07:00 committed by GitHub
parent df2c32be83
commit 534a625235
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 227 additions and 1 deletions

View File

@ -155,6 +155,21 @@ jobs:
# DEBUG: 'mock:benchmarks'
ARTIFACTS_DIR: artifacts/convo-open
- name: Run call history search benchmarks
run: |
set -o pipefail
rm -rf /tmp/mock
xvfb-run --auto-servernum node \
ts/test-mock/benchmarks/call_history_search_bench.js | \
tee benchmark-call-history-search.log
timeout-minutes: 10
env:
NODE_ENV: production
RUN_COUNT: 100
ELECTRON_ENABLE_STACK_DUMPING: on
# DEBUG: 'mock:benchmarks'
ARTIFACTS_DIR: artifacts/call-history-search
- name: Upload benchmark logs on failure
if: failure()
uses: actions/upload-artifact@v4
@ -184,5 +199,6 @@ jobs:
node ./bin/publish.js ../benchmark-large-group-send-with-blocks.log desktop.ci.performance.largeGroupSendWithBlocks
node ./bin/publish.js ../benchmark-large-group-send.log desktop.ci.performance.largeGroupSend
node ./bin/publish.js ../benchmark-convo-open.log desktop.ci.performance.convoOpen
node ./bin/publish.js ../benchmark-call-history-search.log desktop.ci.performance.callHistorySearch
env:
DD_API_KEY: ${{ secrets.DATADOG_API_KEY }}

View File

@ -980,7 +980,7 @@ export function CallsList({
ref={infiniteLoaderRef}
isRowLoaded={isRowLoaded}
loadMoreRows={loadMoreRows}
rowCount={rowCount}
rowCount={searchState.results?.count ?? Infinity}
minimumBatchSize={100}
threshold={30}
>

View File

@ -0,0 +1,210 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { PrimaryDevice } from '@signalapp/mock-server';
import { Proto, StorageState } from '@signalapp/mock-server';
import Long from 'long';
import { sample } from 'lodash';
import { expect } from 'playwright/test';
import { Bootstrap, debug, RUN_COUNT, DISCARD_COUNT } from './fixtures';
import { stats } from '../../util/benchmark/stats';
import { uuidToBytes } from '../../util/uuidToBytes';
import { strictAssert } from '../../util/assert';
import { typeIntoInput } from '../helpers';
const CALL_HISTORY_COUNT = 1000;
function rand<T>(values: ReadonlyArray<T>): T {
const value = sample(values);
strictAssert(value != null, 'must not be null');
return value;
}
const { CallEvent } = Proto.SyncMessage;
const { Type, Direction, Event } = CallEvent;
const Types = [Type.AUDIO_CALL, Type.VIDEO_CALL];
const Directions = [Direction.INCOMING, Direction.OUTGOING];
const Events = [Event.ACCEPTED, Event.NOT_ACCEPTED];
Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise<void> => {
const { server, contacts, phone } = bootstrap;
let state = StorageState.getEmpty();
state = state.updateAccount({
profileKey: phone.profileKey.serialize(),
e164: phone.device.number,
givenName: phone.profileName,
readReceipts: true,
hasCompletedUsernameOnboarding: true,
});
debug('accepting all contacts');
for (const contact of contacts) {
state = state.addContact(contact, {
identityKey: contact.publicKey.serialize(),
profileKey: contact.profileKey.serialize(),
whitelisted: true,
});
}
await phone.setStorageState(state);
debug('linking');
const app = await bootstrap.link();
const { desktop } = bootstrap;
debug('sending messages from all contacts');
await Promise.all(
contacts.map(async contact => {
const timestamp = bootstrap.getTimestamp();
await server.send(
desktop,
await contact.encryptText(
desktop,
`hello from: ${contact.profileName}`,
{ timestamp, sealed: true }
)
);
await server.send(
desktop,
await phone.encryptSyncRead(desktop, {
timestamp: bootstrap.getTimestamp(),
messages: [
{
senderAci: contact.device.aci,
timestamp,
},
],
})
);
})
);
async function sendCallEventSync(
contact: PrimaryDevice,
type: Proto.SyncMessage.CallEvent.Type,
direction: Proto.SyncMessage.CallEvent.Direction,
event: Proto.SyncMessage.CallEvent.Event,
timestamp: number
) {
await phone.sendRaw(
desktop,
{
syncMessage: {
callEvent: {
peerId: uuidToBytes(contact.device.aci),
callId: Long.fromNumber(timestamp),
timestamp: Long.fromNumber(timestamp),
type,
direction,
event,
},
},
},
{ timestamp }
);
}
debug('sending initial call events');
let unreadCount = 0;
await Promise.all(
Array.from({ length: CALL_HISTORY_COUNT }, () => {
const contact = rand(contacts);
const type = rand(Types);
const direction = rand(Directions);
const event = rand(Events);
const timestamp = bootstrap.getTimestamp();
if (
direction === Proto.SyncMessage.CallEvent.Direction.INCOMING &&
event === Proto.SyncMessage.CallEvent.Event.NOT_ACCEPTED
) {
unreadCount += 1;
}
return sendCallEventSync(contact, type, direction, event, timestamp);
})
);
const window = await app.getWindow();
const CallsNavTab = window.getByTestId('NavTabsItem--Calls');
const CallsNavTabUnread = CallsNavTab.locator('.NavTabs__ItemUnreadBadge');
const CallsTabSidebar = window.locator('.CallsTab .NavSidebar');
const SearchBar = CallsTabSidebar.locator('.module-SearchInput__input');
const CallListItem = CallsTabSidebar.locator('.CallsList__ItemTile');
const CreateCallLink = CallListItem.filter({ hasText: 'Create a Call Link' });
const CallsTabDetails = window.locator('.CallsTab__ConversationCallDetails');
const CallsTabDetailsTitle = CallsTabDetails.locator(
'.ConversationDetailsHeader__title'
);
debug('waiting for unread badge to hit correct value', unreadCount);
await CallsNavTabUnread.getByText(`${unreadCount} unread`).waitFor();
debug('opening calls tab');
await CallsNavTab.click();
async function measure(runId: number): Promise<number> {
// setup
const searchContact = contacts[runId % contacts.length];
const OtherCallListItems = CallListItem.filter({
hasNotText: searchContact.profileName,
});
const timestamp = bootstrap.getTimestamp();
const NewCallListItemTime = window.locator(
`.CallsList__ItemCallInfo time[datetime="${new Date(timestamp).toISOString()}"]`
);
const NewCallListItem = CallListItem.filter({
has: NewCallListItemTime,
});
const NewCallDetailsTitle = CallsTabDetailsTitle.filter({
hasText: searchContact.profileName,
});
// measure
const start = Date.now();
// test
await typeIntoInput(SearchBar, searchContact.profileName);
await CreateCallLink.waitFor({ state: 'hidden' }); // hides when searching
await expect(OtherCallListItems).not.toBeAttached();
await sendCallEventSync(
searchContact,
Type.AUDIO_CALL,
Direction.INCOMING,
Event.ACCEPTED,
timestamp
);
await NewCallListItem.click();
await NewCallDetailsTitle.waitFor();
await SearchBar.clear();
await CreateCallLink.waitFor();
// measure
const end = Date.now();
const delta = end - start;
return delta;
}
const deltaList = new Array<number>();
for (let runId = 0; runId < RUN_COUNT + DISCARD_COUNT; runId += 1) {
// eslint-disable-next-line no-await-in-loop
const delta = await measure(runId);
if (runId >= DISCARD_COUNT) {
deltaList.push(delta);
// eslint-disable-next-line no-console
console.log('run=%d info=%j', runId - DISCARD_COUNT, { delta });
} else {
// eslint-disable-next-line no-console
console.log('discarded=%d info=%j', runId, { delta });
}
}
// eslint-disable-next-line no-console
console.log('stats info=%j', { delta: stats(deltaList, [99, 99.8]) });
});