Add basic performance testing via Cypress (#8586)

This commit is contained in:
J. Ryan Stinnett 2022-05-17 15:38:45 +01:00 committed by GitHub
parent 83b3dfa341
commit c122c5cd3b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 169 additions and 18 deletions

View file

@ -88,6 +88,20 @@ jobs:
cypress/videos cypress/videos
cypress/synapselogs cypress/synapselogs
- name: Store benchmark result
if: github.ref == 'refs/heads/develop'
uses: matrix-org/github-action-benchmark@jsperfentry-1
with:
name: Cypress measurements
tool: 'jsperformanceentry'
output-file-path: cypress/performance/measurements.json
# The dashboard is available at https://matrix-org.github.io/matrix-react-sdk/cypress/bench/
benchmark-data-dir-path: cypress/bench
fail-on-alert: false
comment-on-alert: false
github-token: ${{ secrets.DEPLOY_GH_PAGES }}
auto-push: ${{ github.ref == 'refs/heads/develop' }}
app-tests: app-tests:
name: Element Web Integration Tests name: Element Web Integration Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest

View file

@ -40,20 +40,18 @@ jobs:
test/end-to-end-tests/synapse/installations/consent/homeserver.log test/end-to-end-tests/synapse/installations/consent/homeserver.log
retention-days: 14 retention-days: 14
- name: Download previous benchmark data
uses: actions/cache@v1
with:
path: ./cache
key: ${{ runner.os }}-benchmark
- name: Store benchmark result - name: Store benchmark result
if: github.ref == 'refs/heads/develop'
uses: matrix-org/github-action-benchmark@jsperfentry-1 uses: matrix-org/github-action-benchmark@jsperfentry-1
with: with:
tool: 'jsperformanceentry' tool: 'jsperformanceentry'
output-file-path: test/end-to-end-tests/performance-entries.json output-file-path: test/end-to-end-tests/performance-entries.json
# This is the default dashboard path. It's included here anyway to
# make the difference from the Cypress variant in
# `element-build-and-test.yaml` more obvious.
# The dashboard is available at https://matrix-org.github.io/matrix-react-sdk/dev/bench/
benchmark-data-dir-path: dev/bench
fail-on-alert: false fail-on-alert: false
comment-on-alert: false comment-on-alert: false
# Only temporary to monitor where failures occur
alert-comment-cc-users: '@gsouquet'
github-token: ${{ secrets.DEPLOY_GH_PAGES }} github-token: ${{ secrets.DEPLOY_GH_PAGES }}
auto-push: ${{ github.ref == 'refs/heads/develop' }} auto-push: ${{ github.ref == 'refs/heads/develop' }}

1
.gitignore vendored
View file

@ -27,3 +27,4 @@ package-lock.json
# These could have files in them but don't currently # These could have files in them but don't currently
# Cypress will still auto-create them though... # Cypress will still auto-create them though...
/cypress/fixtures /cypress/fixtures
/cypress/performance

8
cypress/global.d.ts vendored
View file

@ -18,6 +18,7 @@ import "matrix-js-sdk/src/@types/global";
import type { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client"; import type { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client";
import type { RoomMemberEvent } from "matrix-js-sdk/src/models/room-member"; import type { RoomMemberEvent } from "matrix-js-sdk/src/models/room-member";
import type { MatrixDispatcher } from "../src/dispatcher/dispatcher"; import type { MatrixDispatcher } from "../src/dispatcher/dispatcher";
import type PerformanceMonitor from "../src/performance";
declare global { declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace // eslint-disable-next-line @typescript-eslint/no-namespace
@ -27,6 +28,7 @@ declare global {
matrixClient?: MatrixClient; matrixClient?: MatrixClient;
}; };
mxDispatcher: MatrixDispatcher; mxDispatcher: MatrixDispatcher;
mxPerformanceMonitor: PerformanceMonitor;
beforeReload?: boolean; // for detecting reloads beforeReload?: boolean; // for detecting reloads
// Partial type for the matrix-js-sdk module, exported by browser-matrix // Partial type for the matrix-js-sdk module, exported by browser-matrix
matrixcs: { matrixcs: {
@ -38,7 +40,11 @@ declare global {
} }
interface Window { interface Window {
mxDispatcher: MatrixDispatcher; // to appease the MatrixDispatcher import // to appease the MatrixDispatcher import
mxDispatcher: MatrixDispatcher;
// to appease the PerformanceMonitor import
mxPerformanceMonitor: PerformanceMonitor;
mxPerformanceEntryNames: any;
} }
} }

View file

@ -42,11 +42,15 @@ describe("Registration", () => {
cy.get("#mx_RegistrationForm_username").type("alice"); cy.get("#mx_RegistrationForm_username").type("alice");
cy.get("#mx_RegistrationForm_password").type("totally a great password"); cy.get("#mx_RegistrationForm_password").type("totally a great password");
cy.get("#mx_RegistrationForm_passwordConfirm").type("totally a great password"); cy.get("#mx_RegistrationForm_passwordConfirm").type("totally a great password");
cy.startMeasuring("create-account");
cy.get(".mx_Login_submit").click(); cy.get(".mx_Login_submit").click();
cy.get(".mx_RegistrationEmailPromptDialog button.mx_Dialog_primary").click(); cy.get(".mx_RegistrationEmailPromptDialog button.mx_Dialog_primary").click();
cy.stopMeasuring("create-account");
cy.get(".mx_InteractiveAuthEntryComponents_termsPolicy input").click(); cy.get(".mx_InteractiveAuthEntryComponents_termsPolicy input").click();
cy.startMeasuring("from-submit-to-home");
cy.get(".mx_InteractiveAuthEntryComponents_termsSubmit").click(); cy.get(".mx_InteractiveAuthEntryComponents_termsSubmit").click();
cy.url().should('contain', '/#/home'); cy.url().should('contain', '/#/home');
cy.stopMeasuring("from-submit-to-home");
}); });
}); });

