Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 [<hook>] [--all] [--spotlight] [--url=<url>] [--fields=<fields>] [--format=<format>] [--order=<order>] [--orderby=<fields>] [--search=<pattern>]
wp profile hook [<hook>] [--all] [--spotlight] [--url=<url>] [--fields=<fields>] [--format=<format>] [--order=<order>] [--orderby=<fields>] [--search=<pattern>] [--plugin]
~~~

In order to profile callbacks on a specific hook, the action or filter
Expand Down Expand Up @@ -164,6 +164,9 @@ will need to execute during the course of the request.
[--search=<pattern>]
Filter callbacks to those matching the given search pattern (case-insensitive).

[--plugin]
Group callback metrics by plugin.

**EXAMPLES**

# Profile a hook.
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"wp-cli/wp-cli": "^2.13"
},
"require-dev": {
"wp-cli/extension-command": "^2",
"wp-cli/wp-cli-tests": "^5"
},
"config": {
Expand Down
22 changes: 22 additions & 0 deletions features/profile-hook.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
<?php
/**
* Plugin Name: Resource Test
*/
function resource_test_init_callback() {
wp_cache_get( 'resource_test' );
}
add_action( 'init', 'resource_test_init_callback' );
"""
And I run `wp plugin activate resource-test`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot to make this work, you need to install wp-cli/extension-command as a composer development dependency.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in 9aa3a6d: added wp-cli/extension-command to require-dev in composer.json so Behat scenarios can use wp plugin ... commands.


When I run `wp profile hook init --plugin --fields=plugin,cache_hits,cache_misses`
Then STDOUT should be a table containing rows:
| plugin | cache_hits | cache_misses |
| resource-test | 0 | 1 |
| total (1) | 0 | 1 |
And STDERR should be empty

Scenario: Hooks should only be called once
Given a WP install
And a wp-content/mu-plugins/action-test.php file:
Expand Down
2 changes: 1 addition & 1 deletion features/profile.feature
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Feature: Basic profile usage
"""
usage: wp profile eval <php-code> [--hook[=<hook>]] [--fields=<fields>] [--format=<format>] [--order=<order>] [--orderby=<fields>]
or: wp profile eval-file <file> [--hook[=<hook>]] [--fields=<fields>] [--format=<format>] [--order=<order>] [--orderby=<fields>]
or: wp profile hook [<hook>] [--all] [--spotlight] [--url=<url>] [--fields=<fields>] [--format=<format>] [--order=<order>] [--orderby=<fields>] [--search=<pattern>]
or: wp profile hook [<hook>] [--all] [--spotlight] [--url=<url>] [--fields=<fields>] [--format=<format>] [--order=<order>] [--orderby=<fields>] [--search=<pattern>] [--plugin]
or: wp profile queries [--url=<url>] [--hook=<hook>] [--callback=<callback>] [--time_threshold=<seconds>] [--fields=<fields>] [--format=<format>] [--order=<order>] [--orderby=<fields>]
or: wp profile stage [<stage>] [--all] [--spotlight] [--url=<url>] [--fields=<fields>] [--format=<format>] [--order=<order>] [--orderby=<fields>]

Expand Down
88 changes: 85 additions & 3 deletions src/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,9 @@ public function stage( $args, $assoc_args ) {
* [--search=<pattern>]
* : Filter callbacks to those matching the given search pattern (case-insensitive).
*
* [--plugin]
* : Group callback metrics by plugin.
*
* ## EXAMPLES
*
* # Profile a hook.
Expand All @@ -266,12 +269,13 @@ 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, 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 );
$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' );
$order = is_string( $order_val ) ? $order_val : 'ASC';
Expand All @@ -288,7 +292,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' );
Expand Down Expand Up @@ -319,6 +325,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 );
}

Expand Down Expand Up @@ -792,4 +804,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 === strpos( $location_file, 'mu-plugins/' ) ) {
$location_file = substr( $location_file, strlen( 'mu-plugins/' ) );
}

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;
}
}
Loading