Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 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.
Comment thread
swissspidy marked this conversation as resolved.

**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`
Comment thread
swissspidy marked this conversation as resolved.

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
136 changes: 133 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.
Comment thread
swissspidy marked this conversation as resolved.
Outdated
*
* ## 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,122 @@ 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 ) . '%';
}
}
Comment thread
swissspidy marked this conversation as resolved.
Outdated

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 = str_replace( '\\', '/', $location_parts[0] );

Comment thread
swissspidy marked this conversation as resolved.
Outdated
foreach ( array( 'wp-content/plugins/', 'plugins/' ) as $prefix ) {
$position = strpos( $location_file, $prefix );
while ( false !== $position ) {
if ( 0 !== $position ) {
if ( '/' !== substr( $location_file, $position - 1, 1 ) ) {
$position = strpos( $location_file, $prefix, $position + 1 );
continue;
}
}

$location_file = substr( $location_file, $position + strlen( $prefix ) );
$segments = explode( '/', $location_file );
return $segments[0];
}
}

if ( defined( 'WP_PLUGIN_DIR' ) ) {
$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 );
return $segments[0];
}

if ( 'php' === pathinfo( $location_file, PATHINFO_EXTENSION ) ) {
return pathinfo( $location_file, PATHINFO_FILENAME );
}
}
}

$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 ) {
if ( '/' !== substr( $location_file, $position - 1, 1 ) ) {
$position = strpos( $location_file, $prefix, $position + 1 );
continue;
}
}

$location_file = substr( $location_file, $position + strlen( $prefix ) );
$found_mu_prefix = true;
break 2;
}
}

if ( ! $found_mu_prefix ) {
return null;
}

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