diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index c5c8848026d986..7f239652774df1 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -84,6 +84,9 @@ jobs: run: | ./bin/plugin/cli.js perf $(echo $BRANCHES | tr ',' ' ') --tests-branch $GITHUB_SHA --wp-version "$WP_VERSION" + - name: Add workflow summary + run: cat ${{ env.WP_ARTIFACTS_PATH }}/summary.md >> $GITHUB_STEP_SUMMARY + - name: Archive performance results if: success() uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 diff --git a/bin/plugin/commands/performance.js b/bin/plugin/commands/performance.js index 9d9b39fce09845..65b8a770d3764a 100644 --- a/bin/plugin/commands/performance.js +++ b/bin/plugin/commands/performance.js @@ -96,6 +96,79 @@ async function runTestSuite( testSuite, testRunnerDir, runKey ) { ); } +/** + * Formats an array of objects as a Markdown table. + * + * For example, this array: + * + * [ + * { + * foo: 123, + * bar: 456, + * baz: 'Yes', + * }, + * { + * foo: 777, + * bar: 999, + * baz: 'No', + * } + * ] + * + * Will result in the following table: + * + * | foo | bar | baz | + * |-----|-----|-----| + * | 123 | 456 | Yes | + * | 777 | 999 | No | + * + * @param {Array} rows Table rows. + * @return {string} Markdown table content. + */ +function formatAsMarkdownTable( rows ) { + let result = ''; + + if ( ! rows.length ) { + return result; + } + + const headers = Object.keys( rows[ 0 ] ); + for ( const header of headers ) { + result += `| ${ header } `; + } + result += '|\n'; + for ( let i = 0; i < headers.length; i++ ) { + result += '| ------ '; + } + result += '|\n'; + + for ( const row of rows ) { + for ( const value of Object.values( row ) ) { + result += `| ${ value } `; + } + result += '|\n'; + } + + return result; +} + +/** + * Nicely formats a given value. + * + * @param {string} metric Metric. + * @param {number} value + */ +function formatValue( metric, value ) { + if ( 'wpMemoryUsage' === metric ) { + return `${ ( value / Math.pow( 10, 6 ) ).toFixed( 2 ) } MB`; + } + + if ( 'wpDbQueries' === metric ) { + return value.toString(); + } + + return `${ value } ms`; +} + /** * Runs the performances tests on an array of branches and output the result. * @@ -387,7 +460,7 @@ async function runPerformanceTests( branches, options ) { return readJSONFile( file ); } ); - const metrics = Object.keys( resultsRounds[ 0 ] ); + const metrics = Object.keys( resultsRounds[ 0 ] ?? {} ); results[ testSuite ][ branch ] = {}; for ( const metric of metrics ) { @@ -401,6 +474,7 @@ async function runPerformanceTests( branches, options ) { } } } + const calculatedResultsPath = path.join( ARTIFACTS_PATH, testSuite + RESULTS_FILE_SUFFIX @@ -424,6 +498,10 @@ async function runPerformanceTests( branches, options ) { ) ); + let summaryMarkdown = `## Performance Test Results\n\n`; + + summaryMarkdown += `Please note that client side metrics **exclude** the server response time.\n\n`; + for ( const testSuite of testSuites ) { logAtIndent( 0, formats.success( testSuite ) ); @@ -435,7 +513,10 @@ async function runPerformanceTests( branches, options ) { ) ) { for ( const [ metric, value ] of Object.entries( metrics ) ) { invertedResult[ metric ] = invertedResult[ metric ] || {}; - invertedResult[ metric ][ branch ] = `${ value } ms`; + invertedResult[ metric ][ branch ] = formatValue( + metric, + value + ); } } @@ -457,7 +538,37 @@ async function runPerformanceTests( branches, options ) { // Print the results. console.table( invertedResult ); + + // Use yet another structure to generate a Markdown table. + + const rows = []; + + for ( const [ metric, resultBranches ] of Object.entries( + invertedResult + ) ) { + /** + * @type {Record< string, string >} + */ + const row = { + Metric: metric, + }; + + for ( const [ branch, value ] of Object.entries( + resultBranches + ) ) { + row[ branch ] = value; + } + rows.push( row ); + } + + summaryMarkdown += `**${ testSuite }**\n\n`; + summaryMarkdown += `${ formatAsMarkdownTable( rows ) }\n`; } + + fs.writeFileSync( + path.join( ARTIFACTS_PATH, 'summary.md' ), + summaryMarkdown + ); } module.exports = { diff --git a/packages/e2e-tests/mu-plugins/server-timing.php b/packages/e2e-tests/mu-plugins/server-timing.php new file mode 100644 index 00000000000000..84771f980ff7fc --- /dev/null +++ b/packages/e2e-tests/mu-plugins/server-timing.php @@ -0,0 +1,91 @@ +num_queries; + + $header_values = array(); + foreach ( $server_timing_values as $slug => $value ) { + if ( is_float( $value ) ) { + $value = round( $value * 1000.0, 2 ); + } + $header_values[] = sprintf( '%1$s;dur=%2$s', $slug, $value ); + } + header( 'Server-Timing: ' . implode( ', ', $header_values ) ); + + echo $output; + }, + PHP_INT_MIN + ); + + return $template; + }, + PHP_INT_MAX +); + +add_action( + 'admin_init', + static function () { + global $timestart, $wpdb; + + ob_start(); + + add_action( + 'shutdown', + static function () use ( $wpdb, $timestart ) { + $output = ob_get_clean(); + + $server_timing_values = array(); + + $server_timing_values['wpTotal'] = microtime( true ) - $timestart; + + /* + * While values passed via Server-Timing are intended to be durations, + * any numeric value can actually be passed. + * This is a nice little trick as it allows to easily get this information in JS. + */ + $server_timing_values['wpMemoryUsage'] = memory_get_usage(); + $server_timing_values['wpDbQueries'] = $wpdb->num_queries; + + $header_values = array(); + foreach ( $server_timing_values as $slug => $value ) { + if ( is_float( $value ) ) { + $value = round( $value * 1000.0, 2 ); + } + $header_values[] = sprintf( '%1$s;dur=%2$s', $slug, $value ); + } + header( 'Server-Timing: ' . implode( ', ', $header_values ) ); + + echo $output; + }, + PHP_INT_MIN + ); + }, + PHP_INT_MAX +); diff --git a/test/performance/config/performance-reporter.ts b/test/performance/config/performance-reporter.ts index c82c092f304788..b449e36540404e 100644 --- a/test/performance/config/performance-reporter.ts +++ b/test/performance/config/performance-reporter.ts @@ -13,7 +13,7 @@ import type { /** * Internal dependencies */ -import { average, median, minimum, maximum, round } from '../utils'; +import { average, median, round } from '../utils'; export interface WPRawPerformanceResults { timeToFirstByte: number[]; @@ -36,6 +36,11 @@ export interface WPRawPerformanceResults { loadPatterns: number[]; listViewOpen: number[]; navigate: number[]; + wpBeforeTemplate: number[]; + wpTemplate: number[]; + wpTotal: number[]; + wpMemoryUsage: number[]; + wpDbQueries: number[]; } export interface WPPerformanceResults { @@ -59,6 +64,11 @@ export interface WPPerformanceResults { loadPatterns?: number; listViewOpen?: number; navigate?: number; + wpBeforeTemplate?: number; + wpTemplate?: number; + wpTotal?: number; + wpMemoryUsage?: number; + wpDbQueries?: number; } /** @@ -92,6 +102,11 @@ export function curateResults( loadPatterns: average( results.loadPatterns ), listViewOpen: average( results.listViewOpen ), navigate: median( results.navigate ), + wpBeforeTemplate: median( results.wpBeforeTemplate ), + wpTemplate: median( results.wpTemplate ), + wpTotal: median( results.wpTotal ), + wpMemoryUsage: median( results.wpMemoryUsage ), + wpDbQueries: median( results.wpDbQueries ), }; return ( diff --git a/test/performance/specs/front-end-block-theme.spec.js b/test/performance/specs/front-end-block-theme.spec.js index ca48535a21a467..8a0f7b6cf391a2 100644 --- a/test/performance/specs/front-end-block-theme.spec.js +++ b/test/performance/specs/front-end-block-theme.spec.js @@ -52,6 +52,13 @@ test.describe( 'Front End Performance', () => { results.largestContentfulPaint.push( lcp ); results.timeToFirstByte.push( ttfb ); results.lcpMinusTtfb.push( lcp - ttfb ); + + const serverTiming = await metrics.getServerTiming(); + + for ( const [ key, value ] of Object.entries( serverTiming ) ) { + results[ key ] ??= []; + results[ key ].push( value ); + } } } ); } diff --git a/test/performance/specs/front-end-classic-theme.spec.js b/test/performance/specs/front-end-classic-theme.spec.js index 0b6c3ec22c0465..64929086079fe6 100644 --- a/test/performance/specs/front-end-classic-theme.spec.js +++ b/test/performance/specs/front-end-classic-theme.spec.js @@ -51,6 +51,13 @@ test.describe( 'Front End Performance', () => { results.largestContentfulPaint.push( lcp ); results.timeToFirstByte.push( ttfb ); results.lcpMinusTtfb.push( lcp - ttfb ); + + const serverTiming = await metrics.getServerTiming(); + + for ( const [ key, value ] of Object.entries( serverTiming ) ) { + results[ key ] ??= []; + results[ key ].push( value ); + } } } ); } diff --git a/test/performance/specs/post-editor.spec.js b/test/performance/specs/post-editor.spec.js index 8852e4ff627aa4..37e38d0d9ec479 100644 --- a/test/performance/specs/post-editor.spec.js +++ b/test/performance/specs/post-editor.spec.js @@ -89,6 +89,15 @@ test.describe( 'Post Editor Performance', () => { } } ); + + const serverTiming = await metrics.getServerTiming(); + + for ( const [ key, value ] of Object.entries( + serverTiming + ) ) { + results[ key ] ??= []; + results[ key ].push( value ); + } } } ); } diff --git a/test/performance/specs/site-editor.spec.js b/test/performance/specs/site-editor.spec.js index c4c83f0d4c140a..e8a4e3529334dd 100644 --- a/test/performance/specs/site-editor.spec.js +++ b/test/performance/specs/site-editor.spec.js @@ -101,6 +101,15 @@ test.describe( 'Site Editor Performance', () => { } } ); + + const serverTiming = await metrics.getServerTiming(); + + for ( const [ key, value ] of Object.entries( + serverTiming + ) ) { + results[ key ] ??= []; + results[ key ].push( value ); + } } } ); }