Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6dd9c91
Initial plan
Copilot Nov 8, 2025
1aa88d1
Add basic wp profile queries command implementation
Copilot Nov 8, 2025
7e45434
Add hook and callback filtering support for queries command
Copilot Nov 8, 2025
60d5396
Add documentation for wp profile queries command
Copilot Nov 8, 2025
4a5f4cb
Update src/QueryLogger.php
swissspidy Nov 8, 2025
26ec9f7
Update src/Logger.php
swissspidy Nov 8, 2025
071cb94
Update features/profile.feature
swissspidy Nov 8, 2025
9e5aa60
Improve first test scenario to add custom queries
Copilot Nov 8, 2025
ec1564a
Lint fix
swissspidy Nov 10, 2025
28e1e4d
Fix PHP warnings for non-numeric values in Formatter
Copilot Nov 10, 2025
1f255b9
Address code review feedback: fix callback filtering and reset query_…
Copilot Nov 10, 2025
cc09ca2
Update src/Command.php
swissspidy Nov 10, 2025
4faf915
Merge branch 'main' into copilot/add-wp-profile-queries
swissspidy Jan 16, 2026
71b0e54
Fix test failures: add hook to callback loggers and improve Formatter…
Copilot Jan 16, 2026
7a42ddb
Use last-logger-wins for query map to show most specific hook/callback
Copilot Feb 9, 2026
91c69cf
Document query map complexity and performance characteristics
Copilot Feb 9, 2026
b4b128a
Merge branch 'main' into copilot/add-wp-profile-queries
swissspidy Mar 2, 2026
a23e1be
Fix hook filtering to skip loggers without hook property
Copilot Mar 2, 2026
44bd90a
Update src/Command.php
swissspidy Mar 2, 2026
67d57b7
Clean up some now obsolete tests
swissspidy Mar 3, 2026
1764ddd
Fix root cause of hook/callback filtering returning 0 results
Copilot Mar 3, 2026
5b79ac3
Merge branch 'main' into copilot/add-wp-profile-queries
swissspidy Mar 19, 2026
bb8a0b3
Add `@skipglobalargcheck`
swissspidy Mar 19, 2026
4e585ae
Merge branch 'main' into copilot/add-wp-profile-queries
swissspidy Mar 20, 2026
3c41965
Fix callback filter: restore stripos for substring matching after nam…
Copilot Mar 20, 2026
3eea3a3
Lint fix
swissspidy Mar 21, 2026
92a804b
Lint fix
swissspidy Mar 21, 2026
e94bede
Resolve merge conflicts and fix PHPStan/linting issues
swissspidy Apr 14, 2026
2e0ca4b
`time_threshold` filter and improved formatting
swissspidy Apr 14, 2026
95f3612
Update test
swissspidy Apr 14, 2026
6f00cbe
Update readme
swissspidy Apr 14, 2026
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>]
~~~
Comment thread
swissspidy marked this conversation as resolved.

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
100 changes: 100 additions & 0 deletions features/profile-queries.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
Feature: Profile database queries

@require-wp-4.0
Scenario: Show all database queries
Given a WP install

When I run `wp profile queries --fields=time`
Then STDOUT should contain:
"""
time
"""
And STDOUT should contain:
"""
total
"""
And STDERR should be empty
Comment thread
swissspidy marked this conversation as resolved.

@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
}
Comment thread
swissspidy marked this conversation as resolved.
Outdated

$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 );
Comment thread
swissspidy marked this conversation as resolved.
Outdated
if ( false === stripos( $normalized_callback, $normalized_filter ) ) {
continue;
}
Comment thread
swissspidy marked this conversation as resolved.
}
Comment thread
swissspidy marked this conversation as resolved.

// Skip if filtering by hook and this isn't the right one
Comment thread
swissspidy marked this conversation as resolved.
Outdated
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 thread
swissspidy marked this conversation as resolved.
}
}
}

// 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 thread
swissspidy marked this conversation as resolved.
}

// 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
5 changes: 5 additions & 0 deletions src/Logger.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@

class Logger {

public $time = 0;

Check warning on line 7 in src/Logger.php

View workflow job for this annotation

GitHub Actions / code-quality / PHPCS

Equals sign not aligned with surrounding assignments; expected 8 spaces but found 16 spaces
public $query_count = 0;

Check warning on line 8 in src/Logger.php

View workflow job for this annotation

GitHub Actions / code-quality / PHPCS

Equals sign not aligned with surrounding assignments; expected 1 space but found 9 spaces
public $query_time = 0;

Check warning on line 9 in src/Logger.php

View workflow job for this annotation

GitHub Actions / code-quality / PHPCS

Equals sign not aligned with surrounding assignments; expected 2 spaces but found 10 spaces
Comment thread
swissspidy marked this conversation as resolved.
Outdated
/**
* @var array Array of query indices tracked during this logger's execution.
*/
public $query_indices = array();
public $cache_hits = 0;
public $cache_misses = 0;
public $cache_ratio = null;
Expand Down Expand Up @@ -87,6 +91,7 @@
for ( $i = $this->query_offset; $i < $query_total_count; $i++ ) {
$this->query_time += $wpdb->queries[ $i ][1];
++$this->query_count;
$this->query_indices[] = $i;
}
Comment thread
swissspidy marked this conversation as resolved.
}

Expand Down
Loading
Loading