Skip to content
Draft
Show file tree
Hide file tree
Changes from 10 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
85 changes: 85 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,91 @@ will need to execute during the course of the request.



### wp profile queries

Profile database queries and their execution time.

~~~
wp profile queries [--url=<url>] [--hook=<hook>] [--callback=<callback>] [--fields=<fields>] [--format=<format>] [--order=<order>] [--orderby=<fields>]
~~~

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
+--------------------------------------+---------+
| query | time |
+--------------------------------------+---------+
| SELECT option_value FROM wp_options | 0.0001s |
| SELECT * FROM wp_posts WHERE ID = 1 | 0.0003s |
+--------------------------------------+---------+
| total (2) | 0.0004s |
+--------------------------------------+---------+

# Show queries executed during the 'init' hook
$ wp profile queries --hook=init --fields=query,time,callback
+--------------------------------------+---------+------------------+
| query | time | callback |
+--------------------------------------+---------+------------------+
| SELECT * FROM wp_users | 0.0002s | my_init_func() |
+--------------------------------------+---------+------------------+
| total (1) | 0.0002s | |
+--------------------------------------+---------+------------------+

# Show queries executed by a specific callback
$ wp profile queries --callback='WP_Query->get_posts()' --fields=query,time
+--------------------------------------+---------+
| query | time |
+--------------------------------------+---------+
| SELECT * FROM wp_posts | 0.0004s |
+--------------------------------------+---------+
| total (1) | 0.0004s |
+--------------------------------------+---------+



### wp profile eval

Profile arbitrary code execution.
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"profile stage",
"profile hook",
"profile eval",
"profile eval-file"
"profile eval-file",
"profile queries"
],
"readme": {
"sections": [
Expand Down
121 changes: 121 additions & 0 deletions features/profile-queries.feature
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
1 change: 1 addition & 0 deletions features/profile.feature
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,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>]
or: wp profile queries [--url=<url>] [--hook=<hook>] [--callback=<callback>] [--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>]

See 'wp help profile <command>' for more information on a specific command.
Expand Down
151 changes: 151 additions & 0 deletions src/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

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

When both --hook and --callback are 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.

Copilot uses AI. Check for mistakes.

$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 );
if ( false === stripos( $normalized_callback, $normalized_filter ) ) {
continue;
}
}

// Skip if filtering by hook and this isn't the right one
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
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

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

[nitpick] Potential issue with query map overwriting. When multiple loggers track the same query index (e.g., nested hooks), the check ! isset( $query_map[ $query_index ] ) at line 602 prevents overwriting. This means the first logger's hook/callback will be used, but in nested scenarios, this might not be the most specific or accurate mapping. Consider whether the first-logger-wins approach is the intended behavior, or if you need to track multiple hooks/callbacks per query.

Copilot uses AI. Check for mistakes.
}
}
}

// 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
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

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

[nitpick] Performance consideration: The nested loop structure (lines 583-610 and 616-630) could be inefficient for sites with many queries and hooks. For N loggers and M queries, this is O(N*Q + M) where Q is queries per logger. If $wpdb->queries contains thousands of entries, this might be slow. Consider optimizing by building the query map in a single pass if performance becomes an issue, though the current implementation is clear and correct for typical use cases.

Copilot uses AI. Check for mistakes.
}

// 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.
*
Expand Down
Loading
Loading