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 01/10] 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 02/10] 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 03/10] 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 04/10] 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": { From 62e296bec5c40fe92bedffd63a67031e76d666ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 09:00:45 +0000 Subject: [PATCH 05/10] Fix plugin grouping to ignore non-plugin callbacks Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Command.php | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Command.php b/src/Command.php index 59d1193b..2a98ce29 100644 --- a/src/Command.php +++ b/src/Command.php @@ -857,10 +857,26 @@ private static function group_by_plugin( $loggers ) { */ private static function plugin_from_location( $location ) { $location_parts = explode( ':', $location, 2 ); - $location_file = $location_parts[0]; + $location_file = ltrim( $location_parts[0], '/' ); - if ( 0 === strpos( $location_file, 'mu-plugins/' ) ) { + if ( 0 === strpos( $location_file, 'wp-content/plugins/' ) ) { + $location_file = substr( $location_file, strlen( 'wp-content/plugins/' ) ); + $segments = explode( '/', $location_file ); + return $segments[0]; + } + + if ( 0 === strpos( $location_file, 'plugins/' ) ) { + $location_file = substr( $location_file, strlen( 'plugins/' ) ); + $segments = explode( '/', $location_file ); + return $segments[0]; + } + + if ( 0 === strpos( $location_file, 'wp-content/mu-plugins/' ) ) { + $location_file = substr( $location_file, strlen( 'wp-content/mu-plugins/' ) ); + } elseif ( 0 === strpos( $location_file, 'mu-plugins/' ) ) { $location_file = substr( $location_file, strlen( 'mu-plugins/' ) ); + } else { + return null; } if ( false !== strpos( $location_file, '/' ) ) { From fe9f7612d2e7aedd45c7e39ba00cbd5ed6cc6da0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 09:40:13 +0000 Subject: [PATCH 06/10] Fix plugin grouping for normalized callback locations Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Command.php | 51 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/src/Command.php b/src/Command.php index 2a98ce29..c1e2c994 100644 --- a/src/Command.php +++ b/src/Command.php @@ -857,25 +857,48 @@ private static function group_by_plugin( $loggers ) { */ private static function plugin_from_location( $location ) { $location_parts = explode( ':', $location, 2 ); - $location_file = ltrim( $location_parts[0], '/' ); + $location_file = str_replace( '\\', '/', $location_parts[0] ); + + foreach ( array( 'wp-content/plugins/', 'plugins/' ) as $prefix ) { + $position = strpos( $location_file, $prefix ); + while ( false !== $position ) { + if ( 0 === $position || '/' === $location_file[ $position - 1 ] ) { + $location_file = substr( $location_file, $position + strlen( $prefix ) ); + $segments = explode( '/', $location_file ); + return $segments[0]; + } + $position = strpos( $location_file, $prefix, $position + 1 ); + } + } - if ( 0 === strpos( $location_file, 'wp-content/plugins/' ) ) { - $location_file = substr( $location_file, strlen( 'wp-content/plugins/' ) ); - $segments = explode( '/', $location_file ); - return $segments[0]; + if ( defined( 'WP_PLUGIN_DIR' ) ) { + $plugin_path = rtrim( str_replace( '\\', '/', WP_PLUGIN_DIR ), '/' ) . '/' . ltrim( $location_file, '/' ); + if ( file_exists( $plugin_path ) ) { + 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 ); + } + } } - if ( 0 === strpos( $location_file, 'plugins/' ) ) { - $location_file = substr( $location_file, strlen( 'plugins/' ) ); - $segments = explode( '/', $location_file ); - return $segments[0]; + $found_mu_prefix = false; + foreach ( array( 'wp-content/mu-plugins/', 'mu-plugins/' ) as $prefix ) { + $position = strpos( $location_file, $prefix ); + while ( false !== $position ) { + if ( 0 === $position || '/' === $location_file[ $position - 1 ] ) { + $location_file = substr( $location_file, $position + strlen( $prefix ) ); + $found_mu_prefix = true; + break 2; + } + $position = strpos( $location_file, $prefix, $position + 1 ); + } } - if ( 0 === strpos( $location_file, 'wp-content/mu-plugins/' ) ) { - $location_file = substr( $location_file, strlen( 'wp-content/mu-plugins/' ) ); - } elseif ( 0 === strpos( $location_file, 'mu-plugins/' ) ) { - $location_file = substr( $location_file, strlen( 'mu-plugins/' ) ); - } else { + if ( ! $found_mu_prefix ) { return null; } From 621cd578a6a3a8b98fce533f1b4edb8c3b179b6b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 09:43:33 +0000 Subject: [PATCH 07/10] Handle normalized plugin locations in hook grouping Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Command.php | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/Command.php b/src/Command.php index c1e2c994..d1ffca9e 100644 --- a/src/Command.php +++ b/src/Command.php @@ -862,17 +862,22 @@ private static function plugin_from_location( $location ) { foreach ( array( 'wp-content/plugins/', 'plugins/' ) as $prefix ) { $position = strpos( $location_file, $prefix ); while ( false !== $position ) { - if ( 0 === $position || '/' === $location_file[ $position - 1 ] ) { - $location_file = substr( $location_file, $position + strlen( $prefix ) ); - $segments = explode( '/', $location_file ); - return $segments[0]; + if ( 0 !== $position ) { + if ( '/' !== substr( $location_file, $position - 1, 1 ) ) { + $position = strpos( $location_file, $prefix, $position + 1 ); + continue; + } } - $position = strpos( $location_file, $prefix, $position + 1 ); + + $location_file = substr( $location_file, $position + strlen( $prefix ) ); + $segments = explode( '/', $location_file ); + return $segments[0]; } } if ( defined( 'WP_PLUGIN_DIR' ) ) { - $plugin_path = rtrim( str_replace( '\\', '/', WP_PLUGIN_DIR ), '/' ) . '/' . ltrim( $location_file, '/' ); + $normalized_plugin_dir = rtrim( str_replace( '\\', '/', WP_PLUGIN_DIR ), '/' ); + $plugin_path = $normalized_plugin_dir . '/' . ltrim( $location_file, '/' ); if ( file_exists( $plugin_path ) ) { if ( false !== strpos( $location_file, '/' ) ) { $segments = explode( '/', $location_file ); @@ -889,12 +894,16 @@ private static function plugin_from_location( $location ) { foreach ( array( 'wp-content/mu-plugins/', 'mu-plugins/' ) as $prefix ) { $position = strpos( $location_file, $prefix ); while ( false !== $position ) { - if ( 0 === $position || '/' === $location_file[ $position - 1 ] ) { - $location_file = substr( $location_file, $position + strlen( $prefix ) ); - $found_mu_prefix = true; - break 2; + if ( 0 !== $position ) { + if ( '/' !== substr( $location_file, $position - 1, 1 ) ) { + $position = strpos( $location_file, $prefix, $position + 1 ); + continue; + } } - $position = strpos( $location_file, $prefix, $position + 1 ); + + $location_file = substr( $location_file, $position + strlen( $prefix ) ); + $found_mu_prefix = true; + break 2; } } From b111d3f54fd5fbd7391b559a72d94c106012e8f5 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 2 Jun 2026 14:58:03 +0200 Subject: [PATCH 08/10] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/Command.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Command.php b/src/Command.php index d1ffca9e..f3b03b3c 100644 --- a/src/Command.php +++ b/src/Command.php @@ -839,10 +839,10 @@ private static function group_by_plugin( $loggers ) { $plugins[ $plugin ]->request_count += $logger->request_count; } - foreach ( $plugins as $plugin ) { - $total_cache = $plugin->cache_hits + $plugin->cache_misses; + foreach ( $plugins as $plugin_logger ) { + $total_cache = $plugin_logger->cache_hits + $plugin_logger->cache_misses; if ( $total_cache ) { - $plugin->cache_ratio = round( ( $plugin->cache_hits / $total_cache ) * 100, 2 ) . '%'; + $plugin_logger->cache_ratio = round( ( $plugin_logger->cache_hits / $total_cache ) * 100, 2 ) . '%'; } } From 8f638762437be3461b4cdd5d6a9464b2827955c0 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 2 Jun 2026 14:58:25 +0200 Subject: [PATCH 09/10] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/Command.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Command.php b/src/Command.php index f3b03b3c..f4fd5bab 100644 --- a/src/Command.php +++ b/src/Command.php @@ -856,9 +856,11 @@ private static function group_by_plugin( $loggers ) { * @return string|null */ private static function plugin_from_location( $location ) { - $location_parts = explode( ':', $location, 2 ); - $location_file = str_replace( '\\', '/', $location_parts[0] ); - + $location_file = str_replace( '\\', '/', $location ); + $colon_pos = strrpos( $location_file, ':' ); + if ( false !== $colon_pos && ctype_digit( substr( $location_file, $colon_pos + 1 ) ) ) { + $location_file = substr( $location_file, 0, $colon_pos ); + } foreach ( array( 'wp-content/plugins/', 'plugins/' ) as $prefix ) { $position = strpos( $location_file, $prefix ); while ( false !== $position ) { From 7171c1e837c4c50a78b69bb44e797edf240179ee Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 2 Jun 2026 14:58:37 +0200 Subject: [PATCH 10/10] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/Command.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Command.php b/src/Command.php index f4fd5bab..e2d408e2 100644 --- a/src/Command.php +++ b/src/Command.php @@ -245,7 +245,7 @@ public function stage( $args, $assoc_args ) { * : Filter callbacks to those matching the given search pattern (case-insensitive). * * [--plugin] - * : Group callback metrics by plugin. + * : Group callback metrics by plugin. Requires --all or a specific hook. * * ## EXAMPLES *