From 11059a3b58cc5c9bf543f934995177a8b055c710 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 18 May 2020 13:47:21 +0100 Subject: [PATCH] Adds a command to run performance tests across branches --- bin/plugin/cli.js | 9 ++ bin/plugin/commands/performance.js | 228 +++++++++++++++++++++++++++++ bin/plugin/config.js | 18 +++ bin/plugin/lib/git.js | 68 +++++++++ bin/plugin/lib/utils.js | 5 +- bin/tsconfig.json | 3 + package-lock.json | 6 + package.json | 1 + 8 files changed, 336 insertions(+), 2 deletions(-) create mode 100644 bin/plugin/commands/performance.js diff --git a/bin/plugin/cli.js b/bin/plugin/cli.js index fc09399de675da..d99bd94f72a1aa 100755 --- a/bin/plugin/cli.js +++ b/bin/plugin/cli.js @@ -11,6 +11,7 @@ const program = require( 'commander' ); const { releaseRC, releaseStable } = require( './commands/release' ); const { prepublishNpmStablePackages } = require( './commands/packages' ); const { getReleaseChangelog } = require( './commands/changelog' ); +const { runPerformanceTests } = require( './commands/performance' ); program .command( 'release-plugin-rc' ) @@ -42,4 +43,12 @@ program .description( 'Generates a changelog from merged Pull Requests' ) .action( getReleaseChangelog ); +program + .command( 'performance-tests [branches...]' ) + .alias( 'perf' ) + .description( + 'Runs performance tests on two separate branches and outputs the result' + ) + .action( runPerformanceTests ); + program.parse( process.argv ); diff --git a/bin/plugin/commands/performance.js b/bin/plugin/commands/performance.js new file mode 100644 index 00000000000000..e5aefd87a853cf --- /dev/null +++ b/bin/plugin/commands/performance.js @@ -0,0 +1,228 @@ +/** + * External dependencies + */ +const { v4: uuid } = require( 'uuid' ); +const path = require( 'path' ); +const os = require( 'os' ); + +/** + * Internal dependencies + */ +const { formats, log } = require( '../lib/logger' ); +const { + runShellScript, + readJSONFile, + askForConfirmation, +} = require( '../lib/utils' ); +const git = require( '../lib/git' ); +const config = require( '../config' ); + +/** + * @typedef WPRawPerformanceResults + * + * @property {number[]} load Load Time. + * @property {number[]} domcontentloaded DOM Contentloaded time. + * @property {number[]} type Average type time. + * @property {number[]} focus Average block selection time. + */ + +/** + * @typedef WPPerformanceResults + * + * @property {number} load Load Time. + * @property {number} domcontentloaded DOM Contentloaded time. + * @property {number} type Average type time. + * @property {number} minType Minium type time. + * @property {number} maxType Maximum type time. + * @property {number} focus Average block selection time. + * @property {number} minFocus Min block selection time. + * @property {number} maxFocus Max block selection time. + */ +/** + * @typedef WPFormattedPerformanceResults + * + * @property {string} load Load Time. + * @property {string} domcontentloaded DOM Contentloaded time. + * @property {string} type Average type time. + * @property {string} minType Minium type time. + * @property {string} maxType Maximum type time. + * @property {string} focus Average block selection time. + * @property {string} minFocus Min block selection time. + * @property {string} maxFocus Max block selection time. + */ + +/** + * Computes the average number from an array numbers. + * + * @param {number[]} array + * @return {number} Average. + */ +function average( array ) { + return array.reduce( ( a, b ) => a + b ) / array.length; +} + +/** + * Computes the median number from an array numbers. + * + * @param {number[]} array + * @return {number} Median. + */ +const median = ( array ) => { + const mid = Math.floor( array.length / 2 ), + numbers = [ ...array ].sort( ( a, b ) => a - b ); + return array.length % 2 !== 0 + ? numbers[ mid ] + : ( numbers[ mid - 1 ] + numbers[ mid ] ) / 2; +}; + +/** + * Rounds and format a time passed in milliseconds. + * + * @param {number} number + * @return {string} Formatted time. + */ +function formatTime( number ) { + const factor = Math.pow( 10, 2 ); + return Math.round( number * factor ) / factor + ' ms'; +} + +/** + * Curate the raw performance results. + * + * @param {WPRawPerformanceResults} results + * @return {WPPerformanceResults} Curated Performance results. + */ +function curateResults( results ) { + return { + load: average( results.load ), + domcontentloaded: average( results.domcontentloaded ), + type: average( results.type ), + minType: Math.min( ...results.type ), + maxType: Math.max( ...results.type ), + focus: average( results.focus ), + minFocus: Math.min( ...results.focus ), + maxFocus: Math.max( ...results.focus ), + }; +} + +/** + * Runs the performance tests on a given branch. + * + * @param {string} performanceTestDirectory Path to the performance tests' clone. + * @param {string} environmentDirectory Path to the plugin environment's clone. + * @param {string} branch Branch name. + * + * @return {Promise} Performance results for the branch. + */ +async function getPerformanceResultsForBranch( + performanceTestDirectory, + environmentDirectory, + branch +) { + log( '>> Fetching the ' + formats.success( branch ) + ' branch' ); + await git.checkoutRemoteBranch( environmentDirectory, branch ); + + log( '>> Building the ' + formats.success( branch ) + ' branch' ); + await runShellScript( + 'npm install && npm run build', + environmentDirectory + ); + + log( + '>> Running the test on the ' + formats.success( branch ) + ' branch' + ); + const results = []; + for ( let i = 0; i < 3; i++ ) { + await runShellScript( + 'npm run test-performance', + performanceTestDirectory + ); + const rawResults = await readJSONFile( + path.join( + performanceTestDirectory, + 'packages/e2e-tests/specs/performance/results.json' + ) + ); + results.push( curateResults( rawResults ) ); + } + + return { + load: formatTime( median( results.map( ( r ) => r.load ) ) ), + domcontentloaded: formatTime( + median( results.map( ( r ) => r.domcontentloaded ) ) + ), + type: formatTime( median( results.map( ( r ) => r.type ) ) ), + minType: formatTime( median( results.map( ( r ) => r.minType ) ) ), + maxType: formatTime( median( results.map( ( r ) => r.maxType ) ) ), + focus: formatTime( median( results.map( ( r ) => r.focus ) ) ), + minFocus: formatTime( median( results.map( ( r ) => r.minFocus ) ) ), + maxFocus: formatTime( median( results.map( ( r ) => r.maxFocus ) ) ), + }; +} + +/** + * Runs the performances tests on an array of branches and output the result. + * + * @param {string[]} branches Branches to compare + */ +async function runPerformanceTests( branches ) { + // The default value doesn't work because commander provides an array. + if ( branches.length === 0 ) { + branches = [ 'master' ]; + } + + log( + formats.title( '\nšŸ’ƒ Performance Tests šŸ•ŗ\n\n' ), + 'Welcome! This tool runs the performance tests on multiple branches and displays a comparison table.\n' + + 'In order to run the tests, the tool is going to load a WordPress environment on 8888 and 8889 ports.\n' + + 'Make sure these ports are not used before continuing.\n' + ); + + await askForConfirmation( 'Ready to go? ' ); + + log( '>> Cloning the repository' ); + const performanceTestDirectory = await git.clone( config.gitRepositoryURL ); + const environmentDirectory = path.join( os.tmpdir(), uuid() ); + log( + '>> Perf Tests Directory : ' + + formats.success( performanceTestDirectory ) + ); + log( + '>> Environment Directory : ' + formats.success( environmentDirectory ) + ); + + log( '>> Installing dependencies' ); + // The build packages is necessary for the performance folder + await runShellScript( + 'npm install && npm run build:packages', + performanceTestDirectory + ); + await runShellScript( + 'cp -aR ' + performanceTestDirectory + ' ' + environmentDirectory + ); + + log( '>> Starting the WordPress environment' ); + await runShellScript( 'npm run wp-env start' ); + + /** + * @type {Object.} + */ + const results = {}; + for ( const branch of branches ) { + results[ branch ] = await getPerformanceResultsForBranch( + performanceTestDirectory, + environmentDirectory, + branch + ); + } + + log( '>> Stopping the WordPress environment' ); + await runShellScript( 'npm run wp-env stop' ); + + log( '\n>> šŸŽ‰ Results.\n' ); + console.table( results ); +} + +module.exports = { + runPerformanceTests, +}; diff --git a/bin/plugin/config.js b/bin/plugin/config.js index ae16ab86925b93..a1c160ec7cb6c9 100644 --- a/bin/plugin/config.js +++ b/bin/plugin/config.js @@ -1,5 +1,23 @@ const gitRepoOwner = 'WordPress'; +/** + * @typedef WPPluginCLIConfig + * + * @property {string} slug Slug. + * @property {string} name Name. + * @property {string} team Github Team Name. + * @property {string} githubRepositoryOwner Github Repository Owner. + * @property {string} githubRepositoryName Github Repository Name. + * @property {string} pluginEntryPoint Plugin Entry Point File. + * @property {string} buildZipCommand Build Plugin ZIP command. + * @property {string} wpRepositoryReleasesURL WordPress Repository Tags URL. + * @property {string} gitRepositoryURL Git Repository URL. + * @property {string} svnRepositoryURL SVN Repository URL. + */ + +/** + * @type {WPPluginCLIConfig} + */ const config = { slug: 'gutenberg', name: 'Gutenberg', diff --git a/bin/plugin/lib/git.js b/bin/plugin/lib/git.js index f0052ba8cae9a2..e2a51451109592 100644 --- a/bin/plugin/lib/git.js +++ b/bin/plugin/lib/git.js @@ -6,6 +6,13 @@ const SimpleGit = require( 'simple-git/promise' ); const os = require( 'os' ); const { v4: uuid } = require( 'uuid' ); +/** + * Clones a Github repository. + * + * @param {string} repositoryUrl + * + * @return {Promise} Repository local Path + */ async function clone( repositoryUrl ) { const gitWorkingDirectoryPath = path.join( os.tmpdir(), uuid() ); const simpleGit = SimpleGit(); @@ -13,6 +20,15 @@ async function clone( repositoryUrl ) { return gitWorkingDirectoryPath; } +/** + * Commits changes to the repository. + * + * @param {string} gitWorkingDirectoryPath Local repository path. + * @param {string} message Commit message. + * @param {string[]} filesToAdd Files to add. + * + * @return {Promise} Commit Hash + */ async function commit( gitWorkingDirectoryPath, message, filesToAdd = [] ) { const simpleGit = SimpleGit( gitWorkingDirectoryPath ); await simpleGit.add( filesToAdd ); @@ -22,36 +38,76 @@ async function commit( gitWorkingDirectoryPath, message, filesToAdd = [] ) { return commitHash; } +/** + * Creates a local branch. + * + * @param {string} gitWorkingDirectoryPath Local repository path. + * @param {string} branchName Branch Name + */ async function createLocalBranch( gitWorkingDirectoryPath, branchName ) { const simpleGit = SimpleGit( gitWorkingDirectoryPath ); await simpleGit.checkoutLocalBranch( branchName ); } +/** + * Checkout a local branch. + * + * @param {string} gitWorkingDirectoryPath Local repository path. + * @param {string} branchName Branch Name + */ async function checkoutRemoteBranch( gitWorkingDirectoryPath, branchName ) { const simpleGit = SimpleGit( gitWorkingDirectoryPath ); await simpleGit.checkout( branchName ); } +/** + * Creates a local tag. + * + * @param {string} gitWorkingDirectoryPath Local repository path. + * @param {string} tagName Tag Name + */ async function createLocalTag( gitWorkingDirectoryPath, tagName ) { const simpleGit = SimpleGit( gitWorkingDirectoryPath ); await simpleGit.addTag( tagName ); } +/** + * Pushes a local branch to the origin. + * + * @param {string} gitWorkingDirectoryPath Local repository path. + * @param {string} branchName Branch Name + */ async function pushBranchToOrigin( gitWorkingDirectoryPath, branchName ) { const simpleGit = SimpleGit( gitWorkingDirectoryPath ); await simpleGit.push( 'origin', branchName ); } +/** + * Pushes tags to the origin. + * + * @param {string} gitWorkingDirectoryPath Local repository path. + */ async function pushTagsToOrigin( gitWorkingDirectoryPath ) { const simpleGit = SimpleGit( gitWorkingDirectoryPath ); await simpleGit.pushTags( 'origin' ); } +/** + * Discard local changes. + * + * @param {string} gitWorkingDirectoryPath Local repository path. + */ async function discardLocalChanges( gitWorkingDirectoryPath ) { const simpleGit = SimpleGit( gitWorkingDirectoryPath ); await simpleGit.reset( 'hard' ); } +/** + * Reset local branch against the origin. + * + * @param {string} gitWorkingDirectoryPath Local repository path. + * @param {string} branchName Branch Name + */ async function resetLocalBranchAgainstOrigin( gitWorkingDirectoryPath, branchName @@ -62,6 +118,12 @@ async function resetLocalBranchAgainstOrigin( await simpleGit.pull( 'origin', branchName ); } +/** + * Cherry-picks a commit into master + * + * @param {string} gitWorkingDirectoryPath Local repository path. + * @param {string} commitHash Branch Name + */ async function cherrypickCommitIntoBranch( gitWorkingDirectoryPath, commitHash @@ -71,6 +133,12 @@ async function cherrypickCommitIntoBranch( await simpleGit.raw( [ 'cherry-pick', commitHash ] ); } +/** + * Replaces the local branch's content with the content from another branch. + * + * @param {string} gitWorkingDirectoryPath Local repository path. + * @param {string} sourceBranchName Branch Name + */ async function replaceContentFromRemoteBranch( gitWorkingDirectoryPath, sourceBranchName diff --git a/bin/plugin/lib/utils.js b/bin/plugin/lib/utils.js index 96267ccb8ea0a2..8acc5d10a993f0 100644 --- a/bin/plugin/lib/utils.js +++ b/bin/plugin/lib/utils.js @@ -1,6 +1,7 @@ /** * External dependencies */ +// @ts-ignore const inquirer = require( 'inquirer' ); const fs = require( 'fs' ); const childProcess = require( 'child_process' ); @@ -14,13 +15,13 @@ const { log, formats } = require( './logger' ); * Utility to run a child script * * @param {string} script Script to run. - * @param {string?} cwd Working directory. + * @param {string=} cwd Working directory. */ function runShellScript( script, cwd ) { childProcess.execSync( script, { cwd, env: { - NO_CHECKS: true, + NO_CHECKS: 'true', PATH: process.env.PATH, HOME: process.env.HOME, }, diff --git a/bin/tsconfig.json b/bin/tsconfig.json index 6bc3f36b623aba..d0d42ece07f692 100644 --- a/bin/tsconfig.json +++ b/bin/tsconfig.json @@ -18,8 +18,11 @@ "./check-latest-npm.js", "./plugin/config.js", "./plugin/commands/changelog.js", + "./plugin/commands/performance.js", "./plugin/lib/version.js", "./plugin/lib/logger.js", + "./plugin/lib/utils.js", + "./plugin/lib/git.js", "./validate-package-lock.js", ] } diff --git a/package-lock.json b/package-lock.json index 089fec0a2fd9e7..5313d35913ad91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9391,6 +9391,12 @@ "integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==", "dev": true }, + "@types/uuid": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-7.0.2.tgz", + "integrity": "sha512-8Ly3zIPTnT0/8RCU6Kg/G3uTICf9sRwYOpUzSIM3503tLIKcnJPRuinHhXngJUy2MntrEf6dlpOHXJju90Qh5w==", + "dev": true + }, "@types/vfile": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/vfile/-/vfile-3.0.2.tgz", diff --git a/package.json b/package.json index ffeac46636aaf0..19ac155744f356 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "@types/react-dom": "16.9.5", "@types/requestidlecallback": "0.3.1", "@types/sprintf-js": "1.1.2", + "@types/uuid": "7.0.2", "@wordpress/babel-plugin-import-jsx-pragma": "file:packages/babel-plugin-import-jsx-pragma", "@wordpress/babel-plugin-makepot": "file:packages/babel-plugin-makepot", "@wordpress/babel-preset-default": "file:packages/babel-preset-default",