Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c27d533
Initial plan
Copilot Nov 7, 2025
a7f209a
Add wp profile requests command
Copilot Nov 7, 2025
9c3cf1d
Update test to include requests command in usage output
Copilot Nov 7, 2025
69f6d56
Avoid adding 'all' hook when profiling requests
Copilot Nov 7, 2025
8418ad2
Fix hook parameter signatures to match WordPress
Copilot Nov 7, 2025
49258a1
Lint fixes
swissspidy Nov 7, 2025
9ae6961
Improve test with mu-plugin making HTTP requests
Copilot Nov 10, 2025
721c27a
Add HTTP mocking to test to avoid real requests
Copilot Nov 10, 2025
a31e12b
Update src/Profiler.php
swissspidy Nov 10, 2025
dbbb4f1
Update src/Profiler.php
swissspidy Nov 10, 2025
64c6426
Handle preempted HTTP requests by resetting properties
Copilot Nov 10, 2025
d20638b
Merge branch 'main' into copilot/introduce-wp-profile-requests
swissspidy Jan 16, 2026
2f5632b
Lint fix
swissspidy Jan 16, 2026
b601c03
Fix http_api_debug hook registration to pass all 5 parameters
Copilot Jan 16, 2026
9b66b2c
Handle preempted/mocked HTTP requests in wp_request_begin
Copilot Jan 16, 2026
5f2f176
Fix PHP warning by checking if value is numeric before summing
Copilot Jan 17, 2026
fe1eeed
Undo change again
swissspidy Jan 17, 2026
6275bc2
Merge branch 'main' into copilot/introduce-wp-profile-requests
swissspidy Feb 9, 2026
2a709ee
Merge branch 'main' into copilot/introduce-wp-profile-requests
swissspidy Mar 22, 2026
f8afe79
Merge branch 'main' into copilot/introduce-wp-profile-requests
swissspidy Apr 14, 2026
48b010f
Merge branch 'main' into copilot/introduce-wp-profile-requests
swissspidy Apr 14, 2026
07dbf41
fix test
swissspidy Apr 14, 2026
1682ae1
Update features/profile-requests.feature
swissspidy Apr 15, 2026
420c465
Update src/Profiler.php
swissspidy Apr 15, 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
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 requests"
],
"readme": {
"sections": [
Expand Down
37 changes: 37 additions & 0 deletions features/profile-requests.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
Feature: Profile HTTP requests

Scenario: Profile HTTP requests during WordPress load
Comment thread
swissspidy marked this conversation as resolved.
Given a WP install

When I run `wp profile requests --fields=method,url,status,time`
Then STDOUT should contain:
"""
method
"""
And STDOUT should contain:
"""
url
"""
And STDOUT should contain:
"""
status
"""
And STDOUT should contain:
"""
time
"""

Scenario: Profile shows no requests when none are made
Given a WP install
And a wp-content/mu-plugins/no-requests.php file:
"""
<?php
// Don't make any HTTP requests
add_filter( 'pre_http_request', '__return_false', 1 );
Comment thread
swissspidy marked this conversation as resolved.
Outdated
"""

When I run `wp profile requests --fields=method,url`
Then STDOUT should contain:
"""
total (0)
"""
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 requests [--url=<url>] [--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
72 changes: 72 additions & 0 deletions src/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,78 @@ public function hook( $args, $assoc_args ) {
$formatter->display_items( $loggers, true, $order, $orderby );
}

/**
* Profile HTTP requests made during the WordPress load process.
*
* Monitors all HTTP requests made during the WordPress load process,
* displaying information about each request including URL, method,
* execution time, and response code.
*
* ## OPTIONS
*
* [--url=<url>]
* : Execute a request against a specified URL. Defaults to the home URL.
*
* [--fields=<fields>]
* : Limit the output to specific fields. Default is all 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
*
* # List all HTTP requests during page load
* $ wp profile requests
* +-----------+----------------------------+----------+---------+
* | method | url | status | time |
* +-----------+----------------------------+----------+---------+
* | GET | https://api.example.com | 200 | 0.2341s |
* | POST | https://api.example.com | 201 | 0.1653s |
* +-----------+----------------------------+----------+---------+
* | total (2) | | | 0.3994s |
* +-----------+----------------------------+----------+---------+
*
* @when before_wp_load
*/
public function requests( $args, $assoc_args ) {
$order = Utils\get_flag_value( $assoc_args, 'order', 'ASC' );
$orderby = Utils\get_flag_value( $assoc_args, 'orderby', null );

$profiler = new Profiler( 'request', false );
$profiler->run();

$fields = array(
'method',
'url',
'status',
'time',
);
$formatter = new Formatter( $assoc_args, $fields );
$loggers = $profiler->get_loggers();

$formatter->display_items( $loggers, true, $order, $orderby );
}

/**
* Profile arbitrary code execution.
*
Expand Down
56 changes: 49 additions & 7 deletions src/Profiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ class Profiler {
private $tick_cache_hit_offset = null;
private $tick_cache_miss_offset = null;

private $request_start_time = null;
private $request_args = null;

public function __construct( $type, $focus ) {
$this->type = $type;
$this->focus = $focus;
Expand Down Expand Up @@ -124,11 +127,11 @@ function () {
) {
$start_hook = substr( $this->focus, 0, -6 );
WP_CLI::add_wp_hook( $start_hook, array( $this, 'wp_tick_profile_begin' ), 9999 );
} else {
} elseif ( 'request' !== $this->type ) {
WP_CLI::add_wp_hook( 'all', array( $this, 'wp_hook_begin' ) );
}
WP_CLI::add_wp_hook( 'pre_http_request', array( $this, 'wp_request_begin' ) );
WP_CLI::add_wp_hook( 'http_api_debug', array( $this, 'wp_request_end' ) );
WP_CLI::add_wp_hook( 'pre_http_request', array( $this, 'wp_request_begin' ), 10, 3 );
WP_CLI::add_wp_hook( 'http_api_debug', array( $this, 'wp_request_end' ), 10, 5 );
$this->load_wordpress_with_template();
}

Expand Down Expand Up @@ -381,21 +384,60 @@ public function handle_function_tick() {
/**
* Profiling request time for any active Loggers
*/
public function wp_request_begin( $filter_value = null ) {
public function wp_request_begin( $preempt = null, $parsed_args = null, $url = null ) {
foreach ( Logger::$active_loggers as $logger ) {
$logger->start_request_timer();
}
return $filter_value;

// For request profiling, capture details of each HTTP request
if ( 'request' === $this->type ) {
$this->request_start_time = microtime( true );
$this->request_args = array(
'url' => $url,
'method' => isset( $parsed_args['method'] ) ? $parsed_args['method'] : 'GET',
Comment thread
swissspidy marked this conversation as resolved.
Outdated
);
}
Comment thread
swissspidy marked this conversation as resolved.

return $preempt;
}

/**
* Profiling request time for any active Loggers
*/
public function wp_request_end( $filter_value = null ) {
public function wp_request_end( $response = null ) {
Comment thread
swissspidy marked this conversation as resolved.
foreach ( Logger::$active_loggers as $logger ) {
$logger->stop_request_timer();
}
return $filter_value;

// For request profiling, log individual request
if ( 'request' === $this->type && ! is_null( $this->request_start_time ) ) {
$request_time = microtime( true ) - $this->request_start_time;
$status = '';

// Extract status code from response
if ( is_wp_error( $response ) ) {
$status = 'Error';
} elseif ( is_array( $response ) && isset( $response['response']['code'] ) ) {
$status = $response['response']['code'];
}

$logger = new Logger(
array(
'method' => $this->request_args['method'],
'url' => $this->request_args['url'],
'status' => $status,
)
);
$logger->time = $request_time;

$this->loggers[] = $logger;

// Reset for next request
$this->request_start_time = null;
$this->request_args = null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The logic inside this if block is very similar to the logic for handling preempted requests in wp_request_begin. To improve maintainability, consider extracting this duplicated code into a private helper method for logging HTTP requests. This would centralize the logic for creating a Logger instance, assigning properties, and adding it to the loggers array.


return $response;
}

/**
Expand Down
Loading