View file

@ -49,9 +49,11 @@ describe("Login", () => {
cy.get("#mx_LoginForm_username").type(username); cy.get("#mx_LoginForm_username").type(username);
cy.get("#mx_LoginForm_password").type(password); cy.get("#mx_LoginForm_password").type(password);
cy.startMeasuring("from-submit-to-home");
cy.get(".mx_Login_submit").click(); cy.get(".mx_Login_submit").click();
cy.url().should('contain', '/#/home'); cy.url().should('contain', '/#/home');
cy.stopMeasuring("from-submit-to-home");
}); });
}); });
}); });

View file

@ -54,10 +54,12 @@ describe("Create Room", () => {
// Fill room address // Fill room address
cy.get('[label="Room address"]').type("test-room-1"); cy.get('[label="Room address"]').type("test-room-1");
// Submit // Submit
cy.startMeasuring("from-submit-to-room");
cy.get(".mx_Dialog_primary").click(); cy.get(".mx_Dialog_primary").click();
}); });
cy.url().should("contain", "/#/room/#test-room-1:localhost"); cy.url().should("contain", "/#/room/#test-room-1:localhost");
cy.stopMeasuring("from-submit-to-room");
cy.get(".mx_RoomHeader_nametext").contains(name); cy.get(".mx_RoomHeader_nametext").contains(name);
cy.get(".mx_RoomHeader_topic").contains(topic); cy.get(".mx_RoomHeader_topic").contains(topic);
}); });

View file

@ -16,13 +16,15 @@ limitations under the License.
/// <reference types="cypress" /> /// <reference types="cypress" />
import { synapseDocker } from "./synapsedocker";
import PluginEvents = Cypress.PluginEvents; import PluginEvents = Cypress.PluginEvents;
import PluginConfigOptions = Cypress.PluginConfigOptions; import PluginConfigOptions = Cypress.PluginConfigOptions;
import { performance } from "./performance";
import { synapseDocker } from "./synapsedocker";
/** /**
* @type {Cypress.PluginConfig} * @type {Cypress.PluginConfig}
*/ */
export default function(on: PluginEvents, config: PluginConfigOptions) { export default function(on: PluginEvents, config: PluginConfigOptions) {
performance(on, config);
synapseDocker(on, config); synapseDocker(on, config);
} }

View file

@ -0,0 +1,47 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/// <reference types="cypress" />
import * as path from "path";
import * as fse from "fs-extra";
import PluginEvents = Cypress.PluginEvents;
import PluginConfigOptions = Cypress.PluginConfigOptions;
// This holds all the performance measurements throughout the run
let bufferedMeasurements: PerformanceEntry[] = [];
function addMeasurements(measurements: PerformanceEntry[]): void {
bufferedMeasurements = bufferedMeasurements.concat(measurements);
return null;
}
async function writeMeasurementsFile() {
try {
const measurementsPath = path.join("cypress", "performance", "measurements.json");
await fse.outputJSON(measurementsPath, bufferedMeasurements, {
spaces: 4,
});
} finally {
bufferedMeasurements = [];
}
}
export function performance(on: PluginEvents, config: PluginConfigOptions) {
on("task", { addMeasurements });
on("after:run", writeMeasurementsFile);
}

View file

@ -201,7 +201,7 @@ async function synapseStop(id: string): Promise<void> {
synapses.delete(id); synapses.delete(id);
console.log(`Stopped synapse id ${id}.`); console.log(`Stopped synapse id ${id}.`);
// cypres deliberately fails if you return 'undefined', so // cypress deliberately fails if you return 'undefined', so
// return null to signal all is well and we've handled the task. // return null to signal all is well and we've handled the task.
return null; return null;
} }

View file

