From 79afa198700270f957eb1519f316940cef0c8b3d Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Thu, 6 Oct 2022 15:40:15 -0700 Subject: [PATCH 01/40] Implement foundation for a Server-Timing API, including default metric for WordPress execution prior to serving the template. --- load.php | 4 + .../class-perflab-server-timing-metric.php | 93 ++++++++++ server-timing/class-perflab-server-timing.php | 163 ++++++++++++++++++ server-timing/load.php | 77 +++++++++ 4 files changed, 337 insertions(+) create mode 100644 server-timing/class-perflab-server-timing-metric.php create mode 100644 server-timing/class-perflab-server-timing.php create mode 100644 server-timing/load.php diff --git a/load.php b/load.php index 1eb2e7ab57..f90a866abf 100644 --- a/load.php +++ b/load.php @@ -240,6 +240,10 @@ function perflab_load_active_and_valid_modules() { perflab_load_active_and_valid_modules(); +require_once PERFLAB_PLUGIN_DIR_PATH . 'server-timing/class-perflab-server-timing-metric.php'; +require_once PERFLAB_PLUGIN_DIR_PATH . 'server-timing/class-perflab-server-timing.php'; +require_once PERFLAB_PLUGIN_DIR_PATH . 'server-timing/load.php'; + // Only load admin integration when in admin. if ( is_admin() ) { require_once PERFLAB_PLUGIN_DIR_PATH . 'admin/load.php'; diff --git a/server-timing/class-perflab-server-timing-metric.php b/server-timing/class-perflab-server-timing-metric.php new file mode 100644 index 0000000000..7593bf6cf2 --- /dev/null +++ b/server-timing/class-perflab-server-timing-metric.php @@ -0,0 +1,93 @@ +slug = $slug; + } + + /** + * Gets the metric slug. + * + * @since n.e.x.t + * + * @return string The metric slug. + */ + public function get_slug() { + return $this->slug; + } + + /** + * Sets the metric value. + * + * @since n.e.x.t + * + * @param int|float $value The metric value to set, in milliseconds. + */ + public function set_value( $value ) { + if ( ! is_int( $value ) && ! is_float( $value ) ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: PHP parameter name */ + sprintf( __( 'The %s parameter must be an integer or float.', 'performance-lab' ), '$value' ), + '' + ); + return; + } + + if ( headers_sent() ) { + _doing_it_wrong( + __METHOD__, + __( 'Metrics can only be measured before headers have been sent.', 'performance-lab' ), + '' + ); + } + + $this->value = $value; + } + + /** + * Gets the metric value. + * + * @since n.e.x.t + * + * @return int|float|null The metric value, or null if none set. + */ + public function get_value() { + return $this->value; + } +} diff --git a/server-timing/class-perflab-server-timing.php b/server-timing/class-perflab-server-timing.php new file mode 100644 index 0000000000..0e27493ed8 --- /dev/null +++ b/server-timing/class-perflab-server-timing.php @@ -0,0 +1,163 @@ + null, + 'access_cap' => null, + ) + ); + if ( ! $args['measure_callback'] || ! is_callable( $args['measure_callback'] ) ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: PHP parameter name */ + sprintf( __( 'The %s argument is required and must be a callable.', 'performance-lab' ), '$args["measure_callback"]' ), + '' + ); + return; + } + if ( ! $args['access_cap'] || ! is_string( $args['access_cap'] ) ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: PHP parameter name */ + sprintf( __( 'The %s argument is required and must be a string.', 'performance-lab' ), '$args["access_cap"]' ), + '' + ); + return; + } + + $this->registered_metrics[ $metric_slug ] = new Perflab_Server_Timing_Metric( $metric_slug ); + $this->registered_metrics_data[ $metric_slug ] = $args; + + // If the current user has already been determined and they lack the necessary access, + // do not even attempt to calculate the metric. + if ( did_action( 'set_current_user' ) && ! current_user_can( $args['access_cap'] ) ) { + return; + } + + // Otherwise, call the measuring callback and pass the metric instance to it. + call_user_func( $args['measure_callback'], $this->registered_metrics[ $metric_slug ] ); + } + + /** + * Outputs the Server-Timing header. + * + * This method must be called before rendering the page. + * + * @since n.e.x.t + */ + public function add_header() { + if ( headers_sent() ) { + _doing_it_wrong( + __METHOD__, + __( 'The method must be called before headers have been sent.', 'performance-lab' ), + '' + ); + return; + } + + $header_value = $this->get_header_value(); + if ( ! $header_value ) { + return; + } + + header( sprintf( 'Server-Timing: %s', $header_value ), false ); + } + + /** + * Gets the value for the Server-Timing header. + * + * @since n.e.x.t + * + * @return string The Server-Timing header value. + */ + public function get_header_value() { + // Get all metric header values, as long as the current user has access to the metric. + $metric_header_values = array_filter( + array_map( + function( Perflab_Server_Timing_Metric $metric ) { + $value = $metric->get_value(); + if ( null === $value ) { + return null; + } + + // Check the registered capability here to ensure no metric without access is exposed. + if ( ! current_user_can( $this->registered_metrics_data[ $metric->get_slug() ]['access_cap'] ) ) { + return null; + } + + return $this->format_metric_header_value( $metric ); + }, + $this->registered_metrics + ), + function( $value ) { + return null !== $value; + } + ); + + return implode( ', ', $metric_header_values ); + } + + /** + * Formats the header segment for a single metric. + * + * @since n.e.x.t + * + * @param Perflab_Server_Timing_Metric $metric The metric to format. + * @return string Segment for the Server-Timing header. + */ + private function format_metric_header_value( Perflab_Server_Timing_Metric $metric ) { + $value = $metric->get_value(); + if ( is_float( $value ) ) { + $value = round( $value, 2 ); + } + return sprintf( 'wp-%1$s;dur=%2$s', $metric->get_slug(), $value ); + } +} diff --git a/server-timing/load.php b/server-timing/load.php new file mode 100644 index 0000000000..c3fa8b98e1 --- /dev/null +++ b/server-timing/load.php @@ -0,0 +1,77 @@ +add_header(); + return $passthrough; + }, + PHP_INT_MAX + ); + + /** + * Initialization hook for the Server-Timing API. + * + * @since n.e.x.t + * + * @param Perflab_Server_Timing $server_timing Server-Timing API to register metrics. + */ + do_action( 'perflab_server_timing_init', $server_timing ); +} +add_action( 'plugins_loaded', 'perflab_server_timing', 0 ); + +/** + * Registers the default Server-Timing metrics. + * + * @since n.e.x.t + * + * @param Perflab_Server_Timing $server_timing Server-Timing API to register metrics. + */ +function perflab_register_default_server_timing_metrics( $server_timing ) { + // WordPress execution prior to serving the template. + $server_timing->register_metric( + 'before-template', + array( + 'measure_callback' => function( $metric ) { + global $timestart; + + // Use original value of global in case a plugin messes with it. + $start_time = $timestart; + + add_filter( + 'template_include', + function( $passthrough ) use ( $metric, $start_time ) { + $metric->set_value( ( microtime( true ) - $start_time ) * 1000.0 ); + return $passthrough; + }, + PHP_INT_MAX - 1 + ); + }, + 'access_cap' => 'exist', + ) + ); +} +add_action( 'perflab_server_timing_init', 'perflab_register_default_server_timing_metrics' ); From 1ee4ec82c26b2d3bc3d46814e64d1d0b34243c20 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Mon, 10 Oct 2022 16:27:58 -0700 Subject: [PATCH 02/40] Implement perflab_wrap_server_timing() utility function for callbacks. --- server-timing/load.php | 49 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/server-timing/load.php b/server-timing/load.php index c3fa8b98e1..7d8249f156 100644 --- a/server-timing/load.php +++ b/server-timing/load.php @@ -75,3 +75,52 @@ function( $passthrough ) use ( $metric, $start_time ) { ); } add_action( 'perflab_server_timing_init', 'perflab_register_default_server_timing_metrics' ); + +/** + * Wraps a callback (e.g. for an action or filter) to be measured and included in the Server-Timing header. + * + * @since n.e.x.t + * + * @param callable $callback The callback to wrap. + * @param string $metric_slug The metric slug to use within the Server-Timing header. + * @param string $access_cap Capability required to view the metric. If this is a public metric, this needs to be + * set to "exist". + */ +function perflab_wrap_server_timing( $callback, $metric_slug, $access_cap ) { + // Gain access to Perflab_Server_Timing_Metric instance. + $server_timing_metric = null; + add_action( + 'perflab_server_timing_init', + function( $server_timing ) use ( &$server_timing_metric, $metric_slug, $access_cap ) { + $server_timing->register_metric( + $metric_slug, + array( + 'measure_callback' => function( $metric ) use ( &$server_timing_metric ) { + $server_timing_metric = $metric; + }, + 'access_cap' => $access_cap, + ) + ); + } + ); + + return function( ...$callback_args ) use ( &$server_timing_metric, $callback ) { + // If metric instance was not set, this metric should not be calculated. + if ( null === $server_timing_metric ) { + return call_user_func_array( $callback, $callback_args ); + } + + // Store start time (in microseconds). + $start_time = microtime( true ); + + // Execute the callback. + $result = call_user_func_array( $callback, $callback_args ); + + // Calculate total time (in milliseconds) and set it for the metric. + $total_time = ( microtime( true ) - $start_time ) * 1000.0; + $server_timing_metric->set_value( $total_time ); + + // Return result (e.g. in case this is a filter callback). + return $result; + }; +} From 0e89e48e762bae283d50711de7c2bfecae3602f3 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Mon, 10 Oct 2022 17:21:15 -0700 Subject: [PATCH 03/40] Introduce perflab_server_timing_use_output_buffer() and also record template execution time if it is enabled. --- server-timing/load.php | 89 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 80 insertions(+), 9 deletions(-) diff --git a/server-timing/load.php b/server-timing/load.php index 7d8249f156..659336bdf8 100644 --- a/server-timing/load.php +++ b/server-timing/load.php @@ -22,15 +22,36 @@ function perflab_server_timing() { $server_timing = new Perflab_Server_Timing(); - // The 'template_include' filter is the very last point before HTML is rendered. - add_filter( - 'template_include', - function( $passthrough ) use ( $server_timing ) { - $server_timing->add_header(); - return $passthrough; - }, - PHP_INT_MAX - ); + if ( perflab_server_timing_use_output_buffer() ) { + // The 'template_include' filter is the very last point before HTML is rendered. + add_filter( + 'template_include', + function( $passthrough ) { + ob_start(); + return $passthrough; + }, + PHP_INT_MAX + ); + add_action( + 'wp_footer', + function() use ( $server_timing ) { + $output = ob_get_clean(); + $server_timing->add_header(); + echo $output; + }, + PHP_INT_MAX + ); + } else { + // The 'template_include' filter is the very last point before HTML is rendered. + add_filter( + 'template_include', + function( $passthrough ) use ( $server_timing ) { + $server_timing->add_header(); + return $passthrough; + }, + PHP_INT_MAX + ); + } /** * Initialization hook for the Server-Timing API. @@ -43,6 +64,24 @@ function( $passthrough ) use ( $server_timing ) { } add_action( 'plugins_loaded', 'perflab_server_timing', 0 ); +/** + * Returns whether an output buffer should be used to gather Server-Timing metrics during template rendering. + * + * @since n.e.x.t + * + * @return bool True if an output buffer should be used, false otherwise. + */ +function perflab_server_timing_use_output_buffer() { + /** + * Filters whether an output buffer should be used to be able to gather additional Server-Timing metrics. + * + * @since n.e.x.t + * + * @param bool $use_output_buffer Whether to use an output buffer. + */ + return apply_filters( 'perflab_server_timing_use_output_buffer', false ); +} + /** * Registers the default Server-Timing metrics. * @@ -73,6 +112,38 @@ function( $passthrough ) use ( $metric, $start_time ) { 'access_cap' => 'exist', ) ); + + if ( perflab_server_timing_use_output_buffer() ) { + // WordPress execution while serving the template. + $server_timing->register_metric( + 'template', + array( + 'measure_callback' => function( $metric ) { + $start_time = null; + + add_filter( + 'template_include', + function( $passthrough ) use ( &$start_time ) { + $start_time = microtime( true ); + return $passthrough; + }, + PHP_INT_MAX - 1 + ); + add_action( + 'wp_footer', + function() use ( $metric, &$start_time ) { + if ( null === $start_time ) { + return; + } + $metric->set_value( ( microtime( true ) - $start_time ) * 1000.0 ); + }, + PHP_INT_MAX - 1 + ); + }, + 'access_cap' => 'exist', + ) + ); + } } add_action( 'perflab_server_timing_init', 'perflab_register_default_server_timing_metrics' ); From a048bb6ba3aff33307a34a73923e2b4b44cce459 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Mon, 10 Oct 2022 17:36:56 -0700 Subject: [PATCH 04/40] Use shutdown action instead of wp_footer when using an output buffer. --- server-timing/load.php | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/server-timing/load.php b/server-timing/load.php index 659336bdf8..6bdeb0258c 100644 --- a/server-timing/load.php +++ b/server-timing/load.php @@ -26,21 +26,21 @@ function perflab_server_timing() { // The 'template_include' filter is the very last point before HTML is rendered. add_filter( 'template_include', - function( $passthrough ) { + function( $passthrough ) use ( $server_timing ) { ob_start(); + add_action( + 'shutdown', + function() use ( $server_timing ) { + $output = ob_get_clean(); + $server_timing->add_header(); + echo $output; + }, + -1000 + ); return $passthrough; }, PHP_INT_MAX ); - add_action( - 'wp_footer', - function() use ( $server_timing ) { - $output = ob_get_clean(); - $server_timing->add_header(); - echo $output; - }, - PHP_INT_MAX - ); } else { // The 'template_include' filter is the very last point before HTML is rendered. add_filter( @@ -130,14 +130,15 @@ function( $passthrough ) use ( &$start_time ) { PHP_INT_MAX - 1 ); add_action( - 'wp_footer', + 'shutdown', function() use ( $metric, &$start_time ) { if ( null === $start_time ) { return; } $metric->set_value( ( microtime( true ) - $start_time ) * 1000.0 ); }, - PHP_INT_MAX - 1 + // phpcs:ignore PHPCompatibility.Constants.NewConstants.php_int_minFound + defined( 'PHP_INT_MIN' ) ? PHP_INT_MIN : -1001 ); }, 'access_cap' => 'exist', From d31ca57cdbb1bba6ced60d7497ecf64e1fa3d870 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Tue, 11 Oct 2022 17:30:57 -0700 Subject: [PATCH 05/40] Expose DB queries Server-Timing metrics if SAVEQUERIES is enabled. --- server-timing/load.php | 60 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/server-timing/load.php b/server-timing/load.php index 6bdeb0258c..76c8902f44 100644 --- a/server-timing/load.php +++ b/server-timing/load.php @@ -113,6 +113,36 @@ function( $passthrough ) use ( $metric, $start_time ) { ) ); + // Variable to subtract from total query time after template. + $queries_before_template_time_seconds = null; + + if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { + // WordPress database query time before template. + $server_timing->register_metric( + 'before-template-db-queries', + array( + 'measure_callback' => function( $metric ) use ( &$queries_before_template_time_seconds ) { + add_filter( + 'template_include', + function( $passthrough ) use ( $metric, &$queries_before_template_time_seconds ) { + $queries_before_template_time_seconds = array_reduce( + $GLOBALS['wpdb']->queries, + function( $acc, $query ) { + return $acc + $query[1]; + }, + 0.0 + ); + $metric->set_value( $queries_before_template_time_seconds * 1000.0 ); + return $passthrough; + }, + PHP_INT_MAX - 1 + ); + }, + 'access_cap' => 'exist', + ) + ); + } + if ( perflab_server_timing_use_output_buffer() ) { // WordPress execution while serving the template. $server_timing->register_metric( @@ -144,6 +174,36 @@ function() use ( $metric, &$start_time ) { 'access_cap' => 'exist', ) ); + + if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { + // WordPress database query time within template. + $server_timing->register_metric( + 'template-db-queries', + array( + 'measure_callback' => function( $metric ) use ( &$queries_before_template_time_seconds ) { + add_action( + 'shutdown', + function() use ( $metric, &$queries_before_template_time_seconds ) { + if ( null === $queries_before_template_time_seconds ) { + return; + } + $total_queries_time_seconds = array_reduce( + $GLOBALS['wpdb']->queries, + function( $acc, $query ) { + return $acc + $query[1]; + }, + 0.0 + ); + $metric->set_value( ( $total_queries_time_seconds - $queries_before_template_time_seconds ) * 1000.0 ); + }, + // phpcs:ignore PHPCompatibility.Constants.NewConstants.php_int_minFound + defined( 'PHP_INT_MIN' ) ? PHP_INT_MIN : -1001 + ); + }, + 'access_cap' => 'exist', + ) + ); + } } } add_action( 'perflab_server_timing_init', 'perflab_register_default_server_timing_metrics' ); From 51c4222c60327d5b40667dda5edc3b6b64afa269 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Mon, 31 Oct 2022 15:29:19 -0700 Subject: [PATCH 06/40] Revamp Server-Timing API to simplify API usage. --- .../class-perflab-server-timing-metric.php | 47 ++- server-timing/class-perflab-server-timing.php | 72 ++++- server-timing/load.php | 294 ++++++++---------- 3 files changed, 239 insertions(+), 174 deletions(-) diff --git a/server-timing/class-perflab-server-timing-metric.php b/server-timing/class-perflab-server-timing-metric.php index 7593bf6cf2..ff3e1e7c19 100644 --- a/server-timing/class-perflab-server-timing-metric.php +++ b/server-timing/class-perflab-server-timing-metric.php @@ -22,13 +22,21 @@ class Perflab_Server_Timing_Metric { private $slug; /** - * The metric value. + * The metric value in milliseconds. * * @since n.e.x.t * @var int|float|null */ private $value; + /** + * The value measured before relevant execution logic in seconds, if used. + * + * @since n.e.x.t + * @var float|null + */ + private $before_value; + /** * Constructor. * @@ -54,6 +62,9 @@ public function get_slug() { /** * Sets the metric value. * + * Alternatively to setting the metric value directly, the {@see Perflab_Server_Timing_Metric::measure_before()} + * and {@see Perflab_Server_Timing_Metric::measure_after()} methods can be used to further simplify measuring. + * * @since n.e.x.t * * @param int|float $value The metric value to set, in milliseconds. @@ -90,4 +101,38 @@ public function set_value( $value ) { public function get_value() { return $this->value; } + + /** + * Captures the current time, as a reference point to calculate the duration of a task afterwards. + * + * This should be used in combination with {@see Perflab_Server_Timing_Metric::measure_after()}. Alternatively, + * {@see Perflab_Server_Timing_Metric::set_value()} can be used to set a calculated value manually. + * + * @since n.e.x.t + */ + public function measure_before() { + $this->before_value = microtime( true ); + } + + /** + * Captures the current time and compares it to the reference point to calculate a task's duration. + * + * This should be used in combination with {@see Perflab_Server_Timing_Metric::measure_before()}. Alternatively, + * {@see Perflab_Server_Timing_Metric::set_value()} can be used to set a calculated value manually. + * + * @since n.e.x.t + */ + public function measure_after() { + if ( ! $this->before_value ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: PHP method name */ + sprintf( __( 'The %s method must be called before.', 'performance-lab' ), __CLASS__ . '::measure_before()' ), + '' + ); + return; + } + + $this->set_value( ( microtime( true ) - $this->before_value ) * 1000.0 ); + } } diff --git a/server-timing/class-perflab-server-timing.php b/server-timing/class-perflab-server-timing.php index 0e27493ed8..09c12914f1 100644 --- a/server-timing/class-perflab-server-timing.php +++ b/server-timing/class-perflab-server-timing.php @@ -32,6 +32,8 @@ class Perflab_Server_Timing { /** * Registers a metric to calculate for the Server-Timing header. * + * This method must be called before the {@see 'perflab_server_timing_send_header'} hook. + * * @since n.e.x.t * * @param string $metric_slug The metric slug. @@ -47,6 +49,16 @@ class Perflab_Server_Timing { * } */ public function register_metric( $metric_slug, array $args ) { + if ( did_action( 'perflab_server_timing_send_header' ) && ! doing_action( 'perflab_server_timing_send_header' ) ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: WordPress action name */ + sprintf( __( 'The method must be called before or during the %s action.', 'performance-lab' ), 'perflab_server_timing_send_header' ), + '' + ); + return; + } + $args = wp_parse_args( $args, array( @@ -93,7 +105,7 @@ public function register_metric( $metric_slug, array $args ) { * * @since n.e.x.t */ - public function add_header() { + public function send_header() { if ( headers_sent() ) { _doing_it_wrong( __METHOD__, @@ -103,6 +115,15 @@ public function add_header() { return; } + /** + * Fires right before the Server-Timing header is sent. + * + * This action is the last possible point to register a Server-Timing metric. + * + * @since n.e.x.t + */ + do_action( 'perflab_server_timing_send_header' ); + $header_value = $this->get_header_value(); if ( ! $header_value ) { return; @@ -145,6 +166,55 @@ function( $value ) { return implode( ', ', $metric_header_values ); } + /** + * Returns whether an output buffer should be used to gather Server-Timing metrics during template rendering. + * + * @since n.e.x.t + * + * @return bool True if an output buffer should be used, false otherwise. + */ + public function use_output_buffer() { + /** + * Filters whether an output buffer should be used to be able to gather additional Server-Timing metrics. + * + * @since n.e.x.t + * + * @param bool $use_output_buffer Whether to use an output buffer. + */ + return apply_filters( 'perflab_server_timing_use_output_buffer', false ); + } + + /** + * Hook callback for the 'template_include' filter. + * + * This effectively initializes the class to send the Server-Timing header at the right point. + * + * This method is solely intended for internal use within WordPress. + * + * @since n.e.x.t + * + * @param mixed $passthrough Optional. Filter value. Default null. + * @return mixed Unmodified value of $passthrough. + */ + public function on_template_include( $passthrough = null ) { + if ( ! $this->use_output_buffer() ) { + $this->send_header(); + return $passthrough; + } + + ob_start(); + add_action( + 'shutdown', + function() { + $output = ob_get_clean(); + $this->send_header(); + echo $output; + }, + -1000 + ); + return $passthrough; + } + /** * Formats the header segment for a single metric. * diff --git a/server-timing/load.php b/server-timing/load.php index 76c8902f44..fb5a798172 100644 --- a/server-timing/load.php +++ b/server-timing/load.php @@ -7,62 +7,48 @@ */ /** - * Initializes the Server-Timing API. + * Provides access the Server-Timing API. * - * This fires the {@see 'perflab_server_timing_init'} action that should be used to register metrics. + * When called for the first time, this also initializes the API to schedule the header for output. + * In case that no metrics are registered, this is still called on {@see 'wp_loaded'}, so that even then it still fires + * its action hooks as expected. * * @since n.e.x.t */ function perflab_server_timing() { static $server_timing; - if ( null !== $server_timing ) { - return; + if ( null === $server_timing ) { + $server_timing = new Perflab_Server_Timing(); + add_filter( 'template_include', array( $server_timing, 'on_template_include' ), PHP_INT_MAX ); } - $server_timing = new Perflab_Server_Timing(); - - if ( perflab_server_timing_use_output_buffer() ) { - // The 'template_include' filter is the very last point before HTML is rendered. - add_filter( - 'template_include', - function( $passthrough ) use ( $server_timing ) { - ob_start(); - add_action( - 'shutdown', - function() use ( $server_timing ) { - $output = ob_get_clean(); - $server_timing->add_header(); - echo $output; - }, - -1000 - ); - return $passthrough; - }, - PHP_INT_MAX - ); - } else { - // The 'template_include' filter is the very last point before HTML is rendered. - add_filter( - 'template_include', - function( $passthrough ) use ( $server_timing ) { - $server_timing->add_header(); - return $passthrough; - }, - PHP_INT_MAX - ); - } + return $server_timing; +} +add_action( 'wp_loaded', 'perflab_server_timing' ); - /** - * Initialization hook for the Server-Timing API. - * - * @since n.e.x.t - * - * @param Perflab_Server_Timing $server_timing Server-Timing API to register metrics. - */ - do_action( 'perflab_server_timing_init', $server_timing ); +/** + * Registers a metric to calculate for the Server-Timing header. + * + * This method must be called before the {@see 'perflab_server_timing_send_header'} hook. + * + * @since n.e.x.t + * + * @param string $metric_slug The metric slug. + * @param array $args { + * Arguments for the metric. + * + * @type callable $measure_callback The callback that initiates calculating the metric value. It will receive + * the Perflab_Server_Timing_Metric instance as a parameter, in order to set + * the value when it has been calculated. Metric values must be provided in + * milliseconds. + * @type string $access_cap Capability required to view the metric. If this is a public metric, this + * needs to be set to "exist". + * } + */ +function perflab_server_timing_register_metric( $metric_slug, array $args ) { + perflab_server_timing()->register_metric( $metric_slug, $args ); } -add_action( 'plugins_loaded', 'perflab_server_timing', 0 ); /** * Returns whether an output buffer should be used to gather Server-Timing metrics during template rendering. @@ -72,141 +58,110 @@ function( $passthrough ) use ( $server_timing ) { * @return bool True if an output buffer should be used, false otherwise. */ function perflab_server_timing_use_output_buffer() { - /** - * Filters whether an output buffer should be used to be able to gather additional Server-Timing metrics. - * - * @since n.e.x.t - * - * @param bool $use_output_buffer Whether to use an output buffer. - */ - return apply_filters( 'perflab_server_timing_use_output_buffer', false ); + return perflab_server_timing()->use_output_buffer(); } /** * Registers the default Server-Timing metrics. * * @since n.e.x.t - * - * @param Perflab_Server_Timing $server_timing Server-Timing API to register metrics. */ -function perflab_register_default_server_timing_metrics( $server_timing ) { - // WordPress execution prior to serving the template. - $server_timing->register_metric( - 'before-template', - array( - 'measure_callback' => function( $metric ) { - global $timestart; - - // Use original value of global in case a plugin messes with it. - $start_time = $timestart; - - add_filter( - 'template_include', - function( $passthrough ) use ( $metric, $start_time ) { - $metric->set_value( ( microtime( true ) - $start_time ) * 1000.0 ); - return $passthrough; - }, - PHP_INT_MAX - 1 - ); - }, - 'access_cap' => 'exist', - ) - ); - - // Variable to subtract from total query time after template. - $queries_before_template_time_seconds = null; - - if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { - // WordPress database query time before template. - $server_timing->register_metric( - 'before-template-db-queries', +function perflab_register_default_server_timing_metrics() { + $calculate_before_template_metrics = function( $passthrough = null ) { + // WordPress execution prior to serving the template. + perflab_server_timing_register_metric( + 'before-template', array( - 'measure_callback' => function( $metric ) use ( &$queries_before_template_time_seconds ) { - add_filter( - 'template_include', - function( $passthrough ) use ( $metric, &$queries_before_template_time_seconds ) { - $queries_before_template_time_seconds = array_reduce( - $GLOBALS['wpdb']->queries, - function( $acc, $query ) { - return $acc + $query[1]; - }, - 0.0 - ); - $metric->set_value( $queries_before_template_time_seconds * 1000.0 ); - return $passthrough; - }, - PHP_INT_MAX - 1 - ); + 'measure_callback' => function( $metric ) { + // The 'timestart' global is set right at the beginning of WordPress execution. + $metric->set_value( ( microtime( true ) - $GLOBALS['timestart'] ) * 1000.0 ); }, 'access_cap' => 'exist', ) ); - } + if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { + // WordPress database query time before template. + perflab_server_timing_register_metric( + 'before-template-db-queries', + array( + 'measure_callback' => function( $metric ) { + // Store this value in a global to later subtract it from total query time after template. + $GLOBALS['perflab_query_time_before_template'] = array_reduce( + $GLOBALS['wpdb']->queries, + function( $acc, $query ) { + return $acc + $query[1]; + }, + 0.0 + ); + $metric->set_value( $GLOBALS['perflab_query_time_before_template'] * 1000.0 ); + }, + 'access_cap' => 'exist', + ) + ); + } + + return $passthrough; + }; + + // If output buffering is used, explicitly measure only the time before serving the template. + // Otherwise, the Server-Timing header will be sent before serving the template anyway. if ( perflab_server_timing_use_output_buffer() ) { - // WordPress execution while serving the template. - $server_timing->register_metric( - 'template', - array( - 'measure_callback' => function( $metric ) { - $start_time = null; + add_filter( 'template_include', $calculate_before_template_metrics, PHP_INT_MAX ); + } else { + add_action( 'perflab_server_timing_send_header', $calculate_before_template_metrics, PHP_INT_MAX ); + } - add_filter( - 'template_include', - function( $passthrough ) use ( &$start_time ) { - $start_time = microtime( true ); - return $passthrough; - }, - PHP_INT_MAX - 1 - ); - add_action( - 'shutdown', - function() use ( $metric, &$start_time ) { - if ( null === $start_time ) { - return; - } - $metric->set_value( ( microtime( true ) - $start_time ) * 1000.0 ); + // Template-related metrics can only be recorded if output buffering is used. + if ( perflab_server_timing_use_output_buffer() ) { + add_filter( + 'template_include', + function( $passthrough = null ) { + // WordPress execution while serving the template. + perflab_server_timing_register_metric( + 'template', + array( + 'measure_callback' => function( $metric ) { + $metric->measure_before(); + add_action( 'perflab_server_timing_send_header', array( $metric, 'measure_after' ), PHP_INT_MAX ); }, - // phpcs:ignore PHPCompatibility.Constants.NewConstants.php_int_minFound - defined( 'PHP_INT_MIN' ) ? PHP_INT_MIN : -1001 - ); - }, - 'access_cap' => 'exist', - ) + 'access_cap' => 'exist', + ) + ); + + return $passthrough; + }, + PHP_INT_MAX ); if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { - // WordPress database query time within template. - $server_timing->register_metric( - 'template-db-queries', - array( - 'measure_callback' => function( $metric ) use ( &$queries_before_template_time_seconds ) { - add_action( - 'shutdown', - function() use ( $metric, &$queries_before_template_time_seconds ) { - if ( null === $queries_before_template_time_seconds ) { - return; - } - $total_queries_time_seconds = array_reduce( + add_action( + 'perflab_server_timing_send_header', + function() { + // WordPress database query time within template. + perflab_server_timing_register_metric( + 'template-db-queries', + array( + 'measure_callback' => function( $metric ) { + $total_query_time = array_reduce( $GLOBALS['wpdb']->queries, function( $acc, $query ) { return $acc + $query[1]; }, 0.0 ); - $metric->set_value( ( $total_queries_time_seconds - $queries_before_template_time_seconds ) * 1000.0 ); + $metric->set_value( ( $total_query_time - $GLOBALS['perflab_query_time_before_template'] ) * 1000.0 ); }, - // phpcs:ignore PHPCompatibility.Constants.NewConstants.php_int_minFound - defined( 'PHP_INT_MIN' ) ? PHP_INT_MIN : -1001 - ); - }, - 'access_cap' => 'exist', - ) + 'access_cap' => 'exist', + ) + ); + }, + PHP_INT_MAX ); } } } -add_action( 'perflab_server_timing_init', 'perflab_register_default_server_timing_metrics' ); +add_action( 'plugins_loaded', 'perflab_register_default_server_timing_metrics' ); /** * Wraps a callback (e.g. for an action or filter) to be measured and included in the Server-Timing header. @@ -219,38 +174,33 @@ function( $acc, $query ) { * set to "exist". */ function perflab_wrap_server_timing( $callback, $metric_slug, $access_cap ) { - // Gain access to Perflab_Server_Timing_Metric instance. - $server_timing_metric = null; - add_action( - 'perflab_server_timing_init', - function( $server_timing ) use ( &$server_timing_metric, $metric_slug, $access_cap ) { - $server_timing->register_metric( - $metric_slug, - array( - 'measure_callback' => function( $metric ) use ( &$server_timing_metric ) { - $server_timing_metric = $metric; - }, - 'access_cap' => $access_cap, - ) - ); - } - ); + return function( ...$callback_args ) use ( $callback, $metric_slug, $access_cap ) { + // Gain access to Perflab_Server_Timing_Metric instance. + $server_timing_metric = null; + + perflab_server_timing_register_metric( + $metric_slug, + array( + 'measure_callback' => function( $metric ) use ( &$server_timing_metric ) { + $server_timing_metric = $metric; + }, + 'access_cap' => $access_cap, + ) + ); - return function( ...$callback_args ) use ( &$server_timing_metric, $callback ) { // If metric instance was not set, this metric should not be calculated. if ( null === $server_timing_metric ) { return call_user_func_array( $callback, $callback_args ); } - // Store start time (in microseconds). - $start_time = microtime( true ); + // Measure time before the callback. + $server_timing_metric->measure_before(); // Execute the callback. $result = call_user_func_array( $callback, $callback_args ); - // Calculate total time (in milliseconds) and set it for the metric. - $total_time = ( microtime( true ) - $start_time ) * 1000.0; - $server_timing_metric->set_value( $total_time ); + // Measure time after the callback and calculate total. + $server_timing_metric->measure_after(); // Return result (e.g. in case this is a filter callback). return $result; From 26cfc27ee7f88efb27320fbca45e0d20caebf45c Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Wed, 23 Nov 2022 15:26:42 -0800 Subject: [PATCH 07/40] Move logic to register default Server-Timing metrics to its own file. --- load.php | 1 + server-timing/defaults.php | 143 +++++++++++++++++++++++++++++++++++++ server-timing/load.php | 102 -------------------------- 3 files changed, 144 insertions(+), 102 deletions(-) create mode 100644 server-timing/defaults.php diff --git a/load.php b/load.php index 31d936e7b2..de6ba5d3d4 100644 --- a/load.php +++ b/load.php @@ -259,6 +259,7 @@ function perflab_load_active_and_valid_modules() { require_once PERFLAB_PLUGIN_DIR_PATH . 'server-timing/class-perflab-server-timing-metric.php'; require_once PERFLAB_PLUGIN_DIR_PATH . 'server-timing/class-perflab-server-timing.php'; require_once PERFLAB_PLUGIN_DIR_PATH . 'server-timing/load.php'; +require_once PERFLAB_PLUGIN_DIR_PATH . 'server-timing/defaults.php'; // Only load admin integration when in admin. if ( is_admin() ) { diff --git a/server-timing/defaults.php b/server-timing/defaults.php new file mode 100644 index 0000000000..3708892f96 --- /dev/null +++ b/server-timing/defaults.php @@ -0,0 +1,143 @@ + function( $metric ) { + // The 'timestart' global is set right at the beginning of WordPress execution. + $metric->set_value( ( microtime( true ) - $GLOBALS['timestart'] ) * 1000.0 ); + }, + 'access_cap' => 'exist', + ) + ); + + if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { + // WordPress database query time before template. + perflab_server_timing_register_metric( + 'before-template-db-queries', + array( + 'measure_callback' => function( $metric ) { + // Store this value in a global to later subtract it from total query time after template. + $GLOBALS['perflab_query_time_before_template'] = array_reduce( + $GLOBALS['wpdb']->queries, + function( $acc, $query ) { + return $acc + $query[1]; + }, + 0.0 + ); + $metric->set_value( $GLOBALS['perflab_query_time_before_template'] * 1000.0 ); + }, + 'access_cap' => 'exist', + ) + ); + } + }; + + // If output buffering is used, explicitly measure only the time before serving the template. + // Otherwise, the Server-Timing header will be sent before serving the template anyway. + // We need to check for output buffer usage in the callback so that e.g. plugins and theme can + // modify the value prior to the check. + add_filter( + 'template_include', + function( $passthrough ) use ( $calculate_before_template_metrics ) { + if ( perflab_server_timing_use_output_buffer() ) { + $calculate_before_template_metrics(); + } + return $passthrough; + }, + PHP_INT_MAX + ); + add_action( + 'perflab_server_timing_send_header', + function() use ( $calculate_before_template_metrics ) { + if ( ! perflab_server_timing_use_output_buffer() ) { + $calculate_before_template_metrics(); + } + }, + PHP_INT_MAX + ); +} +perflab_register_default_server_timing_before_template_metrics(); + +/** + * Registers the default Server-Timing metrics for while rendering the template. + * + * These metrics should be registered at a later point, e.g. the 'wp_loaded' action. + * They will only be registered if the Server-Timing API is configured to use an + * output buffer for the site's template. + * + * @since n.e.x.t + */ +function perflab_register_default_server_timing_template_metrics() { + // Template-related metrics can only be recorded if output buffering is used. + if ( ! perflab_server_timing_use_output_buffer() ) { + return; + } + + add_filter( + 'template_include', + function( $passthrough = null ) { + // WordPress execution while serving the template. + perflab_server_timing_register_metric( + 'template', + array( + 'measure_callback' => function( $metric ) { + $metric->measure_before(); + add_action( 'perflab_server_timing_send_header', array( $metric, 'measure_after' ), PHP_INT_MAX ); + }, + 'access_cap' => 'exist', + ) + ); + + return $passthrough; + }, + PHP_INT_MAX + ); + + if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { + add_action( + 'perflab_server_timing_send_header', + function() { + // WordPress database query time within template. + perflab_server_timing_register_metric( + 'template-db-queries', + array( + 'measure_callback' => function( $metric ) { + // This global should always be set when this is called, but check just in case. + if ( ! isset( $GLOBALS['perflab_query_time_before_template'] ) ) { + return; + } + $total_query_time = array_reduce( + $GLOBALS['wpdb']->queries, + function( $acc, $query ) { + return $acc + $query[1]; + }, + 0.0 + ); + $metric->set_value( ( $total_query_time - $GLOBALS['perflab_query_time_before_template'] ) * 1000.0 ); + }, + 'access_cap' => 'exist', + ) + ); + }, + PHP_INT_MAX + ); + } +} +add_action( 'wp_loaded', 'perflab_register_default_server_timing_template_metrics' ); diff --git a/server-timing/load.php b/server-timing/load.php index fb5a798172..4e351a9756 100644 --- a/server-timing/load.php +++ b/server-timing/load.php @@ -61,108 +61,6 @@ function perflab_server_timing_use_output_buffer() { return perflab_server_timing()->use_output_buffer(); } -/** - * Registers the default Server-Timing metrics. - * - * @since n.e.x.t - */ -function perflab_register_default_server_timing_metrics() { - $calculate_before_template_metrics = function( $passthrough = null ) { - // WordPress execution prior to serving the template. - perflab_server_timing_register_metric( - 'before-template', - array( - 'measure_callback' => function( $metric ) { - // The 'timestart' global is set right at the beginning of WordPress execution. - $metric->set_value( ( microtime( true ) - $GLOBALS['timestart'] ) * 1000.0 ); - }, - 'access_cap' => 'exist', - ) - ); - - if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { - // WordPress database query time before template. - perflab_server_timing_register_metric( - 'before-template-db-queries', - array( - 'measure_callback' => function( $metric ) { - // Store this value in a global to later subtract it from total query time after template. - $GLOBALS['perflab_query_time_before_template'] = array_reduce( - $GLOBALS['wpdb']->queries, - function( $acc, $query ) { - return $acc + $query[1]; - }, - 0.0 - ); - $metric->set_value( $GLOBALS['perflab_query_time_before_template'] * 1000.0 ); - }, - 'access_cap' => 'exist', - ) - ); - } - - return $passthrough; - }; - - // If output buffering is used, explicitly measure only the time before serving the template. - // Otherwise, the Server-Timing header will be sent before serving the template anyway. - if ( perflab_server_timing_use_output_buffer() ) { - add_filter( 'template_include', $calculate_before_template_metrics, PHP_INT_MAX ); - } else { - add_action( 'perflab_server_timing_send_header', $calculate_before_template_metrics, PHP_INT_MAX ); - } - - // Template-related metrics can only be recorded if output buffering is used. - if ( perflab_server_timing_use_output_buffer() ) { - add_filter( - 'template_include', - function( $passthrough = null ) { - // WordPress execution while serving the template. - perflab_server_timing_register_metric( - 'template', - array( - 'measure_callback' => function( $metric ) { - $metric->measure_before(); - add_action( 'perflab_server_timing_send_header', array( $metric, 'measure_after' ), PHP_INT_MAX ); - }, - 'access_cap' => 'exist', - ) - ); - - return $passthrough; - }, - PHP_INT_MAX - ); - - if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { - add_action( - 'perflab_server_timing_send_header', - function() { - // WordPress database query time within template. - perflab_server_timing_register_metric( - 'template-db-queries', - array( - 'measure_callback' => function( $metric ) { - $total_query_time = array_reduce( - $GLOBALS['wpdb']->queries, - function( $acc, $query ) { - return $acc + $query[1]; - }, - 0.0 - ); - $metric->set_value( ( $total_query_time - $GLOBALS['perflab_query_time_before_template'] ) * 1000.0 ); - }, - 'access_cap' => 'exist', - ) - ); - }, - PHP_INT_MAX - ); - } - } -} -add_action( 'plugins_loaded', 'perflab_register_default_server_timing_metrics' ); - /** * Wraps a callback (e.g. for an action or filter) to be measured and included in the Server-Timing header. * From 096bb87fc14fbf68ff8f4df584369d8e46091104 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Wed, 23 Nov 2022 16:16:17 -0800 Subject: [PATCH 08/40] Place object-cache.php drop-in to load Server-Timing API early enough for additional key measurements. --- load.php | 62 +++++++++++++++++++++++++++++++--- server-timing/object-cache.php | 53 +++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 server-timing/object-cache.php diff --git a/load.php b/load.php index de6ba5d3d4..feac2a66d6 100644 --- a/load.php +++ b/load.php @@ -21,6 +21,16 @@ define( 'PERFLAB_MODULES_SETTING', 'perflab_modules_settings' ); define( 'PERFLAB_MODULES_SCREEN', 'perflab-modules' ); +// If the constant isn't defined yet, it means the Performance Lab object cache file is not loaded. +if ( ! defined( 'PERFLAB_OBJECT_CACHE_DROPIN_VERSION' ) ) { + define( 'PERFLAB_OBJECT_CACHE_DROPIN_VERSION', false ); +} + +require_once PERFLAB_PLUGIN_DIR_PATH . 'server-timing/class-perflab-server-timing-metric.php'; +require_once PERFLAB_PLUGIN_DIR_PATH . 'server-timing/class-perflab-server-timing.php'; +require_once PERFLAB_PLUGIN_DIR_PATH . 'server-timing/load.php'; +require_once PERFLAB_PLUGIN_DIR_PATH . 'server-timing/defaults.php'; + /** * Registers the performance modules setting. * @@ -253,13 +263,55 @@ function perflab_load_active_and_valid_modules() { require_once PERFLAB_PLUGIN_DIR_PATH . 'modules/' . $module . '/load.php'; } } - perflab_load_active_and_valid_modules(); -require_once PERFLAB_PLUGIN_DIR_PATH . 'server-timing/class-perflab-server-timing-metric.php'; -require_once PERFLAB_PLUGIN_DIR_PATH . 'server-timing/class-perflab-server-timing.php'; -require_once PERFLAB_PLUGIN_DIR_PATH . 'server-timing/load.php'; -require_once PERFLAB_PLUGIN_DIR_PATH . 'server-timing/defaults.php'; +/** + * Places the Performance Lab's object cache drop-in in the drop-ins folder. + * + * This only runs in WP Admin to not have any potential performance impact on + * the frontend. + * + * This function will short-circuit if the constant + * 'PERFLAB_DISABLE_OBJECT_CACHE_DROPIN' is set as true. + * + * @since n.e.x.t + */ +function perflab_maybe_set_object_cache_dropin() { + global $wp_filesystem; + + // Bail if disabled via constant. + if ( defined( 'PERFLAB_DISABLE_OBJECT_CACHE_DROPIN' ) && PERFLAB_DISABLE_OBJECT_CACHE_DROPIN ) { + return; + } + + // Bail if already placed. + if ( PERFLAB_OBJECT_CACHE_DROPIN_VERSION ) { + return; + } + + // Bail if already attempted before timeout has been completed. + // This is present in case placing the file fails for some reason, to avoid + // excessively retrying to place it on every request. + $timeout = get_transient( 'perflab_set_object_cache_dropin' ); + if ( false !== $timeout ) { + return; + } + + if ( $wp_filesystem || WP_Filesystem() ) { + // If there is an actual object-cache.php file, rename it. + // The Performance Lab object-cache.php will still load it, so the + // behavior does not change. + if ( $wp_filesystem->exists( WP_CONTENT_DIR . '/object-cache.php' ) ) { + $wp_filesystem->move( WP_CONTENT_DIR . '/object-cache.php', WP_CONTENT_DIR . '/object-cache-orig.php' ); + } + + $wp_filesystem->copy( PERFLAB_PLUGIN_DIR_PATH . 'server-timing/object-cache.php', WP_CONTENT_DIR . '/object-cache.php' ); + } + + // Set timeout of 1 hour before retrying again (only in case of failure). + set_transient( 'perflab_set_object_cache_dropin', true, HOUR_IN_SECONDS ); +} +add_action( 'admin_init', 'perflab_maybe_set_object_cache_dropin' ); // Only load admin integration when in admin. if ( is_admin() ) { diff --git a/server-timing/object-cache.php b/server-timing/object-cache.php new file mode 100644 index 0000000000..b0ff07d5b3 --- /dev/null +++ b/server-timing/object-cache.php @@ -0,0 +1,53 @@ + Date: Wed, 23 Nov 2022 16:21:41 -0800 Subject: [PATCH 09/40] Add alloptions query to metrics to cover, in support of #526. --- server-timing/defaults.php | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/server-timing/defaults.php b/server-timing/defaults.php index 3708892f96..988cfb1816 100644 --- a/server-timing/defaults.php +++ b/server-timing/defaults.php @@ -72,6 +72,35 @@ function() use ( $calculate_before_template_metrics ) { }, PHP_INT_MAX ); + + // Measure duration of autoloaded options query. + // Requires the Performance Lab object-cache.php drop-in to be present in order to work. + add_filter( + 'query', + function( $query ) { + global $wpdb; + if ( "SELECT option_name, option_value FROM $wpdb->options WHERE autoload = 'yes'" !== $query ) { + return $query; + } + perflab_server_timing_register_metric( + 'load-alloptions-query', + array( + 'measure_callback' => function( $metric ) { + $metric->measure_before(); + add_filter( + 'pre_cache_alloptions', + function( $passthrough ) use ( $metric ) { + $metric->measure_after(); + return $passthrough; + } + ); + }, + 'access_cap' => 'exist', + ) + ); + return $query; + } + ); } perflab_register_default_server_timing_before_template_metrics(); From 94304921e4e48af89f5961470a9c8f8659a44b36 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Fri, 2 Dec 2022 09:38:54 -0800 Subject: [PATCH 10/40] Add isset and is_array check to account for odd database implementations. --- server-timing/defaults.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/server-timing/defaults.php b/server-timing/defaults.php index 988cfb1816..1af819b600 100644 --- a/server-timing/defaults.php +++ b/server-timing/defaults.php @@ -33,6 +33,11 @@ function perflab_register_default_server_timing_before_template_metrics() { 'before-template-db-queries', array( 'measure_callback' => function( $metric ) { + // This should never happen, but some odd database implementations may be doing it wrong. + if ( ! isset( $GLOBALS['wpdb']->queries ) || ! is_array( $GLOBALS['wpdb']->queries ) ) { + return; + } + // Store this value in a global to later subtract it from total query time after template. $GLOBALS['perflab_query_time_before_template'] = array_reduce( $GLOBALS['wpdb']->queries, @@ -148,10 +153,11 @@ function() { 'template-db-queries', array( 'measure_callback' => function( $metric ) { - // This global should always be set when this is called, but check just in case. + // This global should typically be set when this is called, but check just in case. if ( ! isset( $GLOBALS['perflab_query_time_before_template'] ) ) { return; } + $total_query_time = array_reduce( $GLOBALS['wpdb']->queries, function( $acc, $query ) { From fb5150e767d123483dfef351f862d9c921be8017 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Thu, 8 Dec 2022 13:44:05 -0800 Subject: [PATCH 11/40] Consider either performance-lab or performance as plugin directory name, since both of them are commonly used. --- server-timing/object-cache.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server-timing/object-cache.php b/server-timing/object-cache.php index b0ff07d5b3..a18b26ae81 100644 --- a/server-timing/object-cache.php +++ b/server-timing/object-cache.php @@ -35,8 +35,13 @@ function perflab_load_server_timing_api_from_dropin() { return; } - $plugin_dir = ( defined( 'WP_PLUGIN_DIR' ) ? WP_PLUGIN_DIR : WP_CONTENT_DIR . '/plugins' ) . '/performance-lab/'; + $plugins_dir = defined( 'WP_PLUGIN_DIR' ) ? WP_PLUGIN_DIR : WP_CONTENT_DIR . '/plugins'; + $plugin_dir = $plugins_dir . '/performance-lab/'; if ( ! file_exists( $plugin_dir . 'server-timing/load.php' ) ) { + $plugin_dir = $plugins_dir . '/performance/'; + if ( ! file_exists( $plugin_dir . 'server-timing/load.php' ) ) { + return; + } return; } From 2c48a19983f265508e49c925e6913e20b0ca187a Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Thu, 8 Dec 2022 13:48:30 -0800 Subject: [PATCH 12/40] Follow up fix to last commit. --- server-timing/object-cache.php | 1 - 1 file changed, 1 deletion(-) diff --git a/server-timing/object-cache.php b/server-timing/object-cache.php index a18b26ae81..be973a18a1 100644 --- a/server-timing/object-cache.php +++ b/server-timing/object-cache.php @@ -42,7 +42,6 @@ function perflab_load_server_timing_api_from_dropin() { if ( ! file_exists( $plugin_dir . 'server-timing/load.php' ) ) { return; } - return; } require_once $plugin_dir . 'server-timing/class-perflab-server-timing-metric.php'; From 2ff02a763e5c8a397cc9a48008e43804749bf1fd Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Thu, 8 Dec 2022 17:12:42 -0800 Subject: [PATCH 13/40] Add missing return doc. --- server-timing/load.php | 1 + 1 file changed, 1 insertion(+) diff --git a/server-timing/load.php b/server-timing/load.php index 4e351a9756..22b06b3d9f 100644 --- a/server-timing/load.php +++ b/server-timing/load.php @@ -70,6 +70,7 @@ function perflab_server_timing_use_output_buffer() { * @param string $metric_slug The metric slug to use within the Server-Timing header. * @param string $access_cap Capability required to view the metric. If this is a public metric, this needs to be * set to "exist". + * @return callable Callback function that will run $callback and measure its execution time once called. */ function perflab_wrap_server_timing( $callback, $metric_slug, $access_cap ) { return function( ...$callback_args ) use ( $callback, $metric_slug, $access_cap ) { From e501b5d6eafbe54dd1ca920baa46ba7fdac873e6 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Thu, 8 Dec 2022 17:21:32 -0800 Subject: [PATCH 14/40] Introduce has_registered_metric method. --- server-timing/class-perflab-server-timing.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/server-timing/class-perflab-server-timing.php b/server-timing/class-perflab-server-timing.php index 09c12914f1..be91e2fea7 100644 --- a/server-timing/class-perflab-server-timing.php +++ b/server-timing/class-perflab-server-timing.php @@ -98,6 +98,18 @@ public function register_metric( $metric_slug, array $args ) { call_user_func( $args['measure_callback'], $this->registered_metrics[ $metric_slug ] ); } + /** + * Checks whether the given metric has been registered. + * + * @since n.e.x.t + * + * @param string $metric_slug The metric slug. + * @return bool True if registered, false otherwise. + */ + public function has_registered_metric( $metric_slug ) { + return isset( $this->registered_metrics[ $metric_slug ] ) && isset( $this->registered_metrics_data[ $metric_slug ] ); + } + /** * Outputs the Server-Timing header. * From 8115d8252eb8c6ca81f5489f77bbf5ee8baa8ff4 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Thu, 8 Dec 2022 17:41:51 -0800 Subject: [PATCH 15/40] Add tests for Server-Timing API global functions. --- tests/server-timing/load-tests.php | 56 ++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 tests/server-timing/load-tests.php diff --git a/tests/server-timing/load-tests.php b/tests/server-timing/load-tests.php new file mode 100644 index 0000000000..da8ab9a5e6 --- /dev/null +++ b/tests/server-timing/load-tests.php @@ -0,0 +1,56 @@ +assertInstanceOf( Perflab_Server_Timing::class, $server_timing ); + $this->assertSame( PHP_INT_MAX, has_filter( 'template_include', array( $server_timing, 'on_template_include' ) ), 'template_include filter not added' ); + + $server_timing2 = perflab_server_timing(); + $this->assertSame( $server_timing, $server_timing2, 'Different instance returned' ); + } + + public function test_perflab_server_timing_register_metric() { + $this->assertFalse( perflab_server_timing()->has_registered_metric( 'test-metric' ) ); + + perflab_server_timing_register_metric( + 'test-metric', + array( + 'measure_callback' => function( $metric ) { + $metric->set_value( 100 ); + }, + 'access_cap' => 'exist', + ) + ); + $this->assertTrue( perflab_server_timing()->has_registered_metric( 'test-metric' ) ); + } + + public function test_perflab_server_timing_use_output_buffer() { + $this->assertFalse( perflab_server_timing_use_output_buffer() ); + + add_filter( 'perflab_server_timing_use_output_buffer', '__return_true' ); + $this->assertTrue( perflab_server_timing_use_output_buffer() ); + } + + public function test_perflab_wrap_server_timing() { + $cb = function() { + return 123; + }; + + $wrapped = perflab_wrap_server_timing( $cb, 'wrapped-cb-without-capability', 'manage_options' ); + $this->assertSame( 123, $wrapped(), 'Wrapped callback without capability did not return expected value' ); + $this->assertTrue( perflab_server_timing()->has_registered_metric( 'wrapped-cb-without-capability' ), 'Wrapped callback metric should be registered despite lack of capability' ); + $this->assertStringNotContainsString( 'wrapped-cb-without-capability', perflab_server_timing()->get_header_value(), 'Wrapped callback was measured despite lack of capability' ); + + $wrapped = perflab_wrap_server_timing( $cb, 'wrapped-cb-with-capability', 'exist' ); + $this->assertSame( 123, $wrapped(), 'Wrapped callback with capability did not return expected value' ); + $this->assertTrue( perflab_server_timing()->has_registered_metric( 'wrapped-cb-with-capability' ), 'Wrapped callback metric should be registered' ); + $this->assertStringContainsString( 'wrapped-cb-with-capability', perflab_server_timing()->get_header_value(), 'Wrapped callback was not measured despite having necessary capability' ); + } +} From 5f96a0ee92a3fd8eb83104da4960ef3e52f16e4f Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Thu, 8 Dec 2022 18:04:52 -0800 Subject: [PATCH 16/40] Remove overly strict check that is not feasible for unit testing. --- server-timing/class-perflab-server-timing-metric.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/server-timing/class-perflab-server-timing-metric.php b/server-timing/class-perflab-server-timing-metric.php index ff3e1e7c19..c8587c5b60 100644 --- a/server-timing/class-perflab-server-timing-metric.php +++ b/server-timing/class-perflab-server-timing-metric.php @@ -80,14 +80,6 @@ public function set_value( $value ) { return; } - if ( headers_sent() ) { - _doing_it_wrong( - __METHOD__, - __( 'Metrics can only be measured before headers have been sent.', 'performance-lab' ), - '' - ); - } - $this->value = $value; } From a2afcb998470d8af395284ee96ba7c5728d6866b Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Fri, 9 Dec 2022 14:04:05 -0800 Subject: [PATCH 17/40] Include check for extra safety. --- server-timing/defaults.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server-timing/defaults.php b/server-timing/defaults.php index 1af819b600..283c2aca0f 100644 --- a/server-timing/defaults.php +++ b/server-timing/defaults.php @@ -158,6 +158,11 @@ function() { return; } + // This should never happen, but some odd database implementations may be doing it wrong. + if ( ! isset( $GLOBALS['wpdb']->queries ) || ! is_array( $GLOBALS['wpdb']->queries ) ) { + return; + } + $total_query_time = array_reduce( $GLOBALS['wpdb']->queries, function( $acc, $query ) { From 9d826f5f3c8697a033385fc3d3f428c2d62b0143 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Fri, 9 Dec 2022 14:42:20 -0800 Subject: [PATCH 18/40] Prevent duplicate registration and warn about it, fix potential issue with load-alloptions-query default metric. --- server-timing/class-perflab-server-timing.php | 10 ++++++++++ server-timing/defaults.php | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/server-timing/class-perflab-server-timing.php b/server-timing/class-perflab-server-timing.php index be91e2fea7..af2b765bfe 100644 --- a/server-timing/class-perflab-server-timing.php +++ b/server-timing/class-perflab-server-timing.php @@ -49,6 +49,16 @@ class Perflab_Server_Timing { * } */ public function register_metric( $metric_slug, array $args ) { + if ( isset( $this->registered_metrics[ $metric_slug ] ) ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: metric slug */ + sprintf( __( 'A metric with the slug %s is already registered.', 'performance-lab' ), $metric_slug ), + '' + ); + return; + } + if ( did_action( 'perflab_server_timing_send_header' ) && ! doing_action( 'perflab_server_timing_send_header' ) ) { _doing_it_wrong( __METHOD__, diff --git a/server-timing/defaults.php b/server-timing/defaults.php index 283c2aca0f..c5d22d10d9 100644 --- a/server-timing/defaults.php +++ b/server-timing/defaults.php @@ -87,6 +87,10 @@ function( $query ) { if ( "SELECT option_name, option_value FROM $wpdb->options WHERE autoload = 'yes'" !== $query ) { return $query; } + // In case the autoloaded options query is run again, prevent re-registering it and do not measure again. + if ( perflab_server_timing()->has_registered_metric( 'load-alloptions-query' ) ) { + return $query; + } perflab_server_timing_register_metric( 'load-alloptions-query', array( From 33cf71888ac452a2fd7e752a707751294b88fc5e Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Fri, 9 Dec 2022 14:43:36 -0800 Subject: [PATCH 19/40] Fix perflab_wrap_server_timing() to consider potential mmultiple calls to the wrapped callback. --- server-timing/load.php | 22 +++++++++++++--------- tests/server-timing/load-tests.php | 5 +++++ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/server-timing/load.php b/server-timing/load.php index 22b06b3d9f..4c1bf1b62f 100644 --- a/server-timing/load.php +++ b/server-timing/load.php @@ -77,15 +77,19 @@ function perflab_wrap_server_timing( $callback, $metric_slug, $access_cap ) { // Gain access to Perflab_Server_Timing_Metric instance. $server_timing_metric = null; - perflab_server_timing_register_metric( - $metric_slug, - array( - 'measure_callback' => function( $metric ) use ( &$server_timing_metric ) { - $server_timing_metric = $metric; - }, - 'access_cap' => $access_cap, - ) - ); + // Only register the metric the first time the function is called. + // For now, this also means only the first function call is measured. + if ( ! perflab_server_timing()->has_registered_metric( $metric_slug ) ) { + perflab_server_timing_register_metric( + $metric_slug, + array( + 'measure_callback' => function( $metric ) use ( &$server_timing_metric ) { + $server_timing_metric = $metric; + }, + 'access_cap' => $access_cap, + ) + ); + } // If metric instance was not set, this metric should not be calculated. if ( null === $server_timing_metric ) { diff --git a/tests/server-timing/load-tests.php b/tests/server-timing/load-tests.php index da8ab9a5e6..592252e4d7 100644 --- a/tests/server-timing/load-tests.php +++ b/tests/server-timing/load-tests.php @@ -5,6 +5,9 @@ * @package performance-lab */ +/** + * @group server-timing + */ class Server_Timing_Load_Tests extends WP_UnitTestCase { public function test_perflab_server_timing() { @@ -52,5 +55,7 @@ public function test_perflab_wrap_server_timing() { $this->assertSame( 123, $wrapped(), 'Wrapped callback with capability did not return expected value' ); $this->assertTrue( perflab_server_timing()->has_registered_metric( 'wrapped-cb-with-capability' ), 'Wrapped callback metric should be registered' ); $this->assertStringContainsString( 'wrapped-cb-with-capability', perflab_server_timing()->get_header_value(), 'Wrapped callback was not measured despite having necessary capability' ); + + $this->assertSame( 123, $wrapped(), 'Calling wrapped callback multiple times should not result in warning' ); } } From 44fd9e5835e323599f6b8c2d1451cdf17e9ea7ac Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Fri, 9 Dec 2022 14:49:11 -0800 Subject: [PATCH 20/40] Reintroduce revised condition to warn about measuring that happens too late. --- server-timing/class-perflab-server-timing-metric.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/server-timing/class-perflab-server-timing-metric.php b/server-timing/class-perflab-server-timing-metric.php index c8587c5b60..3de3881361 100644 --- a/server-timing/class-perflab-server-timing-metric.php +++ b/server-timing/class-perflab-server-timing-metric.php @@ -80,6 +80,16 @@ public function set_value( $value ) { return; } + if ( did_action( 'perflab_server_timing_send_header' ) && ! doing_action( 'perflab_server_timing_send_header' ) ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: WordPress action name */ + sprintf( __( 'The method must be called before or during the %s action.', 'performance-lab' ), 'perflab_server_timing_send_header' ), + '' + ); + return; + } + $this->value = $value; } From 06dd8f07ecec7a148af4ce3c5c28d0a093061e11 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Fri, 9 Dec 2022 15:36:19 -0800 Subject: [PATCH 21/40] Add test coverage for Perflab_Server_Timing class. --- .../perflab-server-timing-tests.php | 196 ++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 tests/server-timing/perflab-server-timing-tests.php diff --git a/tests/server-timing/perflab-server-timing-tests.php b/tests/server-timing/perflab-server-timing-tests.php new file mode 100644 index 0000000000..0c6969eeca --- /dev/null +++ b/tests/server-timing/perflab-server-timing-tests.php @@ -0,0 +1,196 @@ + function() {}, + 'access_cap' => 'exist', + ); + + self::$admin_id = $factory->user->create( array( 'role' => 'administrator' ) ); + } + + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_id ); + } + + public function setUp() { + parent::setUp(); + $this->server_timing = new Perflab_Server_Timing(); + } + + public function test_register_metric_stores_metrics_and_runs_measure_callback() { + $called = false; + $this->server_timing->register_metric( + 'test-metric', + array( + 'measure_callback' => function() use ( &$called ) { + $called = true; + }, + 'access_cap' => 'exist', + ) + ); + + $this->assertTrue( $this->server_timing->has_registered_metric( 'test-metric' ), 'Metric not registered' ); + $this->assertTrue( $called, 'Measure callback not run' ); + } + + public function test_register_metric_runs_measure_callback_based_on_access_cap() { + $called = false; + $args = array( + 'measure_callback' => function() use ( &$called ) { + $called = true; + }, + 'access_cap' => 'manage_options', // Admin capability. + ); + + $this->server_timing->register_metric( 'test-metric', $args ); + + $this->assertTrue( $this->server_timing->has_registered_metric( 'test-metric' ), 'Metric without cap should still be registered' ); + $this->assertFalse( $called, 'Measure callback without cap must not be run' ); + + wp_set_current_user( self::$admin_id ); + $this->server_timing->register_metric( 'test-metric-2', $args ); + + $this->assertTrue( $this->server_timing->has_registered_metric( 'test-metric-2' ), 'Metric with cap should be registered' ); + $this->assertTrue( $called, 'Measure callback with cap should be run' ); + } + + public function test_register_metric_prevents_duplicates() { + $this->setExpectedIncorrectUsage( Perflab_Server_Timing::class . '::register_metric' ); + + $this->server_timing->register_metric( 'duplicate-metric', self::$dummy_args ); + $this->server_timing->register_metric( 'duplicate-metric', self::$dummy_args ); + + $this->assertTrue( $this->server_timing->has_registered_metric( 'duplicate-metric' ) ); + } + + public function test_register_metric_prevents_late_registration() { + $this->setExpectedIncorrectUsage( Perflab_Server_Timing::class . '::register_metric' ); + + $this->server_timing->register_metric( 'registered-in-time', self::$dummy_args ); + do_action( 'perflab_server_timing_send_header' ); + $this->server_timing->register_metric( 'registered-too-late', self::$dummy_args ); + + $this->assertTrue( $this->server_timing->has_registered_metric( 'registered-in-time' ), 'Metric registered in time should be stored' ); + $this->assertFalse( $this->server_timing->has_registered_metric( 'registered-too-late' ), 'Metric registered too late should not be stored' ); + } + + public function test_register_metric_requires_measure_callback() { + $this->setExpectedIncorrectUsage( Perflab_Server_Timing::class . '::register_metric' ); + + $this->server_timing->register_metric( + 'metric-without-measure-callback', + array( 'access_cap' => 'exist' ) + ); + + $this->assertFalse( $this->server_timing->has_registered_metric( 'metric-without-measure-callback' ) ); + } + + public function test_register_metric_requires_access_cap() { + $this->setExpectedIncorrectUsage( Perflab_Server_Timing::class . '::register_metric' ); + + $this->server_timing->register_metric( + 'metric-without-access-cap', + array( 'measure_callback' => function() {} ) + ); + + $this->assertFalse( $this->server_timing->has_registered_metric( 'metric-without-access-cap' ) ); + } + + public function test_has_registered_metric() { + $this->assertFalse( $this->server_timing->has_registered_metric( 'metric-to-check-for' ), 'Metric should not be available before registration' ); + + $this->server_timing->register_metric( 'metric-to-check-for', self::$dummy_args ); + $this->assertTrue( $this->server_timing->has_registered_metric( 'metric-to-check-for' ), 'Metric should be available after registration' ); + } + + /** + * @dataProvider data_get_header_value + */ + public function test_get_header_value( $expected, $metrics ) { + foreach ( $metrics as $metric_slug => $args ) { + $this->server_timing->register_metric( $metric_slug, $args ); + } + $this->assertSame( $expected, $this->server_timing->get_header_value() ); + } + + public function data_get_header_value() { + $measure_42 = function( $metric ) { + $metric->set_value( 42 ); + }; + $measure_300 = function( $metric ) { + $metric->set_value( 300 ); + }; + $measure_12point345 = function( $metric ) { + $metric->set_value( 12.345 ); + }; + + return array( + 'single metric' => array( + 'wp-integer;dur=300', + array( + 'integer' => array( + 'measure_callback' => $measure_300, + 'access_cap' => 'exist', + ), + ), + ), + 'multiple metrics' => array( + 'wp-integer;dur=300, wp-float;dur=12.35, wp-bttf;dur=42, wp-bttf2;dur=42', + array( + 'integer' => array( + 'measure_callback' => $measure_300, + 'access_cap' => 'exist', + ), + 'float' => array( + 'measure_callback' => $measure_12point345, + 'access_cap' => 'exist', + ), + 'bttf' => array( + 'measure_callback' => $measure_42, + 'access_cap' => 'exist', + ), + 'bttf2' => array( + 'measure_callback' => $measure_42, + 'access_cap' => 'exist', + ), + ), + ), + 'metrics with partially missing cap' => array( + 'wp-with-cap;dur=42', + array( + 'without-cap' => array( + 'measure_callback' => $measure_42, + 'access_cap' => 'cap_that_nobody_has', + ), + 'with-cap' => array( + 'measure_callback' => $measure_42, + 'access_cap' => 'exist', + ), + ), + ), + ); + } + + public function test_use_output_buffer() { + $this->assertFalse( $this->server_timing->use_output_buffer() ); + + add_filter( 'perflab_server_timing_use_output_buffer', '__return_true' ); + $this->assertTrue( $this->server_timing->use_output_buffer() ); + } +} From 946fdb9510dfeef3ddc4b4aebdbfbbdd2067495c Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Fri, 9 Dec 2022 15:53:19 -0800 Subject: [PATCH 22/40] Add test coverage for Perflab_Server_Timing_Metric class. --- .../perflab-server-timing-metric-tests.php | 72 +++++++++++++++++++ .../perflab-server-timing-tests.php | 2 +- 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 tests/server-timing/perflab-server-timing-metric-tests.php diff --git a/tests/server-timing/perflab-server-timing-metric-tests.php b/tests/server-timing/perflab-server-timing-metric-tests.php new file mode 100644 index 0000000000..fdad15d3e7 --- /dev/null +++ b/tests/server-timing/perflab-server-timing-metric-tests.php @@ -0,0 +1,72 @@ +metric = new Perflab_Server_Timing_Metric( 'test-metric' ); + } + + public function test_get_slug() { + $this->assertSame( 'test-metric', $this->metric->get_slug() ); + } + + public function test_set_value_with_integer() { + $this->metric->set_value( 123 ); + $this->assertSame( 123, $this->metric->get_value() ); + } + + public function test_set_value_with_float() { + $this->metric->set_value( 123.4567 ); + $this->assertSame( 123.4567, $this->metric->get_value() ); + } + + public function test_set_value_requires_integer_or_float() { + $this->setExpectedIncorrectUsage( Perflab_Server_Timing_Metric::class . '::set_value' ); + + $this->metric->set_value( 'not-a-number' ); + $this->assertNull( $this->metric->get_value() ); + } + + public function test_set_value_prevents_late_measurement() { + $this->setExpectedIncorrectUsage( Perflab_Server_Timing_Metric::class . '::set_value' ); + + $this->metric->set_value( 2 ); + do_action( 'perflab_server_timing_send_header' ); + $this->metric->set_value( 3 ); + + $this->assertSame( 2, $this->metric->get_value() ); + } + + public function test_get_value() { + $this->metric->set_value( 86.42 ); + $this->assertSame( 86.42, $this->metric->get_value() ); + } + + public function test_measure_before_and_after_correctly() { + $this->metric->measure_before(); + sleep( 1 ); + $this->metric->measure_after(); + + // Loose float comparison with 100ms delta, since measurement won't be exactly 1000ms. + $this->assertEquals( 1000.0, $this->metric->get_value(), '', 100.0 ); + } + + public function test_measure_after_without_before() { + $this->setExpectedIncorrectUsage( Perflab_Server_Timing_Metric::class . '::measure_after' ); + + $this->metric->measure_after(); + + $this->assertNull( $this->metric->get_value() ); + } +} diff --git a/tests/server-timing/perflab-server-timing-tests.php b/tests/server-timing/perflab-server-timing-tests.php index 0c6969eeca..fb78ef9def 100644 --- a/tests/server-timing/perflab-server-timing-tests.php +++ b/tests/server-timing/perflab-server-timing-tests.php @@ -1,6 +1,6 @@ Date: Fri, 9 Dec 2022 16:07:26 -0800 Subject: [PATCH 23/40] Add at least basic test for placing object-cache.php drop-in. --- tests/load-tests.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/load-tests.php b/tests/load-tests.php index 4a521e70f9..2035880739 100644 --- a/tests/load-tests.php +++ b/tests/load-tests.php @@ -226,4 +226,16 @@ function( $module_settings, $module_dir ) { array() ); } + + public function test_perflab_maybe_set_object_cache_dropin() { + $this->assertFalse( file_exists( WP_CONTENT_DIR . '/object-cache.php' ) ); + $this->assertFalse( PERFLAB_OBJECT_CACHE_DROPIN_VERSION ); + + perflab_maybe_set_object_cache_dropin(); + $this->assertTrue( file_exists( WP_CONTENT_DIR . '/object-cache.php' ) ); + + // Clean up. This is okay to be run after the assertion since otherwise + // the file does not exist anyway. + $GLOBALS['wp_filesystem']->delete( WP_CONTENT_DIR . '/object-cache.php' ); + } } From e88410e806296b257468fe705f0440e70ab71c8b Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Mon, 12 Dec 2022 10:06:00 -0800 Subject: [PATCH 24/40] Add comment about global used. Co-authored-by: Mukesh Panchal --- load.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/load.php b/load.php index feac2a66d6..f30b2e4b7f 100644 --- a/load.php +++ b/load.php @@ -275,6 +275,8 @@ function perflab_load_active_and_valid_modules() { * 'PERFLAB_DISABLE_OBJECT_CACHE_DROPIN' is set as true. * * @since n.e.x.t + * + * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. */ function perflab_maybe_set_object_cache_dropin() { global $wp_filesystem; From e9eba75c8315cadb5693928e9092ae57298d1a8a Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Mon, 12 Dec 2022 10:10:16 -0800 Subject: [PATCH 25/40] Rename object-cache.php to object-cache.copy.php to indicate its usage better. --- load.php | 2 +- server-timing/{object-cache.php => object-cache.copy.php} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename server-timing/{object-cache.php => object-cache.copy.php} (100%) diff --git a/load.php b/load.php index fa60a8f6db..f4c3485dc9 100644 --- a/load.php +++ b/load.php @@ -307,7 +307,7 @@ function perflab_maybe_set_object_cache_dropin() { $wp_filesystem->move( WP_CONTENT_DIR . '/object-cache.php', WP_CONTENT_DIR . '/object-cache-orig.php' ); } - $wp_filesystem->copy( PERFLAB_PLUGIN_DIR_PATH . 'server-timing/object-cache.php', WP_CONTENT_DIR . '/object-cache.php' ); + $wp_filesystem->copy( PERFLAB_PLUGIN_DIR_PATH . 'server-timing/object-cache.copy.php', WP_CONTENT_DIR . '/object-cache.php' ); } // Set timeout of 1 hour before retrying again (only in case of failure). diff --git a/server-timing/object-cache.php b/server-timing/object-cache.copy.php similarity index 100% rename from server-timing/object-cache.php rename to server-timing/object-cache.copy.php From 6f2f1c59cbb294c4871bf14ac4ba3912176796b5 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Mon, 12 Dec 2022 10:37:05 -0800 Subject: [PATCH 26/40] Delete custom object-cache.php drop-in on deactivation (and restore original if applicable). --- load.php | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/load.php b/load.php index f4c3485dc9..4c65e05bcf 100644 --- a/load.php +++ b/load.php @@ -315,6 +315,49 @@ function perflab_maybe_set_object_cache_dropin() { } add_action( 'admin_init', 'perflab_maybe_set_object_cache_dropin' ); +/** + * Removes the Performance Lab's object cache drop-in from the drop-ins folder. + * + * This function should be run on plugin deactivation. If there was another original + * object-cache.php drop-in file (renamed in `perflab_maybe_set_object_cache_dropin()` + * to object-cache-orig.php), it will be restored. + * + * This function will short-circuit if the constant + * 'PERFLAB_DISABLE_OBJECT_CACHE_DROPIN' is set as true. + * + * @since n.e.x.t + * + * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. + */ +function perflab_maybe_remove_object_cache_dropin() { + global $wp_filesystem; + + // Bail if disabled via constant. + if ( defined( 'PERFLAB_DISABLE_OBJECT_CACHE_DROPIN' ) && PERFLAB_DISABLE_OBJECT_CACHE_DROPIN ) { + return; + } + + // Bail if custom drop-in not present anyway. + if ( ! PERFLAB_OBJECT_CACHE_DROPIN_VERSION ) { + return; + } + + if ( $wp_filesystem || WP_Filesystem() ) { + // If there is an actual object-cache.php file, restore it + // and override the Performance Lab file. + // Otherwise just delete the Performance Lab file. + if ( $wp_filesystem->exists( WP_CONTENT_DIR . '/object-cache-orig.php' ) ) { + $wp_filesystem->move( WP_CONTENT_DIR . '/object-cache-orig.php', WP_CONTENT_DIR . '/object-cache.php', true ); + } else { + $wp_filesystem->delete( WP_CONTENT_DIR . '/object-cache.php' ); + } + } + + // Delete transient for drop-in check in case the plugin is reactivated shortly after. + delete_transient( 'perflab_set_object_cache_dropin' ); +} +register_deactivation_hook( __FILE__, 'perflab_maybe_remove_object_cache_dropin' ); + // Only load admin integration when in admin. if ( is_admin() ) { require_once PERFLAB_PLUGIN_DIR_PATH . 'admin/load.php'; From a5f9f5a8bd24d0a89738bd6e5067069d86894081 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Mon, 12 Dec 2022 10:41:47 -0800 Subject: [PATCH 27/40] Use more specific -orig.php suffix in case any other active plugin is doing something similar, to avoid conflicts. --- load.php | 8 ++++---- server-timing/object-cache.copy.php | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/load.php b/load.php index 4c65e05bcf..db4269479f 100644 --- a/load.php +++ b/load.php @@ -304,7 +304,7 @@ function perflab_maybe_set_object_cache_dropin() { // The Performance Lab object-cache.php will still load it, so the // behavior does not change. if ( $wp_filesystem->exists( WP_CONTENT_DIR . '/object-cache.php' ) ) { - $wp_filesystem->move( WP_CONTENT_DIR . '/object-cache.php', WP_CONTENT_DIR . '/object-cache-orig.php' ); + $wp_filesystem->move( WP_CONTENT_DIR . '/object-cache.php', WP_CONTENT_DIR . '/object-cache-plst-orig.php' ); } $wp_filesystem->copy( PERFLAB_PLUGIN_DIR_PATH . 'server-timing/object-cache.copy.php', WP_CONTENT_DIR . '/object-cache.php' ); @@ -320,7 +320,7 @@ function perflab_maybe_set_object_cache_dropin() { * * This function should be run on plugin deactivation. If there was another original * object-cache.php drop-in file (renamed in `perflab_maybe_set_object_cache_dropin()` - * to object-cache-orig.php), it will be restored. + * to object-cache-plst-orig.php), it will be restored. * * This function will short-circuit if the constant * 'PERFLAB_DISABLE_OBJECT_CACHE_DROPIN' is set as true. @@ -346,8 +346,8 @@ function perflab_maybe_remove_object_cache_dropin() { // If there is an actual object-cache.php file, restore it // and override the Performance Lab file. // Otherwise just delete the Performance Lab file. - if ( $wp_filesystem->exists( WP_CONTENT_DIR . '/object-cache-orig.php' ) ) { - $wp_filesystem->move( WP_CONTENT_DIR . '/object-cache-orig.php', WP_CONTENT_DIR . '/object-cache.php', true ); + if ( $wp_filesystem->exists( WP_CONTENT_DIR . '/object-cache-plst-orig.php' ) ) { + $wp_filesystem->move( WP_CONTENT_DIR . '/object-cache-plst-orig.php', WP_CONTENT_DIR . '/object-cache.php', true ); } else { $wp_filesystem->delete( WP_CONTENT_DIR . '/object-cache.php' ); } diff --git a/server-timing/object-cache.copy.php b/server-timing/object-cache.copy.php index be973a18a1..59af853349 100644 --- a/server-timing/object-cache.copy.php +++ b/server-timing/object-cache.copy.php @@ -52,6 +52,6 @@ function perflab_load_server_timing_api_from_dropin() { perflab_load_server_timing_api_from_dropin(); // Load the original object cache drop-in if present. -if ( file_exists( WP_CONTENT_DIR . '/object-cache-orig.php' ) ) { - require_once WP_CONTENT_DIR . '/object-cache-orig.php'; +if ( file_exists( WP_CONTENT_DIR . '/object-cache-plst-orig.php' ) ) { + require_once WP_CONTENT_DIR . '/object-cache-plst-orig.php'; } From fdcad81d3eb61f2bc2465d538167b7e7a191b5aa Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Mon, 12 Dec 2022 10:45:24 -0800 Subject: [PATCH 28/40] Fix legacy test setUp method names to be set_up. --- tests/server-timing/perflab-server-timing-metric-tests.php | 4 ++-- tests/server-timing/perflab-server-timing-tests.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/server-timing/perflab-server-timing-metric-tests.php b/tests/server-timing/perflab-server-timing-metric-tests.php index fdad15d3e7..f53c2b9456 100644 --- a/tests/server-timing/perflab-server-timing-metric-tests.php +++ b/tests/server-timing/perflab-server-timing-metric-tests.php @@ -12,8 +12,8 @@ class Perflab_Server_Timing_Metric_Tests extends WP_UnitTestCase { private $metric; - public function setUp() { - parent::setUp(); + public function set_up() { + parent::set_up(); $this->metric = new Perflab_Server_Timing_Metric( 'test-metric' ); } diff --git a/tests/server-timing/perflab-server-timing-tests.php b/tests/server-timing/perflab-server-timing-tests.php index fb78ef9def..e7fb6a9865 100644 --- a/tests/server-timing/perflab-server-timing-tests.php +++ b/tests/server-timing/perflab-server-timing-tests.php @@ -28,8 +28,8 @@ public static function wpTearDownAfterClass() { self::delete_user( self::$admin_id ); } - public function setUp() { - parent::setUp(); + public function set_up() { + parent::set_up(); $this->server_timing = new Perflab_Server_Timing(); } From 08c05d5c3c365bebfef51a401cac198a07fb41f1 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Mon, 12 Dec 2022 11:01:33 -0800 Subject: [PATCH 29/40] Fix test warning and account for filesystem that does not allow writing to WP_CONTENT_DIR. --- tests/load-tests.php | 8 ++++++++ .../server-timing/perflab-server-timing-metric-tests.php | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/load-tests.php b/tests/load-tests.php index 99f36b3aee..1c8d170417 100644 --- a/tests/load-tests.php +++ b/tests/load-tests.php @@ -238,6 +238,14 @@ function( $module_settings, $module_dir ) { } public function test_perflab_maybe_set_object_cache_dropin() { + if ( ! $GLOBALS['wp_filesystem'] ) { + WP_Filesystem(); + } + + if ( ! $GLOBALS['wp_filesystem']->is_writable( WP_CONTENT_DIR ) ) { + $this->markTestSkipped( 'This system does not allow file modifications within WP_CONTENT_DIR.' ); + } + $this->assertFalse( file_exists( WP_CONTENT_DIR . '/object-cache.php' ) ); $this->assertFalse( PERFLAB_OBJECT_CACHE_DROPIN_VERSION ); diff --git a/tests/server-timing/perflab-server-timing-metric-tests.php b/tests/server-timing/perflab-server-timing-metric-tests.php index f53c2b9456..7dd28d10b1 100644 --- a/tests/server-timing/perflab-server-timing-metric-tests.php +++ b/tests/server-timing/perflab-server-timing-metric-tests.php @@ -59,7 +59,7 @@ public function test_measure_before_and_after_correctly() { $this->metric->measure_after(); // Loose float comparison with 100ms delta, since measurement won't be exactly 1000ms. - $this->assertEquals( 1000.0, $this->metric->get_value(), '', 100.0 ); + $this->assertEqualsWithDelta( 1000.0, $this->metric->get_value(), 100.0 ); } public function test_measure_after_without_before() { From 03ad67e5ca6aa517e4c0364b31c16621b678e451 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Mon, 12 Dec 2022 11:16:08 -0800 Subject: [PATCH 30/40] Further fixes around WP_Filesystem() usage. --- tests/load-tests.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/load-tests.php b/tests/load-tests.php index 1c8d170417..991f7f213a 100644 --- a/tests/load-tests.php +++ b/tests/load-tests.php @@ -238,19 +238,19 @@ function( $module_settings, $module_dir ) { } public function test_perflab_maybe_set_object_cache_dropin() { - if ( ! $GLOBALS['wp_filesystem'] ) { - WP_Filesystem(); + if ( ! $GLOBALS['wp_filesystem'] && ! WP_Filesystem() ) { + $this->markTestSkipped( 'Filesystem cannot be initialized.' ); } if ( ! $GLOBALS['wp_filesystem']->is_writable( WP_CONTENT_DIR ) ) { $this->markTestSkipped( 'This system does not allow file modifications within WP_CONTENT_DIR.' ); } - $this->assertFalse( file_exists( WP_CONTENT_DIR . '/object-cache.php' ) ); + $this->assertFalse( $GLOBALS['wp_filesystem']->exists( WP_CONTENT_DIR . '/object-cache.php' ) ); $this->assertFalse( PERFLAB_OBJECT_CACHE_DROPIN_VERSION ); perflab_maybe_set_object_cache_dropin(); - $this->assertTrue( file_exists( WP_CONTENT_DIR . '/object-cache.php' ) ); + $this->assertTrue( $GLOBALS['wp_filesystem']->exists( WP_CONTENT_DIR . '/object-cache.php' ) ); // Clean up. This is okay to be run after the assertion since otherwise // the file does not exist anyway. From 7c87c3104ba9aaf337904601daecad128c258d0b Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Mon, 12 Dec 2022 11:40:52 -0800 Subject: [PATCH 31/40] Change wp-env config to force direct filesystem access, similar to core test suite. --- .wp-env.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.wp-env.json b/.wp-env.json index ad5592d9eb..0c705227b0 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -3,6 +3,9 @@ "plugins": [ "." ], "env": { "tests": { + "config": { + "FS_METHOD": "direct" + }, "phpVersion": "7.4" } } From ca837cd9b74d8cb5be22efdad87861c1b649225e Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Wed, 14 Dec 2022 08:26:28 -0800 Subject: [PATCH 32/40] Fix documentation wording issue. Co-authored-by: Adam Silverstein --- server-timing/defaults.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-timing/defaults.php b/server-timing/defaults.php index c5d22d10d9..2ca0cbae84 100644 --- a/server-timing/defaults.php +++ b/server-timing/defaults.php @@ -114,7 +114,7 @@ function( $passthrough ) use ( $metric ) { perflab_register_default_server_timing_before_template_metrics(); /** - * Registers the default Server-Timing metrics for while rendering the template. + * Registers the default Server-Timing metrics while rendering the template. * * These metrics should be registered at a later point, e.g. the 'wp_loaded' action. * They will only be registered if the Server-Timing API is configured to use an From e8a4badfd8dba0e02752984e258c278f7854d77c Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Wed, 14 Dec 2022 08:29:47 -0800 Subject: [PATCH 33/40] Clarify output buffer intention and why it would be useful to enable it. --- server-timing/class-perflab-server-timing.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server-timing/class-perflab-server-timing.php b/server-timing/class-perflab-server-timing.php index af2b765bfe..d9b112fb9a 100644 --- a/server-timing/class-perflab-server-timing.php +++ b/server-timing/class-perflab-server-timing.php @@ -191,6 +191,10 @@ function( $value ) { /** * Returns whether an output buffer should be used to gather Server-Timing metrics during template rendering. * + * Without an output buffer, it is only possible to cover metrics from before serving the template, i.e. before + * the HTML output starts. Therefore sites that would like to gather metrics while serving the template should + * enable this via the {@see 'perflab_server_timing_use_output_buffer'} filter. + * * @since n.e.x.t * * @return bool True if an output buffer should be used, false otherwise. @@ -199,6 +203,10 @@ public function use_output_buffer() { /** * Filters whether an output buffer should be used to be able to gather additional Server-Timing metrics. * + * Without an output buffer, it is only possible to cover metrics from before serving the template, i.e. before + * the HTML output starts. Therefore sites that would like to gather metrics while serving the template should + * enable this. + * * @since n.e.x.t * * @param bool $use_output_buffer Whether to use an output buffer. From ebb4824b76606bd4858ab7ac6ef6989df2a986cc Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Wed, 14 Dec 2022 08:41:20 -0800 Subject: [PATCH 34/40] Use PHP_INT_MIN if available instead of arbitrary -1000. --- server-timing/class-perflab-server-timing.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server-timing/class-perflab-server-timing.php b/server-timing/class-perflab-server-timing.php index d9b112fb9a..3afc1d964f 100644 --- a/server-timing/class-perflab-server-timing.php +++ b/server-timing/class-perflab-server-timing.php @@ -240,7 +240,8 @@ function() { $this->send_header(); echo $output; }, - -1000 + // phpcs:ignore PHPCompatibility.Constants.NewConstants + defined( 'PHP_INT_MIN' ) ? PHP_INT_MIN : -1000 ); return $passthrough; } From 887f6b1ac74d9038150f55ddab51af0a48f043ed Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Wed, 14 Dec 2022 08:46:46 -0800 Subject: [PATCH 35/40] Move logic to check for null metric value to a more logical place. --- server-timing/class-perflab-server-timing.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/server-timing/class-perflab-server-timing.php b/server-timing/class-perflab-server-timing.php index 3afc1d964f..e5df77207e 100644 --- a/server-timing/class-perflab-server-timing.php +++ b/server-timing/class-perflab-server-timing.php @@ -166,11 +166,6 @@ public function get_header_value() { $metric_header_values = array_filter( array_map( function( Perflab_Server_Timing_Metric $metric ) { - $value = $metric->get_value(); - if ( null === $value ) { - return null; - } - // Check the registered capability here to ensure no metric without access is exposed. if ( ! current_user_can( $this->registered_metrics_data[ $metric->get_slug() ]['access_cap'] ) ) { return null; @@ -252,10 +247,16 @@ function() { * @since n.e.x.t * * @param Perflab_Server_Timing_Metric $metric The metric to format. - * @return string Segment for the Server-Timing header. + * @return string|null Segment for the Server-Timing header, or null if no value set. */ private function format_metric_header_value( Perflab_Server_Timing_Metric $metric ) { $value = $metric->get_value(); + + // If no value is set, make sure it's just passed through. + if ( null === $value ) { + return null; + } + if ( is_float( $value ) ) { $value = round( $value, 2 ); } From 5a25b2c27e81697b4a8c097ca54e1e54f7104fe7 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Wed, 14 Dec 2022 08:49:35 -0800 Subject: [PATCH 36/40] Only add filter for measuring autoloaded options query if object-cache.php drop-in is present. --- server-timing/defaults.php | 61 ++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/server-timing/defaults.php b/server-timing/defaults.php index 2ca0cbae84..88d578be9d 100644 --- a/server-timing/defaults.php +++ b/server-timing/defaults.php @@ -79,37 +79,40 @@ function() use ( $calculate_before_template_metrics ) { ); // Measure duration of autoloaded options query. - // Requires the Performance Lab object-cache.php drop-in to be present in order to work. - add_filter( - 'query', - function( $query ) { - global $wpdb; - if ( "SELECT option_name, option_value FROM $wpdb->options WHERE autoload = 'yes'" !== $query ) { - return $query; - } - // In case the autoloaded options query is run again, prevent re-registering it and do not measure again. - if ( perflab_server_timing()->has_registered_metric( 'load-alloptions-query' ) ) { + // Requires the Performance Lab object-cache.php drop-in to be present in order to work, + // which is why the constant is checked below. + if ( PERFLAB_OBJECT_CACHE_DROPIN_VERSION ) { + add_filter( + 'query', + function( $query ) { + global $wpdb; + if ( "SELECT option_name, option_value FROM $wpdb->options WHERE autoload = 'yes'" !== $query ) { + return $query; + } + // In case the autoloaded options query is run again, prevent re-registering it and do not measure again. + if ( perflab_server_timing()->has_registered_metric( 'load-alloptions-query' ) ) { + return $query; + } + perflab_server_timing_register_metric( + 'load-alloptions-query', + array( + 'measure_callback' => function( $metric ) { + $metric->measure_before(); + add_filter( + 'pre_cache_alloptions', + function( $passthrough ) use ( $metric ) { + $metric->measure_after(); + return $passthrough; + } + ); + }, + 'access_cap' => 'exist', + ) + ); return $query; } - perflab_server_timing_register_metric( - 'load-alloptions-query', - array( - 'measure_callback' => function( $metric ) { - $metric->measure_before(); - add_filter( - 'pre_cache_alloptions', - function( $passthrough ) use ( $metric ) { - $metric->measure_after(); - return $passthrough; - } - ); - }, - 'access_cap' => 'exist', - ) - ); - return $query; - } - ); + ); + } } perflab_register_default_server_timing_before_template_metrics(); From 6c71103caccb50e8173babcc3e26d2ab79d07ceb Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Wed, 14 Dec 2022 09:01:07 -0800 Subject: [PATCH 37/40] Add doc comments to clarify use of SAVEQUERIES constant. --- server-timing/defaults.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server-timing/defaults.php b/server-timing/defaults.php index 88d578be9d..81fc095543 100644 --- a/server-timing/defaults.php +++ b/server-timing/defaults.php @@ -27,6 +27,7 @@ function perflab_register_default_server_timing_before_template_metrics() { ) ); + // SQL query time is only measured if the SAVEQUERIES constant is set to true. if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { // WordPress database query time before template. perflab_server_timing_register_metric( @@ -151,6 +152,7 @@ function( $passthrough = null ) { PHP_INT_MAX ); + // SQL query time is only measured if the SAVEQUERIES constant is set to true. if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { add_action( 'perflab_server_timing_send_header', From 1d26d911e58d88cd90d31c558f64b8dbecc10430 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Wed, 14 Dec 2022 09:03:00 -0800 Subject: [PATCH 38/40] Fix .wp-env.json indentation to use spaces per .editorconfig guidelines. --- .wp-env.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.wp-env.json b/.wp-env.json index 0c705227b0..9af20e97ab 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -1,12 +1,12 @@ { - "core": null, - "plugins": [ "." ], - "env": { - "tests": { + "core": null, + "plugins": [ "." ], + "env": { + "tests": { "config": { "FS_METHOD": "direct" }, - "phpVersion": "7.4" - } - } + "phpVersion": "7.4" + } + } } From ddd8fe3739fad0bb27f2a3096f5b982f1e288705 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Wed, 14 Dec 2022 11:30:54 -0800 Subject: [PATCH 39/40] Rename get_header_value() method to get_header(). --- server-timing/class-perflab-server-timing.php | 4 ++-- tests/server-timing/load-tests.php | 4 ++-- tests/server-timing/perflab-server-timing-tests.php | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/server-timing/class-perflab-server-timing.php b/server-timing/class-perflab-server-timing.php index e5df77207e..f45749a5f0 100644 --- a/server-timing/class-perflab-server-timing.php +++ b/server-timing/class-perflab-server-timing.php @@ -146,7 +146,7 @@ public function send_header() { */ do_action( 'perflab_server_timing_send_header' ); - $header_value = $this->get_header_value(); + $header_value = $this->get_header(); if ( ! $header_value ) { return; } @@ -161,7 +161,7 @@ public function send_header() { * * @return string The Server-Timing header value. */ - public function get_header_value() { + public function get_header() { // Get all metric header values, as long as the current user has access to the metric. $metric_header_values = array_filter( array_map( diff --git a/tests/server-timing/load-tests.php b/tests/server-timing/load-tests.php index 592252e4d7..3dfec9b29e 100644 --- a/tests/server-timing/load-tests.php +++ b/tests/server-timing/load-tests.php @@ -49,12 +49,12 @@ public function test_perflab_wrap_server_timing() { $wrapped = perflab_wrap_server_timing( $cb, 'wrapped-cb-without-capability', 'manage_options' ); $this->assertSame( 123, $wrapped(), 'Wrapped callback without capability did not return expected value' ); $this->assertTrue( perflab_server_timing()->has_registered_metric( 'wrapped-cb-without-capability' ), 'Wrapped callback metric should be registered despite lack of capability' ); - $this->assertStringNotContainsString( 'wrapped-cb-without-capability', perflab_server_timing()->get_header_value(), 'Wrapped callback was measured despite lack of capability' ); + $this->assertStringNotContainsString( 'wrapped-cb-without-capability', perflab_server_timing()->get_header(), 'Wrapped callback was measured despite lack of capability' ); $wrapped = perflab_wrap_server_timing( $cb, 'wrapped-cb-with-capability', 'exist' ); $this->assertSame( 123, $wrapped(), 'Wrapped callback with capability did not return expected value' ); $this->assertTrue( perflab_server_timing()->has_registered_metric( 'wrapped-cb-with-capability' ), 'Wrapped callback metric should be registered' ); - $this->assertStringContainsString( 'wrapped-cb-with-capability', perflab_server_timing()->get_header_value(), 'Wrapped callback was not measured despite having necessary capability' ); + $this->assertStringContainsString( 'wrapped-cb-with-capability', perflab_server_timing()->get_header(), 'Wrapped callback was not measured despite having necessary capability' ); $this->assertSame( 123, $wrapped(), 'Calling wrapped callback multiple times should not result in warning' ); } diff --git a/tests/server-timing/perflab-server-timing-tests.php b/tests/server-timing/perflab-server-timing-tests.php index e7fb6a9865..3617ed23e7 100644 --- a/tests/server-timing/perflab-server-timing-tests.php +++ b/tests/server-timing/perflab-server-timing-tests.php @@ -120,16 +120,16 @@ public function test_has_registered_metric() { } /** - * @dataProvider data_get_header_value + * @dataProvider data_get_header */ - public function test_get_header_value( $expected, $metrics ) { + public function test_get_header( $expected, $metrics ) { foreach ( $metrics as $metric_slug => $args ) { $this->server_timing->register_metric( $metric_slug, $args ); } - $this->assertSame( $expected, $this->server_timing->get_header_value() ); + $this->assertSame( $expected, $this->server_timing->get_header() ); } - public function data_get_header_value() { + public function data_get_header() { $measure_42 = function( $metric ) { $metric->set_value( 42 ); }; From 569637dcca8249a7c2dd9228905bb5e3eaf764b4 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Thu, 15 Dec 2022 09:12:26 -0800 Subject: [PATCH 40/40] Also allow using numeric strings to set Server-Timing metric values. --- server-timing/class-perflab-server-timing-metric.php | 9 +++++++-- .../server-timing/perflab-server-timing-metric-tests.php | 7 ++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/server-timing/class-perflab-server-timing-metric.php b/server-timing/class-perflab-server-timing-metric.php index 3de3881361..cf139d1b25 100644 --- a/server-timing/class-perflab-server-timing-metric.php +++ b/server-timing/class-perflab-server-timing-metric.php @@ -70,11 +70,11 @@ public function get_slug() { * @param int|float $value The metric value to set, in milliseconds. */ public function set_value( $value ) { - if ( ! is_int( $value ) && ! is_float( $value ) ) { + if ( ! is_numeric( $value ) ) { _doing_it_wrong( __METHOD__, /* translators: %s: PHP parameter name */ - sprintf( __( 'The %s parameter must be an integer or float.', 'performance-lab' ), '$value' ), + sprintf( __( 'The %s parameter must be an integer, float, or numeric string.', 'performance-lab' ), '$value' ), '' ); return; @@ -90,6 +90,11 @@ public function set_value( $value ) { return; } + // In case e.g. a numeric string is passed, cast it. + if ( ! is_int( $value ) && ! is_float( $value ) ) { + $value = (float) $value; + } + $this->value = $value; } diff --git a/tests/server-timing/perflab-server-timing-metric-tests.php b/tests/server-timing/perflab-server-timing-metric-tests.php index 7dd28d10b1..61fa929351 100644 --- a/tests/server-timing/perflab-server-timing-metric-tests.php +++ b/tests/server-timing/perflab-server-timing-metric-tests.php @@ -31,7 +31,12 @@ public function test_set_value_with_float() { $this->assertSame( 123.4567, $this->metric->get_value() ); } - public function test_set_value_requires_integer_or_float() { + public function test_set_value_with_numeric_string() { + $this->metric->set_value( '123.4567' ); + $this->assertSame( 123.4567, $this->metric->get_value() ); + } + + public function test_set_value_requires_integer_or_float_or_numeric_string() { $this->setExpectedIncorrectUsage( Perflab_Server_Timing_Metric::class . '::set_value' ); $this->metric->set_value( 'not-a-number' );