diff --git a/packages/env/README.md b/packages/env/README.md index fe3e5dea3bf2b2..1c2b9adb5cb1fb 100644 --- a/packages/env/README.md +++ b/packages/env/README.md @@ -289,6 +289,11 @@ Options: them in a comma-separated list: `--xdebug=develop,coverage`. See https://xdebug.org/docs/all_settings#mode for information about Xdebug modes. [string] + --spx Enables SPX profiling. If not passed, SPX is turned off. If no + mode is set, uses "enabled". SPX is a simple profiling extension + with a built-in web UI. See + https://github.com/NoiseByNorthwest/php-spx for more information. + [string] --scripts Execute any configured lifecycle scripts. [boolean] [default: true] ``` @@ -756,6 +761,29 @@ php_value memory_limit 2G This is useful if there are options you'd like to add to `php.ini`, which is difficult to access in this environment. +### Using SPX Profiling + +SPX is a simple profiling extension for PHP that provides low-overhead profiling with a built-in web UI. When enabled with `--spx`, you can access the SPX profiling interface to analyze your application's performance. + +To enable SPX profiling: + +```sh +wp-env start --spx +``` + +Once enabled, you can access the SPX web UI by visiting any page in your WordPress environment with the query parameters `?SPX_KEY=dev&SPX_UI_URI=/`. For example: + +- Development site: `http://localhost:8888/?SPX_KEY=dev&SPX_UI_URI=/` +- Test site: `http://localhost:8889/?SPX_KEY=dev&SPX_UI_URI=/` + +From the SPX interface, you can: +- Enable profiling for subsequent requests +- View flame graphs and performance metrics +- Analyze function call timelines +- Examine memory usage and other performance data + +SPX provides a more lightweight alternative to Xdebug for profiling, with minimal performance overhead and an intuitive web-based interface. + ## Contributing to this package This is an individual package that's part of the Gutenberg project. The project is organized as a monorepo. It's made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to [npm](https://www.npmjs.com/) and used by [WordPress](https://make.wordpress.org/core/) as well as other software projects. diff --git a/packages/env/lib/cli.js b/packages/env/lib/cli.js index 896df6cd59fed0..e206972b8511d4 100644 --- a/packages/env/lib/cli.js +++ b/packages/env/lib/cli.js @@ -14,6 +14,7 @@ const { execSync } = require( 'child_process' ); const pkg = require( '../package.json' ); const env = require( './env' ); const parseXdebugMode = require( './parse-xdebug-mode' ); +const parseSpxMode = require( './parse-spx-mode' ); const { RUN_CONTAINERS, validateRunContainer, @@ -139,6 +140,12 @@ module.exports = function cli() { coerce: parseXdebugMode, type: 'string', } ); + args.option( 'spx', { + describe: + 'Enables SPX profiling. If not passed, SPX is turned off. If no mode is set, uses "enabled". SPX is a simple profiling extension with a built-in web UI. See https://github.com/NoiseByNorthwest/php-spx for more information.', + coerce: parseSpxMode, + type: 'string', + } ); args.option( 'scripts', { type: 'boolean', describe: 'Execute any configured lifecycle scripts.', diff --git a/packages/env/lib/commands/start.js b/packages/env/lib/commands/start.js index db05b82060d2c5..c50a05719f8898 100644 --- a/packages/env/lib/commands/start.js +++ b/packages/env/lib/commands/start.js @@ -46,6 +46,7 @@ const CONFIG_CACHE_KEY = 'config_checksum'; * @param {Object} options.spinner A CLI spinner which indicates progress. * @param {boolean} options.update If true, update sources. * @param {string} options.xdebug The Xdebug mode to set. + * @param {string} options.spx The SPX mode to set. * @param {boolean} options.scripts Indicates whether or not lifecycle scripts should be executed. * @param {boolean} options.debug True if debug mode is enabled. */ @@ -53,6 +54,7 @@ module.exports = async function start( { spinner, update, xdebug, + spx, scripts, debug, } ) { @@ -63,6 +65,7 @@ module.exports = async function start( { spinner, debug, xdebug, + spx, writeChanges: true, } ); diff --git a/packages/env/lib/init-config.js b/packages/env/lib/init-config.js index 7239a390f16205..d55dbf64c2b3e5 100644 --- a/packages/env/lib/init-config.js +++ b/packages/env/lib/init-config.js @@ -26,6 +26,7 @@ const buildDockerComposeConfig = require( './build-docker-compose-config' ); * @param {Object} options.spinner A CLI spinner which indicates progress. * @param {boolean} options.debug True if debug mode is enabled. * @param {string} options.xdebug The Xdebug mode to set. Defaults to "off". + * @param {string} options.spx The SPX mode to set. Defaults to "off". * @param {boolean} options.writeChanges If true, writes the parsed config to the * required docker files like docker-compose * and Dockerfile. By default, this is false @@ -37,6 +38,7 @@ module.exports = async function initConfig( { spinner, debug, xdebug = 'off', + spx = 'off', writeChanges = false, } ) { const config = await loadConfig( path.resolve( '.' ) ); @@ -47,6 +49,11 @@ module.exports = async function initConfig( { // so that Docker will rebuild the image whenever the xdebug flag changes. config.xdebug = xdebug; + // Adding this to the config allows the start command to understand that the + // config has changed when only the spx param has changed. This is needed + // so that Docker will rebuild the image whenever the spx flag changes. + config.spx = spx; + const dockerComposeConfig = buildDockerComposeConfig( config ); if ( config.debug ) { @@ -233,6 +240,12 @@ RUN echo "#$HOST_UID ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers`; config.env[ env ].phpVersion ); + dockerFileContent += getSpxConfig( + config.spx, + config.env[ env ].phpVersion, + service + ); + // Add better PHP settings. dockerFileContent += ` RUN echo 'upload_max_filesize = 1G' >> /usr/local/etc/php/php.ini @@ -304,3 +317,50 @@ RUN echo 'xdebug.start_with_request=yes' >> /usr/local/etc/php/php.ini RUN echo 'xdebug.mode=${ xdebugMode }' >> /usr/local/etc/php/php.ini RUN echo 'xdebug.client_host="host.docker.internal"' >> /usr/local/etc/php/php.ini`; } + +/** + * Gets the SPX config based on the options in the config object. + * + * @param {string} spxMode The SPX mode set in the config. + * @param {string} phpVersion The php version set in the environment. + * @param {string} service The service name. + * @return {string} The SPX config -- can be an empty string when it's not used. + */ +function getSpxConfig( spxMode = 'off', phpVersion, service ) { + if ( spxMode === 'off' || service === 'cli' ) { + return ''; + } + + if ( phpVersion ) { + const versionTokens = phpVersion.split( '.' ); + const majorVer = parseInt( versionTokens[ 0 ] ); + const minorVer = parseInt( versionTokens[ 1 ] ); + + if ( isNaN( majorVer ) || isNaN( minorVer ) ) { + throw new ValidationError( + 'Something went wrong when parsing the PHP version.' + ); + } + + // SPX requires PHP 5.4 or higher + if ( majorVer < 5 || ( majorVer === 5 && minorVer < 4 ) ) { + throw new ValidationError( + `Cannot use SPX with PHP < 5.4. Your PHP version is ${ phpVersion }.` + ); + } + } + + return ` +# Install SPX profiler +RUN apt-get update -qy +RUN apt-get install -qy git zlib1g-dev +RUN cd /tmp && git clone https://github.com/NoiseByNorthwest/php-spx.git +RUN cd /tmp/php-spx && git checkout release/latest +RUN cd /tmp/php-spx && phpize && ./configure && make && make install +RUN docker-php-ext-enable spx +RUN echo 'spx.http_enabled=1' >> /usr/local/etc/php/php.ini +RUN echo 'spx.http_key="dev"' >> /usr/local/etc/php/php.ini +RUN echo 'spx.http_ip_whitelist="*"' >> /usr/local/etc/php/php.ini +RUN echo 'spx.data_dir="/tmp/spx"' >> /usr/local/etc/php/php.ini +RUN mkdir -p /tmp/spx && chmod 777 /tmp/spx`; +} diff --git a/packages/env/lib/parse-spx-mode.js b/packages/env/lib/parse-spx-mode.js new file mode 100644 index 00000000000000..68e2f61b5c23ca --- /dev/null +++ b/packages/env/lib/parse-spx-mode.js @@ -0,0 +1,41 @@ +'use strict'; + +// SPX is a simple profiling extension for PHP +// See https://github.com/NoiseByNorthwest/php-spx +const SPX_MODES = [ 'off', 'enabled' ]; + +/** + * Custom parsing for the SPX mode set via yargs. This function ensures three things: + * 1. If the --spx flag was not set, set it to 'off'. + * 2. If the --spx flag was set by itself, default to 'enabled'. + * 3. If the --spx flag includes modes, make sure they are accepted by SPX. + * + * @param {string|undefined} value The user-set mode of SPX; undefined if there is no --spx flag. + * @return {string} The SPX mode to use with defaults applied. + */ +module.exports = function parseSpxMode( value ) { + if ( value === undefined ) { + return 'off'; + } + if ( typeof value !== 'string' ) { + throwSpxModeError( value ); + } + + if ( value.length === 0 || value === 'undefined' ) { + return 'enabled'; + } + + if ( ! SPX_MODES.includes( value ) ) { + throwSpxModeError( value ); + } + + return value; +}; + +function throwSpxModeError( value ) { + throw new Error( + `"${ value }" is not a mode recognized by SPX. Valid modes are: ${ SPX_MODES.join( + ', ' + ) }` + ); +} diff --git a/packages/env/lib/test/parse-spx-mode.js b/packages/env/lib/test/parse-spx-mode.js new file mode 100644 index 00000000000000..2999f56179838c --- /dev/null +++ b/packages/env/lib/test/parse-spx-mode.js @@ -0,0 +1,32 @@ +'use strict'; + +/** + * Internal dependencies + */ +const parseSpxMode = require( '../parse-spx-mode' ); + +describe( 'parseSpxMode', () => { + it( 'errors with invalid values', () => { + const errorMessage = 'is not a mode recognized by SPX'; + expect( () => parseSpxMode( true ) ).toThrow( errorMessage ); + expect( () => parseSpxMode( false ) ).toThrow( errorMessage ); + expect( () => parseSpxMode( 1 ) ).toThrow( errorMessage ); + } ); + + it( 'sets the SPX mode to "off" if no --spx flag is passed', () => { + const result = parseSpxMode( undefined ); + expect( result ).toBe( 'off' ); + } ); + + it( 'sets the SPX mode to "enabled" if no mode is specified', () => { + const result = parseSpxMode( '' ); + expect( result ).toBe( 'enabled' ); + } ); + + it( 'errors with a mix of valid and invalid modes', () => { + const fakeMode = 'invalidmode'; + expect( () => parseSpxMode( `enabled,${ fakeMode }` ) ).toThrow( + fakeMode + ); + } ); +} );