-
Notifications
You must be signed in to change notification settings - Fork 29
Introduce wp profile queries command
#207
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
base: main
Are you sure you want to change the base?
Changes from 10 commits
6dd9c91
1aa88d1
7e45434
60d5396
4a5f4cb
26ec9f7
071cb94
9e5aa60
ec1564a
28e1e4d
1f255b9
cc09ca2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| Feature: Profile database queries | ||
|
|
||
| @require-wp-4.0 | ||
| Scenario: Show all database queries | ||
| Given a WP install | ||
| And a wp-content/mu-plugins/test-queries.php file: | ||
| """ | ||
| <?php | ||
| add_action( 'init', function() { | ||
| global $wpdb; | ||
| $wpdb->query( "SELECT 1 as test_query_one" ); | ||
| $wpdb->query( "SELECT 2 as test_query_two" ); | ||
| }); | ||
| """ | ||
|
|
||
| When I run `wp profile queries --fields=query,time` | ||
| Then STDOUT should contain: | ||
| """ | ||
| query | ||
| """ | ||
| And STDOUT should contain: | ||
| """ | ||
| time | ||
| """ | ||
| And STDOUT should contain: | ||
| """ | ||
| SELECT 1 as test_query_one | ||
| """ | ||
| And STDOUT should contain: | ||
| """ | ||
| SELECT 2 as test_query_two | ||
| """ | ||
| And STDOUT should contain: | ||
| """ | ||
| total | ||
| """ | ||
| And STDERR should be empty | ||
|
|
||
| @require-wp-4.0 | ||
| Scenario: Show queries with specific fields | ||
| Given a WP install | ||
|
|
||
| When I run `wp profile queries --fields=query,time` | ||
| Then STDOUT should contain: | ||
| """ | ||
| query | ||
| """ | ||
| And STDOUT should contain: | ||
| """ | ||
| time | ||
| """ | ||
| And STDOUT should contain: | ||
| """ | ||
| SELECT | ||
| """ | ||
| And STDERR should be empty | ||
|
|
||
| @require-wp-4.0 | ||
| Scenario: Order queries by execution time | ||
| Given a WP install | ||
|
|
||
| When I run `wp profile queries --fields=time --orderby=time --order=DESC` | ||
| Then STDOUT should contain: | ||
| """ | ||
| time | ||
| """ | ||
| And STDERR should be empty | ||
|
|
||
| @require-wp-4.0 | ||
| Scenario: Display queries in JSON format | ||
| Given a WP install | ||
|
|
||
| When I run `wp profile queries --format=json --fields=query,time` | ||
| Then STDOUT should contain: | ||
| """ | ||
| "query" | ||
| """ | ||
| And STDOUT should contain: | ||
| """ | ||
| "time" | ||
| """ | ||
| And STDERR should be empty | ||
|
|
||
| @require-wp-4.0 | ||
| Scenario: Filter queries by hook | ||
| Given a WP install | ||
| And a wp-content/mu-plugins/query-test.php file: | ||
| """ | ||
| <?php | ||
| add_action( 'init', function() { | ||
| global $wpdb; | ||
| $wpdb->query( "SELECT 1 as test_query" ); | ||
| }); | ||
| """ | ||
|
|
||
| When I run `wp profile queries --hook=init --fields=query,callback` | ||
| Then STDOUT should contain: | ||
| """ | ||
| SELECT 1 as test_query | ||
| """ | ||
| And STDERR should be empty | ||
|
|
||
| @require-wp-4.0 | ||
| Scenario: Filter queries by callback | ||
| Given a WP install | ||
| And a wp-content/mu-plugins/callback-test.php file: | ||
| """ | ||
| <?php | ||
| function my_test_callback() { | ||
| global $wpdb; | ||
| $wpdb->query( "SELECT 2 as callback_test" ); | ||
| } | ||
| add_action( 'init', 'my_test_callback' ); | ||
| """ | ||
|
|
||
| When I run `wp profile queries --callback=my_test_callback --fields=query,hook` | ||
| Then STDOUT should contain: | ||
| """ | ||
| SELECT 2 as callback_test | ||
| """ | ||
| And STDERR should be empty |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -493,6 +493,157 @@ private static function include_file( $file ) { | |
| include $file; | ||
| } | ||
|
|
||
| /** | ||
| * Profile database queries and their execution time. | ||
| * | ||
| * Displays all database queries executed during a WordPress request, | ||
| * along with their execution time and caller information. You can filter | ||
| * queries to only show those executed during a specific hook or by a | ||
| * specific callback. | ||
| * | ||
| * ## OPTIONS | ||
| * | ||
| * [--url=<url>] | ||
| * : Execute a request against a specified URL. Defaults to the home URL. | ||
| * | ||
| * [--hook=<hook>] | ||
| * : Filter queries to only show those executed during a specific hook. | ||
| * | ||
| * [--callback=<callback>] | ||
| * : Filter queries to only show those executed by a specific callback. | ||
| * | ||
| * [--fields=<fields>] | ||
| * : Limit the output to specific fields. | ||
| * | ||
| * [--format=<format>] | ||
| * : Render output in a particular format. | ||
| * --- | ||
| * default: table | ||
| * options: | ||
| * - table | ||
| * - json | ||
| * - yaml | ||
| * - csv | ||
| * --- | ||
| * | ||
| * [--order=<order>] | ||
| * : Ascending or Descending order. | ||
| * --- | ||
| * default: ASC | ||
| * options: | ||
| * - ASC | ||
| * - DESC | ||
| * --- | ||
| * | ||
| * [--orderby=<fields>] | ||
| * : Set orderby which field. | ||
| * | ||
| * ## EXAMPLES | ||
| * | ||
| * # Show all queries with their execution time | ||
| * $ wp profile queries --fields=query,time | ||
| * | ||
| * # Show queries executed during the 'init' hook | ||
| * $ wp profile queries --hook=init --fields=query,time,caller | ||
| * | ||
| * # Show queries executed by a specific callback | ||
| * $ wp profile queries --callback='WP_Query->get_posts()' --fields=query,time | ||
| * | ||
| * # Show queries ordered by execution time | ||
| * $ wp profile queries --fields=query,time --orderby=time --order=DESC | ||
| * | ||
| * @when before_wp_load | ||
| */ | ||
| public function queries( $args, $assoc_args ) { | ||
| global $wpdb; | ||
|
|
||
| $hook = Utils\get_flag_value( $assoc_args, 'hook' ); | ||
| $callback = Utils\get_flag_value( $assoc_args, 'callback' ); | ||
| $order = Utils\get_flag_value( $assoc_args, 'order', 'ASC' ); | ||
| $orderby = Utils\get_flag_value( $assoc_args, 'orderby', null ); | ||
|
|
||
| // Set up profiler to track hooks and callbacks | ||
| $type = null; | ||
| $focus = null; | ||
| if ( $hook ) { | ||
| $type = 'hook'; | ||
| $focus = $hook; | ||
| } elseif ( $callback ) { | ||
| $type = 'hook'; | ||
| $focus = true; // Profile all hooks to find the specific callback | ||
| } | ||
|
|
||
| $profiler = new Profiler( $type, $focus ); | ||
| $profiler->run(); | ||
|
|
||
| // Build a map of query indices to hooks/callbacks | ||
| $query_map = array(); | ||
| if ( $hook || $callback ) { | ||
| $loggers = $profiler->get_loggers(); | ||
| foreach ( $loggers as $logger ) { | ||
| // Skip if filtering by callback and this isn't the right one | ||
| if ( $callback && isset( $logger->callback ) ) { | ||
| // Normalize callback for comparison | ||
| $normalized_callback = str_replace( array( '->', '::' ), '', (string) $logger->callback ); | ||
| $normalized_filter = str_replace( array( '->', '::' ), '', $callback ); | ||
swissspidy marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if ( false === stripos( $normalized_callback, $normalized_filter ) ) { | ||
| continue; | ||
| } | ||
| } | ||
swissspidy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Skip if filtering by hook and this isn't the right one | ||
swissspidy marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if ( $hook && isset( $logger->hook ) && $logger->hook !== $hook ) { | ||
| continue; | ||
| } | ||
|
|
||
| // Get the query indices for this logger | ||
| if ( isset( $logger->query_indices ) && ! empty( $logger->query_indices ) ) { | ||
| foreach ( $logger->query_indices as $query_index ) { | ||
| if ( ! isset( $query_map[ $query_index ] ) ) { | ||
| $query_map[ $query_index ] = array( | ||
| 'hook' => isset( $logger->hook ) ? $logger->hook : null, | ||
| 'callback' => isset( $logger->callback ) ? $logger->callback : null, | ||
| ); | ||
| } | ||
| } | ||
|
Comment on lines
+610
to
+617
|
||
| } | ||
| } | ||
| } | ||
|
|
||
| // Get all queries | ||
| $queries = array(); | ||
| if ( ! empty( $wpdb->queries ) ) { | ||
| foreach ( $wpdb->queries as $index => $query_data ) { | ||
| // If filtering by hook/callback, only include queries in the map | ||
| if ( ( $hook || $callback ) && ! isset( $query_map[ $index ] ) ) { | ||
| continue; | ||
| } | ||
|
|
||
| $query_obj = new QueryLogger( | ||
| $query_data[0], // SQL query | ||
| $query_data[1], // Time | ||
| isset( $query_data[2] ) ? $query_data[2] : '', // Caller | ||
| isset( $query_map[ $index ]['hook'] ) ? $query_map[ $index ]['hook'] : null, | ||
| isset( $query_map[ $index ]['callback'] ) ? $query_map[ $index ]['callback'] : null | ||
| ); | ||
| $queries[] = $query_obj; | ||
| } | ||
|
Comment on lines
586
to
639
|
||
| } | ||
|
|
||
| // Set up fields for output | ||
| $fields = array( 'query', 'time', 'caller' ); | ||
| if ( $hook && ! $callback ) { | ||
| $fields = array( 'query', 'time', 'callback', 'caller' ); | ||
| } elseif ( $callback && ! $hook ) { | ||
| $fields = array( 'query', 'time', 'hook', 'caller' ); | ||
| } elseif ( $hook && $callback ) { | ||
| $fields = array( 'query', 'time', 'hook', 'callback', 'caller' ); | ||
| } | ||
|
|
||
| $formatter = new Formatter( $assoc_args, $fields ); | ||
| $formatter->display_items( $queries, true, $order, $orderby ); | ||
| } | ||
|
|
||
| /** | ||
| * Filter loggers with zero-ish values. | ||
| * | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When both
--hookand--callbackare provided, the profiler setup logic only uses the hook value. The condition on line 571 (elseif ( $callback )) will never be reached when both are present. This means the profiler won't track all hooks to find the specific callback, which could lead to incomplete filtering. Consider handling the case where both are provided explicitly, or adjust the logic to ensure both parameters are considered properly.