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
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
160 changes: 160 additions & 0 deletions src/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,166 @@
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 && $callback ) {
// When both are provided, profile all hooks to find the specific callback
$type = 'hook';
$focus = true;
} elseif ( $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 logger doesn't have a callback
if ( $callback && ! isset( $logger->callback ) ) {
continue;
}

// Skip if filtering by callback and this isn't the right one
if ( $callback && isset( $logger->callback ) ) {
// Normalize callback for comparison
$normalized_callback = trim((string) $logger->callback);

Check failure on line 596 in src/Command.php

View workflow job for this annotation

GitHub Actions / code-quality / PHPCS

Expected 1 spaces before closing parenthesis; 0 found

Check failure on line 596 in src/Command.php

View workflow job for this annotation

GitHub Actions / code-quality / PHPCS

Expected a space before the type cast open parenthesis; none found

Check failure on line 596 in src/Command.php

View workflow job for this annotation

GitHub Actions / code-quality / PHPCS

Expected 1 spaces after opening parenthesis; 0 found
$normalized_filter = trim($callback);

Check failure on line 597 in src/Command.php

View workflow job for this annotation

GitHub Actions / code-quality / PHPCS

Expected 1 spaces before closing parenthesis; 0 found

Check failure on line 597 in src/Command.php

View workflow job for this annotation

GitHub Actions / code-quality / PHPCS

Expected 1 spaces after opening parenthesis; 0 found
if ( false === stripos( $normalized_callback, $normalized_filter ) ) {
continue;
}
}

// Skip if filtering for a specific 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;
}
}

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