From 046adf497f608a48c441e199df940a4cf656150c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 11:01:59 +0000 Subject: [PATCH 1/4] Initial plan From e6f6d815ceefe768886ca65729a22d7bad48400d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 11:08:50 +0000 Subject: [PATCH 2/4] Add --plugin aggregation for profile hook Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- README.md | 5 +- features/profile-hook.feature | 22 +++++++++ features/profile.feature | 2 +- src/Command.php | 87 ++++++++++++++++++++++++++++++++++- 4 files changed, 112 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 92789069..53bebfda 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ render based on the main query, and renders it. Profile key metrics for WordPress hooks (actions and filters). ~~~ -wp profile hook [] [--all] [--spotlight] [--url=] [--fields=] [--format=] [--order=] [--orderby=] [--search=] +wp profile hook [] [--all] [--spotlight] [--url=] [--fields=] [--format=] [--order=] [--orderby=] [--search=] [--plugin] ~~~ In order to profile callbacks on a specific hook, the action or filter @@ -164,6 +164,9 @@ will need to execute during the course of the request. [--search=] Filter callbacks to those matching the given search pattern (case-insensitive). + [--plugin] + Group callback metrics by plugin. + **EXAMPLES** # Profile a hook. diff --git a/features/profile-hook.feature b/features/profile-hook.feature index 20dc8287..e1dd1981 100644 --- a/features/profile-hook.feature +++ b/features/profile-hook.feature @@ -152,6 +152,28 @@ Feature: Profile a specific hook """ And STDERR should be empty + Scenario: Group callback metrics by plugin + Given a WP install + And a wp-content/plugins/resource-test/resource-test.php file: + """ + [--hook[=]] [--fields=] [--format=] [--order=] [--orderby=] or: wp profile eval-file [--hook[=]] [--fields=] [--format=] [--order=] [--orderby=] - or: wp profile hook [] [--all] [--spotlight] [--url=] [--fields=] [--format=] [--order=] [--orderby=] [--search=] + or: wp profile hook [] [--all] [--spotlight] [--url=] [--fields=] [--format=] [--order=] [--orderby=] [--search=] [--plugin] or: wp profile queries [--url=] [--hook=] [--callback=] [--time_threshold=] [--fields=] [--format=] [--order=] [--orderby=] or: wp profile stage [] [--all] [--spotlight] [--url=] [--fields=] [--format=] [--order=] [--orderby=] diff --git a/src/Command.php b/src/Command.php index 1f7d4463..5fcfde25 100644 --- a/src/Command.php +++ b/src/Command.php @@ -244,6 +244,9 @@ public function stage( $args, $assoc_args ) { * [--search=] * : Filter callbacks to those matching the given search pattern (case-insensitive). * + * [--plugin] + * : Group callback metrics by plugin. + * * ## EXAMPLES * * # Profile a hook. @@ -266,12 +269,14 @@ public function stage( $args, $assoc_args ) { * @when before_wp_load * * @param array{0?: string} $args Positional arguments. - * @param array{all?: bool, spotlight?: bool, url?: string, fields?: string, format: string, order: string, orderby?: string} $assoc_args + * @param array{all?: bool, spotlight?: bool, plugin?: bool, url?: string, fields?: string, format: string, order: string, orderby?: string} $assoc_args * @return void */ public function hook( $args, $assoc_args ) { $focus = Utils\get_flag_value( $assoc_args, 'all', isset( $args[0] ) ? $args[0] : null ); + /** @var array $assoc_args */ + $plugin = Utils\get_flag_value( $assoc_args, 'plugin', false ); $order_val = Utils\get_flag_value( $assoc_args, 'order', 'ASC' ); $order = is_string( $order_val ) ? $order_val : 'ASC'; @@ -288,7 +293,9 @@ public function hook( $args, $assoc_args ) { remove_all_actions( 'shutdown' ); } - if ( $focus ) { + if ( $focus && $plugin ) { + $base = array( 'plugin' ); + } elseif ( $focus ) { $base = array( 'callback', 'location' ); } else { $base = array( 'hook', 'callback_count' ); @@ -319,6 +326,12 @@ public function hook( $args, $assoc_args ) { } $loggers = self::filter_by_callback( $loggers, $search ); } + if ( $plugin ) { + if ( ! $focus ) { + WP_CLI::error( '--plugin requires --all or a specific hook.' ); + } + $loggers = self::group_by_plugin( $loggers ); + } $formatter->display_items( $loggers, true, $order, $orderby ); } @@ -792,4 +805,74 @@ function ( $logger ) use ( $pattern ) { } ); } + + /** + * Group callback loggers by plugin. + * + * @param array<\WP_CLI\Profile\Logger> $loggers + * @return array<\WP_CLI\Profile\Logger> + */ + private static function group_by_plugin( $loggers ) { + $plugins = array(); + + foreach ( $loggers as $logger ) { + if ( ! isset( $logger->location ) ) { + continue; + } + $plugin = self::plugin_from_location( $logger->location ); + if ( null === $plugin ) { + continue; + } + if ( ! isset( $plugins[ $plugin ] ) ) { + $plugins[ $plugin ] = new Logger( + array( + 'plugin' => $plugin, + ) + ); + } + + $plugins[ $plugin ]->time += $logger->time; + $plugins[ $plugin ]->query_time += $logger->query_time; + $plugins[ $plugin ]->query_count += $logger->query_count; + $plugins[ $plugin ]->cache_hits += $logger->cache_hits; + $plugins[ $plugin ]->cache_misses += $logger->cache_misses; + $plugins[ $plugin ]->request_time += $logger->request_time; + $plugins[ $plugin ]->request_count += $logger->request_count; + } + + foreach ( $plugins as $plugin ) { + $total_cache = $plugin->cache_hits + $plugin->cache_misses; + if ( $total_cache ) { + $plugin->cache_ratio = round( ( $plugin->cache_hits / $total_cache ) * 100, 2 ) . '%'; + } + } + + return array_values( $plugins ); + } + + /** + * Extract plugin slug from a callback location. + * + * @param string $location + * @return string|null + */ + private static function plugin_from_location( $location ) { + $location_parts = explode( ':', $location, 2 ); + $location_file = $location_parts[0]; + + if ( 0 === stripos( $location_file, 'mu-plugins/' ) ) { + $location_file = substr( $location_file, 11 ); + } + + if ( false !== strpos( $location_file, '/' ) ) { + $segments = explode( '/', $location_file ); + return $segments[0]; + } + + if ( 'php' === pathinfo( $location_file, PATHINFO_EXTENSION ) ) { + return pathinfo( $location_file, PATHINFO_FILENAME ); + } + + return null; + } } From 322ac7ee6fed0daf0491e4e9afd46d6c9e442909 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 11:11:26 +0000 Subject: [PATCH 3/4] Support wp profile hook --plugin output Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Command.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Command.php b/src/Command.php index 5fcfde25..59d1193b 100644 --- a/src/Command.php +++ b/src/Command.php @@ -269,13 +269,12 @@ public function stage( $args, $assoc_args ) { * @when before_wp_load * * @param array{0?: string} $args Positional arguments. - * @param array{all?: bool, spotlight?: bool, plugin?: bool, url?: string, fields?: string, format: string, order: string, orderby?: string} $assoc_args + * @param array{all?: bool, spotlight?: bool, plugin?: bool, search?: string, url?: string, fields?: string, format: string, order: string, orderby?: string} $assoc_args * @return void */ public function hook( $args, $assoc_args ) { - $focus = Utils\get_flag_value( $assoc_args, 'all', isset( $args[0] ) ? $args[0] : null ); - /** @var array $assoc_args */ + $focus = Utils\get_flag_value( $assoc_args, 'all', isset( $args[0] ) ? $args[0] : null ); $plugin = Utils\get_flag_value( $assoc_args, 'plugin', false ); $order_val = Utils\get_flag_value( $assoc_args, 'order', 'ASC' ); @@ -860,8 +859,8 @@ private static function plugin_from_location( $location ) { $location_parts = explode( ':', $location, 2 ); $location_file = $location_parts[0]; - if ( 0 === stripos( $location_file, 'mu-plugins/' ) ) { - $location_file = substr( $location_file, 11 ); + if ( 0 === strpos( $location_file, 'mu-plugins/' ) ) { + $location_file = substr( $location_file, strlen( 'mu-plugins/' ) ); } if ( false !== strpos( $location_file, '/' ) ) { From 9aa3a6da02d454261376522073c08261b6e2918b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 11:24:27 +0000 Subject: [PATCH 4/4] Add extension-command as Composer dev dependency Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index df286ea3..efbdcd79 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,7 @@ "wp-cli/wp-cli": "^2.13" }, "require-dev": { + "wp-cli/extension-command": "^2", "wp-cli/wp-cli-tests": "^5" }, "config": {