diff --git a/composer.json b/composer.json index 7c2eee3a..03dc8d7e 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,8 @@ "profile stage", "profile hook", "profile eval", - "profile eval-file" + "profile eval-file", + "profile requests" ], "readme": { "sections": [ diff --git a/features/profile-requests.feature b/features/profile-requests.feature new file mode 100644 index 00000000..50ddd1f7 --- /dev/null +++ b/features/profile-requests.feature @@ -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: + """ + '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: + """ + [--hook[=]] [--fields=] [--format=] [--order=] [--orderby=] or: wp profile eval-file [--hook[=]] [--fields=] [--format=] [--order=] [--orderby=] or: wp profile hook [] [--all] [--spotlight] [--url=] [--fields=] [--format=] [--order=] [--orderby=] + or: wp profile requests [--url=] [--fields=] [--format=] [--order=] [--orderby=] or: wp profile stage [] [--all] [--spotlight] [--url=] [--fields=] [--format=] [--order=] [--orderby=] See 'wp help profile ' for more information on a specific command. diff --git a/src/Command.php b/src/Command.php index b3d6936d..7ebf489c 100644 --- a/src/Command.php +++ b/src/Command.php @@ -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=] + * : Execute a request against a specified URL. Defaults to the home URL. + * + * [--fields=] + * : Limit the output to specific fields. Default is all fields. + * + * [--format=] + * : Render output in a particular format. + * --- + * default: table + * options: + * - table + * - json + * - yaml + * - csv + * --- + * + * [--order=] + * : Ascending or Descending order. + * --- + * default: ASC + * options: + * - ASC + * - DESC + * --- + * + * [--orderby=] + * : 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. * diff --git a/src/Profiler.php b/src/Profiler.php index 7d376575..228d4799 100644 --- a/src/Profiler.php +++ b/src/Profiler.php @@ -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; @@ -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(); } @@ -381,21 +384,65 @@ 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', + ); + } + + 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 ) { 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; } /**