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
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
2 changes: 1 addition & 1 deletion src/Formatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ function ( $a, $b ) use ( $order, $orderby ) {
if ( ! is_null( $value ) ) {
$totals[ $i ][] = $value;
}
} else {
} elseif ( is_numeric( $value ) ) {
$totals[ $i ] += $value;
}
if ( stripos( $fields[ $i ], '_time' ) || 'time' === $fields[ $i ] ) {
Expand Down
87 changes: 81 additions & 6 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,10 +127,10 @@ 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( 'pre_http_request', array( $this, 'wp_request_begin' ), 10, 3 );
WP_CLI::add_wp_hook( 'http_api_debug', array( $this, 'wp_request_end' ) );
$this->load_wordpress_with_template();
}
Expand Down Expand Up @@ -381,21 +384,93 @@ 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 ) {
// 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',
);

// If request is preempted (mocked), log it now since http_api_debug won't fire
if ( false !== $preempt && ! is_null( $preempt ) ) {
$request_time = 0; // Preempted requests happen instantly
$status = '';

// Extract status code from preempted response
if ( is_wp_error( $preempt ) ) {
$status = 'Error';
} elseif ( is_array( $preempt ) && isset( $preempt['response']['code'] ) ) {
$status = $preempt['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 $preempt;
}

/**
* Profiling request time for any active Loggers
*/
public function wp_request_end( $filter_value = null ) {
public function wp_request_end( $response = null ) {
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