@ -40,7 +40,7 @@ Cypress.Commands.add("getBot", (synapse: SynapseInstance, displayName?: string):
const username = Cypress._.uniqueId("userId_"); const username = Cypress._.uniqueId("userId_");
const password = Cypress._.uniqueId("password_"); const password = Cypress._.uniqueId("password_");
return cy.registerUser(synapse, username, password, displayName).then(credentials => { return cy.registerUser(synapse, username, password, displayName).then(credentials => {
return cy.window().then(win => { return cy.window({ log: false }).then(win => {
const cli = new win.matrixcs.MatrixClient({ const cli = new win.matrixcs.MatrixClient({
baseUrl: synapse.baseUrl, baseUrl: synapse.baseUrl,
userId: credentials.userId, userId: credentials.userId,

View file

@ -46,11 +46,11 @@ declare global {
} }
Cypress.Commands.add("getClient", (): Chainable<MatrixClient | undefined> => { Cypress.Commands.add("getClient", (): Chainable<MatrixClient | undefined> => {
return cy.window().then(win => win.mxMatrixClientPeg.matrixClient); return cy.window({ log: false }).then(win => win.mxMatrixClientPeg.matrixClient);
}); });
Cypress.Commands.add("createRoom", (options: ICreateRoomOpts): Chainable<string> => { Cypress.Commands.add("createRoom", (options: ICreateRoomOpts): Chainable<string> => {
return cy.window().then(async win => { return cy.window({ log: false }).then(async win => {
const cli = win.mxMatrixClientPeg.matrixClient; const cli = win.mxMatrixClientPeg.matrixClient;
const resp = await cli.createRoom(options); const resp = await cli.createRoom(options);
const roomId = resp.room_id; const roomId = resp.room_id;

View file

@ -16,6 +16,7 @@ limitations under the License.
/// <reference types="cypress" /> /// <reference types="cypress" />
import "./performance";
import "./synapse"; import "./synapse";
import "./login"; import "./login";
import "./client"; import "./client";

View file

@ -43,7 +43,7 @@ declare global {
Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: string): Chainable<UserCredentials> => { Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: string): Chainable<UserCredentials> => {
// XXX: work around Cypress not clearing IDB between tests // XXX: work around Cypress not clearing IDB between tests
cy.window().then(win => { cy.window({ log: false }).then(win => {
win.indexedDB.databases().then(databases => { win.indexedDB.databases().then(databases => {
databases.forEach(database => { databases.forEach(database => {
win.indexedDB.deleteDatabase(database.name); win.indexedDB.deleteDatabase(database.name);
@ -73,7 +73,7 @@ Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: str
}, },
}); });
}).then(response => { }).then(response => {
cy.window().then(win => { cy.window({ log: false }).then(win => {
// Seed the localStorage with the required credentials // Seed the localStorage with the required credentials
win.localStorage.setItem("mx_hs_url", synapse.baseUrl); win.localStorage.setItem("mx_hs_url", synapse.baseUrl);
win.localStorage.setItem("mx_user_id", response.body.user_id); win.localStorage.setItem("mx_user_id", response.body.user_id);

View file

@ -0,0 +1,74 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/// <reference types="cypress" />
import Chainable = Cypress.Chainable;
import AUTWindow = Cypress.AUTWindow;
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
/**
* Start measuring the duration of some task.
* @param task The task name.
*/
startMeasuring(task: string): Chainable<AUTWindow>;
/**
* Stop measuring the duration of some task.
* The duration is reported in the Cypress log.
* @param task The task name.
*/
stopMeasuring(task: string): Chainable<AUTWindow>;
}
}
}
function getPrefix(task: string): string {
return `cy:${Cypress.spec.name.split(".")[0]}:${task}`;
}
function startMeasuring(task: string): Chainable<AUTWindow> {
return cy.window({ log: false }).then((win) => {
win.mxPerformanceMonitor.start(getPrefix(task));
});
}
function stopMeasuring(task: string): Chainable<AUTWindow> {
return cy.window({ log: false }).then((win) => {
const measure = win.mxPerformanceMonitor.stop(getPrefix(task));
cy.log(`**${task}** ${measure.duration} ms`);
});
}
Cypress.Commands.add("startMeasuring", startMeasuring);
Cypress.Commands.add("stopMeasuring", stopMeasuring);
Cypress.on("window:before:unload", (event: BeforeUnloadEvent) => {
const doc = event.target as Document;
if (doc.location.href === "about:blank") return;
const win = doc.defaultView as AUTWindow;
if (!win.mxPerformanceMonitor) return;
const entries = win.mxPerformanceMonitor.getEntries().filter(entry => {
return entry.name.startsWith("cy:");
});
if (!entries || entries.length === 0) return;
cy.task("addMeasurements", entries);
});
// Needed to make this file a module
export { };

View file

@ -63,7 +63,7 @@ function startSynapse(template: string): Chainable<SynapseInstance> {
function stopSynapse(synapse?: SynapseInstance): Chainable<AUTWindow> { function stopSynapse(synapse?: SynapseInstance): Chainable<AUTWindow> {
if (!synapse) return; if (!synapse) return;
// Navigate away from app to stop the background network requests which will race with Synapse shutting down // Navigate away from app to stop the background network requests which will race with Synapse shutting down
return cy.window().then((win) => { return cy.window({ log: false }).then((win) => {
win.location.href = 'about:blank'; win.location.href = 'about:blank';
cy.task("synapseStop", synapse.synapseId); cy.task("synapseStop", synapse.synapseId);
}); });

View file

@ -71,7 +71,7 @@ export default class PerformanceMonitor {
* with the start marker * with the start marker
* @param name Name of the recording * @param name Name of the recording
* @param id Specify an identifier appended to the measurement name * @param id Specify an identifier appended to the measurement name
* @returns {void} * @returns The measurement
*/ */
stop(name: string, id?: string): PerformanceEntry { stop(name: string, id?: string): PerformanceEntry {
if (!this.supportsPerformanceApi()) { if (!this.supportsPerformanceApi()) {