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",
|
||||
"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
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}`,
|
||||
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.
|
||||
|
|
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