Skip to content
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
51 changes: 51 additions & 0 deletions features/profile-requests.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
Feature: Profile HTTP requests

Scenario: Profile HTTP requests during WordPress load
Given a WP install
And that HTTP requests to https://www.apple.com/ will respond with:
"""
HTTP/1.1 200
Content-Type: text/plain

Hello world
"""
And that HTTP requests to https://www.example.com/ will respond with:
"""
HTTP/1.1 201
Content-Type: application/json

{"status":"created"}
"""
And a wp-content/mu-plugins/http-requests.php file:
"""
<?php
add_action( 'muplugins_loaded', function() {
wp_remote_get( 'https://www.apple.com/' );
wp_remote_post( 'https://www.example.com/', array( 'body' => 'test' ) );
});
"""

When I run `wp profile requests --fields=method,url`
Then STDOUT should be a table containing rows:
| method | url |
| GET | https://www.apple.com/ |
| POST | https://www.example.com/ |
And STDOUT should contain:
"""
total (2)
"""

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 );
"""

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
61 changes: 54 additions & 7 deletions src/Profiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
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 @@
) {
$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,65 @@
/**
* 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 ) {
// Reset properties first to handle cases where previous request was preempted
$this->request_start_time = null;
$this->request_args = null;

// Now capture the new request details
$this->request_start_time = microtime( true );
$this->request_args = array(
'url' => $url,
'method' => ( is_array( $parsed_args ) && isset( $parsed_args['method'] ) ) ? $parsed_args['method'] : 'GET',
);
}

return $preempt;
}

/**
* Profiling request time for any active Loggers
*/
public function wp_request_end( $filter_value = null ) {
public function wp_request_end( $response = null, $context = null, $class = null, $parsed_args = null, $url = null ) {

Check warning on line 412 in src/Profiler.php

View workflow job for this annotation

GitHub Actions / code-quality / PHPCS

It is recommended not to use reserved keyword "class" as function parameter name. Found: $class

Check warning on line 412 in src/Profiler.php

View workflow job for this annotation

GitHub Actions / code-quality / PHPCS

The method parameter $url is never used

Check warning on line 412 in src/Profiler.php

View workflow job for this annotation

GitHub Actions / code-quality / PHPCS

The method parameter $parsed_args is never used

Check warning on line 412 in src/Profiler.php

View workflow job for this annotation

GitHub Actions / code-quality / PHPCS

The method parameter $class is never used

Check warning on line 412 in src/Profiler.php

View workflow job for this annotation

GitHub Actions / code-quality / PHPCS

The method parameter $context is never used
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;
}

return $response;
}

/**
Expand Down
Loading