Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance Tests: Separate page setup from test #53808

Merged
merged 18 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions test/performance/fixtures/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { PerfUtils } from './perf-utils';
export { Metrics } from './metrics';
146 changes: 146 additions & 0 deletions test/performance/fixtures/metrics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
export class Metrics {
constructor( { page } ) {
this.page = page;
}

async getTimeToFirstByte() {
return await this.page.evaluate( () => {
// Based on https://web.dev/ttfb/#measure-ttfb-in-javascript
return new Promise( ( resolve ) => {
new PerformanceObserver( ( entryList ) => {
const [ pageNav ] =
entryList.getEntriesByType( 'navigation' );
resolve( pageNav.responseStart );
} ).observe( {
type: 'navigation',
buffered: true,
} );
} );
} );
}

async getLargestContentfulPaint() {
return await this.page.evaluate( () => {
// Based on https://www.checklyhq.com/learn/headless/basics-performance#largest-contentful-paint-api-largest-contentful-paint
return new Promise( ( resolve ) => {
new PerformanceObserver( ( entryList ) => {
const entries = entryList.getEntries();
// The last entry is the largest contentful paint.
const largestPaintEntry = entries.at( -1 );

resolve( largestPaintEntry.startTime );
} ).observe( {
type: 'largest-contentful-paint',
buffered: true,
} );
} );
} );
}

async getLoadingDurations() {
return await this.page.evaluate( () => {
const [
{
requestStart,
responseStart,
responseEnd,
domContentLoadedEventEnd,
loadEventEnd,
},
] = performance.getEntriesByType( 'navigation' );
const paintTimings = performance.getEntriesByType( 'paint' );

return {
// Server side metric.
serverResponse: responseStart - requestStart,
// For client side metrics, consider the end of the response (the
// browser receives the HTML) as the start time (0).
firstPaint:
paintTimings.find( ( { name } ) => name === 'first-paint' )
.startTime - responseEnd,
domContentLoaded: domContentLoadedEventEnd - responseEnd,
loaded: loadEventEnd - responseEnd,
firstContentfulPaint:
paintTimings.find(
( { name } ) => name === 'first-contentful-paint'
).startTime - responseEnd,
// This is evaluated right after Playwright found the block selector.
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
firstBlock: performance.now() - responseEnd,
};
} );
}

getTypingEventDurations( trace ) {
return [
getEventDurationsForType( trace, isKeyDownEvent ),
getEventDurationsForType( trace, isKeyPressEvent ),
getEventDurationsForType( trace, isKeyUpEvent ),
];
}

getSelectionEventDurations( trace ) {
return [
getEventDurationsForType( trace, isFocusEvent ),
getEventDurationsForType( trace, isFocusInEvent ),
];
}

getClickEventDurations( trace ) {
return [ getEventDurationsForType( trace, isClickEvent ) ];
}

getHoverEventDurations( trace ) {
return [
getEventDurationsForType( trace, isMouseOverEvent ),
getEventDurationsForType( trace, isMouseOutEvent ),
];
}
}

function isEvent( item ) {
return (
item.cat === 'devtools.timeline' &&
item.name === 'EventDispatch' &&
item.dur &&
item.args &&
item.args.data
);
}

function isKeyDownEvent( item ) {
return isEvent( item ) && item.args.data.type === 'keydown';
}

function isKeyPressEvent( item ) {
return isEvent( item ) && item.args.data.type === 'keypress';
}

function isKeyUpEvent( item ) {
return isEvent( item ) && item.args.data.type === 'keyup';
}

function isFocusEvent( item ) {
return isEvent( item ) && item.args.data.type === 'focus';
}

function isFocusInEvent( item ) {
return isEvent( item ) && item.args.data.type === 'focusin';
}

function isClickEvent( item ) {
return isEvent( item ) && item.args.data.type === 'click';
}

function isMouseOverEvent( item ) {
return isEvent( item ) && item.args.data.type === 'mouseover';
}

function isMouseOutEvent( item ) {
return isEvent( item ) && item.args.data.type === 'mouseout';
}

function getEventDurationsForType( trace, filterFunction ) {
return trace.traceEvents
.filter( filterFunction )
.map( ( item ) => item.dur / 1000 );
}
146 changes: 146 additions & 0 deletions test/performance/fixtures/perf-utils.js
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Let's refactor this file into typescript if the tooling allows it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored in 5c67b2d

Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* WordPress dependencies
*/
import { expect } from '@wordpress/e2e-test-utils-playwright';

/**
* External dependencies
*/
import fs from 'fs';
import path from 'path';

/**
* Internal dependencies
*/
import { readFile } from '../utils.js';

