Add frame rate measuring for performance tests

This commit is contained in:
AzazelN28 2023-06-06 12:52:25 +02:00
parent 43b876fee4
commit 2c600ea5ed
10 changed files with 299 additions and 4 deletions

13
.editorconfig Normal file
View File

@ -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

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
v18.16.0

7
.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"singleQuote": false,
"trailingComma": "all",
"tabWidth": 2,
"useTabs": false,
"semi": true
}

View File

@ -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": {

201
pages/performance-page.js Normal file
View File

@ -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<PerformanceEntry[]>}
*/
stopObservingLongTasks() {
return this.page.evaluate(() => {
window.penpotPerformanceObserver.disconnect();
return window.penpotPerformanceObserver.takeRecords();
})
}
/**
* Injects a frame rate recorder into the page.
*
* @returns {Promise<void>}
*/
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<void>}
*/
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<FrameRateEntry[]>}
*/
stopRecordingFrameRate() {
return this.page.evaluate(() => {
window.penpotFrameRateRecorder.removeEventListener('ended', window.penpotFrameRateRecorder_OnEnded)
window.penpotFrameRateRecorder.stop()
return window.penpotFrameRateRecorder.takeRecords()
})
}
}

View File

@ -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.

View File

@ -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);
});

View File

@ -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 }) => {
})

View File

@ -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 }) => {
})

View File

@ -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 }) => {
})