mirror of
https://github.com/penpot/penpotqa.git
synced 2024-07-06 04:51:46 +00:00
Add frame rate measuring for performance tests
This commit is contained in:
parent
43b876fee4
commit
2c600ea5ed
13
.editorconfig
Normal file
13
.editorconfig
Normal 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
|
7
.prettierrc
Normal file
7
.prettierrc
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"semi": true
|
||||||
|
}
|
|
@ -4,9 +4,10 @@
|
||||||
"description": "QA Test for Penpot",
|
"description": "QA Test for Penpot",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npx playwright test --project=chrome",
|
"test": "npx playwright test --project=chrome -gv 'PERF'",
|
||||||
"firefox": "npx playwright test --project=firefox",
|
"firefox": "npx playwright test --project=firefox -gv 'PERF'",
|
||||||
"webkit": "npx playwright test --project=webkit",
|
"webkit": "npx playwright test --project=webkit -gv 'PERF'",
|
||||||
|
"performance": "npx playwright test --project=chrome -g 'PERF'",
|
||||||
"prettier": "npx prettier --write ."
|
"prettier": "npx prettier --write ."
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
201
pages/performance-page.js
Normal file
201
pages/performance-page.js
Normal 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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ const config = {
|
||||||
snapshotPathTemplate: `{testDir}/{testFileDir}/{testFileName}-snapshots/{projectName}/{arg}{ext}`,
|
snapshotPathTemplate: `{testDir}/{testFileDir}/{testFileName}-snapshots/{projectName}/{arg}{ext}`,
|
||||||
testDir: "./tests",
|
testDir: "./tests",
|
||||||
/* Maximum time one test can run for. */
|
/* Maximum time one test can run for. */
|
||||||
timeout: process.env.CI ? 80 * 1000 : 50 * 1000,
|
timeout: process.env.CI ? 80 * 1000 : 555550 * 1000,
|
||||||
expect: {
|
expect: {
|
||||||
/**
|
/**
|
||||||
* Maximum time expect() should wait for the condition to be met.
|
* Maximum time expect() should wait for the condition to be met.
|
||||||
|
|
29
tests/performance/measure.spec.js
Normal file
29
tests/performance/measure.spec.js
Normal 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);
|
||||||
|
});
|
9
tests/performance/render.spec.js
Normal file
9
tests/performance/render.spec.js
Normal 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 }) => {
|
||||||
|
|
||||||
|
})
|
25
tests/performance/shapes.spec.js
Normal file
25
tests/performance/shapes.spec.js
Normal 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 }) => {
|
||||||
|
|
||||||
|
})
|
9
tests/performance/view.spec.js
Normal file
9
tests/performance/view.spec.js
Normal 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 }) => {
|
||||||
|
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user