export class PerfUtils {
constructor( { browser, page } ) {
this.browser = browser;
this.page = page;
}

async getCanvas() {
// Handles both legacy and iframed canvas.
return await Promise.any( [
( async () => {
const legacyCanvasLocator = this.page.locator(
'.wp-block-post-content'
);
await legacyCanvasLocator.waitFor( {
timeout: 120_000,
} );
return legacyCanvasLocator;
} )(),
( async () => {
const iframedCanvasLocator = this.page.frameLocator(
'[name=editor-canvas]'
);
await iframedCanvasLocator
.locator( 'body' )
.waitFor( { timeout: 120_000 } );
return iframedCanvasLocator;
} )(),
] );
}

async saveDraft() {
await this.page
.getByRole( 'button', { name: 'Save draft' } )
.click( { timeout: 60_000 } );
await expect(
this.page.getByRole( 'button', { name: 'Saved' } )
).toBeDisabled();

return this.page.url();
}

async disableAutosave() {
await this.page.evaluate( () => {
return window.wp.data
.dispatch( 'core/editor' )
.updateEditorSettings( {
autosaveInterval: 100000000000,
localAutosaveInterval: 100000000000,
} );
} );

const { autosaveInterval } = await this.page.evaluate( () => {
return window.wp.data.select( 'core/editor' ).getEditorSettings();
} );

expect( autosaveInterval ).toBe( 100000000000 );
}

async enterSiteEditorEditMode() {
const canvas = await this.getCanvas();

await canvas.locator( 'body' ).click();
// Second click is needed for the legacy edit mode.
await canvas
.getByRole( 'document', { name: /Block:( Post)? Content/ } )
.click();

return canvas;
}

async loadBlocksForSmallPostWithContainers() {
return await this.loadBlocksFromHtml(
path.join(
process.env.ASSETS_PATH,
'small-post-with-containers.html'
)
);
}

async loadBlocksForLargePost() {
return await this.loadBlocksFromHtml(
path.join( process.env.ASSETS_PATH, 'large-post.html' )
);
}

async loadBlocksFromHtml( filepath ) {
if ( ! fs.existsSync( filepath ) ) {
throw new Error( `File not found: ${ filepath }` );
}

return await this.page.evaluate( ( html ) => {
const { parse } = window.wp.blocks;
const { dispatch } = window.wp.data;
const blocks = parse( html );

blocks.forEach( ( block ) => {
if ( block.name === 'core/image' ) {
delete block.attributes.id;
delete block.attributes.url;
}
} );

dispatch( 'core/block-editor' ).resetBlocks( blocks );
}, readFile( filepath ) );
}

async load1000Paragraphs() {
await this.page.evaluate( () => {
const { createBlock } = window.wp.blocks;
const { dispatch } = window.wp.data;
const blocks = Array.from( { length: 1000 } ).map( () =>
createBlock( 'core/paragraph' )
);
dispatch( 'core/block-editor' ).resetBlocks( blocks );
} );
}

async startTracing( options = {} ) {
return await this.browser.startTracing( this.page, {
screenshots: false,
categories: [ 'devtools.timeline' ],
...options,
} );
}

async stopTracing() {
const traceBuffer = await this.browser.stopTracing();
return JSON.parse( traceBuffer.toString() );
}
}
61 changes: 21 additions & 40 deletions test/performance/specs/front-end-block-theme.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
/* eslint-disable playwright/no-conditional-in-test, playwright/expect-expect */

/**
* WordPress dependencies
*/
const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
import { test } from '@wordpress/e2e-test-utils-playwright';

/**
* Internal dependencies
*/
import { Metrics } from '../fixtures';

const results = {
timeToFirstByte: [],
Expand All @@ -10,7 +17,12 @@ const results = {
};

test.describe( 'Front End Performance', () => {
test.use( { storageState: {} } ); // User will be logged out.
test.use( {
storageState: {}, // User will be logged out.
metrics: async ( { page }, use ) => {
await use( new Metrics( { page } ) );
},
} );

test.beforeAll( async ( { requestUtils } ) => {
await requestUtils.activateTheme( 'twentytwentythree' );
Expand All @@ -26,51 +38,18 @@ test.describe( 'Front End Performance', () => {

const samples = 16;
const throwaway = 0;
const rounds = samples + throwaway;
for ( let i = 0; i < rounds; i++ ) {
const iterations = samples + throwaway;
for ( let i = 0; i < iterations; i++ ) {
test( `Measure TTFB, LCP, and LCP-TTFB (${
i + 1
} of ${ rounds })`, async ( { page } ) => {
} of ${ iterations })`, async ( { page, metrics } ) => {
// Go to the base URL.
// eslint-disable-next-line playwright/no-networkidle
await page.goto( '/', { waitUntil: 'networkidle' } );

// Take the measurements.
const [ lcp, ttfb ] = await page.evaluate( () => {
return Promise.all( [
// Measure the Largest Contentful Paint time.
// Based on https://www.checklyhq.com/learn/headless/basics-performance#largest-contentful-paint-api-largest-contentful-paint
new Promise( ( resolve ) => {
new PerformanceObserver( ( entryList ) => {
const entries = entryList.getEntries();
// The last entry is the largest contentful paint.
const largestPaintEntry = entries.at( -1 );

resolve( largestPaintEntry.startTime );
} ).observe( {
type: 'largest-contentful-paint',
buffered: true,
} );
} ),
// Measure the Time To First Byte.
// Based on https://web.dev/ttfb/#measure-ttfb-in-javascript
new Promise( ( resolve ) => {
new PerformanceObserver( ( entryList ) => {
const [ pageNav ] =
entryList.getEntriesByType( 'navigation' );

resolve( pageNav.responseStart );
} ).observe( {
type: 'navigation',
buffered: true,
} );
} ),
] );
} );

// Ensure the numbers are valid.
expect( lcp ).toBeGreaterThan( 0 );
expect( ttfb ).toBeGreaterThan( 0 );
const ttfb = await metrics.getTimeToFirstByte();
const lcp = await metrics.getLargestContentfulPaint();

// Save the results.
if ( i >= throwaway ) {
Expand All @@ -81,3 +60,5 @@ test.describe( 'Front End Performance', () => {
} );
}
} );

/* eslint-enable playwright/no-conditional-in-test, playwright/expect-expect */
Loading
Loading