Modernize Benchmarks CI
This commit is contained in:
parent
12c78c742f
commit
61b7eebfcf
17 changed files with 149 additions and 58 deletions
27
.github/workflows/benchmark.yml
vendored
27
.github/workflows/benchmark.yml
vendored
|
@ -23,6 +23,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
repository: 'signalapp/Mock-Signal-Server-Private'
|
repository: 'signalapp/Mock-Signal-Server-Private'
|
||||||
path: 'Mock-Server'
|
path: 'Mock-Server'
|
||||||
|
ref: 'alpha'
|
||||||
token: ${{ secrets.AUTOMATED_GITHUB_PAT }}
|
token: ${{ secrets.AUTOMATED_GITHUB_PAT }}
|
||||||
|
|
||||||
- name: Setup node.js
|
- name: Setup node.js
|
||||||
|
@ -56,21 +57,40 @@ jobs:
|
||||||
run: yarn build:webpack
|
run: yarn build:webpack
|
||||||
|
|
||||||
- name: Copy CI configuration
|
- 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
|
- name: Setup hosts
|
||||||
run: sudo echo "127.0.0.1 mock.signal.org" | sudo tee -a /etc/hosts
|
run: sudo echo "127.0.0.1 mock.signal.org" | sudo tee -a /etc/hosts
|
||||||
|
|
||||||
- name: Run benchmarks
|
- name: Run startup benchmarks
|
||||||
run: |
|
run: |
|
||||||
|
set -o pipefail
|
||||||
|
rm -rf /tmp/mock
|
||||||
xvfb-run --auto-servernum \
|
xvfb-run --auto-servernum \
|
||||||
ts-node Mock-Server/scripts/load-test.ts ./node_modules/.bin/electron . | tee benchmark.log || \
|
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
|
timeout-minutes: 10
|
||||||
env:
|
env:
|
||||||
|
NODE_ENV: production
|
||||||
RUN_COUNT: 10
|
RUN_COUNT: 10
|
||||||
ELECTRON_ENABLE_STACK_DUMPING: on
|
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
|
- name: Clone benchmark branch
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
|
@ -83,6 +103,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
npm ci
|
||||||
node ./bin/collect.js ../benchmark.log data.json
|
node ./bin/collect.js ../benchmark.log data.json
|
||||||
|
node ./bin/collect.js ../send-benchmark.log send-data.json
|
||||||
npm run build
|
npm run build
|
||||||
git config --global user.email "no-reply@signal.org"
|
git config --global user.email "no-reply@signal.org"
|
||||||
git config --global user.name "Signal Bot"
|
git config --global user.name "Signal Bot"
|
||||||
|
|
|
@ -7,8 +7,10 @@ import { app } from 'electron';
|
||||||
import { start } from './base_config';
|
import { start } from './base_config';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
|
|
||||||
// Use separate data directory for development
|
// Use separate data directory for benchmarks & development
|
||||||
if (config.has('storageProfile')) {
|
if (config.has('storagePath')) {
|
||||||
|
app.setPath('userData', String(config.get('storagePath')));
|
||||||
|
} else if (config.has('storageProfile')) {
|
||||||
const userData = join(
|
const userData = join(
|
||||||
app.getPath('appData'),
|
app.getPath('appData'),
|
||||||
`Signal-${config.get('storageProfile')}`
|
`Signal-${config.get('storageProfile')}`
|
||||||
|
|
15
main.js
15
main.js
|
@ -1172,19 +1172,14 @@ app.on('ready', async () => {
|
||||||
console.log('Processed count:', processedCount);
|
console.log('Processed count:', processedCount);
|
||||||
console.log('Messages per second:', messagesPerSec);
|
console.log('Messages per second:', messagesPerSec);
|
||||||
|
|
||||||
if (enableCI) {
|
event.sender.send('ci:event', 'app-loaded', {
|
||||||
console._log(
|
|
||||||
'ci: app_loaded=%s',
|
|
||||||
JSON.stringify({
|
|
||||||
loadTime,
|
loadTime,
|
||||||
sqlInitTime,
|
sqlInitTime,
|
||||||
preloadTime,
|
preloadTime,
|
||||||
connectTime,
|
connectTime,
|
||||||
processedCount,
|
processedCount,
|
||||||
messagesPerSec,
|
messagesPerSec,
|
||||||
})
|
});
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const userDataPath = await getRealPath(app.getPath('userData'));
|
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) => {
|
ipc.on('set-badge-count', (event, count) => {
|
||||||
app.badgeCount = count;
|
app.badgeCount = count;
|
||||||
});
|
});
|
||||||
|
|
12
preload.js
12
preload.js
|
@ -582,12 +582,12 @@ try {
|
||||||
getRegionCode: () => window.storage.get('regionCode'),
|
getRegionCode: () => window.storage.get('regionCode'),
|
||||||
logger: window.log,
|
logger: window.log,
|
||||||
});
|
});
|
||||||
window.CI = config.enableCI
|
|
||||||
? {
|
if (config.enableCI) {
|
||||||
setProvisioningURL: url => ipc.send('set-provisioning-url', url),
|
const { CI, electronRequire } = require('./ts/CI');
|
||||||
deviceName: title,
|
window.CI = new CI(title);
|
||||||
|
window.electronRequire = electronRequire;
|
||||||
}
|
}
|
||||||
: undefined;
|
|
||||||
|
|
||||||
// these need access to window.Signal:
|
// these need access to window.Signal:
|
||||||
require('./ts/models/messages');
|
require('./ts/models/messages');
|
||||||
|
@ -616,7 +616,7 @@ try {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (config.environment === 'test') {
|
if (config.environment === 'test') {
|
||||||
require('./preload_test.js');
|
require('./preload_test');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
|
|
77
ts/CI.ts
Normal file
77
ts/CI.ts
Normal file
|
@ -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<string, Array<ResolveType>>();
|
||||||
|
|
||||||
|
private readonly completedEvents = new Map<string, Array<unknown>>();
|
||||||
|
|
||||||
|
constructor(public readonly deviceName: string) {
|
||||||
|
ipcRenderer.on('ci:event', (_, event, data) => {
|
||||||
|
this.handleEvent(event, data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async waitForEvent(event: string): Promise<unknown> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -86,7 +86,7 @@ export type Props = {
|
||||||
const MAX_LENGTH = 64 * 1024;
|
const MAX_LENGTH = 64 * 1024;
|
||||||
const BASE_CLASS_NAME = 'module-composition-input';
|
const BASE_CLASS_NAME = 'module-composition-input';
|
||||||
|
|
||||||
export const CompositionInput: React.ComponentType<Props> = props => {
|
export function CompositionInput(props: Props): React.ReactElement {
|
||||||
const {
|
const {
|
||||||
i18n,
|
i18n,
|
||||||
disabled,
|
disabled,
|
||||||
|
@ -644,4 +644,4 @@ export const CompositionInput: React.ComponentType<Props> = props => {
|
||||||
</Reference>
|
</Reference>
|
||||||
</Manager>
|
</Manager>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
|
@ -478,6 +478,10 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
status === 'viewed')
|
status === 'viewed')
|
||||||
) {
|
) {
|
||||||
const delta = Date.now() - timestamp;
|
const delta = Date.now() - timestamp;
|
||||||
|
window.CI?.handleEvent('message:send-complete', {
|
||||||
|
timestamp,
|
||||||
|
delta,
|
||||||
|
});
|
||||||
window.log.info(
|
window.log.info(
|
||||||
`Message.tsx: Rendered 'send complete' for message ${timestamp}; took ${delta}ms`
|
`Message.tsx: Rendered 'send complete' for message ${timestamp}; took ${delta}ms`
|
||||||
);
|
);
|
||||||
|
|
|
@ -54,7 +54,7 @@ type PropsType = {
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo(
|
export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo(
|
||||||
({
|
function BaseConversationListItem({
|
||||||
acceptedMessageRequest,
|
acceptedMessageRequest,
|
||||||
avatarPath,
|
avatarPath,
|
||||||
checked,
|
checked,
|
||||||
|
@ -79,7 +79,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo
|
||||||
title,
|
title,
|
||||||
unblurredAvatarPath,
|
unblurredAvatarPath,
|
||||||
unreadCount,
|
unreadCount,
|
||||||
}) => {
|
}) {
|
||||||
const isUnread = isConversationUnread({ markedUnread, unreadCount });
|
const isUnread = isConversationUnread({ markedUnread, unreadCount });
|
||||||
|
|
||||||
const isAvatarNoteToSelf = isBoolean(isNoteToSelf)
|
const isAvatarNoteToSelf = isBoolean(isNoteToSelf)
|
||||||
|
|
|
@ -47,7 +47,7 @@ type PropsHousekeepingType = {
|
||||||
type PropsType = PropsDataType & PropsHousekeepingType;
|
type PropsType = PropsDataType & PropsHousekeepingType;
|
||||||
|
|
||||||
export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
|
export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
|
||||||
({
|
function ContactCheckbox({
|
||||||
about,
|
about,
|
||||||
acceptedMessageRequest,
|
acceptedMessageRequest,
|
||||||
avatarPath,
|
avatarPath,
|
||||||
|
@ -65,7 +65,7 @@ export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
|
||||||
title,
|
title,
|
||||||
type,
|
type,
|
||||||
unblurredAvatarPath,
|
unblurredAvatarPath,
|
||||||
}) => {
|
}) {
|
||||||
const disabled = Boolean(disabledReason);
|
const disabled = Boolean(disabledReason);
|
||||||
|
|
||||||
const headerName = isMe ? (
|
const headerName = isMe ? (
|
||||||
|
|
|
@ -34,7 +34,7 @@ type PropsHousekeepingType = {
|
||||||
type PropsType = PropsDataType & PropsHousekeepingType;
|
type PropsType = PropsDataType & PropsHousekeepingType;
|
||||||
|
|
||||||
export const ContactListItem: FunctionComponent<PropsType> = React.memo(
|
export const ContactListItem: FunctionComponent<PropsType> = React.memo(
|
||||||
({
|
function ContactListItem({
|
||||||
about,
|
about,
|
||||||
acceptedMessageRequest,
|
acceptedMessageRequest,
|
||||||
avatarPath,
|
avatarPath,
|
||||||
|
@ -50,7 +50,7 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
|
||||||
title,
|
title,
|
||||||
type,
|
type,
|
||||||
unblurredAvatarPath,
|
unblurredAvatarPath,
|
||||||
}) => {
|
}) {
|
||||||
const headerName = isMe ? (
|
const headerName = isMe ? (
|
||||||
i18n('noteToSelf')
|
i18n('noteToSelf')
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -63,7 +63,7 @@ type PropsHousekeeping = {
|
||||||
export type Props = PropsData & PropsHousekeeping;
|
export type Props = PropsData & PropsHousekeeping;
|
||||||
|
|
||||||
export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
||||||
({
|
function ConversationListItem({
|
||||||
acceptedMessageRequest,
|
acceptedMessageRequest,
|
||||||
avatarPath,
|
avatarPath,
|
||||||
color,
|
color,
|
||||||
|
@ -87,7 +87,7 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
||||||
typingContact,
|
typingContact,
|
||||||
unblurredAvatarPath,
|
unblurredAvatarPath,
|
||||||
unreadCount,
|
unreadCount,
|
||||||
}) => {
|
}) {
|
||||||
const headerName = isMe ? (
|
const headerName = isMe ? (
|
||||||
i18n('noteToSelf')
|
i18n('noteToSelf')
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -12,7 +12,7 @@ type PropsType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CreateNewGroupButton: FunctionComponent<PropsType> = React.memo(
|
export const CreateNewGroupButton: FunctionComponent<PropsType> = React.memo(
|
||||||
({ i18n, onClick }) => {
|
function CreateNewGroupButton({ i18n, onClick }) {
|
||||||
const title = i18n('createNewGroupButton');
|
const title = i18n('createNewGroupButton');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -143,7 +143,7 @@ function getFilteredBodyRanges(
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MessageSearchResult: FunctionComponent<PropsType> = React.memo(
|
export const MessageSearchResult: FunctionComponent<PropsType> = React.memo(
|
||||||
({
|
function MessageSearchResult({
|
||||||
body,
|
body,
|
||||||
bodyRanges,
|
bodyRanges,
|
||||||
conversationId,
|
conversationId,
|
||||||
|
@ -154,7 +154,7 @@ export const MessageSearchResult: FunctionComponent<PropsType> = React.memo(
|
||||||
sentAt,
|
sentAt,
|
||||||
snippet,
|
snippet,
|
||||||
to,
|
to,
|
||||||
}) => {
|
}) {
|
||||||
const onClickItem = useCallback(() => {
|
const onClickItem = useCallback(() => {
|
||||||
openConversationInternal({ conversationId, messageId: id });
|
openConversationInternal({ conversationId, messageId: id });
|
||||||
}, [openConversationInternal, conversationId, id]);
|
}, [openConversationInternal, conversationId, id]);
|
||||||
|
|
|
@ -25,7 +25,7 @@ type PropsHousekeeping = {
|
||||||
export type Props = PropsData & PropsHousekeeping;
|
export type Props = PropsData & PropsHousekeeping;
|
||||||
|
|
||||||
export const StartNewConversation: FunctionComponent<Props> = React.memo(
|
export const StartNewConversation: FunctionComponent<Props> = React.memo(
|
||||||
({ i18n, onClick, phoneNumber }) => {
|
function StartNewConversation({ i18n, onClick, phoneNumber }) {
|
||||||
const messageText = (
|
const messageText = (
|
||||||
<div className={TEXT_CLASS_NAME}>{i18n('startConversation')}</div>
|
<div className={TEXT_CLASS_NAME}>{i18n('startConversation')}</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -222,9 +222,7 @@ export default class AccountManager extends EventTarget {
|
||||||
}
|
}
|
||||||
const url = getProvisioningUrl(uuid, pubKey);
|
const url = getProvisioningUrl(uuid, pubKey);
|
||||||
|
|
||||||
if (window.CI) {
|
window.CI?.setProvisioningURL(url);
|
||||||
window.CI.setProvisioningURL(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
setProvisioningUrl(url);
|
setProvisioningUrl(url);
|
||||||
request.respond(200, 'OK');
|
request.respond(200, 'OK');
|
||||||
|
|
8
ts/window.d.ts
vendored
8
ts/window.d.ts
vendored
|
@ -117,6 +117,7 @@ import { MessageController } from './util/MessageController';
|
||||||
import { isValidGuid } from './util/isValidGuid';
|
import { isValidGuid } from './util/isValidGuid';
|
||||||
import { StateType } from './state/reducer';
|
import { StateType } from './state/reducer';
|
||||||
import { SystemTraySetting } from './types/SystemTraySetting';
|
import { SystemTraySetting } from './types/SystemTraySetting';
|
||||||
|
import { CI } from './CI';
|
||||||
|
|
||||||
export { Long } from 'long';
|
export { Long } from 'long';
|
||||||
|
|
||||||
|
@ -277,12 +278,7 @@ declare global {
|
||||||
};
|
};
|
||||||
|
|
||||||
Backbone: typeof Backbone;
|
Backbone: typeof Backbone;
|
||||||
CI:
|
CI?: CI;
|
||||||
| {
|
|
||||||
setProvisioningURL: (url: string) => void;
|
|
||||||
deviceName: string;
|
|
||||||
}
|
|
||||||
| undefined;
|
|
||||||
Accessibility: {
|
Accessibility: {
|
||||||
reducedMotionSetting: boolean;
|
reducedMotionSetting: boolean;
|
||||||
};
|
};
|
||||||
|
|
|
@ -31,7 +31,10 @@ const EXTERNAL_MODULE = new Set([
|
||||||
'zkgroup',
|
'zkgroup',
|
||||||
|
|
||||||
// Uses fast-glob and dynamic requires
|
// Uses fast-glob and dynamic requires
|
||||||
'./preload_test.js',
|
'./preload_test',
|
||||||
|
|
||||||
|
// Needs to exports `electronRequire`
|
||||||
|
'./ts/CI',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const preloadConfig: Configuration = {
|
const preloadConfig: Configuration = {
|
||||||
|
@ -43,11 +46,12 @@ const preloadConfig: Configuration = {
|
||||||
optimization: {
|
optimization: {
|
||||||
minimizer: [
|
minimizer: [
|
||||||
new TerserPlugin({
|
new TerserPlugin({
|
||||||
|
parallel: true,
|
||||||
terserOptions: {
|
terserOptions: {
|
||||||
mangle: {
|
mangle: false,
|
||||||
|
keep_classnames: true,
|
||||||
keep_fnames: true,
|
keep_fnames: true,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue