From 2c600ea5ed03b9ba825033ed74db75722fe7c898 Mon Sep 17 00:00:00 2001 From: AzazelN28 Date: Tue, 6 Jun 2023 12:52:25 +0200 Subject: [PATCH] Add frame rate measuring for performance tests --- .editorconfig | 13 ++ .nvmrc | 1 + .prettierrc | 7 ++ package.json | 7 +- pages/performance-page.js | 201 ++++++++++++++++++++++++++++++ playwright.config.js | 2 +- tests/performance/measure.spec.js | 29 +++++ tests/performance/render.spec.js | 9 ++ tests/performance/shapes.spec.js | 25 ++++ tests/performance/view.spec.js | 9 ++ 10 files changed, 299 insertions(+), 4 deletions(-) create mode 100644 .editorconfig create mode 100644 .nvmrc create mode 100644 .prettierrc create mode 100644 pages/performance-page.js create mode 100644 tests/performance/measure.spec.js create mode 100644 tests/performance/render.spec.js create mode 100644 tests/performance/shapes.spec.js create mode 100644 tests/performance/view.spec.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..db8dfb1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +charset = utf-8 + +indent_size = 2 +indent_style = space + +end_of_line = lf + +insert_final_newline = true + +trim_trailing_whitespace = true \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..8ddbc0c --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v18.16.0 diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..a87b9ec --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "singleQuote": false, + "trailingComma": "all", + "tabWidth": 2, + "useTabs": false, + "semi": true +} diff --git a/package.json b/package.json index df4d8c7..0798b66 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,10 @@ "description": "QA Test for Penpot", "main": "index.js", "scripts": { - "test": "npx playwright test --project=chrome", - "firefox": "npx playwright test --project=firefox", - "webkit": "npx playwright test --project=webkit", + "test": "npx playwright test --project=chrome -gv 'PERF'", + "firefox": "npx playwright test --project=firefox -gv 'PERF'", + "webkit": "npx playwright test --project=webkit -gv 'PERF'", + "performance": "npx playwright test --project=chrome -g 'PERF'", "prettier": "npx prettier --write ." }, "repository": { diff --git a/pages/performance-page.js b/pages/performance-page.js new file mode 100644 index 0000000..c1a8972 --- /dev/null +++ b/pages/performance-page.js @@ -0,0 +1,201 @@ +const { expect } = require("@playwright/test"); +const { BasePage } = require("./base-page"); +exports.PerformancePage = class PerformancePage extends BasePage { + /** + * @param {import('@playwright/test').Page} page + */ + constructor(page) { + super(page); + } + + /** + * Starts observing long tasks. + */ + startObservingLongTasks() { + return this.page.evaluate(() => { + window.penpotPerformanceObserver = new PerformanceObserver((list) => { + console.log(list.getEntries()); + }); + window.penpotPerformanceObserver.observe({ entryTypes: ['longtask', 'taskattribution'] }); + }) + } + + /** + * Stops observing long tasks. + * + * @returns {Promise} + */ + stopObservingLongTasks() { + return this.page.evaluate(() => { + window.penpotPerformanceObserver.disconnect(); + return window.penpotPerformanceObserver.takeRecords(); + }) + } + + /** + * Injects a frame rate recorder into the page. + * + * @returns {Promise} + */ + injectFrameRateRecorder() { + return this.page.evaluate(() => { + /** + * Why is all this code inside the evaluate function? + * + * The evaluate function is executed in the browser context, so + * it has access to the DOM and the JavaScript runtime. This + * means that we can use it to create helper classes and functions + * that will be used to record the frame rate. If we define these + * classes and functions outside of the evaluate function, they + * will not be available in the browser context. + */ + + /** + * TimestampedDataRecorder is a helper class that records a + * value and a timestamp. + */ + class TimestampedDataRecorder { + #index = 0 + #values = null + #timestamps = null + + constructor(maxRecords) { + this.#index = 0 + this.#values = new Uint32Array(maxRecords) + this.#timestamps = new Float32Array(maxRecords) + } + + reset(clearData = true) { + this.#index = 0 + if (clearData) { + this.#values.fill(0) + this.#timestamps.fill(0) + } + } + + record(value) { + this.#values[this.#index] = value + this.#timestamps[this.#index] = performance.now() + this.#index = (this.#index + 1) % this.#values.length + if (this.#index === 0) { + return false + } + return true + } + + takeRecords() { + const records = [] + for (let i = 0; i < this.#index; i++) { + records.push({ + value: this.#values[i], + timestamp: this.#timestamps[i] + }) + } + return records + } + } + + /** + * FrameRateRecorder is a helper class that records + * the number of frames. + */ + class FrameRateRecorder extends EventTarget { + #recorder = null + #frameId = null + #frameCount = 0 + #framesPerSecond = 0 + #startTime = 0 + #shouldStopWhenFull = true + + constructor({ maxRecords = 60 * 60, shouldStopWhenFull = true }) { + super() + this.#frameId = null + this.#frameCount = 0 + this.#startTime = 0 + this.#shouldStopWhenFull = shouldStopWhenFull + this.#recorder = new TimestampedDataRecorder(maxRecords) + } + + get framesPerSecond() { + return this.#framesPerSecond + } + + #onFrame = (currentTime) => { + this.#frameCount++ + let shouldStop = false + if (currentTime - this.#startTime >= 1000) { + this.#startTime = currentTime + this.#framesPerSecond = this.#frameCount + this.#frameCount = 0 + + const shouldContinue = this.#recorder.record(this.#framesPerSecond) + if (!shouldContinue && this.#shouldStopWhenFull) { + shouldStop = true + } + } + if (shouldStop) { + if (this.stop()) { + this.dispatchEvent(new Event('ended')) + } + return + } + this.#frameId = window.requestAnimationFrame(this.#onFrame) + } + + takeRecords() { + return this.#recorder.takeRecords() + } + + start() { + if (this.#frameId !== null) { + return false + } + this.#recorder.reset() + this.#startTime = performance.now() + this.#frameId = window.requestAnimationFrame(this.#onFrame) + return true + } + + stop() { + if (this.#frameId === null) { + return false + } + window.cancelAnimationFrame(this.#frameId) + this.#frameId = null + return true + } + } + + window.FrameRateRecorder = FrameRateRecorder + }) + } + + /** + * Starts recording the frame rate. + * + * @returns {Promise} + */ + startRecordingFrameRate({ maxRecords = 60 * 60, shouldStopWhenFull = true } = {}) { + return this.page.evaluate(({ maxRecords, shouldStopWhenFull }) => { + window.penpotFrameRateRecorder_OnEnded = () => { + throw new Error('Insufficient buffer size to record frame rate.') + } + window.penpotFrameRateRecorder = new FrameRateRecorder({ maxRecords, shouldStopWhenFull }) + window.penpotFrameRateRecorder.addEventListener('ended', window.penpotFrameRateRecorder_OnEnded) + window.penpotFrameRateRecorder.start() + }, { maxRecords, shouldStopWhenFull }) + } + + /** + * Stops recording the frame rate. + * + * @returns {Promise} + */ + stopRecordingFrameRate() { + return this.page.evaluate(() => { + window.penpotFrameRateRecorder.removeEventListener('ended', window.penpotFrameRateRecorder_OnEnded) + window.penpotFrameRateRecorder.stop() + return window.penpotFrameRateRecorder.takeRecords() + }) + } +} diff --git a/playwright.config.js b/playwright.config.js index 4eab5b0..de7fd1d 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -12,7 +12,7 @@ const config = { snapshotPathTemplate: `{testDir}/{testFileDir}/{testFileName}-snapshots/{projectName}/{arg}{ext}`, testDir: "./tests", /* Maximum time one test can run for. */ - timeout: process.env.CI ? 80 * 1000 : 50 * 1000, + timeout: process.env.CI ? 80 * 1000 : 555550 * 1000, expect: { /** * Maximum time expect() should wait for the condition to be met. diff --git a/tests/performance/measure.spec.js b/tests/performance/measure.spec.js new file mode 100644 index 0000000..d958164 --- /dev/null +++ b/tests/performance/measure.spec.js @@ -0,0 +1,29 @@ +import { expect, test } from '@playwright/test' +import { PerformancePage } from '../../pages/performance-page' + +function waitForSeconds(timeInSeconds) { + return new Promise((resolve) => setTimeout(() => resolve(), timeInSeconds * 1000)); +} + +test('PERF Measure frames per second', async ({ page }) => { + const performancePage = new PerformancePage(page); + await performancePage.injectFrameRateRecorder(); + await performancePage.startRecordingFrameRate(); + await waitForSeconds(3); + const records = await performancePage.stopRecordingFrameRate(); + // This is an example of how to use the performance page + // API, in idle time we should have average ~60 frames per second. + const averageFrameRate = records.reduce((acc, record) => acc + record.value, 0) / records.length; + expect(averageFrameRate).toBeGreaterThan(55); + expect(averageFrameRate).toBeLessThan(65); +}) + +test('PERF Measure long tasks', async ({ page }) => { + const performancePage = new PerformancePage(page); + await performancePage.startObservingLongTasks(); + await waitForSeconds(3); + const records = await performancePage.stopObservingLongTasks(); + // This is an example of how to use the performance page + // API, in idle time we should have no long tasks. + expect(records.length).toBe(0); +}); diff --git a/tests/performance/render.spec.js b/tests/performance/render.spec.js new file mode 100644 index 0000000..055167d --- /dev/null +++ b/tests/performance/render.spec.js @@ -0,0 +1,9 @@ +const { test } = require("@playwright/test"); + +test('PERF Render shapes with blur', async ({ page }) => { + +}) + +test('PERF Render shapes with drop-shadow', async ({ page }) => { + +}) diff --git a/tests/performance/shapes.spec.js b/tests/performance/shapes.spec.js new file mode 100644 index 0000000..cfa63b1 --- /dev/null +++ b/tests/performance/shapes.spec.js @@ -0,0 +1,25 @@ +const { test } = require("@playwright/test"); + +test('PERF Rotate single shape', async ({ page }) => { + +}) + +test('PERF Scale single shape', async ({ page }) => { + +}) + +test('PERF Move single shape', async ({ page }) => { + +}) + +test('PERF Rotate multiple shapes', async ({ page }) => { + +}) + +test('PERF Scale multiple shapes', async ({ page }) => { + +}) + +test('PERF Move multiple shapes', async ({ page }) => { + +}) diff --git a/tests/performance/view.spec.js b/tests/performance/view.spec.js new file mode 100644 index 0000000..be40253 --- /dev/null +++ b/tests/performance/view.spec.js @@ -0,0 +1,9 @@ +const { test } = require("@playwright/test"); + +test('PERF Pan viewport in workspace', async ({ page }) => { + +}) + +test('PERF Zoom viewport in workspace', async ({ page }) => { + +})