From d9be78527190b1123c7383a94b88a4c7b747606c Mon Sep 17 00:00:00 2001 From: Bart Kalisz Date: Thu, 17 Aug 2023 15:37:11 +0200 Subject: [PATCH 01/14] Utilize the new context created internally ...instead of manually creating a new one. This is good practice as it allows us to utilize the Playwright fixtures for each page that is created, which is not possible when creating contexts manually. It also improves consistency as it ensures each page is created with the same initial params. --- .../specs/front-end-classic-theme.spec.js | 2 +- test/performance/specs/post-editor.spec.js | 155 ++++++++++-------- test/performance/specs/site-editor.spec.js | 129 +++++++-------- test/performance/utils.js | 9 + 4 files changed, 154 insertions(+), 141 deletions(-) diff --git a/test/performance/specs/front-end-classic-theme.spec.js b/test/performance/specs/front-end-classic-theme.spec.js index 880da94a11c60..1996a1846661c 100644 --- a/test/performance/specs/front-end-classic-theme.spec.js +++ b/test/performance/specs/front-end-classic-theme.spec.js @@ -27,7 +27,7 @@ test.describe( 'Front End Performance', () => { const throwaway = 0; const rounds = samples + throwaway; for ( let i = 1; i <= rounds; i++ ) { - test( `Report TTFB, LCP, and LCP-TTFB (${ i } of ${ rounds })`, async ( { + test( `Measure TTFB, LCP, and LCP-TTFB (${ i } of ${ rounds })`, async ( { page, } ) => { // Go to the base URL. diff --git a/test/performance/specs/post-editor.spec.js b/test/performance/specs/post-editor.spec.js index f808a4076d39f..90b8cd865c780 100644 --- a/test/performance/specs/post-editor.spec.js +++ b/test/performance/specs/post-editor.spec.js @@ -17,6 +17,7 @@ const { getHoverEventDurations, getSelectionEventDurations, getLoadingDurations, + disableAutosave, loadBlocksFromHtml, load1000Paragraphs, sum, @@ -49,83 +50,70 @@ test.describe( 'Post Editor Performance', () => { } ); } ); - test.beforeEach( async ( { admin, page } ) => { - await admin.createNewPost(); - // Disable auto-save to avoid impacting the metrics. - await page.evaluate( () => { - window.wp.data.dispatch( 'core/editor' ).updateEditorSettings( { - autosaveInterval: 100000000000, - localAutosaveInterval: 100000000000, - } ); - } ); - } ); + test.describe( 'Loading', () => { + let draftURL = null; - test( 'Loading', async ( { browser, page } ) => { - // Turn the large post HTML into blocks and insert. - await loadBlocksFromHtml( - page, - path.join( process.env.ASSETS_PATH, 'large-post.html' ) - ); + test( 'Setup the test page', async ( { admin, page } ) => { + await admin.createNewPost(); + await loadBlocksFromHtml( + page, + path.join( process.env.ASSETS_PATH, 'large-post.html' ) + ); - // Save the draft. - await page - .getByRole( 'button', { name: 'Save draft' } ) - .click( { timeout: 60_000 } ); - await expect( - page.getByRole( 'button', { name: 'Saved' } ) - ).toBeDisabled(); + await page + .getByRole( 'button', { name: 'Save draft' } ) + .click( { timeout: 60_000 } ); - // Get the URL that we will be testing against. - const draftURL = page.url(); + await expect( + page.getByRole( 'button', { name: 'Saved' } ) + ).toBeDisabled(); + + // Get the URL that we will be testing against. + draftURL = page.url(); + } ); - // Start the measurements. const samples = 10; const throwaway = 1; const rounds = throwaway + samples; for ( let i = 0; i < rounds; i++ ) { - // Open a fresh page in a new context to prevent caching. - const testPage = await browser.newPage(); - - // Go to the test page URL. - await testPage.goto( draftURL ); - - // Get canvas (handles both legacy and iframed canvas). - const canvas = await Promise.any( [ - ( async () => { - const legacyCanvasLocator = page.locator( - '.wp-block-post-content' - ); - await legacyCanvasLocator.waitFor(); - return legacyCanvasLocator; - } )(), - ( async () => { - const iframedCanvasLocator = page.frameLocator( - '[name=editor-canvas]' + test( `Get the durations (${ i + 1 } of ${ rounds })`, async ( { + page, + editor, + } ) => { + // Go to the test page. + await page.goto( draftURL ); + + // Wait for canvas (legacy or iframed). + await Promise.any( [ + page.locator( '.wp-block-post-content' ).waitFor(), + page + .frameLocator( '[name=editor-canvas]' ) + .locator( 'body > *' ) + .first() + .waitFor(), + ] ); + + await editor.canvas.locator( '.wp-block' ).first().waitFor( { + timeout: 120_000, + } ); + + // Save the results. + if ( i >= throwaway ) { + const loadingDurations = await getLoadingDurations( page ); + Object.entries( loadingDurations ).forEach( + ( [ metric, duration ] ) => { + results[ metric ].push( duration ); + } ); - await iframedCanvasLocator.locator( 'body' ).waitFor(); - return iframedCanvasLocator; - } )(), - ] ); - - await canvas.locator( '.wp-block' ).first().waitFor( { - timeout: 120_000, + } } ); - - // Save the results. - if ( i >= throwaway ) { - const loadingDurations = await getLoadingDurations( testPage ); - Object.entries( loadingDurations ).forEach( - ( [ metric, duration ] ) => { - results[ metric ].push( duration ); - } - ); - } - - await testPage.close(); } } ); - test( 'Typing', async ( { browser, page, editor } ) => { + test( 'Typing', async ( { admin, browser, page, editor } ) => { + await admin.createNewPost(); + await disableAutosave( page ); + // Load the large post fixture. await loadBlocksFromHtml( page, @@ -166,7 +154,15 @@ test.describe( 'Post Editor Performance', () => { } } ); - test( 'Typing within containers', async ( { browser, page, editor } ) => { + test( 'Typing within containers', async ( { + admin, + browser, + page, + editor, + } ) => { + await admin.createNewPost(); + await disableAutosave( page ); + await loadBlocksFromHtml( page, path.join( @@ -208,7 +204,10 @@ test.describe( 'Post Editor Performance', () => { } } ); - test( 'Selecting blocks', async ( { browser, page, editor } ) => { + test( 'Selecting blocks', async ( { admin, browser, page, editor } ) => { + await admin.createNewPost(); + await disableAutosave( page ); + await load1000Paragraphs( page ); const paragraphs = editor.canvas.locator( '.wp-block' ); @@ -240,7 +239,14 @@ test.describe( 'Post Editor Performance', () => { } } ); - test( 'Opening persistent list view', async ( { browser, page } ) => { + test( 'Opening persistent list view', async ( { + admin, + browser, + page, + } ) => { + await admin.createNewPost(); + await disableAutosave( page ); + await load1000Paragraphs( page ); const listViewToggle = page.getByRole( 'button', { name: 'Document Overview', @@ -275,7 +281,10 @@ test.describe( 'Post Editor Performance', () => { } } ); - test( 'Opening the inserter', async ( { browser, page } ) => { + test( 'Opening the inserter', async ( { admin, browser, page } ) => { + await admin.createNewPost(); + await disableAutosave( page ); + await load1000Paragraphs( page ); const globalInserterToggle = page.getByRole( 'button', { name: 'Toggle block inserter', @@ -310,7 +319,10 @@ test.describe( 'Post Editor Performance', () => { } } ); - test( 'Searching the inserter', async ( { browser, page } ) => { + test( 'Searching the inserter', async ( { admin, browser, page } ) => { + await admin.createNewPost(); + await disableAutosave( page ); + await load1000Paragraphs( page ); const globalInserterToggle = page.getByRole( 'button', { name: 'Toggle block inserter', @@ -351,7 +363,10 @@ test.describe( 'Post Editor Performance', () => { await globalInserterToggle.click(); } ); - test( 'Hovering Inserter Items', async ( { browser, page } ) => { + test( 'Hovering Inserter Items', async ( { admin, browser, page } ) => { + await admin.createNewPost(); + await disableAutosave( page ); + await load1000Paragraphs( page ); const globalInserterToggle = page.getByRole( 'button', { name: 'Toggle block inserter', diff --git a/test/performance/specs/site-editor.spec.js b/test/performance/specs/site-editor.spec.js index c1289ab82a019..68ac05b2ea0db 100644 --- a/test/performance/specs/site-editor.spec.js +++ b/test/performance/specs/site-editor.spec.js @@ -14,6 +14,7 @@ const path = require( 'path' ); const { getTypingEventDurations, getLoadingDurations, + disableAutosave, loadBlocksFromHtml, } = require( '../utils' ); @@ -36,8 +37,6 @@ const results = { listViewOpen: [], }; -let testPageId; - test.describe( 'Site Editor Performance', () => { test.beforeAll( async ( { requestUtils } ) => { await requestUtils.activateTheme( 'emptytheme' ); @@ -56,87 +55,77 @@ test.describe( 'Site Editor Performance', () => { await requestUtils.activateTheme( 'twentytwentyone' ); } ); - test.beforeEach( async ( { admin, page } ) => { - // Start a new page. - await admin.createNewPost( { postType: 'page' } ); - - // Disable auto-save to avoid impacting the metrics. - await page.evaluate( () => { - window.wp.data.dispatch( 'core/editor' ).updateEditorSettings( { - autosaveInterval: 100000000000, - localAutosaveInterval: 100000000000, - } ); - } ); - } ); + test.describe( 'Loading', () => { + let draftURL = null; - test( 'Loading', async ( { browser, page, admin } ) => { - // Load the large post fixture. - await loadBlocksFromHtml( - page, - path.join( process.env.ASSETS_PATH, 'large-post.html' ) - ); - - // Save the draft. - await page - .getByRole( 'button', { name: 'Save draft' } ) - .click( { timeout: 60_000 } ); - await expect( - page.getByRole( 'button', { name: 'Saved' } ) - ).toBeDisabled(); + test( 'Setup the test page', async ( { page, admin } ) => { + // Load the large post fixture. + await admin.createNewPost( { postType: 'page' } ); + await loadBlocksFromHtml( + page, + path.join( process.env.ASSETS_PATH, 'large-post.html' ) + ); - // Get the ID of the saved page. - testPageId = await page.evaluate( () => - new URL( document.location ).searchParams.get( 'post' ) - ); + // Save the draft. + await page + .getByRole( 'button', { name: 'Save draft' } ) + .click( { timeout: 60_000 } ); + await expect( + page.getByRole( 'button', { name: 'Saved' } ) + ).toBeDisabled(); + + // Get the ID of the saved page. + const testPageId = new URL( page.url() ).searchParams.get( 'post' ); + + // Open the test page in Site Editor. + await admin.visitSiteEditor( { + postId: testPageId, + postType: 'page', + } ); - // Open the test page in Site Editor. - await admin.visitSiteEditor( { - postId: testPageId, - postType: 'page', + // Get the URL that we will be testing against. + draftURL = page.url(); } ); - // Get the URL that we will be testing against. - const draftURL = page.url(); - - // Start the measurements. const samples = 10; const throwaway = 1; const rounds = samples + throwaway; for ( let i = 0; i < rounds; i++ ) { - // Open a fresh page in a new context to prevent caching. - const testPage = await browser.newPage(); - - // Go to the test page URL. - await testPage.goto( draftURL ); - - // Wait for the canvas to appear. - await testPage - .locator( '.edit-site-canvas-spinner' ) - .waitFor( { state: 'hidden', timeout: 60_000 } ); - - // Wait for the first block. - await testPage - .frameLocator( 'iframe[name="editor-canvas"]' ) - .locator( '.wp-block' ) - .first() - .waitFor( { timeout: 60_000 } ); - - // Save the results. - if ( i >= throwaway ) { - const loadingDurations = await getLoadingDurations( testPage ); - Object.entries( loadingDurations ).forEach( - ( [ metric, duration ] ) => { - results[ metric ].push( duration ); - } - ); - } - - await testPage.close(); + test( `Get the durations (${ i + 1 } of ${ rounds })`, async ( { + page, + } ) => { + // Go to the test page. + await page.goto( draftURL ); + + // Wait for the canvas. + await page + .locator( '.edit-site-canvas-spinner' ) + .waitFor( { state: 'hidden', timeout: 60_000 } ); + + // Wait for the first block. + await page + .frameLocator( 'iframe[name="editor-canvas"]' ) + .locator( '.wp-block' ) + .first() + .waitFor( { timeout: 60_000 } ); + + // Save the results. + if ( i >= throwaway ) { + const loadingDurations = await getLoadingDurations( page ); + Object.entries( loadingDurations ).forEach( + ( [ metric, duration ] ) => { + results[ metric ].push( duration ); + } + ); + } + } ); } } ); test( 'Typing', async ( { browser, page, admin, editor } ) => { // Load the large post fixture. + await admin.createNewPost( { postType: 'page' } ); + await disableAutosave( page ); await loadBlocksFromHtml( page, path.join( process.env.ASSETS_PATH, 'large-post.html' ) @@ -153,7 +142,7 @@ test.describe( 'Site Editor Performance', () => { ).toBeDisabled(); // Get the ID of the saved page. - testPageId = new URL( page.url() ).searchParams.get( 'post' ); + const testPageId = new URL( page.url() ).searchParams.get( 'post' ); // Open the test page in Site Editor. await admin.visitSiteEditor( { diff --git a/test/performance/utils.js b/test/performance/utils.js index b86d09a10b301..3923d353577bc 100644 --- a/test/performance/utils.js +++ b/test/performance/utils.js @@ -196,3 +196,12 @@ export async function load1000Paragraphs( page ) { dispatch( 'core/block-editor' ).resetBlocks( blocks ); } ); } + +export async function disableAutosave( page ) { + return await page.evaluate( () => { + window.wp.data.dispatch( 'core/editor' ).updateEditorSettings( { + autosaveInterval: 100000000000, + localAutosaveInterval: 100000000000, + } ); + } ); +} From 59be4bcffeed44217e90ba74124fa3f1ee630448 Mon Sep 17 00:00:00 2001 From: Bart Kalisz Date: Thu, 31 Aug 2023 17:48:26 +0200 Subject: [PATCH 02/14] Remove unnecessary waiter --- test/performance/specs/site-editor.spec.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/performance/specs/site-editor.spec.js b/test/performance/specs/site-editor.spec.js index 68ac05b2ea0db..9347b4d987280 100644 --- a/test/performance/specs/site-editor.spec.js +++ b/test/performance/specs/site-editor.spec.js @@ -97,11 +97,6 @@ test.describe( 'Site Editor Performance', () => { // Go to the test page. await page.goto( draftURL ); - // Wait for the canvas. - await page - .locator( '.edit-site-canvas-spinner' ) - .waitFor( { state: 'hidden', timeout: 60_000 } ); - // Wait for the first block. await page .frameLocator( 'iframe[name="editor-canvas"]' ) From 954270a9fc0ce87518a33884e4f15fc5ae6d889d Mon Sep 17 00:00:00 2001 From: Bart Kalisz Date: Thu, 31 Aug 2023 17:59:00 +0200 Subject: [PATCH 03/14] Add more consistency --- test/performance/specs/post-editor.spec.js | 14 ++++++++------ test/performance/specs/site-editor.spec.js | 17 +++++++++++------ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/test/performance/specs/post-editor.spec.js b/test/performance/specs/post-editor.spec.js index 90b8cd865c780..ee659bc6187a4 100644 --- a/test/performance/specs/post-editor.spec.js +++ b/test/performance/specs/post-editor.spec.js @@ -74,7 +74,7 @@ test.describe( 'Post Editor Performance', () => { const samples = 10; const throwaway = 1; - const rounds = throwaway + samples; + const rounds = samples + throwaway; for ( let i = 0; i < rounds; i++ ) { test( `Get the durations (${ i + 1 } of ${ rounds })`, async ( { page, @@ -83,21 +83,21 @@ test.describe( 'Post Editor Performance', () => { // Go to the test page. await page.goto( draftURL ); - // Wait for canvas (legacy or iframed). + // Wait for the editor canvas (legacy or iframed). await Promise.any( [ page.locator( '.wp-block-post-content' ).waitFor(), page .frameLocator( '[name=editor-canvas]' ) - .locator( 'body > *' ) - .first() + .locator( 'body' ) .waitFor(), ] ); + // Wait for the first block to be ready. await editor.canvas.locator( '.wp-block' ).first().waitFor( { timeout: 120_000, } ); - // Save the results. + // Get the durations. if ( i >= throwaway ) { const loadingDurations = await getLoadingDurations( page ); Object.entries( loadingDurations ).forEach( @@ -171,7 +171,7 @@ test.describe( 'Post Editor Performance', () => { ) ); - // Select the block where we type in + // Select the block where we type in. await editor.canvas .getByRole( 'document', { name: 'Paragraph block' } ) .first() @@ -188,6 +188,8 @@ test.describe( 'Post Editor Performance', () => { // probably deserves a dedicated metric itself, though. const throwaway = 1; const rounds = samples + throwaway; + + // Start typing in the middle of the text. await page.keyboard.type( 'x'.repeat( rounds ), { delay: BROWSER_IDLE_WAIT, } ); diff --git a/test/performance/specs/site-editor.spec.js b/test/performance/specs/site-editor.spec.js index 9347b4d987280..34a6b2a29c7fc 100644 --- a/test/performance/specs/site-editor.spec.js +++ b/test/performance/specs/site-editor.spec.js @@ -93,18 +93,23 @@ test.describe( 'Site Editor Performance', () => { for ( let i = 0; i < rounds; i++ ) { test( `Get the durations (${ i + 1 } of ${ rounds })`, async ( { page, + editor, } ) => { // Go to the test page. await page.goto( draftURL ); - // Wait for the first block. + // Wait for the editor canvas. await page - .frameLocator( 'iframe[name="editor-canvas"]' ) - .locator( '.wp-block' ) - .first() - .waitFor( { timeout: 60_000 } ); + .frameLocator( '[name=editor-canvas]' ) + .locator( 'body' ) + .waitFor(); - // Save the results. + // Wait for the first block to be ready. + await editor.canvas.locator( '.wp-block' ).first().waitFor( { + timeout: 120_000, + } ); + + // Get the durations. if ( i >= throwaway ) { const loadingDurations = await getLoadingDurations( page ); Object.entries( loadingDurations ).forEach( From e49ce6bc6cd234575f104aac50d09b5429fd8286 Mon Sep 17 00:00:00 2001 From: Bart Kalisz Date: Fri, 15 Sep 2023 11:50:49 +0200 Subject: [PATCH 04/14] Isolate page setup from test + abstract fixtures --- test/performance/fixtures/index.js | 2 + test/performance/fixtures/metrics.js | 146 ++++ test/performance/fixtures/perf-utils.js | 146 ++++ .../specs/front-end-block-theme.spec.js | 61 +- .../specs/front-end-classic-theme.spec.js | 65 +- test/performance/specs/post-editor.spec.js | 652 ++++++++++-------- test/performance/specs/site-editor.spec.js | 215 +++--- test/performance/utils.js | 147 ---- 8 files changed, 796 insertions(+), 638 deletions(-) create mode 100644 test/performance/fixtures/index.js create mode 100644 test/performance/fixtures/metrics.js create mode 100644 test/performance/fixtures/perf-utils.js diff --git a/test/performance/fixtures/index.js b/test/performance/fixtures/index.js new file mode 100644 index 0000000000000..00edf619398dd --- /dev/null +++ b/test/performance/fixtures/index.js @@ -0,0 +1,2 @@ +export { PerfUtils } from './perf-utils'; +export { Metrics } from './metrics'; diff --git a/test/performance/fixtures/metrics.js b/test/performance/fixtures/metrics.js new file mode 100644 index 0000000000000..b0a9e69d69c3a --- /dev/null +++ b/test/performance/fixtures/metrics.js @@ -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. + 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 ); +} diff --git a/test/performance/fixtures/perf-utils.js b/test/performance/fixtures/perf-utils.js new file mode 100644 index 0000000000000..f1e414c403b49 --- /dev/null +++ b/test/performance/fixtures/perf-utils.js @@ -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() ); + } +} diff --git a/test/performance/specs/front-end-block-theme.spec.js b/test/performance/specs/front-end-block-theme.spec.js index 55845e49f9515..c04007ca60d8e 100644 --- a/test/performance/specs/front-end-block-theme.spec.js +++ b/test/performance/specs/front-end-block-theme.spec.js @@ -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: [], @@ -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' ); @@ -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 ) { @@ -81,3 +60,5 @@ test.describe( 'Front End Performance', () => { } ); } } ); + +/* eslint-enable playwright/no-conditional-in-test, playwright/expect-expect */ diff --git a/test/performance/specs/front-end-classic-theme.spec.js b/test/performance/specs/front-end-classic-theme.spec.js index 3c63107b1cbf0..339b6324f4dda 100644 --- a/test/performance/specs/front-end-classic-theme.spec.js +++ b/test/performance/specs/front-end-classic-theme.spec.js @@ -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: [], @@ -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( 'twentytwentyone' ); @@ -25,51 +37,18 @@ test.describe( 'Front End Performance', () => { const samples = 16; const throwaway = 0; - const rounds = samples + throwaway; - for ( let i = 1; i <= rounds; i++ ) { - test( `Measure TTFB, LCP, and LCP-TTFB (${ i } of ${ rounds })`, async ( { - page, - } ) => { + const iterations = samples + throwaway; + for ( let i = 0; i < iterations; i++ ) { + test( `Measure TTFB, LCP, and LCP-TTFB (${ + i + 1 + } 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 ) { @@ -80,3 +59,5 @@ test.describe( 'Front End Performance', () => { } ); } } ); + +/* eslint-enable playwright/no-conditional-in-test, playwright/expect-expect */ diff --git a/test/performance/specs/post-editor.spec.js b/test/performance/specs/post-editor.spec.js index 13e0664550841..7e4184a5bf3db 100644 --- a/test/performance/specs/post-editor.spec.js +++ b/test/performance/specs/post-editor.spec.js @@ -1,27 +1,15 @@ -/** - * WordPress dependencies - */ -const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); +/* eslint-disable playwright/no-conditional-in-test, playwright/expect-expect */ /** - * External dependencies + * WordPress dependencies */ -const path = require( 'path' ); +import { test, expect } from '@wordpress/e2e-test-utils-playwright'; /** * Internal dependencies */ -const { - getTypingEventDurations, - getClickEventDurations, - getHoverEventDurations, - getSelectionEventDurations, - getLoadingDurations, - disableAutosave, - loadBlocksFromHtml, - load1000Paragraphs, - sum, -} = require( '../utils' ); +import { PerfUtils, Metrics } from '../fixtures'; +import { sum } from '../utils.js'; // See https://github.com/WordPress/gutenberg/issues/51383#issuecomment-1613460429 const BROWSER_IDLE_WAIT = 1000; @@ -43,6 +31,15 @@ const results = { }; test.describe( 'Post Editor Performance', () => { + test.use( { + perfUtils: async ( { browser, page }, use ) => { + await use( new PerfUtils( { browser, page } ) ); + }, + metrics: async ( { page }, use ) => { + await use( new Metrics( { page } ) ); + }, + } ); + test.afterAll( async ( {}, testInfo ) => { await testInfo.attach( 'results', { body: JSON.stringify( results, null, 2 ), @@ -53,56 +50,24 @@ test.describe( 'Post Editor Performance', () => { test.describe( 'Loading', () => { let draftURL = null; - test( 'Setup the test page', async ( { admin, page } ) => { + test( 'Setup the test post', async ( { admin, perfUtils } ) => { await admin.createNewPost(); - await loadBlocksFromHtml( - page, - path.join( process.env.ASSETS_PATH, 'large-post.html' ) - ); - - await page - .getByRole( 'button', { name: 'Save draft' } ) - .click( { timeout: 60_000 } ); - - await expect( - page.getByRole( 'button', { name: 'Saved' } ) - ).toBeDisabled(); - - // Get the URL that we will be testing against. - draftURL = page.url(); + await perfUtils.loadBlocksForLargePost(); + draftURL = await perfUtils.saveDraft(); } ); const samples = 10; const throwaway = 1; - const rounds = samples + throwaway; - for ( let i = 0; i < rounds; i++ ) { - test( `Get the durations (${ i + 1 } of ${ rounds })`, async ( { + const iterations = samples + throwaway; + for ( let i = 1; i <= iterations; i++ ) { + test( `Run the test (${ i } of ${ iterations })`, async ( { page, + perfUtils, + metrics, } ) => { - // Go to the test page. + // Open the test draft. await page.goto( draftURL ); - - // Get canvas (handles both legacy and iframed canvas). - const canvas = await Promise.any( [ - ( async () => { - const legacyCanvasLocator = page.locator( - '.wp-block-post-content' - ); - await legacyCanvasLocator.waitFor( { - timeout: 120_000, - } ); - return legacyCanvasLocator; - } )(), - ( async () => { - const iframedCanvasLocator = page.frameLocator( - '[name=editor-canvas]' - ); - await iframedCanvasLocator - .locator( 'body' ) - .waitFor( { timeout: 120_000 } ); - return iframedCanvasLocator; - } )(), - ] ); + const canvas = await perfUtils.getCanvas(); // Wait for the first block to be visible. await canvas.locator( '.wp-block' ).first().waitFor( { @@ -110,8 +75,10 @@ test.describe( 'Post Editor Performance', () => { } ); // Get the durations. - if ( i >= throwaway ) { - const loadingDurations = await getLoadingDurations( page ); + const loadingDurations = await metrics.getLoadingDurations(); + + // Save the results. + if ( i > throwaway ) { Object.entries( loadingDurations ).forEach( ( [ metric, duration ] ) => { results[ metric ].push( duration ); @@ -122,310 +89,401 @@ test.describe( 'Post Editor Performance', () => { } } ); - test( 'Typing', async ( { admin, browser, page, editor } ) => { - await admin.createNewPost(); - await disableAutosave( page ); + test.describe( 'Typing', () => { + let draftURL = null; - // Load the large post fixture. - await loadBlocksFromHtml( - page, - path.join( process.env.ASSETS_PATH, 'large-post.html' ) - ); + test( 'Setup the test post', async ( { admin, perfUtils, editor } ) => { + await admin.createNewPost(); + await perfUtils.loadBlocksForLargePost(); + await editor.insertBlock( { name: 'core/paragraph' } ); + draftURL = await perfUtils.saveDraft(); + } ); - // Append an empty paragraph. - await editor.insertBlock( { name: 'core/paragraph' } ); + test( 'Run the test', async ( { page, perfUtils, metrics } ) => { + await page.goto( draftURL ); + await perfUtils.disableAutosave(); + const canvas = await perfUtils.getCanvas(); - // Start tracing. - await browser.startTracing( page, { - screenshots: false, - categories: [ 'devtools.timeline' ], - } ); + const paragraph = canvas.getByRole( 'document', { + name: /Empty block/i, + } ); - // The first character typed triggers a longer time (isTyping change). - // It can impact the stability of the metric, so we exclude it. It - // probably deserves a dedicated metric itself, though. - const samples = 10; - const throwaway = 1; - const rounds = samples + throwaway; + // The first character typed triggers a longer time (isTyping change). + // It can impact the stability of the metric, so we exclude it. It + // probably deserves a dedicated metric itself, though. + const samples = 10; + const throwaway = 1; + const iterations = samples + throwaway; + + // Start tracing. + await perfUtils.startTracing(); + + // Type the testing sequence into the empty paragraph. + await paragraph.type( 'x'.repeat( iterations ), { + delay: BROWSER_IDLE_WAIT, + // The extended timeout is needed because the typing is very slow + // and the `delay` value itself does not extend it. + timeout: iterations * BROWSER_IDLE_WAIT * 2, // 2x the total time to be safe. + } ); - // Type the testing sequence into the empty paragraph. - await page.keyboard.type( 'x'.repeat( rounds ), { - delay: BROWSER_IDLE_WAIT, - } ); + // Stop tracing. + const traceResults = await perfUtils.stopTracing(); - // Stop tracing and save results. - const traceBuffer = await browser.stopTracing(); - const traceResults = JSON.parse( traceBuffer.toString() ); - const [ keyDownEvents, keyPressEvents, keyUpEvents ] = - getTypingEventDurations( traceResults ); + // Get the durations. + const [ keyDownEvents, keyPressEvents, keyUpEvents ] = + metrics.getTypingEventDurations( traceResults ); - for ( let i = throwaway; i < rounds; i++ ) { - results.type.push( - keyDownEvents[ i ] + keyPressEvents[ i ] + keyUpEvents[ i ] - ); - } + // Save the results. + for ( let i = throwaway; i < iterations; i++ ) { + results.type.push( + keyDownEvents[ i ] + keyPressEvents[ i ] + keyUpEvents[ i ] + ); + } + } ); } ); - test( 'Typing within containers', async ( { - admin, - browser, - page, - editor, - } ) => { - await admin.createNewPost(); - await disableAutosave( page ); - - await loadBlocksFromHtml( - page, - path.join( - process.env.ASSETS_PATH, - 'small-post-with-containers.html' - ) - ); - - // Select the block where we type in. - await editor.canvas - .getByRole( 'document', { name: 'Paragraph block' } ) - .first() - .click(); - - await browser.startTracing( page, { - screenshots: false, - categories: [ 'devtools.timeline' ], + test.describe( 'Typing within containers', () => { + let draftURL = null; + + test( 'Set up the test post', async ( { admin, perfUtils } ) => { + await admin.createNewPost(); + await perfUtils.loadBlocksForSmallPostWithContainers(); + draftURL = await perfUtils.saveDraft(); } ); - const samples = 10; - // The first character typed triggers a longer time (isTyping change). - // It can impact the stability of the metric, so we exclude it. It - // probably deserves a dedicated metric itself, though. - const throwaway = 1; - const rounds = samples + throwaway; + test( 'Run the test', async ( { page, perfUtils, metrics } ) => { + await page.goto( draftURL ); + await perfUtils.disableAutosave(); + const canvas = await perfUtils.getCanvas(); + + // Select the block where we type in. + const firstParagraph = canvas + .getByRole( 'document', { name: 'Paragraph block' } ) + .first(); + await firstParagraph.click(); + + // The first character typed triggers a longer time (isTyping change). + // It can impact the stability of the metric, so we exclude it. It + // probably deserves a dedicated metric itself, though. + const samples = 10; + const throwaway = 1; + const iterations = samples + throwaway; + + // Start tracing. + await perfUtils.startTracing(); + + // Start typing in the middle of the text. + await firstParagraph.type( 'x'.repeat( iterations ), { + delay: BROWSER_IDLE_WAIT, + // The extended timeout is needed because the typing is very slow + // and the `delay` value itself does not extend it. + timeout: iterations * BROWSER_IDLE_WAIT * 2, // 2x the total time to be safe. + } ); - // Start typing in the middle of the text. - await page.keyboard.type( 'x'.repeat( rounds ), { - delay: BROWSER_IDLE_WAIT, - } ); + // Stop tracing. + const traceResults = await perfUtils.stopTracing(); - const traceBuffer = await browser.stopTracing(); - const traceResults = JSON.parse( traceBuffer.toString() ); - const [ keyDownEvents, keyPressEvents, keyUpEvents ] = - getTypingEventDurations( traceResults ); + // Get the durations. + const [ keyDownEvents, keyPressEvents, keyUpEvents ] = + metrics.getTypingEventDurations( traceResults ); - for ( let i = throwaway; i < rounds; i++ ) { - results.typeContainer.push( - keyDownEvents[ i ] + keyPressEvents[ i ] + keyUpEvents[ i ] - ); - } + // Save the results. + for ( let i = throwaway; i < iterations; i++ ) { + results.typeContainer.push( + keyDownEvents[ i ] + keyPressEvents[ i ] + keyUpEvents[ i ] + ); + } + } ); } ); - test( 'Selecting blocks', async ( { admin, browser, page, editor } ) => { - await admin.createNewPost(); - await disableAutosave( page ); + test.describe( 'Selecting blocks', () => { + let draftURL = null; - await load1000Paragraphs( page ); - const paragraphs = editor.canvas.locator( '.wp-block' ); + test( 'Set up the test post', async ( { admin, perfUtils } ) => { + await admin.createNewPost(); + await perfUtils.load1000Paragraphs(); + draftURL = await perfUtils.saveDraft(); + } ); - const samples = 10; - const throwaway = 1; - const rounds = samples + throwaway; - for ( let i = 0; i < rounds; i++ ) { - // Wait for the browser to be idle before starting the monitoring. - // eslint-disable-next-line no-restricted-syntax - await page.waitForTimeout( BROWSER_IDLE_WAIT ); - await browser.startTracing( page, { - screenshots: false, - categories: [ 'devtools.timeline' ], + test( 'Run the test', async ( { page, perfUtils, metrics } ) => { + await page.goto( draftURL ); + await perfUtils.disableAutosave(); + const canvas = await perfUtils.getCanvas(); + + const paragraphs = canvas.getByRole( 'document', { + name: /Empty block/i, } ); - await paragraphs.nth( i ).click(); + const samples = 10; + const throwaway = 1; + const iterations = samples + throwaway; + for ( let i = 0; i < iterations; i++ ) { + // Wait for the browser to be idle before starting the monitoring. + // eslint-disable-next-line no-restricted-syntax + await page.waitForTimeout( BROWSER_IDLE_WAIT ); - const traceBuffer = await browser.stopTracing(); + // Start tracing. + await perfUtils.startTracing(); - if ( i >= throwaway ) { - const traceResults = JSON.parse( traceBuffer.toString() ); - const allDurations = getSelectionEventDurations( traceResults ); - results.focus.push( - allDurations.reduce( ( acc, eventDurations ) => { - return acc + sum( eventDurations ); - }, 0 ) - ); + // Click the next paragraph. + await paragraphs.nth( i ).click(); + + // Stop tracing. + const traceResults = await perfUtils.stopTracing(); + + // Get the durations. + const allDurations = + metrics.getSelectionEventDurations( traceResults ); + + // Save the results. + if ( i >= throwaway ) { + results.focus.push( + allDurations.reduce( ( acc, eventDurations ) => { + return acc + sum( eventDurations ); + }, 0 ) + ); + } } - } + } ); } ); - test( 'Opening persistent list view', async ( { - admin, - browser, - page, - } ) => { - await admin.createNewPost(); - await disableAutosave( page ); - - await load1000Paragraphs( page ); - const listViewToggle = page.getByRole( 'button', { - name: 'Document Overview', + test.describe( 'Opening persistent List View', () => { + let draftURL = null; + + test( 'Set up the test page', async ( { admin, perfUtils } ) => { + await admin.createNewPost(); + await perfUtils.load1000Paragraphs(); + draftURL = await perfUtils.saveDraft(); } ); - const samples = 10; - const throwaway = 1; - const rounds = samples + throwaway; - for ( let i = 0; i < rounds; i++ ) { - // Wait for the browser to be idle before starting the monitoring. - // eslint-disable-next-line no-restricted-syntax - await page.waitForTimeout( BROWSER_IDLE_WAIT ); - await browser.startTracing( page, { - screenshots: false, - categories: [ 'devtools.timeline' ], + test( 'Run the test', async ( { page, perfUtils, metrics } ) => { + await page.goto( draftURL ); + await perfUtils.disableAutosave(); + + const listViewToggle = page.getByRole( 'button', { + name: 'Document Overview', } ); - // Open List View - await listViewToggle.click(); + const samples = 10; + const throwaway = 1; + const iterations = samples + throwaway; + for ( let i = 0; i < iterations; i++ ) { + // Wait for the browser to be idle before starting the monitoring. + // eslint-disable-next-line no-restricted-syntax + await page.waitForTimeout( BROWSER_IDLE_WAIT ); + + // Start tracing. + await perfUtils.startTracing(); + + // Open List View. + await listViewToggle.click(); + await expect( listViewToggle ).toHaveAttribute( + 'aria-expanded', + 'true' + ); - const traceBuffer = await browser.stopTracing(); + // Stop tracing. + const traceResults = await perfUtils.stopTracing(); - if ( i >= throwaway ) { - const traceResults = JSON.parse( traceBuffer.toString() ); + // Get the durations. const [ mouseClickEvents ] = - getClickEventDurations( traceResults ); - results.listViewOpen.push( mouseClickEvents[ 0 ] ); - } + metrics.getClickEventDurations( traceResults ); - // Close List View - await listViewToggle.click(); - } + // Save the results. + if ( i >= throwaway ) { + results.listViewOpen.push( mouseClickEvents[ 0 ] ); + } + + // Close List View + await listViewToggle.click(); + await expect( listViewToggle ).toHaveAttribute( + 'aria-expanded', + 'false' + ); + } + } ); } ); - test( 'Opening the inserter', async ( { admin, browser, page } ) => { - await admin.createNewPost(); - await disableAutosave( page ); + test.describe( 'Opening Inserter', () => { + let draftURL = null; - await load1000Paragraphs( page ); - const globalInserterToggle = page.getByRole( 'button', { - name: 'Toggle block inserter', + test( 'Set up the test page', async ( { admin, perfUtils } ) => { + await admin.createNewPost(); + await perfUtils.load1000Paragraphs(); + draftURL = await perfUtils.saveDraft(); } ); - const samples = 10; - const throwaway = 1; - const rounds = samples + throwaway; - for ( let i = 0; i < rounds; i++ ) { - // Wait for the browser to be idle before starting the monitoring. - // eslint-disable-next-line no-restricted-syntax - await page.waitForTimeout( BROWSER_IDLE_WAIT ); - await browser.startTracing( page, { - screenshots: false, - categories: [ 'devtools.timeline' ], + test( 'Run the test', async ( { page, perfUtils, metrics } ) => { + // Go to the test page. + await page.goto( draftURL ); + await perfUtils.disableAutosave(); + const globalInserterToggle = page.getByRole( 'button', { + name: 'Toggle block inserter', } ); - // Open Inserter. - await globalInserterToggle.click(); + const samples = 10; + const throwaway = 1; + const iterations = samples + throwaway; + for ( let i = 0; i < iterations; i++ ) { + // Wait for the browser to be idle before starting the monitoring. + // eslint-disable-next-line no-restricted-syntax + await page.waitForTimeout( BROWSER_IDLE_WAIT ); + + // Start tracing. + await perfUtils.startTracing(); + + // Open Inserter. + await globalInserterToggle.click(); + await expect( globalInserterToggle ).toHaveAttribute( + 'aria-expanded', + 'true' + ); - const traceBuffer = await browser.stopTracing(); + // Stop tracing. + const traceResults = await perfUtils.stopTracing(); - if ( i >= throwaway ) { - const traceResults = JSON.parse( traceBuffer.toString() ); + // Get the durations. const [ mouseClickEvents ] = - getClickEventDurations( traceResults ); - results.inserterOpen.push( mouseClickEvents[ 0 ] ); - } + metrics.getClickEventDurations( traceResults ); - // Close Inserter. - await globalInserterToggle.click(); - } + // Save the results. + if ( i >= throwaway ) { + results.inserterOpen.push( mouseClickEvents[ 0 ] ); + } + + // Close Inserter. + await globalInserterToggle.click(); + await expect( globalInserterToggle ).toHaveAttribute( + 'aria-expanded', + 'false' + ); + } + } ); } ); - test( 'Searching the inserter', async ( { admin, browser, page } ) => { - await admin.createNewPost(); - await disableAutosave( page ); + test.describe( 'Searching Inserter', () => { + let draftURL = null; - await load1000Paragraphs( page ); - const globalInserterToggle = page.getByRole( 'button', { - name: 'Toggle block inserter', + test( 'Set up the test page', async ( { admin, perfUtils } ) => { + await admin.createNewPost(); + await perfUtils.load1000Paragraphs(); + draftURL = await perfUtils.saveDraft(); } ); - // Open Inserter. - await globalInserterToggle.click(); - - const samples = 10; - const throwaway = 1; - const rounds = samples + throwaway; - for ( let i = 0; i < rounds; i++ ) { - // Wait for the browser to be idle before starting the monitoring. - // eslint-disable-next-line no-restricted-syntax - await page.waitForTimeout( BROWSER_IDLE_WAIT ); - await browser.startTracing( page, { - screenshots: false, - categories: [ 'devtools.timeline' ], + test( 'Run the test', async ( { page, perfUtils, metrics } ) => { + // Go to the test page. + await page.goto( draftURL ); + await perfUtils.disableAutosave(); + const globalInserterToggle = page.getByRole( 'button', { + name: 'Toggle block inserter', } ); - await page.keyboard.type( 'p' ); + // Open Inserter. + await globalInserterToggle.click(); + await expect( globalInserterToggle ).toHaveAttribute( + 'aria-expanded', + 'true' + ); + + const samples = 10; + const throwaway = 1; + const iterations = samples + throwaway; + for ( let i = 0; i < iterations; i++ ) { + // Wait for the browser to be idle before starting the monitoring. + // eslint-disable-next-line no-restricted-syntax + await page.waitForTimeout( BROWSER_IDLE_WAIT ); + + // Start tracing. + await perfUtils.startTracing(); + + // Type to trigger search. + await page.keyboard.type( 'p' ); - const traceBuffer = await browser.stopTracing(); + // Stop tracing. + const traceResults = await perfUtils.stopTracing(); - if ( i >= throwaway ) { - const traceResults = JSON.parse( traceBuffer.toString() ); + // Get the durations. const [ keyDownEvents, keyPressEvents, keyUpEvents ] = - getTypingEventDurations( traceResults ); - results.inserterSearch.push( - keyDownEvents[ 0 ] + keyPressEvents[ 0 ] + keyUpEvents[ 0 ] - ); - } + metrics.getTypingEventDurations( traceResults ); - await page.keyboard.press( 'Backspace' ); - } + // Save the results. + if ( i >= throwaway ) { + results.inserterSearch.push( + keyDownEvents[ 0 ] + + keyPressEvents[ 0 ] + + keyUpEvents[ 0 ] + ); + } - // Close Inserter. - await globalInserterToggle.click(); + await page.keyboard.press( 'Backspace' ); + } + } ); } ); - test( 'Hovering Inserter Items', async ( { admin, browser, page } ) => { - await admin.createNewPost(); - await disableAutosave( page ); + test.describe( 'Hovering Inserter items', () => { + let draftURL = null; - await load1000Paragraphs( page ); - const globalInserterToggle = page.getByRole( 'button', { - name: 'Toggle block inserter', + test( 'Set up the test page', async ( { admin, perfUtils } ) => { + await admin.createNewPost(); + await perfUtils.load1000Paragraphs(); + draftURL = await perfUtils.saveDraft(); } ); - const paragraphBlockItem = page.locator( - '.block-editor-inserter__menu .editor-block-list-item-paragraph' - ); - const headingBlockItem = page.locator( - '.block-editor-inserter__menu .editor-block-list-item-heading' - ); - // Open Inserter. - await globalInserterToggle.click(); + test( 'Run the test', async ( { page, perfUtils, metrics } ) => { + // Go to the test page. + await page.goto( draftURL ); + await perfUtils.disableAutosave(); - const samples = 10; - const throwaway = 1; - const rounds = samples + throwaway; - for ( let i = 0; i < rounds; i++ ) { - // Wait for the browser to be idle before starting the monitoring. - // eslint-disable-next-line no-restricted-syntax - await page.waitForTimeout( BROWSER_IDLE_WAIT ); - await browser.startTracing( page, { - screenshots: false, - categories: [ 'devtools.timeline' ], + const globalInserterToggle = page.getByRole( 'button', { + name: 'Toggle block inserter', } ); + const paragraphBlockItem = page.locator( + '.block-editor-inserter__menu .editor-block-list-item-paragraph' + ); + const headingBlockItem = page.locator( + '.block-editor-inserter__menu .editor-block-list-item-heading' + ); + + // Open Inserter. + await globalInserterToggle.click(); + await expect( globalInserterToggle ).toHaveAttribute( + 'aria-expanded', + 'true' + ); + + const samples = 10; + const throwaway = 1; + const iterations = samples + throwaway; + for ( let i = 0; i < iterations; i++ ) { + // Wait for the browser to be idle before starting the monitoring. + // eslint-disable-next-line no-restricted-syntax + await page.waitForTimeout( BROWSER_IDLE_WAIT ); - // Hover Items. - await paragraphBlockItem.hover(); - await headingBlockItem.hover(); + // Start tracing. + await perfUtils.startTracing(); - const traceBuffer = await browser.stopTracing(); + // Hover Inserter items. + await paragraphBlockItem.hover(); + await headingBlockItem.hover(); - if ( i >= throwaway ) { - const traceResults = JSON.parse( traceBuffer.toString() ); + // Stop tracing. + const traceResults = await perfUtils.stopTracing(); + + // Get the durations. const [ mouseOverEvents, mouseOutEvents ] = - getHoverEventDurations( traceResults ); - for ( let k = 0; k < mouseOverEvents.length; k++ ) { - results.inserterHover.push( - mouseOverEvents[ k ] + mouseOutEvents[ k ] - ); + metrics.getHoverEventDurations( traceResults ); + + // Save the results. + if ( i >= throwaway ) { + for ( let k = 0; k < mouseOverEvents.length; k++ ) { + results.inserterHover.push( + mouseOverEvents[ k ] + mouseOutEvents[ k ] + ); + } } } - } - - // Close Inserter. - await globalInserterToggle.click(); + } ); } ); } ); + +/* eslint-enable playwright/no-conditional-in-test, playwright/expect-expect */ diff --git a/test/performance/specs/site-editor.spec.js b/test/performance/specs/site-editor.spec.js index d6cc65dd69bd2..6443f953735eb 100644 --- a/test/performance/specs/site-editor.spec.js +++ b/test/performance/specs/site-editor.spec.js @@ -1,22 +1,14 @@ -/** - * WordPress dependencies - */ -const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); +/* eslint-disable playwright/no-conditional-in-test, playwright/expect-expect */ /** - * External dependencies + * WordPress dependencies */ -const path = require( 'path' ); +import { test } from '@wordpress/e2e-test-utils-playwright'; /** * Internal dependencies */ -const { - getTypingEventDurations, - getLoadingDurations, - disableAutosave, - loadBlocksFromHtml, -} = require( '../utils' ); +import { PerfUtils, Metrics } from '../fixtures'; // See https://github.com/WordPress/gutenberg/issues/51383#issuecomment-1613460429 const BROWSER_IDLE_WAIT = 1000; @@ -38,6 +30,15 @@ const results = { }; test.describe( 'Site Editor Performance', () => { + test.use( { + perfUtils: async ( { browser, page }, use ) => { + await use( new PerfUtils( { browser, page } ) ); + }, + metrics: async ( { page }, use ) => { + await use( new Metrics( { page } ) ); + }, + } ); + test.beforeAll( async ( { requestUtils } ) => { await requestUtils.activateTheme( 'emptytheme' ); await requestUtils.deleteAllTemplates( 'wp_template' ); @@ -58,52 +59,42 @@ test.describe( 'Site Editor Performance', () => { test.describe( 'Loading', () => { let draftURL = null; - test( 'Setup the test page', async ( { page, admin } ) => { - // Load the large post fixture. + test( 'Setup the test page', async ( { page, admin, perfUtils } ) => { await admin.createNewPost( { postType: 'page' } ); - await loadBlocksFromHtml( - page, - path.join( process.env.ASSETS_PATH, 'large-post.html' ) - ); - - // Save the draft. - await page - .getByRole( 'button', { name: 'Save draft' } ) - .click( { timeout: 60_000 } ); - await expect( - page.getByRole( 'button', { name: 'Saved' } ) - ).toBeDisabled(); + await perfUtils.loadBlocksForLargePost(); + await perfUtils.saveDraft(); - // Open the test page in Site Editor. await admin.visitSiteEditor( { postId: new URL( page.url() ).searchParams.get( 'post' ), postType: 'page', } ); - // Get the URL that we will be testing against. draftURL = page.url(); } ); const samples = 10; const throwaway = 1; - const rounds = samples + throwaway; - for ( let i = 0; i < rounds; i++ ) { - test( `Get the durations (${ i + 1 } of ${ rounds })`, async ( { + const iterations = samples + throwaway; + for ( let i = 0; i < iterations; i++ ) { + test( `Run the test (${ i + 1 } of ${ iterations })`, async ( { page, + perfUtils, + metrics, } ) => { - // Go to the test page. + // Go to the test draft. await page.goto( draftURL ); + const canvas = await perfUtils.getCanvas(); + + // Wait for the first block to be visible. + await canvas.locator( '.wp-block' ).first().waitFor( { + timeout: 120_000, + } ); - // Wait for the first block. - await page - .frameLocator( 'iframe[name="editor-canvas"]' ) - .locator( '.wp-block' ) - .first() - .waitFor( { timeout: 120_000 } ); + // Get the durations. + const loadingDurations = await metrics.getLoadingDurations(); // Save the results. - if ( i >= throwaway ) { - const loadingDurations = await getLoadingDurations( page ); + if ( i > throwaway ) { Object.entries( loadingDurations ).forEach( ( [ metric, duration ] ) => { results[ metric ].push( duration ); @@ -113,84 +104,84 @@ test.describe( 'Site Editor Performance', () => { } ); } } ); + test.describe( 'Typing', () => { + let draftURL = null; - test( 'Typing', async ( { browser, page, admin, editor } ) => { - // Load the large post fixture. - await admin.createNewPost( { postType: 'page' } ); - await disableAutosave( page ); - await loadBlocksFromHtml( + test( 'Setup the test post', async ( { page, - path.join( process.env.ASSETS_PATH, 'large-post.html' ) - ); - - // Save the draft. - await page - .getByRole( 'button', { name: 'Save draft' } ) - // Loading the large post HTML can take some time so we need a higher - // timeout value here. - .click( { timeout: 60_000 } ); - await expect( - page.getByRole( 'button', { name: 'Saved' } ) - ).toBeDisabled(); - - // Open the test page in Site Editor. - await admin.visitSiteEditor( { - postId: new URL( page.url() ).searchParams.get( 'post' ), - postType: 'page', - } ); + admin, + editor, + perfUtils, + } ) => { + await admin.createNewPost( { postType: 'page' } ); + await perfUtils.loadBlocksForLargePost(); + await editor.insertBlock( { name: 'core/paragraph' } ); + await perfUtils.saveDraft(); + + await admin.visitSiteEditor( { + postId: new URL( page.url() ).searchParams.get( 'post' ), + postType: 'page', + } ); - // Wait for the first paragraph to be ready. - const firstParagraph = editor.canvas - .getByText( 'Lorem ipsum dolor sit amet' ) - .first(); - await firstParagraph.waitFor( { timeout: 120_000 } ); - - // Enter edit mode. - await editor.canvas.locator( 'body' ).click(); - // Second click is needed for the legacy edit mode. - await editor.canvas - .getByRole( 'document', { name: /Block:( Post)? Content/ } ) - .click(); - - // Append an empty paragraph. - // Since `editor.insertBlock( { name: 'core/paragraph' } )` is not - // working in page edit mode, we need to _manually_ insert a new - // paragraph. - await editor.canvas - .getByText( 'Quamquam tu hanc copiosiorem etiam soles dicere.' ) - .last() - .click(); // Enters edit mode for the last post's element, which is a list item. - - await page.keyboard.press( 'Enter' ); // Creates a new list item. - await page.keyboard.press( 'Enter' ); // Exits the list and creates a new paragraph. - - // Start tracing. - await browser.startTracing( page, { - screenshots: false, - categories: [ 'devtools.timeline' ], + draftURL = page.url(); } ); + test( 'Run the test', async ( { page, perfUtils, metrics } ) => { + await page.goto( draftURL ); + await perfUtils.disableAutosave(); + + // Wait for the loader overlay to disappear. This is necessary + // because the overlay is still visible for a while after the editor + // canvas is ready, and we don't want it to affect the typing + // timings. + await page + .locator( '.edit-site-canvas-loader' ) + .waitFor( { state: 'hidden', timeout: 120_000 } ); - // The first character typed triggers a longer time (isTyping change). - // It can impact the stability of the metric, so we exclude it. It - // probably deserves a dedicated metric itself, though. - const samples = 10; - const throwaway = 1; - const rounds = samples + throwaway; + const canvas = await perfUtils.getCanvas(); - // Type the testing sequence into the empty paragraph. - await page.keyboard.type( 'x'.repeat( rounds ), { - delay: BROWSER_IDLE_WAIT, - } ); + // Enter edit mode (second click is needed for the legacy edit mode). + await canvas.locator( 'body' ).click(); + await canvas + .getByRole( 'document', { name: /Block:( Post)? Content/ } ) + .click(); - // Stop tracing and save results. - const traceBuffer = await browser.stopTracing(); - const traceResults = JSON.parse( traceBuffer.toString() ); - const [ keyDownEvents, keyPressEvents, keyUpEvents ] = - getTypingEventDurations( traceResults ); - for ( let i = throwaway; i < rounds; i++ ) { - results.type.push( - keyDownEvents[ i ] + keyPressEvents[ i ] + keyUpEvents[ i ] - ); - } + const paragraph = canvas.getByRole( 'document', { + name: /Empty block/i, + } ); + + // The first character typed triggers a longer time (isTyping change). + // It can impact the stability of the metric, so we exclude it. It + // probably deserves a dedicated metric itself, though. + const samples = 10; + const throwaway = 1; + const iterations = samples + throwaway; + + // Start tracing. + await perfUtils.startTracing(); + + // Type the testing sequence into the empty paragraph. + await paragraph.type( 'x'.repeat( iterations ), { + delay: BROWSER_IDLE_WAIT, + // The extended timeout is needed because the typing is very slow + // and the `delay` value itself does not extend it. + timeout: iterations * BROWSER_IDLE_WAIT * 2, // 2x the total time to be safe. + } ); + + // Stop tracing. + const traceResults = await perfUtils.stopTracing(); + + // Get the durations. + const [ keyDownEvents, keyPressEvents, keyUpEvents ] = + metrics.getTypingEventDurations( traceResults ); + + // Save the results. + for ( let i = throwaway; i < iterations; i++ ) { + results.type.push( + keyDownEvents[ i ] + keyPressEvents[ i ] + keyUpEvents[ i ] + ); + } + } ); } ); } ); + +/* eslint-enable playwright/no-conditional-in-test, playwright/expect-expect */ diff --git a/test/performance/utils.js b/test/performance/utils.js index 3923d353577bc..e14ff71436d73 100644 --- a/test/performance/utils.js +++ b/test/performance/utils.js @@ -58,150 +58,3 @@ export function deleteFile( filePath ) { unlinkSync( filePath ); } } - -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 ); -} - -export function getTypingEventDurations( trace ) { - return [ - getEventDurationsForType( trace, isKeyDownEvent ), - getEventDurationsForType( trace, isKeyPressEvent ), - getEventDurationsForType( trace, isKeyUpEvent ), - ]; -} - -export function getSelectionEventDurations( trace ) { - return [ - getEventDurationsForType( trace, isFocusEvent ), - getEventDurationsForType( trace, isFocusInEvent ), - ]; -} - -export function getClickEventDurations( trace ) { - return [ getEventDurationsForType( trace, isClickEvent ) ]; -} - -export function getHoverEventDurations( trace ) { - return [ - getEventDurationsForType( trace, isMouseOverEvent ), - getEventDurationsForType( trace, isMouseOutEvent ), - ]; -} - -export async function getLoadingDurations( page ) { - return await 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. - firstBlock: performance.now() - responseEnd, - }; - } ); -} - -export async function loadBlocksFromHtml( page, filepath ) { - if ( ! existsSync( filepath ) ) { - throw new Error( `File not found (${ filepath })` ); - } - - return await 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 ) ); -} - -export async function load1000Paragraphs( page ) { - await 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 ); - } ); -} - -export async function disableAutosave( page ) { - return await page.evaluate( () => { - window.wp.data.dispatch( 'core/editor' ).updateEditorSettings( { - autosaveInterval: 100000000000, - localAutosaveInterval: 100000000000, - } ); - } ); -} From cc8158a1923aa093a6bef29a61f7d9e047c9477b Mon Sep 17 00:00:00 2001 From: Bart Kalisz Date: Fri, 15 Sep 2023 11:57:27 +0200 Subject: [PATCH 05/14] We're only waiting for the first block to be available It might still be covered by the loading overlay, which doesn't mean it's not available, which is what we're interested in. --- test/performance/specs/post-editor.spec.js | 2 +- test/performance/specs/site-editor.spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/performance/specs/post-editor.spec.js b/test/performance/specs/post-editor.spec.js index 7e4184a5bf3db..6bfcd6597d04f 100644 --- a/test/performance/specs/post-editor.spec.js +++ b/test/performance/specs/post-editor.spec.js @@ -69,7 +69,7 @@ test.describe( 'Post Editor Performance', () => { await page.goto( draftURL ); const canvas = await perfUtils.getCanvas(); - // Wait for the first block to be visible. + // Wait for the first block. await canvas.locator( '.wp-block' ).first().waitFor( { timeout: 120_000, } ); diff --git a/test/performance/specs/site-editor.spec.js b/test/performance/specs/site-editor.spec.js index 6443f953735eb..ba93519e6e56c 100644 --- a/test/performance/specs/site-editor.spec.js +++ b/test/performance/specs/site-editor.spec.js @@ -85,7 +85,7 @@ test.describe( 'Site Editor Performance', () => { await page.goto( draftURL ); const canvas = await perfUtils.getCanvas(); - // Wait for the first block to be visible. + // Wait for the first block. await canvas.locator( '.wp-block' ).first().waitFor( { timeout: 120_000, } ); From 7000dec29793b8db426b80a4a70bdcdd823aa469 Mon Sep 17 00:00:00 2001 From: Bart Kalisz Date: Mon, 18 Sep 2023 13:50:45 +0200 Subject: [PATCH 06/14] Make the metrics fixture context-agnostic --- test/performance/fixtures/metrics.js | 3 +-- test/performance/specs/post-editor.spec.js | 6 +++++- test/performance/specs/site-editor.spec.js | 6 +++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/test/performance/fixtures/metrics.js b/test/performance/fixtures/metrics.js index b0a9e69d69c3a..ae76b0d1ebad2 100644 --- a/test/performance/fixtures/metrics.js +++ b/test/performance/fixtures/metrics.js @@ -64,8 +64,7 @@ export class Metrics { paintTimings.find( ( { name } ) => name === 'first-contentful-paint' ).startTime - responseEnd, - // This is evaluated right after Playwright found the block selector. - firstBlock: performance.now() - responseEnd, + timeSinceResponseEnd: performance.now() - responseEnd, }; } ); } diff --git a/test/performance/specs/post-editor.spec.js b/test/performance/specs/post-editor.spec.js index 6bfcd6597d04f..81bd4344d18b0 100644 --- a/test/performance/specs/post-editor.spec.js +++ b/test/performance/specs/post-editor.spec.js @@ -81,7 +81,11 @@ test.describe( 'Post Editor Performance', () => { if ( i > throwaway ) { Object.entries( loadingDurations ).forEach( ( [ metric, duration ] ) => { - results[ metric ].push( duration ); + if ( metric === 'timeSinceResponseEnd' ) { + results.firstBlock.push( duration ); + } else { + results[ metric ].push( duration ); + } } ); } diff --git a/test/performance/specs/site-editor.spec.js b/test/performance/specs/site-editor.spec.js index ba93519e6e56c..0a774c5e517eb 100644 --- a/test/performance/specs/site-editor.spec.js +++ b/test/performance/specs/site-editor.spec.js @@ -97,7 +97,11 @@ test.describe( 'Site Editor Performance', () => { if ( i > throwaway ) { Object.entries( loadingDurations ).forEach( ( [ metric, duration ] ) => { - results[ metric ].push( duration ); + if ( metric === 'timeSinceResponseEnd' ) { + results.firstBlock.push( duration ); + } else { + results[ metric ].push( duration ); + } } ); } From 55c071bed0110ca392607108a696ef703c669075 Mon Sep 17 00:00:00 2001 From: Bart Kalisz Date: Wed, 20 Sep 2023 10:34:40 +0200 Subject: [PATCH 07/14] Make sample iteration consistent --- .../specs/front-end-block-theme.spec.js | 11 +++++----- .../specs/front-end-classic-theme.spec.js | 11 +++++----- test/performance/specs/post-editor.spec.js | 20 +++++++++---------- test/performance/specs/site-editor.spec.js | 4 ++-- 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/test/performance/specs/front-end-block-theme.spec.js b/test/performance/specs/front-end-block-theme.spec.js index c04007ca60d8e..7cc74b792486a 100644 --- a/test/performance/specs/front-end-block-theme.spec.js +++ b/test/performance/specs/front-end-block-theme.spec.js @@ -39,10 +39,11 @@ test.describe( 'Front End Performance', () => { const samples = 16; const throwaway = 0; const iterations = samples + throwaway; - for ( let i = 0; i < iterations; i++ ) { - test( `Measure TTFB, LCP, and LCP-TTFB (${ - i + 1 - } of ${ iterations })`, async ( { page, metrics } ) => { + for ( let i = 1; i <= iterations; i++ ) { + test( `Measure TTFB, LCP, and LCP-TTFB (${ i } of ${ iterations })`, async ( { + page, + metrics, + } ) => { // Go to the base URL. // eslint-disable-next-line playwright/no-networkidle await page.goto( '/', { waitUntil: 'networkidle' } ); @@ -52,7 +53,7 @@ test.describe( 'Front End Performance', () => { const lcp = await metrics.getLargestContentfulPaint(); // Save the results. - if ( i >= throwaway ) { + if ( i > throwaway ) { results.largestContentfulPaint.push( lcp ); results.timeToFirstByte.push( ttfb ); results.lcpMinusTtfb.push( lcp - ttfb ); diff --git a/test/performance/specs/front-end-classic-theme.spec.js b/test/performance/specs/front-end-classic-theme.spec.js index 339b6324f4dda..ff6b6f61aaa6c 100644 --- a/test/performance/specs/front-end-classic-theme.spec.js +++ b/test/performance/specs/front-end-classic-theme.spec.js @@ -38,10 +38,11 @@ test.describe( 'Front End Performance', () => { const samples = 16; const throwaway = 0; const iterations = samples + throwaway; - for ( let i = 0; i < iterations; i++ ) { - test( `Measure TTFB, LCP, and LCP-TTFB (${ - i + 1 - } of ${ iterations })`, async ( { page, metrics } ) => { + for ( let i = 1; i <= iterations; i++ ) { + test( `Measure TTFB, LCP, and LCP-TTFB (${ i } of ${ iterations })`, async ( { + page, + metrics, + } ) => { // Go to the base URL. // eslint-disable-next-line playwright/no-networkidle await page.goto( '/', { waitUntil: 'networkidle' } ); @@ -51,7 +52,7 @@ test.describe( 'Front End Performance', () => { const lcp = await metrics.getLargestContentfulPaint(); // Save the results. - if ( i >= throwaway ) { + if ( i > throwaway ) { results.largestContentfulPaint.push( lcp ); results.timeToFirstByte.push( ttfb ); results.lcpMinusTtfb.push( lcp - ttfb ); diff --git a/test/performance/specs/post-editor.spec.js b/test/performance/specs/post-editor.spec.js index 81bd4344d18b0..56b2935a28552 100644 --- a/test/performance/specs/post-editor.spec.js +++ b/test/performance/specs/post-editor.spec.js @@ -221,7 +221,7 @@ test.describe( 'Post Editor Performance', () => { const samples = 10; const throwaway = 1; const iterations = samples + throwaway; - for ( let i = 0; i < iterations; i++ ) { + for ( let i = 1; i <= iterations; i++ ) { // Wait for the browser to be idle before starting the monitoring. // eslint-disable-next-line no-restricted-syntax await page.waitForTimeout( BROWSER_IDLE_WAIT ); @@ -240,7 +240,7 @@ test.describe( 'Post Editor Performance', () => { metrics.getSelectionEventDurations( traceResults ); // Save the results. - if ( i >= throwaway ) { + if ( i > throwaway ) { results.focus.push( allDurations.reduce( ( acc, eventDurations ) => { return acc + sum( eventDurations ); @@ -271,7 +271,7 @@ test.describe( 'Post Editor Performance', () => { const samples = 10; const throwaway = 1; const iterations = samples + throwaway; - for ( let i = 0; i < iterations; i++ ) { + for ( let i = 1; i <= iterations; i++ ) { // Wait for the browser to be idle before starting the monitoring. // eslint-disable-next-line no-restricted-syntax await page.waitForTimeout( BROWSER_IDLE_WAIT ); @@ -294,7 +294,7 @@ test.describe( 'Post Editor Performance', () => { metrics.getClickEventDurations( traceResults ); // Save the results. - if ( i >= throwaway ) { + if ( i > throwaway ) { results.listViewOpen.push( mouseClickEvents[ 0 ] ); } @@ -328,7 +328,7 @@ test.describe( 'Post Editor Performance', () => { const samples = 10; const throwaway = 1; const iterations = samples + throwaway; - for ( let i = 0; i < iterations; i++ ) { + for ( let i = 1; i <= iterations; i++ ) { // Wait for the browser to be idle before starting the monitoring. // eslint-disable-next-line no-restricted-syntax await page.waitForTimeout( BROWSER_IDLE_WAIT ); @@ -351,7 +351,7 @@ test.describe( 'Post Editor Performance', () => { metrics.getClickEventDurations( traceResults ); // Save the results. - if ( i >= throwaway ) { + if ( i > throwaway ) { results.inserterOpen.push( mouseClickEvents[ 0 ] ); } @@ -392,7 +392,7 @@ test.describe( 'Post Editor Performance', () => { const samples = 10; const throwaway = 1; const iterations = samples + throwaway; - for ( let i = 0; i < iterations; i++ ) { + for ( let i = 1; i <= iterations; i++ ) { // Wait for the browser to be idle before starting the monitoring. // eslint-disable-next-line no-restricted-syntax await page.waitForTimeout( BROWSER_IDLE_WAIT ); @@ -411,7 +411,7 @@ test.describe( 'Post Editor Performance', () => { metrics.getTypingEventDurations( traceResults ); // Save the results. - if ( i >= throwaway ) { + if ( i > throwaway ) { results.inserterSearch.push( keyDownEvents[ 0 ] + keyPressEvents[ 0 ] + @@ -458,7 +458,7 @@ test.describe( 'Post Editor Performance', () => { const samples = 10; const throwaway = 1; const iterations = samples + throwaway; - for ( let i = 0; i < iterations; i++ ) { + for ( let i = 1; i <= iterations; i++ ) { // Wait for the browser to be idle before starting the monitoring. // eslint-disable-next-line no-restricted-syntax await page.waitForTimeout( BROWSER_IDLE_WAIT ); @@ -478,7 +478,7 @@ test.describe( 'Post Editor Performance', () => { metrics.getHoverEventDurations( traceResults ); // Save the results. - if ( i >= throwaway ) { + if ( i > throwaway ) { for ( let k = 0; k < mouseOverEvents.length; k++ ) { results.inserterHover.push( mouseOverEvents[ k ] + mouseOutEvents[ k ] diff --git a/test/performance/specs/site-editor.spec.js b/test/performance/specs/site-editor.spec.js index 0a774c5e517eb..38c344f149237 100644 --- a/test/performance/specs/site-editor.spec.js +++ b/test/performance/specs/site-editor.spec.js @@ -75,8 +75,8 @@ test.describe( 'Site Editor Performance', () => { const samples = 10; const throwaway = 1; const iterations = samples + throwaway; - for ( let i = 0; i < iterations; i++ ) { - test( `Run the test (${ i + 1 } of ${ iterations })`, async ( { + for ( let i = 1; i <= iterations; i++ ) { + test( `Run the test (${ i } of ${ iterations })`, async ( { page, perfUtils, metrics, From 1b0074193fe13ea72231bfbe1cf9fc43c36966f4 Mon Sep 17 00:00:00 2001 From: Bart Kalisz Date: Wed, 20 Sep 2023 15:29:55 +0200 Subject: [PATCH 08/14] Merge with the Metrics e2e fixture --- .../src/lighthouse/index.ts | 13 +- .../src/metrics/index.ts | 190 ++++++++++++++++-- .../e2e-test-utils-playwright/src/test.ts | 4 +- test/performance/fixtures/index.js | 1 - test/performance/fixtures/metrics.js | 145 ------------- test/performance/fixtures/perf-utils.js | 18 +- .../specs/front-end-block-theme.spec.js | 7 +- .../specs/front-end-classic-theme.spec.js | 7 +- test/performance/specs/post-editor.spec.js | 53 +++-- test/performance/specs/site-editor.spec.js | 14 +- 10 files changed, 223 insertions(+), 229 deletions(-) delete mode 100644 test/performance/fixtures/metrics.js diff --git a/packages/e2e-test-utils-playwright/src/lighthouse/index.ts b/packages/e2e-test-utils-playwright/src/lighthouse/index.ts index 274799db6c9c2..0ba2c8e67ba61 100644 --- a/packages/e2e-test-utils-playwright/src/lighthouse/index.ts +++ b/packages/e2e-test-utils-playwright/src/lighthouse/index.ts @@ -4,11 +4,16 @@ import type { Page } from '@playwright/test'; import * as lighthouse from 'lighthouse/core/index.cjs'; +type LighthouseConstructorProps = { + page: Page; + port: number; +}; + export class Lighthouse { - constructor( - public readonly page: Page, - public readonly port: number - ) { + page: Page; + port: number; + + constructor( { page, port }: LighthouseConstructorProps ) { this.page = page; this.port = port; } diff --git a/packages/e2e-test-utils-playwright/src/metrics/index.ts b/packages/e2e-test-utils-playwright/src/metrics/index.ts index d90a2f34fc510..7c9a47e7e8f75 100644 --- a/packages/e2e-test-utils-playwright/src/metrics/index.ts +++ b/packages/e2e-test-utils-playwright/src/metrics/index.ts @@ -1,11 +1,51 @@ /** * External dependencies */ -import type { Page } from '@playwright/test'; +import type { Page, Browser } from '@playwright/test'; + +type EventType = + | 'click' + | 'focus' + | 'focusin' + | 'keydown' + | 'keypress' + | 'keyup' + | 'mouseout' + | 'mouseover'; + +interface TraceEvent { + cat: string; + name: string; + dur?: number; + args: { + data?: { + type: EventType; + }; + }; +} + +interface LoadingDurations { + serverResponse: number; + firstPaint: number; + domContentLoaded: number; + loaded: number; + firstContentfulPaint: number; + timeSinceResponseEnd: number; +} + +type MetricsConstructorProps = { + page: Page; +}; export class Metrics { - constructor( public readonly page: Page ) { + browser: Browser; + page: Page; + trace: { traceEvents: TraceEvent[] }; + + constructor( { page }: MetricsConstructorProps ) { this.page = page; + this.browser = page.context().browser()!; + this.trace = { traceEvents: [] }; } /** @@ -37,11 +77,9 @@ export class Metrics { * Returns time to first byte (TTFB) using the Navigation Timing API. * * @see https://web.dev/ttfb/#measure-ttfb-in-javascript - * - * @return {Promise} TTFB value. */ - async getTimeToFirstByte() { - return this.page.evaluate< number >( () => { + async getTimeToFirstByte(): Promise< number > { + return await this.page.evaluate< number >( () => { const { responseStart, startTime } = ( performance.getEntriesByType( 'navigation' @@ -56,11 +94,9 @@ export class Metrics { * * @see https://w3c.github.io/largest-contentful-paint/ * @see https://web.dev/lcp/#measure-lcp-in-javascript - * - * @return {Promise} LCP value. */ - async getLargestContentfulPaint() { - return this.page.evaluate< number >( + async getLargestContentfulPaint(): Promise< number > { + return await this.page.evaluate< number >( () => new Promise( ( resolve ) => { new PerformanceObserver( ( entryList ) => { @@ -82,11 +118,9 @@ export class Metrics { * * @see https://github.com/WICG/layout-instability * @see https://web.dev/cls/#measure-layout-shifts-in-javascript - * - * @return {Promise} CLS value. */ - async getCumulativeLayoutShift() { - return this.page.evaluate< number >( + async getCumulativeLayoutShift(): Promise< number > { + return await this.page.evaluate< number >( () => new Promise( ( resolve ) => { let CLS = 0; @@ -108,4 +142,132 @@ export class Metrics { } ) ); } + + /** + * Returns the loading durations using the Navigation Timing API. All the + * durations exclude the server response time. + */ + async getLoadingDurations(): Promise< LoadingDurations > { + return await this.page.evaluate( () => { + const [ + { + requestStart, + responseStart, + responseEnd, + domContentLoadedEventEnd, + loadEventEnd, + }, + ] = performance.getEntriesByType( + 'navigation' + ) as PerformanceNavigationTiming[]; + const paintTimings = performance.getEntriesByType( + 'paint' + ) as PerformancePaintTiming[]; + + const firstPaintStartTime = paintTimings.find( + ( { name } ) => name === 'first-paint' + )?.startTime as number; + + const firstContentfulPaintStartTime = paintTimings.find( + ( { name } ) => name === 'first-contentful-paint' + )?.startTime as number; + + 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: firstPaintStartTime - responseEnd, + domContentLoaded: domContentLoadedEventEnd - responseEnd, + loaded: loadEventEnd - responseEnd, + firstContentfulPaint: + firstContentfulPaintStartTime - responseEnd, + timeSinceResponseEnd: performance.now() - responseEnd, + }; + } ); + } + + /** + * Starts Chromium tracing with predefined options for performance testing. + * + * @param options Options to pass to `browser.startTracing()`. + */ + async startTracing( options = {} ): Promise< void > { + return await this.browser.startTracing( this.page, { + screenshots: false, + categories: [ 'devtools.timeline' ], + ...options, + } ); + } + + /** + * Stops Chromium tracing and saves the trace. + */ + async stopTracing(): Promise< void > { + const traceBuffer = await this.browser.stopTracing(); + const traceJSON = JSON.parse( traceBuffer.toString() ); + + this.trace = traceJSON; + } + + /** + * Returns the durations of all typing events. + */ + getTypingEventDurations(): number[][] { + return [ + this.getEventDurations( 'keydown' ), + this.getEventDurations( 'keypress' ), + this.getEventDurations( 'keyup' ), + ]; + } + + /** + * Returns the durations of all selection events. + */ + getSelectionEventDurations(): number[][] { + return [ + this.getEventDurations( 'focus' ), + this.getEventDurations( 'focusin' ), + ]; + } + + /** + * Returns the durations of all click events. + */ + getClickEventDurations(): number[][] { + return [ this.getEventDurations( 'click' ) ]; + } + + /** + * Returns the durations of all hover events. + */ + getHoverEventDurations(): number[][] { + return [ + this.getEventDurations( 'mouseover' ), + this.getEventDurations( 'mouseout' ), + ]; + } + + /** + * Returns the durations of all events of a given type. + * + * @param eventType The type of event to filter. + */ + getEventDurations( eventType: EventType ): number[] { + if ( this.trace.traceEvents.length === 0 ) { + throw new Error( + 'No trace events found. Did you forget to call stopTracing()?' + ); + } + + return this.trace.traceEvents + .filter( + ( item: TraceEvent ): boolean => + item.cat === 'devtools.timeline' && + item.name === 'EventDispatch' && + item?.args?.data?.type === eventType && + !! item.dur + ) + .map( ( item ) => ( item.dur ? item.dur / 1000 : 0 ) ); + } } diff --git a/packages/e2e-test-utils-playwright/src/test.ts b/packages/e2e-test-utils-playwright/src/test.ts index c428df46c56f1..778f71b6d770e 100644 --- a/packages/e2e-test-utils-playwright/src/test.ts +++ b/packages/e2e-test-utils-playwright/src/test.ts @@ -176,10 +176,10 @@ const test = base.extend< { scope: 'worker' }, ], lighthouse: async ( { page, lighthousePort }, use ) => { - await use( new Lighthouse( page, lighthousePort ) ); + await use( new Lighthouse( { page, port: lighthousePort } ) ); }, metrics: async ( { page }, use ) => { - await use( new Metrics( page ) ); + await use( new Metrics( { page } ) ); }, } ); diff --git a/test/performance/fixtures/index.js b/test/performance/fixtures/index.js index 00edf619398dd..0f68fc5637f5a 100644 --- a/test/performance/fixtures/index.js +++ b/test/performance/fixtures/index.js @@ -1,2 +1 @@ export { PerfUtils } from './perf-utils'; -export { Metrics } from './metrics'; diff --git a/test/performance/fixtures/metrics.js b/test/performance/fixtures/metrics.js deleted file mode 100644 index ae76b0d1ebad2..0000000000000 --- a/test/performance/fixtures/metrics.js +++ /dev/null @@ -1,145 +0,0 @@ -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, - timeSinceResponseEnd: 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 ); -} diff --git a/test/performance/fixtures/perf-utils.js b/test/performance/fixtures/perf-utils.js index f1e414c403b49..5005d180b0513 100644 --- a/test/performance/fixtures/perf-utils.js +++ b/test/performance/fixtures/perf-utils.js @@ -13,11 +13,10 @@ import path from 'path'; * Internal dependencies */ import { readFile } from '../utils.js'; - export class PerfUtils { - constructor( { browser, page } ) { - this.browser = browser; + constructor( { page } ) { this.page = page; + this.browser = page.context().browser(); } async getCanvas() { @@ -130,17 +129,4 @@ export class PerfUtils { 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() ); - } } diff --git a/test/performance/specs/front-end-block-theme.spec.js b/test/performance/specs/front-end-block-theme.spec.js index 7cc74b792486a..ca48535a21a46 100644 --- a/test/performance/specs/front-end-block-theme.spec.js +++ b/test/performance/specs/front-end-block-theme.spec.js @@ -3,12 +3,7 @@ /** * WordPress dependencies */ -import { test } from '@wordpress/e2e-test-utils-playwright'; - -/** - * Internal dependencies - */ -import { Metrics } from '../fixtures'; +import { test, Metrics } from '@wordpress/e2e-test-utils-playwright'; const results = { timeToFirstByte: [], diff --git a/test/performance/specs/front-end-classic-theme.spec.js b/test/performance/specs/front-end-classic-theme.spec.js index ff6b6f61aaa6c..0b6c3ec22c046 100644 --- a/test/performance/specs/front-end-classic-theme.spec.js +++ b/test/performance/specs/front-end-classic-theme.spec.js @@ -3,12 +3,7 @@ /** * WordPress dependencies */ -import { test } from '@wordpress/e2e-test-utils-playwright'; - -/** - * Internal dependencies - */ -import { Metrics } from '../fixtures'; +import { test, Metrics } from '@wordpress/e2e-test-utils-playwright'; const results = { timeToFirstByte: [], diff --git a/test/performance/specs/post-editor.spec.js b/test/performance/specs/post-editor.spec.js index 56b2935a28552..ecacd33b9aa5b 100644 --- a/test/performance/specs/post-editor.spec.js +++ b/test/performance/specs/post-editor.spec.js @@ -3,12 +3,12 @@ /** * WordPress dependencies */ -import { test, expect } from '@wordpress/e2e-test-utils-playwright'; +import { test, expect, Metrics } from '@wordpress/e2e-test-utils-playwright'; /** * Internal dependencies */ -import { PerfUtils, Metrics } from '../fixtures'; +import { PerfUtils } from '../fixtures'; import { sum } from '../utils.js'; // See https://github.com/WordPress/gutenberg/issues/51383#issuecomment-1613460429 @@ -32,8 +32,8 @@ const results = { test.describe( 'Post Editor Performance', () => { test.use( { - perfUtils: async ( { browser, page }, use ) => { - await use( new PerfUtils( { browser, page } ) ); + perfUtils: async ( { page }, use ) => { + await use( new PerfUtils( { page } ) ); }, metrics: async ( { page }, use ) => { await use( new Metrics( { page } ) ); @@ -120,7 +120,7 @@ test.describe( 'Post Editor Performance', () => { const iterations = samples + throwaway; // Start tracing. - await perfUtils.startTracing(); + await metrics.startTracing(); // Type the testing sequence into the empty paragraph. await paragraph.type( 'x'.repeat( iterations ), { @@ -131,11 +131,11 @@ test.describe( 'Post Editor Performance', () => { } ); // Stop tracing. - const traceResults = await perfUtils.stopTracing(); + await metrics.stopTracing(); // Get the durations. const [ keyDownEvents, keyPressEvents, keyUpEvents ] = - metrics.getTypingEventDurations( traceResults ); + metrics.getTypingEventDurations(); // Save the results. for ( let i = throwaway; i < iterations; i++ ) { @@ -174,7 +174,7 @@ test.describe( 'Post Editor Performance', () => { const iterations = samples + throwaway; // Start tracing. - await perfUtils.startTracing(); + await metrics.startTracing(); // Start typing in the middle of the text. await firstParagraph.type( 'x'.repeat( iterations ), { @@ -185,11 +185,11 @@ test.describe( 'Post Editor Performance', () => { } ); // Stop tracing. - const traceResults = await perfUtils.stopTracing(); + await metrics.stopTracing(); // Get the durations. const [ keyDownEvents, keyPressEvents, keyUpEvents ] = - metrics.getTypingEventDurations( traceResults ); + metrics.getTypingEventDurations(); // Save the results. for ( let i = throwaway; i < iterations; i++ ) { @@ -227,17 +227,16 @@ test.describe( 'Post Editor Performance', () => { await page.waitForTimeout( BROWSER_IDLE_WAIT ); // Start tracing. - await perfUtils.startTracing(); + await metrics.startTracing(); // Click the next paragraph. await paragraphs.nth( i ).click(); // Stop tracing. - const traceResults = await perfUtils.stopTracing(); + await metrics.stopTracing(); // Get the durations. - const allDurations = - metrics.getSelectionEventDurations( traceResults ); + const allDurations = metrics.getSelectionEventDurations(); // Save the results. if ( i > throwaway ) { @@ -277,7 +276,7 @@ test.describe( 'Post Editor Performance', () => { await page.waitForTimeout( BROWSER_IDLE_WAIT ); // Start tracing. - await perfUtils.startTracing(); + await metrics.startTracing(); // Open List View. await listViewToggle.click(); @@ -287,11 +286,10 @@ test.describe( 'Post Editor Performance', () => { ); // Stop tracing. - const traceResults = await perfUtils.stopTracing(); + await metrics.stopTracing(); // Get the durations. - const [ mouseClickEvents ] = - metrics.getClickEventDurations( traceResults ); + const [ mouseClickEvents ] = metrics.getClickEventDurations(); // Save the results. if ( i > throwaway ) { @@ -334,7 +332,7 @@ test.describe( 'Post Editor Performance', () => { await page.waitForTimeout( BROWSER_IDLE_WAIT ); // Start tracing. - await perfUtils.startTracing(); + await metrics.startTracing(); // Open Inserter. await globalInserterToggle.click(); @@ -344,11 +342,10 @@ test.describe( 'Post Editor Performance', () => { ); // Stop tracing. - const traceResults = await perfUtils.stopTracing(); + await metrics.stopTracing(); // Get the durations. - const [ mouseClickEvents ] = - metrics.getClickEventDurations( traceResults ); + const [ mouseClickEvents ] = metrics.getClickEventDurations(); // Save the results. if ( i > throwaway ) { @@ -398,17 +395,17 @@ test.describe( 'Post Editor Performance', () => { await page.waitForTimeout( BROWSER_IDLE_WAIT ); // Start tracing. - await perfUtils.startTracing(); + await metrics.startTracing(); // Type to trigger search. await page.keyboard.type( 'p' ); // Stop tracing. - const traceResults = await perfUtils.stopTracing(); + await metrics.stopTracing(); // Get the durations. const [ keyDownEvents, keyPressEvents, keyUpEvents ] = - metrics.getTypingEventDurations( traceResults ); + metrics.getTypingEventDurations(); // Save the results. if ( i > throwaway ) { @@ -464,18 +461,18 @@ test.describe( 'Post Editor Performance', () => { await page.waitForTimeout( BROWSER_IDLE_WAIT ); // Start tracing. - await perfUtils.startTracing(); + await metrics.startTracing(); // Hover Inserter items. await paragraphBlockItem.hover(); await headingBlockItem.hover(); // Stop tracing. - const traceResults = await perfUtils.stopTracing(); + await metrics.stopTracing(); // Get the durations. const [ mouseOverEvents, mouseOutEvents ] = - metrics.getHoverEventDurations( traceResults ); + metrics.getHoverEventDurations(); // Save the results. if ( i > throwaway ) { diff --git a/test/performance/specs/site-editor.spec.js b/test/performance/specs/site-editor.spec.js index 38c344f149237..21a739dfd5c12 100644 --- a/test/performance/specs/site-editor.spec.js +++ b/test/performance/specs/site-editor.spec.js @@ -3,12 +3,12 @@ /** * WordPress dependencies */ -import { test } from '@wordpress/e2e-test-utils-playwright'; +import { test, Metrics } from '@wordpress/e2e-test-utils-playwright'; /** * Internal dependencies */ -import { PerfUtils, Metrics } from '../fixtures'; +import { PerfUtils } from '../fixtures'; // See https://github.com/WordPress/gutenberg/issues/51383#issuecomment-1613460429 const BROWSER_IDLE_WAIT = 1000; @@ -31,8 +31,8 @@ const results = { test.describe( 'Site Editor Performance', () => { test.use( { - perfUtils: async ( { browser, page }, use ) => { - await use( new PerfUtils( { browser, page } ) ); + perfUtils: async ( { page }, use ) => { + await use( new PerfUtils( { page } ) ); }, metrics: async ( { page }, use ) => { await use( new Metrics( { page } ) ); @@ -161,7 +161,7 @@ test.describe( 'Site Editor Performance', () => { const iterations = samples + throwaway; // Start tracing. - await perfUtils.startTracing(); + await metrics.startTracing(); // Type the testing sequence into the empty paragraph. await paragraph.type( 'x'.repeat( iterations ), { @@ -172,11 +172,11 @@ test.describe( 'Site Editor Performance', () => { } ); // Stop tracing. - const traceResults = await perfUtils.stopTracing(); + await metrics.stopTracing(); // Get the durations. const [ keyDownEvents, keyPressEvents, keyUpEvents ] = - metrics.getTypingEventDurations( traceResults ); + metrics.getTypingEventDurations(); // Save the results. for ( let i = throwaway; i < iterations; i++ ) { From 693caec363e06e8dd29f8a1158b84ae30ed0a6dd Mon Sep 17 00:00:00 2001 From: Bart Kalisz Date: Wed, 20 Sep 2023 16:00:59 +0200 Subject: [PATCH 09/14] Define Trace interface --- packages/e2e-test-utils-playwright/src/metrics/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/e2e-test-utils-playwright/src/metrics/index.ts b/packages/e2e-test-utils-playwright/src/metrics/index.ts index 7c9a47e7e8f75..f14c995b18e21 100644 --- a/packages/e2e-test-utils-playwright/src/metrics/index.ts +++ b/packages/e2e-test-utils-playwright/src/metrics/index.ts @@ -24,6 +24,10 @@ interface TraceEvent { }; } +interface Trace { + traceEvents: TraceEvent[]; +} + interface LoadingDurations { serverResponse: number; firstPaint: number; @@ -40,7 +44,7 @@ type MetricsConstructorProps = { export class Metrics { browser: Browser; page: Page; - trace: { traceEvents: TraceEvent[] }; + trace: Trace; constructor( { page }: MetricsConstructorProps ) { this.page = page; From 33a78ebaa7521edca5f5b68c5cda0db14ed22210 Mon Sep 17 00:00:00 2001 From: Bart Kalisz Date: Thu, 21 Sep 2023 10:45:27 +0200 Subject: [PATCH 10/14] Avoid unnecessart type casting --- .../src/metrics/index.ts | 39 +++++++------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/packages/e2e-test-utils-playwright/src/metrics/index.ts b/packages/e2e-test-utils-playwright/src/metrics/index.ts index f14c995b18e21..a00748d02bd8d 100644 --- a/packages/e2e-test-utils-playwright/src/metrics/index.ts +++ b/packages/e2e-test-utils-playwright/src/metrics/index.ts @@ -28,15 +28,6 @@ interface Trace { traceEvents: TraceEvent[]; } -interface LoadingDurations { - serverResponse: number; - firstPaint: number; - domContentLoaded: number; - loaded: number; - firstContentfulPaint: number; - timeSinceResponseEnd: number; -} - type MetricsConstructorProps = { page: Page; }; @@ -58,7 +49,7 @@ export class Metrics { * @param fields Optional fields to filter. */ async getServerTiming( fields: string[] = [] ) { - return this.page.evaluate< Record< string, number >, string[] >( + return this.page.evaluate( ( f: string[] ) => ( performance.getEntriesByType( @@ -82,8 +73,8 @@ export class Metrics { * * @see https://web.dev/ttfb/#measure-ttfb-in-javascript */ - async getTimeToFirstByte(): Promise< number > { - return await this.page.evaluate< number >( () => { + async getTimeToFirstByte() { + return await this.page.evaluate( () => { const { responseStart, startTime } = ( performance.getEntriesByType( 'navigation' @@ -99,8 +90,8 @@ export class Metrics { * @see https://w3c.github.io/largest-contentful-paint/ * @see https://web.dev/lcp/#measure-lcp-in-javascript */ - async getLargestContentfulPaint(): Promise< number > { - return await this.page.evaluate< number >( + async getLargestContentfulPaint() { + return await this.page.evaluate( () => new Promise( ( resolve ) => { new PerformanceObserver( ( entryList ) => { @@ -123,8 +114,8 @@ export class Metrics { * @see https://github.com/WICG/layout-instability * @see https://web.dev/cls/#measure-layout-shifts-in-javascript */ - async getCumulativeLayoutShift(): Promise< number > { - return await this.page.evaluate< number >( + async getCumulativeLayoutShift() { + return await this.page.evaluate( () => new Promise( ( resolve ) => { let CLS = 0; @@ -151,7 +142,7 @@ export class Metrics { * Returns the loading durations using the Navigation Timing API. All the * durations exclude the server response time. */ - async getLoadingDurations(): Promise< LoadingDurations > { + async getLoadingDurations() { return await this.page.evaluate( () => { const [ { @@ -196,7 +187,7 @@ export class Metrics { * * @param options Options to pass to `browser.startTracing()`. */ - async startTracing( options = {} ): Promise< void > { + async startTracing( options = {} ) { return await this.browser.startTracing( this.page, { screenshots: false, categories: [ 'devtools.timeline' ], @@ -207,7 +198,7 @@ export class Metrics { /** * Stops Chromium tracing and saves the trace. */ - async stopTracing(): Promise< void > { + async stopTracing() { const traceBuffer = await this.browser.stopTracing(); const traceJSON = JSON.parse( traceBuffer.toString() ); @@ -217,7 +208,7 @@ export class Metrics { /** * Returns the durations of all typing events. */ - getTypingEventDurations(): number[][] { + getTypingEventDurations() { return [ this.getEventDurations( 'keydown' ), this.getEventDurations( 'keypress' ), @@ -228,7 +219,7 @@ export class Metrics { /** * Returns the durations of all selection events. */ - getSelectionEventDurations(): number[][] { + getSelectionEventDurations() { return [ this.getEventDurations( 'focus' ), this.getEventDurations( 'focusin' ), @@ -238,14 +229,14 @@ export class Metrics { /** * Returns the durations of all click events. */ - getClickEventDurations(): number[][] { + getClickEventDurations() { return [ this.getEventDurations( 'click' ) ]; } /** * Returns the durations of all hover events. */ - getHoverEventDurations(): number[][] { + getHoverEventDurations() { return [ this.getEventDurations( 'mouseover' ), this.getEventDurations( 'mouseout' ), @@ -257,7 +248,7 @@ export class Metrics { * * @param eventType The type of event to filter. */ - getEventDurations( eventType: EventType ): number[] { + getEventDurations( eventType: EventType ) { if ( this.trace.traceEvents.length === 0 ) { throw new Error( 'No trace events found. Did you forget to call stopTracing()?' From feb23d53a20f9b7986339d636ba323d90c15e442 Mon Sep 17 00:00:00 2001 From: Bart Kalisz Date: Thu, 21 Sep 2023 10:48:07 +0200 Subject: [PATCH 11/14] Assert non-null instead of casting --- packages/e2e-test-utils-playwright/src/metrics/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/e2e-test-utils-playwright/src/metrics/index.ts b/packages/e2e-test-utils-playwright/src/metrics/index.ts index a00748d02bd8d..a2ce9830a199f 100644 --- a/packages/e2e-test-utils-playwright/src/metrics/index.ts +++ b/packages/e2e-test-utils-playwright/src/metrics/index.ts @@ -161,11 +161,11 @@ export class Metrics { const firstPaintStartTime = paintTimings.find( ( { name } ) => name === 'first-paint' - )?.startTime as number; + )!.startTime; const firstContentfulPaintStartTime = paintTimings.find( ( { name } ) => name === 'first-contentful-paint' - )?.startTime as number; + )!.startTime; return { // Server side metric. From 5c67b2d621f9822f474c47190e1f59b094d906a0 Mon Sep 17 00:00:00 2001 From: Bart Kalisz Date: Thu, 21 Sep 2023 11:12:03 +0200 Subject: [PATCH 12/14] Migrate PerfUtils to TS --- .../fixtures/{perf-utils.js => perf-utils.ts} | 51 +++++++++++++++---- 1 file changed, 42 insertions(+), 9 deletions(-) rename test/performance/fixtures/{perf-utils.js => perf-utils.ts} (71%) diff --git a/test/performance/fixtures/perf-utils.js b/test/performance/fixtures/perf-utils.ts similarity index 71% rename from test/performance/fixtures/perf-utils.js rename to test/performance/fixtures/perf-utils.ts index 5005d180b0513..576777a4d44f6 100644 --- a/test/performance/fixtures/perf-utils.js +++ b/test/performance/fixtures/perf-utils.ts @@ -8,19 +8,29 @@ import { expect } from '@wordpress/e2e-test-utils-playwright'; */ import fs from 'fs'; import path from 'path'; +import type { Page } from '@playwright/test'; /** * Internal dependencies */ import { readFile } from '../utils.js'; + +type PerfUtilsConstructorProps = { + page: Page; +}; + export class PerfUtils { - constructor( { page } ) { + page: Page; + + constructor( { page }: PerfUtilsConstructorProps ) { this.page = page; - this.browser = page.context().browser(); } + /** + * Returns the locator for the editor canvas element. This supports either + * the legacy canvas or the iframed canvas. + */ async getCanvas() { - // Handles both legacy and iframed canvas. return await Promise.any( [ ( async () => { const legacyCanvasLocator = this.page.locator( @@ -43,6 +53,9 @@ export class PerfUtils { ] ); } + /** + * Saves the post as a draft and returns its URL. + */ async saveDraft() { await this.page .getByRole( 'button', { name: 'Save draft' } ) @@ -54,6 +67,9 @@ export class PerfUtils { return this.page.url(); } + /** + * Disables the editor autosave function. + */ async disableAutosave() { await this.page.evaluate( () => { return window.wp.data @@ -71,11 +87,13 @@ export class PerfUtils { expect( autosaveInterval ).toBe( 100000000000 ); } + /** + * Enters the Site Editor's edit mode. + */ 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(); @@ -83,32 +101,44 @@ export class PerfUtils { return canvas; } + /** + * Loads blocks from the small post with containers fixture into the editor + * canvas. + */ async loadBlocksForSmallPostWithContainers() { return await this.loadBlocksFromHtml( path.join( - process.env.ASSETS_PATH, + process.env.ASSETS_PATH!, 'small-post-with-containers.html' ) ); } + /** + * Loads blocks from the large post fixture into the editor canvas. + */ async loadBlocksForLargePost() { return await this.loadBlocksFromHtml( - path.join( process.env.ASSETS_PATH, 'large-post.html' ) + path.join( process.env.ASSETS_PATH!, 'large-post.html' ) ); } - async loadBlocksFromHtml( filepath ) { + /** + * Loads blocks from an HTML fixture with given path into the editor canvas. + * + * @param filepath Path to the HTML fixture. + */ + async loadBlocksFromHtml( filepath: string ) { if ( ! fs.existsSync( filepath ) ) { throw new Error( `File not found: ${ filepath }` ); } - return await this.page.evaluate( ( html ) => { + return await this.page.evaluate( ( html: string ) => { const { parse } = window.wp.blocks; const { dispatch } = window.wp.data; const blocks = parse( html ); - blocks.forEach( ( block ) => { + blocks.forEach( ( block: any ) => { if ( block.name === 'core/image' ) { delete block.attributes.id; delete block.attributes.url; @@ -119,6 +149,9 @@ export class PerfUtils { }, readFile( filepath ) ); } + /** + * Generates and loads a 1000 empty paragraphs into the editor canvas. + */ async load1000Paragraphs() { await this.page.evaluate( () => { const { createBlock } = window.wp.blocks; From 71a1d581100e0348b5323fdd615c5095acaf3461 Mon Sep 17 00:00:00 2001 From: Bart Kalisz Date: Thu, 21 Sep 2023 12:21:47 +0200 Subject: [PATCH 13/14] Revert some unwanted removals --- .../e2e-test-utils-playwright/src/metrics/index.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/e2e-test-utils-playwright/src/metrics/index.ts b/packages/e2e-test-utils-playwright/src/metrics/index.ts index a2ce9830a199f..c0843f73ddf83 100644 --- a/packages/e2e-test-utils-playwright/src/metrics/index.ts +++ b/packages/e2e-test-utils-playwright/src/metrics/index.ts @@ -49,7 +49,7 @@ export class Metrics { * @param fields Optional fields to filter. */ async getServerTiming( fields: string[] = [] ) { - return this.page.evaluate( + return this.page.evaluate< Record< string, number >, string[] >( ( f: string[] ) => ( performance.getEntriesByType( @@ -72,9 +72,11 @@ export class Metrics { * Returns time to first byte (TTFB) using the Navigation Timing API. * * @see https://web.dev/ttfb/#measure-ttfb-in-javascript + * + * @return TTFB value. */ async getTimeToFirstByte() { - return await this.page.evaluate( () => { + return await this.page.evaluate< number >( () => { const { responseStart, startTime } = ( performance.getEntriesByType( 'navigation' @@ -89,9 +91,11 @@ export class Metrics { * * @see https://w3c.github.io/largest-contentful-paint/ * @see https://web.dev/lcp/#measure-lcp-in-javascript + * + * @return LCP value. */ async getLargestContentfulPaint() { - return await this.page.evaluate( + return await this.page.evaluate< number >( () => new Promise( ( resolve ) => { new PerformanceObserver( ( entryList ) => { @@ -113,9 +117,11 @@ export class Metrics { * * @see https://github.com/WICG/layout-instability * @see https://web.dev/cls/#measure-layout-shifts-in-javascript + * + * @return CLS value. */ async getCumulativeLayoutShift() { - return await this.page.evaluate( + return await this.page.evaluate< number >( () => new Promise( ( resolve ) => { let CLS = 0; From d12b20d97d9e34b853e1c247d105408ce886ac2a Mon Sep 17 00:00:00 2001 From: Bart Kalisz Date: Thu, 21 Sep 2023 12:28:37 +0200 Subject: [PATCH 14/14] Improve jsdoc consistency --- .../src/metrics/index.ts | 18 ++++++++++-------- test/performance/fixtures/perf-utils.ts | 10 ++++++++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/e2e-test-utils-playwright/src/metrics/index.ts b/packages/e2e-test-utils-playwright/src/metrics/index.ts index c0843f73ddf83..68343f6d7c482 100644 --- a/packages/e2e-test-utils-playwright/src/metrics/index.ts +++ b/packages/e2e-test-utils-playwright/src/metrics/index.ts @@ -147,6 +147,8 @@ export class Metrics { /** * Returns the loading durations using the Navigation Timing API. All the * durations exclude the server response time. + * + * @return Object with loading metrics durations. */ async getLoadingDurations() { return await this.page.evaluate( () => { @@ -189,7 +191,7 @@ export class Metrics { } /** - * Starts Chromium tracing with predefined options for performance testing. + * Starts Chromium tracing with predefined options for performance testing. * * @param options Options to pass to `browser.startTracing()`. */ @@ -212,7 +214,8 @@ export class Metrics { } /** - * Returns the durations of all typing events. + * @return Durations of all traced `keydown`, `keypress`, and `keyup` + * events. */ getTypingEventDurations() { return [ @@ -223,7 +226,7 @@ export class Metrics { } /** - * Returns the durations of all selection events. + * @return Durations of all traced `focus` and `focusin` events. */ getSelectionEventDurations() { return [ @@ -233,14 +236,14 @@ export class Metrics { } /** - * Returns the durations of all click events. + * @return Durations of all traced `click` events. */ getClickEventDurations() { return [ this.getEventDurations( 'click' ) ]; } /** - * Returns the durations of all hover events. + * @return Durations of all traced `mouseover` and `mouseout` events. */ getHoverEventDurations() { return [ @@ -250,9 +253,8 @@ export class Metrics { } /** - * Returns the durations of all events of a given type. - * - * @param eventType The type of event to filter. + * @param eventType Type of event to filter. + * @return Durations of all events of a given type. */ getEventDurations( eventType: EventType ) { if ( this.trace.traceEvents.length === 0 ) { diff --git a/test/performance/fixtures/perf-utils.ts b/test/performance/fixtures/perf-utils.ts index 576777a4d44f6..e66f394234acd 100644 --- a/test/performance/fixtures/perf-utils.ts +++ b/test/performance/fixtures/perf-utils.ts @@ -27,8 +27,10 @@ export class PerfUtils { } /** - * Returns the locator for the editor canvas element. This supports either - * the legacy canvas or the iframed canvas. + * Returns the locator for the editor canvas element. This supports both the + * legacy and the iframed canvas. + * + * @return Locator for the editor canvas element. */ async getCanvas() { return await Promise.any( [ @@ -55,6 +57,8 @@ export class PerfUtils { /** * Saves the post as a draft and returns its URL. + * + * @return URL of the saved draft. */ async saveDraft() { await this.page @@ -89,6 +93,8 @@ export class PerfUtils { /** * Enters the Site Editor's edit mode. + * + * @return Locator for the editor canvas element. */ async enterSiteEditorEditMode() { const canvas = await this.getCanvas();