summaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/performance.yml170
-rw-r--r--tests/performance/compare-results.js242
-rw-r--r--tests/performance/config/global-setup.js4
-rw-r--r--tests/performance/config/performance-reporter.js74
-rw-r--r--tests/performance/log-results.js123
-rw-r--r--tests/performance/playwright.config.js3
-rw-r--r--tests/performance/results.js42
-rw-r--r--tests/performance/specs/admin-l10n.test.js52
-rw-r--r--tests/performance/specs/admin.test.js72
-rw-r--r--tests/performance/specs/home-block-theme-l10n.test.js63
-rw-r--r--tests/performance/specs/home-block-theme.test.js57
-rw-r--r--tests/performance/specs/home-classic-theme-l10n.test.js62
-rw-r--r--tests/performance/specs/home-classic-theme.test.js56
-rw-r--r--tests/performance/specs/home.test.js79
-rw-r--r--tests/performance/utils.js177
-rw-r--r--tests/performance/wp-content/mu-plugins/server-timing.php53
16 files changed, 683 insertions, 646 deletions
diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml
index 62663c730b..d79ae5f801 100644
--- a/.github/workflows/performance.yml
+++ b/.github/workflows/performance.yml
@@ -66,38 +66,45 @@ jobs:
# - Install WordPress.
# - Install WordPress Importer plugin.
# - Import mock data.
+ # - Deactivate WordPress Importer plugin.
# - Update permalink structure.
+ # - Install additional languages.
+ # - Disable external HTTP requests.
+ # - Disable cron.
+ # - List defined constants.
# - Install MU plugin.
# - Run performance tests (current commit).
- # - Print performance tests results.
- # - Check out target commit (target branch or previous commit).
- # - Switch Node.js versions if necessary.
- # - Install npm dependencies.
- # - Build WordPress.
+ # - Download previous build artifact (target branch or previous commit).
+ # - Download artifact.
+ # - Unzip the build.
# - Run any database upgrades.
+ # - Flush cache.
+ # - Delete expired transients.
# - Run performance tests (previous/target commit).
- # - Print target performance tests results.
- # - Reset to original commit.
- # - Switch Node.js versions if necessary.
- # - Install npm dependencies.
# - Set the environment to the baseline version.
# - Run any database upgrades.
+ # - Flush cache.
+ # - Delete expired transients.
# - Run baseline performance tests.
- # - Print baseline performance tests results.
- # - Compare results with base.
+ # - Archive artifacts.
+ # - Compare results.
# - Add workflow summary.
# - Set the base sha.
# - Set commit details.
# - Publish performance results.
# - Ensure version-controlled files are not modified or deleted.
- # - Dispatch workflow run.
performance:
- name: Run performance tests
+ name: Run performance tests ${{ matrix.memcached && '(with memcached)' || '' }}
runs-on: ubuntu-latest
permissions:
contents: read
if: ${{ ( github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' ) && ! contains( github.event.before, '00000000' ) }}
-
+ strategy:
+ fail-fast: false
+ matrix:
+ memcached: [ true, false ]
+ env:
+ LOCAL_PHP_MEMCACHED: ${{ matrix.memcached }}
steps:
- name: Configure environment variables
run: |
@@ -127,14 +134,17 @@ jobs:
run: npm ci
- name: Install Playwright browsers
- run: npx playwright install --with-deps
+ run: npx playwright install --with-deps chromium
- name: Build WordPress
run: npm run build
- name: Start Docker environment
- run: |
- npm run env:start
+ run: npm run env:start
+
+ - name: Install object cache drop-in
+ if: ${{ matrix.memcached }}
+ run: cp src/wp-content/object-cache.php build/wp-content/object-cache.php
- name: Log running Docker containers
run: docker ps -a
@@ -160,9 +170,11 @@ jobs:
npm run env:cli -- import themeunittestdata.wordpress.xml --authors=create --path=/var/www/${{ env.LOCAL_DIR }}
rm themeunittestdata.wordpress.xml
+ - name: Deactivate WordPress Importer plugin
+ run: npm run env:cli -- plugin deactivate wordpress-importer --path=/var/www/${{ env.LOCAL_DIR }}
+
- name: Update permalink structure
- run: |
- npm run env:cli -- rewrite structure '/%year%/%monthnum%/%postname%/' --path=/var/www/${{ env.LOCAL_DIR }}
+ run: npm run env:cli -- rewrite structure '/%year%/%monthnum%/%postname%/' --path=/var/www/${{ env.LOCAL_DIR }}
- name: Install additional languages
run: |
@@ -170,6 +182,17 @@ jobs:
npm run env:cli -- language plugin install de_DE --all --path=/var/www/${{ env.LOCAL_DIR }}
npm run env:cli -- language theme install de_DE --all --path=/var/www/${{ env.LOCAL_DIR }}
+ # Prevent background update checks from impacting test stability.
+ - name: Disable external HTTP requests
+ run: npm run env:cli -- config set WP_HTTP_BLOCK_EXTERNAL true --raw --type=constant --path=/var/www/${{ env.LOCAL_DIR }}
+
+ # Prevent background tasks from impacting test stability.
+ - name: Disable cron
+ run: npm run env:cli -- config set DISABLE_WP_CRON true --raw --type=constant --path=/var/www/${{ env.LOCAL_DIR }}
+
+ - name: List defined constants
+ run: npm run env:cli -- config list --path=/var/www/${{ env.LOCAL_DIR }}
+
- name: Install MU plugin
run: |
mkdir ./${{ env.LOCAL_DIR }}/wp-content/mu-plugins
@@ -178,74 +201,93 @@ jobs:
- name: Run performance tests (current commit)
run: npm run test:performance
- - name: Print performance tests results
- run: node ./tests/performance/results.js
+ - name: Download previous build artifact (target branch or previous commit)
+ uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+ id: get-previous-build
+ with:
+ script: |
+ const artifacts = await github.rest.actions.listArtifactsForRepo({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ name: 'wordpress-build-' + process.env.TARGET_SHA,
+ });
+
+ const matchArtifact = artifacts.data.artifacts[0];
- - name: Check out target commit (target branch or previous commit)
- run: |
- if [[ -z "$TARGET_REF" ]]; then
- git fetch -n origin $TARGET_SHA
- else
- git fetch -n origin $TARGET_REF
- fi
- git reset --hard $TARGET_SHA
+ if ( ! matchArtifact ) {
+ core.setFailed( 'No artifact found!' );
+ return false;
+ }
- - name: Set up Node.js
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
- with:
- node-version-file: '.nvmrc'
- cache: npm
+ const download = await github.rest.actions.downloadArtifact( {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ artifact_id: matchArtifact.id,
+ archive_format: 'zip',
+ } );
- - name: Install npm dependencies
- run: npm ci
+ const fs = require( 'fs' );
+ fs.writeFileSync( '${{ github.workspace }}/before.zip', Buffer.from( download.data ) )
- - name: Build WordPress
- run: npm run build
+ return true;
+
+ - name: Unzip the build
+ if: ${{ steps.get-previous-build.outputs.result }}
+ run: |
+ unzip ${{ github.workspace }}/before.zip
+ unzip -o ${{ github.workspace }}/wordpress.zip
- name: Run any database upgrades
+ if: ${{ steps.get-previous-build.outputs.result }}
run: npm run env:cli -- core update-db --path=/var/www/${{ env.LOCAL_DIR }}
- - name: Run target performance tests (base/previous commit)
- env:
- TEST_RESULTS_PREFIX: before
- run: npm run test:performance
+ - name: Flush cache
+ if: ${{ steps.get-previous-build.outputs.result }}
+ run: npm run env:cli -- cache flush --path=/var/www/${{ env.LOCAL_DIR }}
+
+ - name: Delete expired transients
+ if: ${{ steps.get-previous-build.outputs.result }}
+ run: npm run env:cli -- transient delete --expired --path=/var/www/${{ env.LOCAL_DIR }}
- - name: Print target performance tests results
+ - name: Run target performance tests (previous/target commit)
+ if: ${{ steps.get-previous-build.outputs.result }}
env:
TEST_RESULTS_PREFIX: before
- run: node ./tests/performance/results.js
-
- - name: Reset to original commit
- run: git reset --hard $GITHUB_SHA
-
- - name: Set up Node.js
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
- with:
- node-version-file: '.nvmrc'
- cache: npm
-
- - name: Install npm dependencies
- run: npm ci
+ run: npm run test:performance
- name: Set the environment to the baseline version
+ if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' }}
run: |
npm run env:cli -- core update --version=${{ env.BASE_TAG }} --force --path=/var/www/${{ env.LOCAL_DIR }}
npm run env:cli -- core version --path=/var/www/${{ env.LOCAL_DIR }}
- name: Run any database upgrades
+ if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' }}
run: npm run env:cli -- core update-db --path=/var/www/${{ env.LOCAL_DIR }}
+ - name: Flush cache
+ if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' }}
+ run: npm run env:cli -- cache flush --path=/var/www/${{ env.LOCAL_DIR }}
+
+ - name: Delete expired transients
+ if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' }}
+ run: npm run env:cli -- transient delete --expired --path=/var/www/${{ env.LOCAL_DIR }}
+
- name: Run baseline performance tests
+ if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' }}
env:
TEST_RESULTS_PREFIX: base
run: npm run test:performance
- - name: Print baseline performance tests results
- env:
- TEST_RESULTS_PREFIX: base
- run: node ./tests/performance/results.js
+ - name: Archive artifacts
+ uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
+ if: always()
+ with:
+ name: performance-artifacts${{ matrix.memcached && '-memcached' || '' }}-${{ github.run_id }}
+ path: artifacts
+ if-no-files-found: ignore
- - name: Compare results with base
+ - name: Compare results
run: node ./tests/performance/compare-results.js ${{ runner.temp }}/summary.md
- name: Add workflow summary
@@ -253,7 +295,7 @@ jobs:
- name: Set the base sha
# Only needed when publishing results.
- if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' }}
+ if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' && ! matrix.memcached }}
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
id: base-sha
with:
@@ -264,7 +306,7 @@ jobs:
- name: Set commit details
# Only needed when publishing results.
- if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' }}
+ if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' && ! matrix.memcached }}
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
id: commit-timestamp
with:
@@ -275,7 +317,7 @@ jobs:
- name: Publish performance results
# Only publish results on pushes to trunk.
- if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' }}
+ if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' && ! matrix.memcached }}
env:
BASE_SHA: ${{ steps.base-sha.outputs.result }}
COMMITTED_AT: ${{ steps.commit-timestamp.outputs.result }}
diff --git a/tests/performance/compare-results.js b/tests/performance/compare-results.js
index 6af85a2f12..c9d51e1a11 100644
--- a/tests/performance/compare-results.js
+++ b/tests/performance/compare-results.js
@@ -3,193 +3,155 @@
/**
* External dependencies.
*/
-const fs = require( 'node:fs' );
-const path = require( 'node:path' );
+const { readFileSync, writeFileSync, existsSync } = require( 'node:fs' );
+const { join } = require( 'node:path' );
/**
* Internal dependencies
*/
-const { median } = require( './utils' );
-
-/**
- * Parse test files into JSON objects.
- *
- * @param {string} fileName The name of the file.
- * @returns An array of parsed objects from each file.
- */
-const parseFile = ( fileName ) =>
- JSON.parse(
- fs.readFileSync( path.join( __dirname, '/specs/', fileName ), 'utf8' )
- );
-
-// The list of test suites to log.
-const testSuites = [
- 'admin',
- 'admin-l10n',
- 'home-block-theme',
- 'home-block-theme-l10n',
- 'home-classic-theme',
- 'home-classic-theme-l10n',
-];
-
-// The current commit's results.
-const testResults = Object.fromEntries(
- testSuites
- .filter( ( key ) => fs.existsSync( path.join( __dirname, '/specs/', `${ key }.test.results.json` ) ) )
- .map( ( key ) => [ key, parseFile( `${ key }.test.results.json` ) ] )
-);
-
-// The previous commit's results.
-const prevResults = Object.fromEntries(
- testSuites
- .filter( ( key ) => fs.existsSync( path.join( __dirname, '/specs/', `before-${ key }.test.results.json` ) ) )
- .map( ( key ) => [ key, parseFile( `before-${ key }.test.results.json` ) ] )
-);
+const {
+ median,
+ formatAsMarkdownTable,
+ formatValue,
+ linkToSha,
+ standardDeviation,
+ medianAbsoluteDeviation,
+ accumulateValues,
+} = require( './utils' );
+
+process.env.WP_ARTIFACTS_PATH ??= join( process.cwd(), 'artifacts' );
const args = process.argv.slice( 2 );
-
const summaryFile = args[ 0 ];
/**
- * 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 |
+ * Parse test files into JSON objects.
*
- * @param {Array<Object>} rows Table rows.
- * @returns {string} Markdown table content.
+ * @param {string} fileName The name of the file.
+ * @return {Array<{file: string, title: string, results: Record<string,number[]>[]}>} Parsed object.
*/
-function formatAsMarkdownTable( rows ) {
- let result = '';
- const headers = Object.keys( rows[ 0 ] );
- for ( const header of headers ) {
- result += `| ${ header } `;
- }
- result += '|\n';
- for ( const header of headers ) {
- result += '| ------ ';
- }
- result += '|\n';
-
- for ( const row of rows ) {
- for ( const value of Object.values( row ) ) {
- result += `| ${ value } `;
- }
- result += '|\n';
+function parseFile( fileName ) {
+ const file = join( process.env.WP_ARTIFACTS_PATH, fileName );
+ if ( ! existsSync( file ) ) {
+ return [];
}
- return result;
+ return JSON.parse( readFileSync( file, 'utf8' ) );
}
/**
- * Returns a Markdown link to a Git commit on the current GitHub repository.
- *
- * For example, turns `a5c3785ed8d6a35868bc169f07e40e889087fd2e`
- * into (https://github.com/wordpress/wordpress-develop/commit/36fe58a8c64dcc83fc21bddd5fcf054aef4efb27)[36fe58a].
- *
- * @param {string} sha Commit SHA.
- * @return string Link
+ * @type {Array<{file: string, title: string, results: Record<string,number[]>[]}>}
*/
-function linkToSha(sha) {
- const repoName = process.env.GITHUB_REPOSITORY || 'wordpress/wordpress-develop';
+const beforeStats = parseFile( 'before-performance-results.json' );
- return `[${sha.slice(0, 7)}](https://github.com/${repoName}/commit/${sha})`;
-}
+/**
+ * @type {Array<{file: string, title: string, results: Record<string,number[]>[]}>}
+ */
+const afterStats = parseFile( 'performance-results.json' );
-let summaryMarkdown = `# Performance Test Results\n\n`;
-
-if ( process.env.GITHUB_SHA ) {
- summaryMarkdown += `🛎️ Performance test results for ${ linkToSha( process.env.GITHUB_SHA ) } are in!\n\n`;
-} else {
- summaryMarkdown += `🛎️ Performance test results are in!\n\n`;
-}
+let summaryMarkdown = `## Performance Test Results\n\n`;
if ( process.env.TARGET_SHA ) {
- summaryMarkdown += `This compares the results from this commit with the ones from ${ linkToSha( process.env.TARGET_SHA ) }.\n\n`;
+ if ( beforeStats.length > 0 ) {
+ if (process.env.GITHUB_SHA) {
+ summaryMarkdown += `This compares the results from this commit (${linkToSha(
+ process.env.GITHUB_SHA
+ )}) with the ones from ${linkToSha(process.env.TARGET_SHA)}.\n\n`;
+ } else {
+ summaryMarkdown += `This compares the results from this commit with the ones from ${linkToSha(
+ process.env.TARGET_SHA
+ )}.\n\n`;
+ }
+ } else {
+ summaryMarkdown += `Note: no build was found for the target commit ${linkToSha(process.env.TARGET_SHA)}. No comparison is possible.\n\n`;
+ }
}
+const numberOfRepetitions = afterStats[ 0 ].results.length;
+const numberOfIterations = Object.values( afterStats[ 0 ].results[ 0 ] )[ 0 ]
+ .length;
+
+const repetitions = `${ numberOfRepetitions } ${
+ numberOfRepetitions === 1 ? 'repetition' : 'repetitions'
+}`;
+const iterations = `${ numberOfIterations } ${
+ numberOfIterations === 1 ? 'iteration' : 'iterations'
+}`;
+
+summaryMarkdown += `All numbers are median values over ${ repetitions } with ${ iterations } each.\n\n`;
+
if ( process.env.GITHUB_SHA ) {
summaryMarkdown += `**Note:** Due to the nature of how GitHub Actions work, some variance in the results is expected.\n\n`;
}
console.log( 'Performance Test Results\n' );
-console.log( 'Note: Due to the nature of how GitHub Actions work, some variance in the results is expected.\n' );
-
-/**
- * Nicely formats a given value.
- *
- * @param {string} metric Metric.
- * @param {number} value
- */
-function formatValue( metric, value) {
- if ( null === value ) {
- return 'N/A';
- }
- if ( 'wpMemoryUsage' === metric ) {
- return `${ ( value / Math.pow( 10, 6 ) ).toFixed( 2 ) } MB`;
- }
+console.log(
+ `All numbers are median values over ${ repetitions } with ${ iterations } each.\n`
+);
- return `${ value.toFixed( 2 ) } ms`;
+if ( process.env.GITHUB_SHA ) {
+ console.log(
+ 'Note: Due to the nature of how GitHub Actions work, some variance in the results is expected.\n'
+ );
}
-for ( const key of testSuites ) {
- const current = testResults[ key ] || {};
- const prev = prevResults[ key ] || {};
-
- const title = ( key.charAt( 0 ).toUpperCase() + key.slice( 1 ) ).replace(
- /-+/g,
- ' '
- );
+for ( const { title, results } of afterStats ) {
+ const prevStat = beforeStats.find( ( s ) => s.title === title );
+ /**
+ * @type {Array<Record<string, string>>}
+ */
const rows = [];
- for ( const [ metric, values ] of Object.entries( current ) ) {
- const value = median( values );
- const prevValue = prev[ metric ] ? median( prev[ metric ] ) : null;
+ const newResults = accumulateValues( results );
+ // Only do comparison if the number of results is the same.
+ const prevResults =
+ prevStat && prevStat.results.length === results.length
+ ? accumulateValues( prevStat.results )
+ : {};
+
+ for ( const [ metric, values ] of Object.entries( newResults ) ) {
+ const prevValues = prevResults[ metric ] ? prevResults[ metric ] : null;
- const delta = null !== prevValue ? value - prevValue : 0
+ const value = median( values );
+ const prevValue = prevValues ? median( prevValues ) : 0;
+ const delta = value - prevValue;
const percentage = ( delta / value ) * 100;
+ const showDiff =
+ metric !== 'wpExtObjCache' && ! Number.isNaN( percentage );
+
rows.push( {
Metric: metric,
- Before: formatValue( metric, prevValue ),
+ Before: prevValues ? formatValue( metric, prevValue ) : 'N/A',
After: formatValue( metric, value ),
- 'Diff abs.': formatValue( metric, delta ),
- 'Diff %': `${ percentage.toFixed( 2 ) } %`,
+ 'Diff abs.': showDiff ? formatValue( metric, delta ) : '',
+ 'Diff %': showDiff ? `${ percentage.toFixed( 2 ) } %` : '',
+ STD: showDiff
+ ? formatValue( metric, standardDeviation( values ) )
+ : '',
+ MAD: showDiff
+ ? formatValue( metric, medianAbsoluteDeviation( values ) )
+ : '',
} );
}
+ console.log( title );
if ( rows.length > 0 ) {
- summaryMarkdown += `## ${ title }\n\n`;
- summaryMarkdown += `${ formatAsMarkdownTable( rows ) }\n`;
-
- console.log( title );
console.table( rows );
+ } else {
+ console.log( '(no results)' );
}
+
+ summaryMarkdown += `**${ title }**\n\n`;
+ summaryMarkdown += `${ formatAsMarkdownTable( rows ) }\n`;
}
+writeFileSync(
+ join( process.env.WP_ARTIFACTS_PATH, '/performance-results.md' ),
+ summaryMarkdown
+);
+
if ( summaryFile ) {
- fs.writeFileSync(
- summaryFile,
- summaryMarkdown
- );
+ writeFileSync( summaryFile, summaryMarkdown );
}
diff --git a/tests/performance/config/global-setup.js b/tests/performance/config/global-setup.js
index f3a0a4f26a..25e99a47d8 100644
--- a/tests/performance/config/global-setup.js
+++ b/tests/performance/config/global-setup.js
@@ -30,9 +30,7 @@ async function globalSetup( config ) {
await requestUtils.setupRest();
// Reset the test environment before running the tests.
- await Promise.all( [
- requestUtils.activateTheme( 'twentytwentyone' ),
- ] );
+ await Promise.all( [ requestUtils.activateTheme( 'twentytwentyone' ) ] );
await requestContext.dispose();
}
diff --git a/tests/performance/config/performance-reporter.js b/tests/performance/config/performance-reporter.js
index e557faa135..9617625eed 100644
--- a/tests/performance/config/performance-reporter.js
+++ b/tests/performance/config/performance-reporter.js
@@ -1,13 +1,8 @@
/**
* External dependencies
*/
-import { join, dirname, basename } from 'node:path';
-import { writeFileSync } from 'node:fs';
-
-/**
- * Internal dependencies
- */
-import { getResultsFilename } from '../utils';
+import { join } from 'node:path';
+import { writeFileSync, existsSync, mkdirSync } from 'node:fs';
/**
* @implements {import('@playwright/test/reporter').Reporter}
@@ -15,6 +10,15 @@ import { getResultsFilename } from '../utils';
class PerformanceReporter {
/**
*
+ * @type {Record<string,{title: string; results: Record< string, number[] >[];}>}
+ */
+ allResults = {};
+
+ /**
+ * Called after a test has been finished in the worker process.
+ *
+ * Used to add test results to the final summary of all tests.
+ *
* @param {import('@playwright/test/reporter').TestCase} test
* @param {import('@playwright/test/reporter').TestResult} result
*/
@@ -24,15 +28,59 @@ class PerformanceReporter {
);
if ( performanceResults?.body ) {
- writeFileSync(
- join(
- dirname( test.location.file ),
- getResultsFilename( basename( test.location.file, '.js' ) )
- ),
- performanceResults.body.toString( 'utf-8' )
+ // 0 = empty, 1 = browser, 2 = file name, 3 = test suite name, 4 = test name.
+ const titlePath = test.titlePath();
+ const title = `${ titlePath[ 3 ] } › ${ titlePath[ 4 ] }`;
+
+ // results is an array in case repeatEach is > 1.
+
+ this.allResults[ title ] ??= {
+ file: test.location.file, // Unused, but useful for debugging.
+ results: [],
+ };
+
+ this.allResults[ title ].results.push(
+ JSON.parse( performanceResults.body.toString( 'utf-8' ) )
);
}
}
+
+ /**
+ * Called after all tests have been run, or testing has been interrupted.
+ *
+ * Writes all raw numbers to a file for further processing,
+ * for example to compare with a previous run.
+ *
+ * @param {import('@playwright/test/reporter').FullResult} result
+ */
+ onEnd( result ) {
+ const summary = [];
+
+ for ( const [ title, { file, results } ] of Object.entries(
+ this.allResults
+ ) ) {
+ summary.push( {
+ file,
+ title,
+ results,
+ } );
+ }
+
+ if ( ! existsSync( process.env.WP_ARTIFACTS_PATH ) ) {
+ mkdirSync( process.env.WP_ARTIFACTS_PATH );
+ }
+
+ const prefix = process.env.TEST_RESULTS_PREFIX;
+ const fileNamePrefix = prefix ? `${ prefix }-` : '';
+
+ writeFileSync(
+ join(
+ process.env.WP_ARTIFACTS_PATH,
+ `${ fileNamePrefix }performance-results.json`
+ ),
+ JSON.stringify( summary, null, 2 )
+ );
+ }
}
export default PerformanceReporter;
diff --git a/tests/performance/log-results.js b/tests/performance/log-results.js
index 14c836ff67..66fe1e5291 100644
--- a/tests/performance/log-results.js
+++ b/tests/performance/log-results.js
@@ -1,71 +1,92 @@
#!/usr/bin/env node
+/*
+ * Get the test results and format them in the way required by the API.
+ *
+ * Contains some backward compatibility logic for the original test suite format,
+ * see #59900 for details.
+ */
+
/**
* External dependencies.
*/
-const fs = require( 'fs' );
-const path = require( 'path' );
const https = require( 'https' );
-const [ token, branch, hash, baseHash, timestamp, host ] = process.argv.slice( 2 );
-const { median } = require( './utils' );
-
-// The list of test suites to log.
-const testSuites = [
- 'admin',
- 'admin-l10n',
- 'home-block-theme',
- 'home-block-theme-l10n',
- 'home-classic-theme',
- 'home-classic-theme-l10n',
-];
-
-// A list of results to parse based on test suites.
-const testResults = testSuites.map(( key ) => ({
- key,
- file: `${ key }.test.results.json`,
-}));
-
-// A list of base results to parse based on test suites.
-const baseResults = testSuites.map(( key ) => ({
- key,
- file: `base-${ key }.test.results.json`,
-}));
+const [ token, branch, hash, baseHash, timestamp, host ] =
+ process.argv.slice( 2 );
+const { median, parseFile, accumulateValues } = require( './utils' );
+
+const testSuiteMap = {
+ 'Admin › Locale: en_US': 'admin',
+ 'Admin › Locale: de_DE': 'admin-l10n',
+ 'Front End › Theme: twentytwentyone, Locale: en_US': 'home-classic-theme',
+ 'Front End › Theme: twentytwentyone, Locale: de_DE':
+ 'home-classic-theme-l10n',
+ 'Front End › Theme: twentytwentythree, Locale: en_US': 'home-block-theme',
+ 'Front End › Theme: twentytwentythree, Locale: de_DE':
+ 'home-block-theme-l10n',
+};
/**
- * Parse test files into JSON objects.
- *
- * @param {string} fileName The name of the file.
- * @returns An array of parsed objects from each file.
+ * @type {Array<{file: string, title: string, results: Record<string,number[]>[]}>}
*/
-const parseFile = ( fileName ) => (
- JSON.parse(
- fs.readFileSync( path.join( __dirname, '/specs/', fileName ), 'utf8' )
- )
-);
+const afterStats = parseFile( 'performance-results.json' );
+
+/**
+ * @type {Array<{file: string, title: string, results: Record<string,number[]>[]}>}
+ */
+const baseStats = parseFile( 'base-performance-results.json' );
+
+/**
+ * @type {Record<string, number>}
+ */
+const metrics = {};
+/**
+ * @type {Record<string, number>}
+ */
+const baseMetrics = {};
+
+for ( const { title, results } of afterStats ) {
+ const testSuiteName = testSuiteMap[ title ];
+ if ( ! testSuiteName ) {
+ continue;
+ }
+
+ const baseStat = baseStats.find( ( s ) => s.title === title );
+
+ const currResults = accumulateValues( results );
+ const baseResults = accumulateValues( baseStat.results );
+
+ for ( const [ metric, values ] of Object.entries( currResults ) ) {
+ metrics[ `${ testSuiteName }-${ metric }` ] = median( values );
+ }
+
+ for ( const [ metric, values ] of Object.entries( baseResults ) ) {
+ baseMetrics[ `${ testSuiteName }-${ metric }` ] = median( values );
+ }
+}
+
+process.exit( 0 );
/**
* Gets the array of metrics from a list of results.
*
* @param {Object[]} results A list of results to format.
- * @return {Object[]} Metrics.
+ * @return {Object} Metrics.
*/
const formatResults = ( results ) => {
- return results.reduce(
- ( result, { key, file } ) => {
- return {
- ...result,
- ...Object.fromEntries(
- Object.entries(
- parseFile( file ) ?? {}
- ).map( ( [ metric, value ] ) => [
+ return results.reduce( ( result, { key, file } ) => {
+ return {
+ ...result,
+ ...Object.fromEntries(
+ Object.entries( parseFile( file ) ?? {} ).map(
+ ( [ metric, value ] ) => [
key + '-' + metric,
- median ( value ),
- ] )
- ),
- };
- },
- {}
- );
+ median( value ),
+ ]
+ )
+ ),
+ };
+ }, {} );
};
const data = new TextEncoder().encode(
diff --git a/tests/performance/playwright.config.js b/tests/performance/playwright.config.js
index 1d2781f73c..c4df0e2872 100644
--- a/tests/performance/playwright.config.js
+++ b/tests/performance/playwright.config.js
@@ -23,9 +23,11 @@ const config = defineConfig( {
forbidOnly: !! process.env.CI,
workers: 1,
retries: 0,
+ repeatEach: 2,
timeout: parseInt( process.env.TIMEOUT || '', 10 ) || 600_000, // Defaults to 10 minutes.
// Don't report slow test "files", as we will be running our tests in serial.
reportSlowTests: null,
+ preserveOutput: 'never',
webServer: {
...baseConfig.webServer,
command: 'npm run env:start',
@@ -37,4 +39,3 @@ const config = defineConfig( {
} );
export default config;
-
diff --git a/tests/performance/results.js b/tests/performance/results.js
deleted file mode 100644
index d9f981f5e7..0000000000
--- a/tests/performance/results.js
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/usr/bin/env node
-
-/**
- * External dependencies.
- */
-const fs = require( 'node:fs' );
-const { join } = require( 'node:path' );
-const { median, getResultsFilename } = require( './utils' );
-
-const testSuites = [
- 'admin',
- 'admin-l10n',
- 'home-classic-theme',
- 'home-classic-theme-l10n',
- 'home-block-theme',
- 'home-block-theme-l10n',
-];
-
-console.log( '\n>> 🎉 Results 🎉 \n' );
-
-for ( const testSuite of testSuites ) {
- const resultsFileName = getResultsFilename( testSuite + '.test' );
- const resultsPath = join( __dirname, '/specs/', resultsFileName );
- fs.readFile( resultsPath, "utf8", ( err, data ) => {
- if ( err ) {
- console.log( "File read failed:", err );
- return;
- }
- const convertString = testSuite.charAt( 0 ).toUpperCase() + testSuite.slice( 1 );
- console.log( convertString.replace( /[-]+/g, " " ) + ':' );
-
- tableData = JSON.parse( data );
- const rawResults = [];
-
- for ( var key in tableData ) {
- if ( tableData.hasOwnProperty( key ) ) {
- rawResults[ key ] = median( tableData[ key ] );
- }
- }
- console.table( rawResults );
- });
-}
diff --git a/tests/performance/specs/admin-l10n.test.js b/tests/performance/specs/admin-l10n.test.js
deleted file mode 100644
index a8c9be0997..0000000000
--- a/tests/performance/specs/admin-l10n.test.js
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { test } from '@wordpress/e2e-test-utils-playwright';
-
-/**
- * Internal dependencies
- */
-import { camelCaseDashes } from '../utils';
-
-const results = {
- timeToFirstByte: [],
-};
-
-test.describe( 'Admin (L10N)', () => {
- test.beforeAll( async ( { requestUtils } ) => {
- await requestUtils.activateTheme( 'twentytwentyone' );
- await requestUtils.updateSiteSettings( {
- language: 'de_DE',
- } );
- } );
-
- test.afterAll( async ( { requestUtils }, testInfo ) => {
- await testInfo.attach( 'results', {
- body: JSON.stringify( results, null, 2 ),
- contentType: 'application/json',
- } );
- await requestUtils.updateSiteSettings( {
- language: '',
- } );
- } );
-
- const iterations = Number( process.env.TEST_RUNS );
- for ( let i = 1; i <= iterations; i++ ) {
- test( `Measure load time metrics (${ i } of ${ iterations })`, async ( {
- admin,
- metrics,
- } ) => {
- await admin.visitAdminPage( '/' );
-
- const serverTiming = await metrics.getServerTiming();
-
- for ( const [ key, value ] of Object.entries( serverTiming ) ) {
- results[ camelCaseDashes( key ) ] ??= [];
- results[ camelCaseDashes( key ) ].push( value );
- }
-
- const ttfb = await metrics.getTimeToFirstByte();
- results.timeToFirstByte.push( ttfb );
- } );
- }
-} );
diff --git a/tests/performance/specs/admin.test.js b/tests/performance/specs/admin.test.js
index 9860229114..36bdd9c628 100644
--- a/tests/performance/specs/admin.test.js
+++ b/tests/performance/specs/admin.test.js
@@ -12,35 +12,59 @@ const results = {
timeToFirstByte: [],
};
+const locales = [ 'en_US', 'de_DE' ];
+
test.describe( 'Admin', () => {
- test.beforeAll( async ( { requestUtils } ) => {
- await requestUtils.activateTheme( 'twentytwentyone' );
- } );
-
- test.afterAll( async ( {}, testInfo ) => {
- await testInfo.attach( 'results', {
- body: JSON.stringify( results, null, 2 ),
- contentType: 'application/json',
- } );
- } );
+ for ( const locale of locales ) {
+ test.describe( `Locale: ${ locale }`, () => {
+ test.beforeAll( async ( { requestUtils } ) => {
+ await requestUtils.activateTheme( 'twentytwentyone' );
+ await requestUtils.updateSiteSettings( {
+ language: 'en_US' === locale ? '' : locale,
+ } );
+ } );
- const iterations = Number( process.env.TEST_RUNS );
- for ( let i = 1; i <= iterations; i++ ) {
- test( `Measure load time metrics (${ i } of ${ iterations })`, async ( {
- admin,
- metrics,
- } ) => {
- await admin.visitAdminPage( '/' );
+ test.afterAll( async ( { requestUtils }, testInfo ) => {
+ await testInfo.attach( 'results', {
+ body: JSON.stringify( results, null, 2 ),
+ contentType: 'application/json',
+ } );
- const serverTiming = await metrics.getServerTiming();
+ await requestUtils.updateSiteSettings( {
+ language: '',
+ } );
- for ( const [ key, value ] of Object.entries( serverTiming ) ) {
- results[ camelCaseDashes( key ) ] ??= [];
- results[ camelCaseDashes( key ) ].push( value );
- }
+ results.timeToFirstByte = [];
+ } );
+
+ test.afterAll( async ( {}, testInfo ) => {
+ await testInfo.attach( 'results', {
+ body: JSON.stringify( results, null, 2 ),
+ contentType: 'application/json',
+ } );
+ } );
- const ttfb = await metrics.getTimeToFirstByte();
- results.timeToFirstByte.push( ttfb );
+ const iterations = Number( process.env.TEST_RUNS );
+ for ( let i = 1; i <= iterations; i++ ) {
+ test( `Measure load time metrics (${ i } of ${ iterations })`, async ( {
+ admin,
+ metrics,
+ } ) => {
+ await admin.visitAdminPage( '/' );
+
+ const serverTiming = await metrics.getServerTiming();
+
+ for ( const [ key, value ] of Object.entries(
+ serverTiming
+ ) ) {
+ results[ camelCaseDashes( key ) ] ??= [];
+ results[ camelCaseDashes( key ) ].push( value );
+ }
+
+ const ttfb = await metrics.getTimeToFirstByte();
+ results.timeToFirstByte.push( ttfb );
+ } );
+ }
} );
}
} );
diff --git a/tests/performance/specs/home-block-theme-l10n.test.js b/tests/performance/specs/home-block-theme-l10n.test.js
deleted file mode 100644
index 591925056f..0000000000
--- a/tests/performance/specs/home-block-theme-l10n.test.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { test } from '@wordpress/e2e-test-utils-playwright';
-
-/**
- * Internal dependencies
- */
-import { camelCaseDashes } from '../utils';
-
-const results = {
- timeToFirstByte: [],
- largestContentfulPaint: [],
- lcpMinusTtfb: [],
-};
-
-test.describe( 'Front End - Twenty Twenty Three (L10N)', () => {
- test.use( {
- storageState: {}, // User will be logged out.
- } );
-
- test.beforeAll( async ( { requestUtils } ) => {
- await requestUtils.activateTheme( 'twentytwentythree' );
- await requestUtils.updateSiteSettings( {
- language: 'de_DE',
- } );
- } );
-
- test.afterAll( async ( { requestUtils }, testInfo ) => {
- await testInfo.attach( 'results', {
- body: JSON.stringify( results, null, 2 ),
- contentType: 'application/json',
- } );
- await requestUtils.activateTheme( 'twentytwentyone' );
- await requestUtils.updateSiteSettings( {
- language: '',
- } );
- } );
-
- const iterations = Number( process.env.TEST_RUNS );
- for ( let i = 1; i <= iterations; i++ ) {
- test( `Measure load time metrics (${ i } of ${ iterations })`, async ( {
- page,
- metrics,
- } ) => {
- await page.goto( '/' );
-
- const serverTiming = await metrics.getServerTiming();
-
- for ( const [ key, value ] of Object.entries( serverTiming ) ) {
- results[ camelCaseDashes( key ) ] ??= [];
- results[ camelCaseDashes( key ) ].push( value );
- }
-
- const ttfb = await metrics.getTimeToFirstByte();
- const lcp = await metrics.getLargestContentfulPaint();
-
- results.largestContentfulPaint.push( lcp );
- results.timeToFirstByte.push( ttfb );
- results.lcpMinusTtfb.push( lcp - ttfb );
- } );
- }
-} );
diff --git a/tests/performance/specs/home-block-theme.test.js b/tests/performance/specs/home-block-theme.test.js
deleted file mode 100644
index 00bccc6996..0000000000
--- a/tests/performance/specs/home-block-theme.test.js
+++ /dev/null
@@ -1,57 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { test } from '@wordpress/e2e-test-utils-playwright';
-
-/**
- * Internal dependencies
- */
-import { camelCaseDashes } from '../utils';
-
-const results = {
- timeToFirstByte: [],
- largestContentfulPaint: [],
- lcpMinusTtfb: [],
-};
-
-test.describe( 'Front End - Twenty Twenty Three', () => {
- test.use( {
- storageState: {}, // User will be logged out.
- } );
-
- test.beforeAll( async ( { requestUtils } ) => {
- await requestUtils.activateTheme( 'twentytwentythree' );
- } );
-
- test.afterAll( async ( { requestUtils }, testInfo ) => {
- await testInfo.attach( 'results', {
- body: JSON.stringify( results, null, 2 ),
- contentType: 'application/json',
- } );
- await requestUtils.activateTheme( 'twentytwentyone' );
- } );
-
- const iterations = Number( process.env.TEST_RUNS );
- for ( let i = 1; i <= iterations; i++ ) {
- test( `Measure load time metrics (${ i } of ${ iterations })`, async ( {
- page,
- metrics,
- } ) => {
- await page.goto( '/' );
-
- const serverTiming = await metrics.getServerTiming();
-
- for ( const [ key, value ] of Object.entries( serverTiming ) ) {
- results[ camelCaseDashes( key ) ] ??= [];
- results[ camelCaseDashes( key ) ].push( value );
- }
-
- const ttfb = await metrics.getTimeToFirstByte();
- const lcp = await metrics.getLargestContentfulPaint();
-
- results.largestContentfulPaint.push( lcp );
- results.timeToFirstByte.push( ttfb );
- results.lcpMinusTtfb.push( lcp - ttfb );
- } );
- }
-} );
diff --git a/tests/performance/specs/home-classic-theme-l10n.test.js b/tests/performance/specs/home-classic-theme-l10n.test.js
deleted file mode 100644
index e6f6e1cbb9..0000000000
--- a/tests/performance/specs/home-classic-theme-l10n.test.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { test } from '@wordpress/e2e-test-utils-playwright';
-
-/**
- * Internal dependencies
- */
-import { camelCaseDashes } from '../utils';
-
-const results = {
- timeToFirstByte: [],
- largestContentfulPaint: [],
- lcpMinusTtfb: [],
-};
-
-test.describe( 'Front End - Twenty Twenty One (L10N)', () => {
- test.use( {
- storageState: {}, // User will be logged out.
- } );
-
- test.beforeAll( async ( { requestUtils } ) => {
- await requestUtils.activateTheme( 'twentytwentyone' );
- await requestUtils.updateSiteSettings( {
- language: 'de_DE',
- } );
- } );
-
- test.afterAll( async ( { requestUtils }, testInfo ) => {
- await testInfo.attach( 'results', {
- body: JSON.stringify( results, null, 2 ),
- contentType: 'application/json',
- } );
- await requestUtils.updateSiteSettings( {
- language: '',
- } );
- } );
-
- const iterations = Number( process.env.TEST_RUNS );
- for ( let i = 1; i <= iterations; i++ ) {
- test( `Measure load time metrics (${ i } of ${ iterations })`, async ( {
- page,
- metrics,
- } ) => {
- await page.goto( '/' );
-
- const serverTiming = await metrics.getServerTiming();
-
- for ( const [ key, value ] of Object.entries( serverTiming ) ) {
- results[ camelCaseDashes( key ) ] ??= [];
- results[ camelCaseDashes( key ) ].push( value );
- }
-
- const ttfb = await metrics.getTimeToFirstByte();
- const lcp = await metrics.getLargestContentfulPaint();
-
- results.largestContentfulPaint.push( lcp );
- results.timeToFirstByte.push( ttfb );
- results.lcpMinusTtfb.push( lcp - ttfb );
- } );
- }
-} );
diff --git a/tests/performance/specs/home-classic-theme.test.js b/tests/performance/specs/home-classic-theme.test.js
deleted file mode 100644
index a95e50fa06..0000000000
--- a/tests/performance/specs/home-classic-theme.test.js
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { test } from '@wordpress/e2e-test-utils-playwright';
-
-/**
- * Internal dependencies
- */
-import { camelCaseDashes } from '../utils';
-
-const results = {
- timeToFirstByte: [],
- largestContentfulPaint: [],
- lcpMinusTtfb: [],
-};
-
-test.describe( 'Front End - Twenty Twenty One', () => {
- test.use( {
- storageState: {}, // User will be logged out.
- } );
-
- test.beforeAll( async ( { requestUtils } ) => {
- await requestUtils.activateTheme( 'twentytwentyone' );
- } );
-
- test.afterAll( async ( {}, testInfo ) => {
- await testInfo.attach( 'results', {
- body: JSON.stringify( results, null, 2 ),
- contentType: 'application/json',
- } );
- } );
-
- const iterations = Number( process.env.TEST_RUNS );
- for ( let i = 1; i <= iterations; i++ ) {
- test( `Measure load time metrics (${ i } of ${ iterations })`, async ( {
- page,
- metrics,
- } ) => {
- await page.goto( '/' );
-
- const serverTiming = await metrics.getServerTiming();
-
- for ( const [ key, value ] of Object.entries( serverTiming ) ) {
- results[ camelCaseDashes( key ) ] ??= [];
- results[ camelCaseDashes( key ) ].push( value );
- }
-
- const ttfb = await metrics.getTimeToFirstByte();
- const lcp = await metrics.getLargestContentfulPaint();
-
- results.largestContentfulPaint.push( lcp );
- results.timeToFirstByte.push( ttfb );
- results.lcpMinusTtfb.push( lcp - ttfb );
- } );
- }
-} );
diff --git a/tests/performance/specs/home.test.js b/tests/performance/specs/home.test.js
new file mode 100644
index 0000000000..b88b6adb9f
--- /dev/null
+++ b/tests/performance/specs/home.test.js
@@ -0,0 +1,79 @@
+/**
+ * WordPress dependencies
+ */
+import { test } from '@wordpress/e2e-test-utils-playwright';
+
+/**
+ * Internal dependencies
+ */
+import { camelCaseDashes } from '../utils';
+
+const results = {
+ timeToFirstByte: [],
+ largestContentfulPaint: [],
+ lcpMinusTtfb: [],
+};
+
+const themes = [ 'twentytwentyone', 'twentytwentythree', 'twentytwentyfour' ];
+
+const locales = [ 'en_US', 'de_DE' ];
+
+test.describe( 'Front End', () => {
+ test.use( {
+ storageState: {}, // User will be logged out.
+ } );
+
+ for ( const theme of themes ) {
+ for ( const locale of locales ) {
+ test.describe( `Theme: ${ theme }, Locale: ${ locale }`, () => {
+ test.beforeAll( async ( { requestUtils } ) => {
+ await requestUtils.activateTheme( theme );
+ await requestUtils.updateSiteSettings( {
+ language: 'en_US' === locale ? '' : locale,
+ } );
+ } );
+
+ test.afterAll( async ( { requestUtils }, testInfo ) => {
+ await testInfo.attach( 'results', {
+ body: JSON.stringify( results, null, 2 ),
+ contentType: 'application/json',
+ } );
+
+ await requestUtils.updateSiteSettings( {
+ language: '',
+ } );
+
+ results.largestContentfulPaint = [];
+ results.timeToFirstByte = [];
+ results.lcpMinusTtfb = [];
+ } );
+
+ const iterations = Number( process.env.TEST_RUNS );
+ for ( let i = 1; i <= iterations; i++ ) {
+ test( `Measure load time metrics (${ i } of ${ iterations })`, async ( {
+ page,
+ metrics,
+ } ) => {
+ await page.goto( '/' );
+
+ const serverTiming = await metrics.getServerTiming();
+
+ for ( const [ key, value ] of Object.entries(
+ serverTiming
+ ) ) {
+ results[ camelCaseDashes( key ) ] ??= [];
+ results[ camelCaseDashes( key ) ].push( value );
+ }
+
+ const ttfb = await metrics.getTimeToFirstByte();
+ const lcp = await metrics.getLargestContentfulPaint();
+
+ results.largestContentfulPaint.push( lcp );
+ results.timeToFirstByte.push( ttfb );
+ results.lcpMinusTtfb.push( lcp - ttfb );
+ } );
+ }
+ } );
+ }
+ }
+} );
diff --git a/tests/performance/utils.js b/tests/performance/utils.js
index f56380e9c2..4d023be586 100644
--- a/tests/performance/utils.js
+++ b/tests/performance/utils.js
@@ -1,4 +1,27 @@
/**
+ * External dependencies.
+ */
+const { readFileSync, existsSync } = require( 'node:fs' );
+const { join } = require( 'node:path' );
+
+process.env.WP_ARTIFACTS_PATH ??= join( process.cwd(), 'artifacts' );
+
+/**
+ * Parse test files into JSON objects.
+ *
+ * @param {string} fileName The name of the file.
+ * @return {Array<{file: string, title: string, results: Record<string,number[]>[]}>} Parsed object.
+ */
+function parseFile( fileName ) {
+ const file = join( process.env.WP_ARTIFACTS_PATH, fileName );
+ if ( ! existsSync( file ) ) {
+ return [];
+ }
+
+ return JSON.parse( readFileSync( file, 'utf8' ) );
+}
+
+/**
* Computes the median number from an array numbers.
*
* @param {number[]} array
@@ -13,27 +36,157 @@ function median( array ) {
: ( numbers[ mid - 1 ] + numbers[ mid ] ) / 2;
}
+function camelCaseDashes( str ) {
+ return str.replace( /-([a-z])/g, function ( g ) {
+ return g[ 1 ].toUpperCase();
+ } );
+}
+
/**
- * Gets the result file name.
+ * Formats an array of objects as a Markdown table.
+ *
+ * For example, this array:
*
- * @param {string} fileName File name.
+ * [
+ * {
+ * foo: 123,
+ * bar: 456,
+ * baz: 'Yes',
+ * },
+ * {
+ * foo: 777,
+ * bar: 999,
+ * baz: 'No',
+ * }
+ * ]
*
- * @return {string} Result file name.
+ * Will result in the following table:
+ *
+ * | foo | bar | baz |
+ * |-----|-----|-----|
+ * | 123 | 456 | Yes |
+ * | 777 | 999 | No |
+ *
+ * @param {Array<Object>} rows Table rows.
+ * @returns {string} Markdown table content.
*/
-function getResultsFilename( fileName ) {
- const prefix = process.env.TEST_RESULTS_PREFIX;
- const fileNamePrefix = prefix ? `${ prefix }-` : '';
- return `${fileNamePrefix + fileName}.results.json`;
+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 ( const header of headers ) {
+ result += '| ------ ';
+ }
+ result += '|\n';
+
+ for ( const row of rows ) {
+ for ( const value of Object.values( row ) ) {
+ result += `| ${ value } `;
+ }
+ result += '|\n';
+ }
+
+ return result;
}
-function camelCaseDashes( str ) {
- return str.replace( /-([a-z])/g, function( g ) {
- return g[ 1 ].toUpperCase();
- } );
+/**
+ * Nicely formats a given value.
+ *
+ * @param {string} metric Metric.
+ * @param {number} value
+ */
+function formatValue( metric, value ) {
+ if ( null === value ) {
+ return 'N/A';
+ }
+
+ if ( 'wpMemoryUsage' === metric ) {
+ return `${ ( value / Math.pow( 10, 6 ) ).toFixed( 2 ) } MB`;
+ }
+
+ if ( 'wpExtObjCache' === metric ) {
+ return 1 === value ? 'yes' : 'no';
+ }
+
+ if ( 'wpDbQueries' === metric ) {
+ return value;
+ }
+
+ return `${ value.toFixed( 2 ) } ms`;
+}
+
+/**
+ * Returns a Markdown link to a Git commit on the current GitHub repository.
+ *
+ * For example, turns `a5c3785ed8d6a35868bc169f07e40e889087fd2e`
+ * into (https://github.com/wordpress/wordpress-develop/commit/36fe58a8c64dcc83fc21bddd5fcf054aef4efb27)[36fe58a].
+ *
+ * @param {string} sha Commit SHA.
+ * @return string Link
+ */
+function linkToSha( sha ) {
+ const repoName =
+ process.env.GITHUB_REPOSITORY || 'wordpress/wordpress-develop';
+
+ return `[${ sha.slice(
+ 0,
+ 7
+ ) }](https://github.com/${ repoName }/commit/${ sha })`;
+}
+
+function standardDeviation( array = [] ) {
+ if ( ! array.length ) {
+ return 0;
+ }
+
+ const mean = array.reduce( ( a, b ) => a + b ) / array.length;
+ return Math.sqrt(
+ array
+ .map( ( x ) => Math.pow( x - mean, 2 ) )
+ .reduce( ( a, b ) => a + b ) / array.length
+ );
+}
+
+function medianAbsoluteDeviation( array = [] ) {
+ if ( ! array.length ) {
+ return 0;
+ }
+
+ const med = median( array );
+ return median( array.map( ( a ) => Math.abs( a - med ) ) );
+}
+
+/**
+ *
+ * @param {Array<Record<string, number[]>>} results
+ * @returns {Record<string, number[]>}
+ */
+function accumulateValues( results ) {
+ return results.reduce( ( acc, result ) => {
+ for ( const [ metric, values ] of Object.entries( result ) ) {
+ acc[ metric ] = acc[ metric ] ?? [];
+ acc[ metric ].push( ...values );
+ }
+ return acc;
+ }, {} );
}
module.exports = {
+ parseFile,
median,
- getResultsFilename,
camelCaseDashes,
+ formatAsMarkdownTable,
+ formatValue,
+ linkToSha,
+ standardDeviation,
+ medianAbsoluteDeviation,
+ accumulateValues,
};
diff --git a/tests/performance/wp-content/mu-plugins/server-timing.php b/tests/performance/wp-content/mu-plugins/server-timing.php
index 53f83fea79..abf166fcc8 100644
--- a/tests/performance/wp-content/mu-plugins/server-timing.php
+++ b/tests/performance/wp-content/mu-plugins/server-timing.php
@@ -4,7 +4,7 @@ add_filter(
'template_include',
static function ( $template ) {
- global $timestart;
+ global $timestart, $wpdb;
$server_timing_values = array();
$template_start = microtime( true );
@@ -15,10 +15,7 @@ add_filter(
add_action(
'shutdown',
- static function () use ( $server_timing_values, $template_start ) {
-
- global $timestart;
-
+ static function () use ( $server_timing_values, $template_start, $wpdb ) {
$output = ob_get_clean();
$server_timing_values['template'] = microtime( true ) - $template_start;
@@ -30,7 +27,9 @@ add_filter(
* 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['memory-usage'] = memory_get_usage();
+ $server_timing_values['memory-usage'] = memory_get_usage();
+ $server_timing_values['db-queries'] = $wpdb->num_queries;
+ $server_timing_values['ext-obj-cache'] = wp_using_ext_object_cache() ? 1 : 0;
$header_values = array();
foreach ( $server_timing_values as $slug => $value ) {
@@ -50,3 +49,45 @@ add_filter(
},
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['total'] = 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['memory-usage'] = memory_get_usage();
+ $server_timing_values['db-queries'] = $wpdb->num_queries;
+ $server_timing_values['ext-obj-cache'] = wp_using_ext_object_cache() ? 1 : 0;
+
+ $header_values = array();
+ foreach ( $server_timing_values as $slug => $value ) {
+ if ( is_float( $value ) ) {
+ $value = round( $value * 1000.0, 2 );
+ }
+ $header_values[] = sprintf( 'wp-%1$s;dur=%2$s', $slug, $value );
+ }
+ header( 'Server-Timing: ' . implode( ', ', $header_values ) );
+
+ echo $output;
+ },
+ PHP_INT_MIN
+ );
+ },
+ PHP_INT_MAX
+);