Fix CallsTab infinite scroll loading and add benchmark
This commit is contained in:
parent
df2c32be83
commit
534a625235
3 changed files with 227 additions and 1 deletions
16
.github/workflows/benchmark.yml
vendored
16
.github/workflows/benchmark.yml
vendored
|
@ -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 }}
|
||||
|
|
|
@ -980,7 +980,7 @@ export function CallsList({
|
|||
ref={infiniteLoaderRef}
|
||||
isRowLoaded={isRowLoaded}
|
||||
loadMoreRows={loadMoreRows}
|
||||
rowCount={rowCount}
|
||||
rowCount={searchState.results?.count ?? Infinity}
|
||||
minimumBatchSize={100}
|
||||
threshold={30}
|
||||
>
|
||||
|
|
210
ts/test-mock/benchmarks/call_history_search_bench.ts
Normal file
210
ts/test-mock/benchmarks/call_history_search_bench.ts
Normal 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]) });
|
||||
});
|
Loading…
Reference in a new issue