From 61b7eebfcf410b3edef21cf115de251ebf8eec7c Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Wed, 11 Aug 2021 12:29:07 -0700 Subject: [PATCH] Modernize Benchmarks CI --- .github/workflows/benchmark.yml | 27 ++++++- app/user_config.ts | 6 +- main.js | 27 ++----- preload.js | 14 ++-- ts/CI.ts | 77 +++++++++++++++++++ ts/components/CompositionInput.tsx | 4 +- ts/components/conversation/Message.tsx | 4 + .../BaseConversationListItem.tsx | 4 +- .../conversationList/ContactCheckbox.tsx | 4 +- .../conversationList/ContactListItem.tsx | 4 +- .../conversationList/ConversationListItem.tsx | 4 +- .../conversationList/CreateNewGroupButton.tsx | 2 +- .../conversationList/MessageSearchResult.tsx | 4 +- .../conversationList/StartNewConversation.tsx | 2 +- ts/textsecure/AccountManager.ts | 4 +- ts/window.d.ts | 8 +- webpack-preload.config.ts | 12 ++- 17 files changed, 149 insertions(+), 58 deletions(-) create mode 100644 ts/CI.ts diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 1adbd7122e5d..996f5fbbbf43 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -23,6 +23,7 @@ jobs: with: repository: 'signalapp/Mock-Signal-Server-Private' path: 'Mock-Server' + ref: 'alpha' token: ${{ secrets.AUTOMATED_GITHUB_PAT }} - name: Setup node.js @@ -56,21 +57,40 @@ jobs: run: yarn build:webpack - name: Copy CI configuration - run: cp -rf ./Mock-Server/config/local-development.json ./config/ + run: | + cp -rf ./Mock-Server/config/local-development.json \ + ./config/local-development.json + cp -rf ./config/local-development.json ./config/local-production.json - name: Setup hosts run: sudo echo "127.0.0.1 mock.signal.org" | sudo tee -a /etc/hosts - - name: Run benchmarks + - name: Run startup benchmarks run: | + set -o pipefail + rm -rf /tmp/mock xvfb-run --auto-servernum \ ts-node Mock-Server/scripts/load-test.ts ./node_modules/.bin/electron . | tee benchmark.log || \ - (cat /home/runner/.config/Signal-mock/logs/{app,main}.log && exit 1) + (cat /tmp/mock/logs/{app,main}.log && exit 1) timeout-minutes: 10 env: + NODE_ENV: production RUN_COUNT: 10 ELECTRON_ENABLE_STACK_DUMPING: on + - name: Run send benchmarks + run: | + set -o pipefail + rm -rf /tmp/mock + xvfb-run --auto-servernum \ + ts-node Mock-Server/scripts/send-test.ts ./node_modules/.bin/electron . | tee send-benchmark.log || \ + (cat /tmp/mock/logs/{app,main}.log && exit 1) + timeout-minutes: 10 + env: + NODE_ENV: production + RUN_COUNT: 100 + ELECTRON_ENABLE_STACK_DUMPING: on + - name: Clone benchmark branch uses: actions/checkout@v2 with: @@ -83,6 +103,7 @@ jobs: run: | npm ci node ./bin/collect.js ../benchmark.log data.json + node ./bin/collect.js ../send-benchmark.log send-data.json npm run build git config --global user.email "no-reply@signal.org" git config --global user.name "Signal Bot" diff --git a/app/user_config.ts b/app/user_config.ts index edc69cc9621b..ca128a6d2ae9 100644 --- a/app/user_config.ts +++ b/app/user_config.ts @@ -7,8 +7,10 @@ import { app } from 'electron'; import { start } from './base_config'; import config from './config'; -// Use separate data directory for development -if (config.has('storageProfile')) { +// Use separate data directory for benchmarks & development +if (config.has('storagePath')) { + app.setPath('userData', String(config.get('storagePath'))); +} else if (config.has('storageProfile')) { const userData = join( app.getPath('appData'), `Signal-${config.get('storageProfile')}` diff --git a/main.js b/main.js index 8194c5539d82..b55533a0115e 100644 --- a/main.js +++ b/main.js @@ -1172,19 +1172,14 @@ app.on('ready', async () => { console.log('Processed count:', processedCount); console.log('Messages per second:', messagesPerSec); - if (enableCI) { - console._log( - 'ci: app_loaded=%s', - JSON.stringify({ - loadTime, - sqlInitTime, - preloadTime, - connectTime, - processedCount, - messagesPerSec, - }) - ); - } + event.sender.send('ci:event', 'app-loaded', { + loadTime, + sqlInitTime, + preloadTime, + connectTime, + processedCount, + messagesPerSec, + }); }); const userDataPath = await getRealPath(app.getPath('userData')); @@ -1537,12 +1532,6 @@ app.on('will-finish-launching', () => { }); }); -if (enableCI) { - ipc.on('set-provisioning-url', (event, provisioningURL) => { - console._log('ci: provisioning_url=%j', provisioningURL); - }); -} - ipc.on('set-badge-count', (event, count) => { app.badgeCount = count; }); diff --git a/preload.js b/preload.js index 57cc63692df1..13ba15a5b6a3 100644 --- a/preload.js +++ b/preload.js @@ -582,12 +582,12 @@ try { getRegionCode: () => window.storage.get('regionCode'), logger: window.log, }); - window.CI = config.enableCI - ? { - setProvisioningURL: url => ipc.send('set-provisioning-url', url), - deviceName: title, - } - : undefined; + + if (config.enableCI) { + const { CI, electronRequire } = require('./ts/CI'); + window.CI = new CI(title); + window.electronRequire = electronRequire; + } // these need access to window.Signal: require('./ts/models/messages'); @@ -616,7 +616,7 @@ try { }); if (config.environment === 'test') { - require('./preload_test.js'); + require('./preload_test'); } } catch (error) { /* eslint-disable no-console */ diff --git a/ts/CI.ts b/ts/CI.ts new file mode 100644 index 000000000000..189649ca1f4e --- /dev/null +++ b/ts/CI.ts @@ -0,0 +1,77 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { ipcRenderer } from 'electron'; + +import { explodePromise } from './util/explodePromise'; + +type ResolveType = (data: unknown) => void; + +export const electronRequire = require; + +export class CI { + private readonly eventListeners = new Map>(); + + private readonly completedEvents = new Map>(); + + constructor(public readonly deviceName: string) { + ipcRenderer.on('ci:event', (_, event, data) => { + this.handleEvent(event, data); + }); + } + + public async waitForEvent(event: string): Promise { + const pendingCompleted = this.completedEvents.get(event) || []; + const pending = pendingCompleted.shift(); + if (pending) { + window.log.info(`CI: resolving pending result for ${event}`, pending); + + if (pendingCompleted.length === 0) { + this.completedEvents.delete(event); + } + + return pending; + } + + window.log.info(`CI: waiting for event ${event}`); + const { resolve, promise } = explodePromise(); + + let list = this.eventListeners.get(event); + if (!list) { + list = []; + this.eventListeners.set(event, list); + } + + list.push(resolve); + + return promise; + } + + public setProvisioningURL(url: string): void { + this.handleEvent('provisioning-url', url); + } + + public handleEvent(event: string, data: unknown): void { + const list = this.eventListeners.get(event) || []; + const resolve = list.shift(); + + if (resolve) { + if (list.length === 0) { + this.eventListeners.delete(event); + } + + window.log.info(`CI: got event ${event} with data`, data); + resolve(data); + return; + } + + window.log.info(`CI: postponing event ${event}`); + + let resultList = this.completedEvents.get(event); + if (!resultList) { + resultList = []; + this.completedEvents.set(event, resultList); + } + resultList.push(data); + } +} diff --git a/ts/components/CompositionInput.tsx b/ts/components/CompositionInput.tsx index 604b9a066adb..ab3e69e0e51c 100644 --- a/ts/components/CompositionInput.tsx +++ b/ts/components/CompositionInput.tsx @@ -86,7 +86,7 @@ export type Props = { const MAX_LENGTH = 64 * 1024; const BASE_CLASS_NAME = 'module-composition-input'; -export const CompositionInput: React.ComponentType = props => { +export function CompositionInput(props: Props): React.ReactElement { const { i18n, disabled, @@ -644,4 +644,4 @@ export const CompositionInput: React.ComponentType = props => { ); -}; +} diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 3ba2cc269ff8..3d9f7321f548 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -478,6 +478,10 @@ export class Message extends React.PureComponent { status === 'viewed') ) { const delta = Date.now() - timestamp; + window.CI?.handleEvent('message:send-complete', { + timestamp, + delta, + }); window.log.info( `Message.tsx: Rendered 'send complete' for message ${timestamp}; took ${delta}ms` ); diff --git a/ts/components/conversationList/BaseConversationListItem.tsx b/ts/components/conversationList/BaseConversationListItem.tsx index fe6f8d7a449a..5e583900a860 100644 --- a/ts/components/conversationList/BaseConversationListItem.tsx +++ b/ts/components/conversationList/BaseConversationListItem.tsx @@ -54,7 +54,7 @@ type PropsType = { >; export const BaseConversationListItem: FunctionComponent = React.memo( - ({ + function BaseConversationListItem({ acceptedMessageRequest, avatarPath, checked, @@ -79,7 +79,7 @@ export const BaseConversationListItem: FunctionComponent = React.memo title, unblurredAvatarPath, unreadCount, - }) => { + }) { const isUnread = isConversationUnread({ markedUnread, unreadCount }); const isAvatarNoteToSelf = isBoolean(isNoteToSelf) diff --git a/ts/components/conversationList/ContactCheckbox.tsx b/ts/components/conversationList/ContactCheckbox.tsx index 41196c4dfc54..50255f16deef 100644 --- a/ts/components/conversationList/ContactCheckbox.tsx +++ b/ts/components/conversationList/ContactCheckbox.tsx @@ -47,7 +47,7 @@ type PropsHousekeepingType = { type PropsType = PropsDataType & PropsHousekeepingType; export const ContactCheckbox: FunctionComponent = React.memo( - ({ + function ContactCheckbox({ about, acceptedMessageRequest, avatarPath, @@ -65,7 +65,7 @@ export const ContactCheckbox: FunctionComponent = React.memo( title, type, unblurredAvatarPath, - }) => { + }) { const disabled = Boolean(disabledReason); const headerName = isMe ? ( diff --git a/ts/components/conversationList/ContactListItem.tsx b/ts/components/conversationList/ContactListItem.tsx index 0a4f5f1118fe..65050aabb515 100644 --- a/ts/components/conversationList/ContactListItem.tsx +++ b/ts/components/conversationList/ContactListItem.tsx @@ -34,7 +34,7 @@ type PropsHousekeepingType = { type PropsType = PropsDataType & PropsHousekeepingType; export const ContactListItem: FunctionComponent = React.memo( - ({ + function ContactListItem({ about, acceptedMessageRequest, avatarPath, @@ -50,7 +50,7 @@ export const ContactListItem: FunctionComponent = React.memo( title, type, unblurredAvatarPath, - }) => { + }) { const headerName = isMe ? ( i18n('noteToSelf') ) : ( diff --git a/ts/components/conversationList/ConversationListItem.tsx b/ts/components/conversationList/ConversationListItem.tsx index 4bf4b6f0e6a4..b4548d253f96 100644 --- a/ts/components/conversationList/ConversationListItem.tsx +++ b/ts/components/conversationList/ConversationListItem.tsx @@ -63,7 +63,7 @@ type PropsHousekeeping = { export type Props = PropsData & PropsHousekeeping; export const ConversationListItem: FunctionComponent = React.memo( - ({ + function ConversationListItem({ acceptedMessageRequest, avatarPath, color, @@ -87,7 +87,7 @@ export const ConversationListItem: FunctionComponent = React.memo( typingContact, unblurredAvatarPath, unreadCount, - }) => { + }) { const headerName = isMe ? ( i18n('noteToSelf') ) : ( diff --git a/ts/components/conversationList/CreateNewGroupButton.tsx b/ts/components/conversationList/CreateNewGroupButton.tsx index defcd77cd885..f48c957da6ce 100644 --- a/ts/components/conversationList/CreateNewGroupButton.tsx +++ b/ts/components/conversationList/CreateNewGroupButton.tsx @@ -12,7 +12,7 @@ type PropsType = { }; export const CreateNewGroupButton: FunctionComponent = React.memo( - ({ i18n, onClick }) => { + function CreateNewGroupButton({ i18n, onClick }) { const title = i18n('createNewGroupButton'); return ( diff --git a/ts/components/conversationList/MessageSearchResult.tsx b/ts/components/conversationList/MessageSearchResult.tsx index fd2a741e026a..c322c2ce2013 100644 --- a/ts/components/conversationList/MessageSearchResult.tsx +++ b/ts/components/conversationList/MessageSearchResult.tsx @@ -143,7 +143,7 @@ function getFilteredBodyRanges( } export const MessageSearchResult: FunctionComponent = React.memo( - ({ + function MessageSearchResult({ body, bodyRanges, conversationId, @@ -154,7 +154,7 @@ export const MessageSearchResult: FunctionComponent = React.memo( sentAt, snippet, to, - }) => { + }) { const onClickItem = useCallback(() => { openConversationInternal({ conversationId, messageId: id }); }, [openConversationInternal, conversationId, id]); diff --git a/ts/components/conversationList/StartNewConversation.tsx b/ts/components/conversationList/StartNewConversation.tsx index 2c186744d716..29ab0a375bc3 100644 --- a/ts/components/conversationList/StartNewConversation.tsx +++ b/ts/components/conversationList/StartNewConversation.tsx @@ -25,7 +25,7 @@ type PropsHousekeeping = { export type Props = PropsData & PropsHousekeeping; export const StartNewConversation: FunctionComponent = React.memo( - ({ i18n, onClick, phoneNumber }) => { + function StartNewConversation({ i18n, onClick, phoneNumber }) { const messageText = (
{i18n('startConversation')}
); diff --git a/ts/textsecure/AccountManager.ts b/ts/textsecure/AccountManager.ts index 1899857a0d7c..021d86d39b78 100644 --- a/ts/textsecure/AccountManager.ts +++ b/ts/textsecure/AccountManager.ts @@ -222,9 +222,7 @@ export default class AccountManager extends EventTarget { } const url = getProvisioningUrl(uuid, pubKey); - if (window.CI) { - window.CI.setProvisioningURL(url); - } + window.CI?.setProvisioningURL(url); setProvisioningUrl(url); request.respond(200, 'OK'); diff --git a/ts/window.d.ts b/ts/window.d.ts index 706b258a83af..34a3d2e87817 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -117,6 +117,7 @@ import { MessageController } from './util/MessageController'; import { isValidGuid } from './util/isValidGuid'; import { StateType } from './state/reducer'; import { SystemTraySetting } from './types/SystemTraySetting'; +import { CI } from './CI'; export { Long } from 'long'; @@ -277,12 +278,7 @@ declare global { }; Backbone: typeof Backbone; - CI: - | { - setProvisioningURL: (url: string) => void; - deviceName: string; - } - | undefined; + CI?: CI; Accessibility: { reducedMotionSetting: boolean; }; diff --git a/webpack-preload.config.ts b/webpack-preload.config.ts index 488412b31543..1123af2bfab1 100644 --- a/webpack-preload.config.ts +++ b/webpack-preload.config.ts @@ -31,7 +31,10 @@ const EXTERNAL_MODULE = new Set([ 'zkgroup', // Uses fast-glob and dynamic requires - './preload_test.js', + './preload_test', + + // Needs to exports `electronRequire` + './ts/CI', ]); const preloadConfig: Configuration = { @@ -43,10 +46,11 @@ const preloadConfig: Configuration = { optimization: { minimizer: [ new TerserPlugin({ + parallel: true, terserOptions: { - mangle: { - keep_fnames: true, - }, + mangle: false, + keep_classnames: true, + keep_fnames: true, }, }), ],