-
Notifications
You must be signed in to change notification settings - Fork 0
[v1.0.0] feat(perf): add wp-tooling perf CLI, setup/perf scaffold, and xhprof normalization #37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
bd6a5ab
feat(perf): add wp-tooling perf runner (web-vitals + lighthouse + ser…
Adi-ty 33f6c21
fix(perf): degrade on bad URL, skip Lighthouse on dead page, slim LHR…
Adi-ty 5153373
docs(issues): rename tracking file to reflect #36 and #37 scope
Adi-ty 9255880
fix(perf): require server_env_cwd, fix EBADJSON exit code
Adi-ty 64a3237
fix(perf): Copilot review fixes — spawnSync guard, vitalsError, scrip…
Adi-ty fd4257a
perf: readability refactors in normalize and resolve-module
Adi-ty 80849f6
fix(perf): address Copilot review — web-vitals v5 API, dry-run safety…
Adi-ty c7221b6
fix(scaffolds): scan script maps in inferPlaceholders fallback
Adi-ty 751fba9
Merge remote-tracking branch 'origin/main' into v1.0.0/task/perf-runner
Adi-ty 04bdcdd
fix(perf): address Copilot review — error classification, escaping, n…
Adi-ty f7069f8
fix(perf): address Copilot review — JSON-safe script display, literal…
Adi-ty cdb2586
fix(perf): pass origin+path as WP-CLI --url for subdirectory multisite
Adi-ty cc17614
fix(scaffolds): separate multi-script report entries with commas
Adi-ty 90ac488
fix(perf): validate config types, run lighthouse independently, share…
Adi-ty 89dd8fb
Merge branch 'main' into v1.0.0/task/perf-runner
Adi-ty File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| 22.19.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
77 changes: 77 additions & 0 deletions
77
node-packages/wp-tooling/scaffolds/setup/perf/scaffold.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| { | ||
| "slug": "perf", | ||
| "category": "setup", | ||
| "name": "perf (web-vitals + lighthouse + server xhprof)", | ||
| "description": "Adds .perfrc.json and server-profile.php for two-layer performance testing against a running WordPress environment: lab Core Web Vitals (web-vitals attribution build under headless Chromium) + Lighthouse performance scores, and optional server-side xhprof function profiling via WP-CLI. The server layer needs `composer require --dev rtcamp/wp-dev-tools` (not on Packagist — add it via a path or VCS repository) and the xhprof or tideways_xhprof PHP extension in the WP-CLI environment; without either it degrades gracefully rather than erroring. The server command defaults to wp-env; edit `server.command` in .perfrc.json if your WordPress environment runs WP-CLI some other way (Local, DDEV, a remote SSH host, etc.).", | ||
| "source": "template", | ||
| "files": [ | ||
| { | ||
| "src": "templates/.perfrc.json.mustache", | ||
| "dest": ".perfrc.json" | ||
| }, | ||
| { | ||
| "src": "templates/server-profile.php", | ||
| "dest": "server-profile.php", | ||
| "raw": true | ||
| } | ||
| ], | ||
| "inputs": [ | ||
| { | ||
| "key": "base_url", | ||
| "description": "Base URL of the WordPress environment to test against (e.g. http://localhost:8888).", | ||
| "required": true, | ||
| "transform": "json-escape" | ||
| }, | ||
| { | ||
| "key": "sample_page", | ||
| "description": "Path of a post or page to test, appended to base_url (e.g. /hello-world/ or a permalink path).", | ||
| "default": "/?p=1", | ||
| "transform": "json-escape" | ||
| }, | ||
| { | ||
| "key": "search_page", | ||
| "description": "Path of the search-results page to test, appended to base_url.", | ||
| "default": "/?s=hello", | ||
| "transform": "json-escape" | ||
| }, | ||
| { | ||
| "key": "extra_page", | ||
| "description": "Optional path of one more page to test, appended to base_url. Omitted when empty; add further URLs directly in .perfrc.json.", | ||
| "default": "", | ||
| "transform": "json-escape" | ||
| }, | ||
| { | ||
| "key": "server_enabled", | ||
| "description": "Enable the server-side xhprof layer (needs rtcamp/wp-dev-tools and the xhprof/tideways_xhprof PHP extension in the WP-CLI environment). One of true/false/yes/no.", | ||
| "default": "false" | ||
| }, | ||
| { | ||
| "key": "server_env_cwd", | ||
| "description": "Project path inside the WP-CLI environment, as `wp-env run cli --env-cwd` expects it. Must match where this scaffold just installed server-profile.php — e.g. wp-content/plugins/my-plugin for a plugin, wp-content/themes/my-theme for a theme — or `.` only if this project IS the WordPress root. There's no safe default: guessing wrong means the server layer can't find its own shim.", | ||
| "required": true | ||
| }, | ||
| { | ||
| "key": "server_env_cwd_json", | ||
| "description": "server_env_cwd, JSON-escaped for .perfrc.json.", | ||
| "discover_from": "input:server_env_cwd", | ||
| "transform": "json-escape" | ||
| }, | ||
| { | ||
| "key": "server_env_cwd_shell", | ||
| "description": "server_env_cwd, shell-quoted for the profile:server script.", | ||
| "discover_from": "input:server_env_cwd", | ||
| "transform": "shell-escape" | ||
| } | ||
| ], | ||
| "npm_dev_dependencies": { | ||
| "web-vitals": "^5.3.0", | ||
| "lighthouse": "^13.4.0", | ||
|
Adi-ty marked this conversation as resolved.
|
||
| "puppeteer": "^25.3.0" | ||
|
Adi-ty marked this conversation as resolved.
|
||
| }, | ||
| "scripts": { | ||
| "npm": { | ||
| "test:perf": "wp-tooling perf", | ||
| "profile:server": "wp-env run cli --env-cwd={{server_env_cwd_shell}} -- wp eval-file server-profile.php" | ||
|
Adi-ty marked this conversation as resolved.
|
||
| } | ||
| } | ||
| } | ||
12 changes: 12 additions & 0 deletions
12
node-packages/wp-tooling/scaffolds/setup/perf/templates/.perfrc.json.mustache
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| { | ||
| "urls": [ | ||
| "{{base_url}}/", | ||
| "{{base_url}}{{sample_page}}", | ||
| "{{base_url}}{{search_page}}"{{#extra_page}}, | ||
| "{{base_url}}{{extra_page}}"{{/extra_page}} | ||
| ], | ||
| "server": { | ||
| "enabled": {{#server_enabled}}true{{/server_enabled}}{{^server_enabled}}false{{/server_enabled}}, | ||
| "command": ["npx", "--no-install", "wp-env", "run", "cli", "--env-cwd={{server_env_cwd_json}}", "--", "wp"] | ||
| } | ||
| } |
130 changes: 130 additions & 0 deletions
130
node-packages/wp-tooling/scaffolds/setup/perf/templates/server-profile.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| <?php | ||
| /** | ||
| * Server-side XHProf profile of a front-end render path, for `wp eval-file`. | ||
| * | ||
| * Usage: | ||
| * npm run profile:server -- [<path>] [<top>] | ||
| * # or directly: | ||
| * wp eval-file server-profile.php [<path>] [<top>] [--url=<url>] | ||
| * | ||
| * Profiles the WordPress render path for <path> (default "/") with | ||
| * rtCamp\WPDevTools\Support\XHProfProfiler and prints the top-<top> | ||
| * (default 15) functions by wall time as JSON: { "fn": {ct,wt,cpu,mu,pmu} }. | ||
| * Prints [] when no xhprof/tideways_xhprof backend is loaded, or when | ||
| * rtcamp/wp-dev-tools is not installed (`composer require --dev | ||
| * rtcamp/wp-dev-tools`). A route diagnostic goes to STDERR so a | ||
| * mis-resolved path — or a missing profiler — is visible next to the data. | ||
| * A CLI render approximates but does not equal a web-server request | ||
| * (routing/superglobals and opcache warmth differ). | ||
| * | ||
| * Hardening: redirect_canonical() ends the request with exit(), and exit() | ||
| * bypasses finally — so canonical redirects are unhooked up front, profiling | ||
| * uses start()/stop() rather than profile(), and a shutdown handler drains | ||
| * the output buffer and emits the JSON if some other exit() still terminates | ||
| * the render early. | ||
| * | ||
| * NOTE: no declare(strict_types) here — `wp eval-file` runs the file through | ||
| * eval(), where a declare() is no longer the first statement of the script. | ||
| */ | ||
|
|
||
| if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) { | ||
| exit( 'Run via: wp eval-file server-profile.php [<path>] [<top>]' . PHP_EOL ); | ||
| } | ||
|
|
||
| $server_profile_path = isset( $args[0] ) ? (string) $args[0] : '/'; | ||
| $server_profile_top = isset( $args[1] ) ? max( 1, (int) $args[1] ) : 15; | ||
| $server_profile_backend = function_exists( 'xhprof_enable' ) | ||
| ? 'xhprof' | ||
| : ( function_exists( 'tideways_xhprof_enable' ) ? 'tideways' : 'none' ); | ||
|
|
||
| if ( ! class_exists( \rtCamp\WPDevTools\Support\XHProfProfiler::class ) ) { | ||
| echo wp_json_encode( array() ) . PHP_EOL; | ||
| fwrite( | ||
| STDERR, | ||
| sprintf( | ||
| '[server-profile] path=%s backend=%s profiler=missing — install rtcamp/wp-dev-tools (composer require --dev rtcamp/wp-dev-tools)%s', | ||
| $server_profile_path, | ||
| $server_profile_backend, | ||
| PHP_EOL | ||
| ) | ||
| ); | ||
| exit( 0 ); | ||
| } | ||
|
|
||
| $server_profile_profiler = new \rtCamp\WPDevTools\Support\XHProfProfiler(); | ||
|
|
||
| // A canonical redirect would exit() before stop() runs or the JSON is echoed. | ||
| remove_action( 'template_redirect', 'redirect_canonical' ); | ||
|
|
||
| // Fallback emitter: if the render exit()s anyway, still stop the session and print | ||
| // JSON. Open buffers are discarded first — shutdown output would otherwise flush | ||
| // behind them and partial render HTML would corrupt the JSON on stdout. | ||
| register_shutdown_function( | ||
| static function () use ( $server_profile_profiler, $server_profile_top ): void { | ||
| if ( ! $server_profile_profiler->is_running() ) { | ||
| return; | ||
| } | ||
|
|
||
| while ( ob_get_level() > 0 ) { | ||
| ob_end_clean(); | ||
| } | ||
|
|
||
| echo wp_json_encode( $server_profile_profiler->stop( $server_profile_top, 'server-profile' ) ) . PHP_EOL; | ||
| } | ||
| ); | ||
|
|
||
| // Simulate the front-end request inside this CLI process. Query-string args must land | ||
| // in $_GET too: WP::parse_request() reads query vars from $_GET, not REQUEST_URI — | ||
| // without this, "/?p=123"-style paths silently profile the homepage. | ||
| $_SERVER['REQUEST_URI'] = $server_profile_path; | ||
| parse_str( (string) wp_parse_url( $server_profile_path, PHP_URL_QUERY ), $_GET ); | ||
| $_REQUEST = array_merge( $_REQUEST, $_GET ); | ||
|
|
||
| $server_profile_profiler->start(); | ||
|
|
||
| // wp()/template-loader.php may open their own nested buffers; unwind to | ||
| // the level recorded here rather than assuming only one was opened. | ||
| $server_profile_ob_level = ob_get_level(); | ||
| ob_start(); | ||
| wp(); | ||
| if ( ! defined( 'WP_USE_THEMES' ) ) { | ||
| define( 'WP_USE_THEMES', true ); | ||
| } | ||
| require ABSPATH . WPINC . '/template-loader.php'; | ||
| while ( ob_get_level() > $server_profile_ob_level ) { | ||
| ob_end_clean(); | ||
| } | ||
|
|
||
| echo wp_json_encode( $server_profile_profiler->stop( $server_profile_top, 'server-profile' ) ) . PHP_EOL; | ||
|
|
||
| // Route diagnostic (STDERR): makes a silently mis-routed path -- or a missing | ||
| // profiling backend -- visible next to the JSON. | ||
| $server_profile_query = $GLOBALS['wp_query']; | ||
|
|
||
| $server_profile_route_type = static function ( \WP_Query $query ): string { | ||
| if ( $query->is_singular() ) { | ||
| return $query->is_page() ? 'page' : 'singular'; | ||
| } | ||
| if ( $query->is_home() ) { | ||
| return 'home'; | ||
| } | ||
| if ( $query->is_archive() ) { | ||
| return 'archive'; | ||
| } | ||
| if ( $query->is_404() ) { | ||
| return '404'; | ||
| } | ||
| return 'other'; | ||
| }; | ||
|
|
||
| fwrite( | ||
| STDERR, | ||
| sprintf( | ||
| '[server-profile] path=%s backend=%s resolved=%s object_id=%d%s', | ||
| $server_profile_path, | ||
| $server_profile_backend, | ||
| $server_profile_route_type( $server_profile_query ), | ||
| (int) get_queried_object_id(), | ||
| PHP_EOL | ||
| ) | ||
| ); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| /** | ||
| * perf subcommand registration. | ||
| * | ||
| * The dispatcher (`src/cli/index.js`) auto-discovers every `*.js` file in | ||
| * this directory. Each module must export `{ name, summary, run }`. | ||
| * `run` is required lazily so cold-start cost stays close to a single | ||
| * subcommand's footprint. | ||
| */ | ||
|
|
||
| 'use strict'; | ||
|
|
||
| module.exports = { | ||
| name: 'perf', | ||
| summary: | ||
| 'Run web-vitals + Lighthouse (and optional server xhprof) and emit a normalized performance report', | ||
| run: (argv) => require('../../perf/run').runCli(argv), | ||
| }; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.