diff --git a/plugins/performance-lab/includes/server-timing/class-perflab-server-timing-metric.php b/plugins/performance-lab/includes/server-timing/class-perflab-server-timing-metric.php index fc01e682c3..bd552504d3 100644 --- a/plugins/performance-lab/includes/server-timing/class-perflab-server-timing-metric.php +++ b/plugins/performance-lab/includes/server-timing/class-perflab-server-timing-metric.php @@ -30,12 +30,13 @@ class Perflab_Server_Timing_Metric { private $value; /** - * The value measured before relevant execution logic in seconds, if used. + * The value measured before relevant execution logic in milliseconds, if used. * * @since 1.8.0 - * @var float|null + * @since n.e.x.t Renamed from $before_value to $start_value. + * @var int|float|null */ - private $before_value; + private $start_value; /** * Constructor. @@ -70,23 +71,32 @@ public function get_slug(): string { * @param int|float|mixed $value The metric value to set, in milliseconds. */ public function set_value( $value ): void { - if ( ! is_numeric( $value ) ) { - _doing_it_wrong( - __METHOD__, - /* translators: %s: PHP parameter name */ - sprintf( esc_html__( 'The %s parameter must be an integer, float, or numeric string.', 'performance-lab' ), '$value' ), - '' - ); + if ( ! $this->check_value( $value, __METHOD__ ) ) { return; } - if ( 0 !== did_action( 'perflab_server_timing_send_header' ) && ! doing_action( 'perflab_server_timing_send_header' ) ) { - _doing_it_wrong( - __METHOD__, - /* translators: %s: WordPress action name */ - sprintf( esc_html__( 'The method must be called before or during the %s action.', 'performance-lab' ), 'perflab_server_timing_send_header' ), - '' - ); + // In case e.g. a numeric string is passed, cast it. + if ( ! is_int( $value ) && ! is_float( $value ) ) { + $value = (float) $value; + } + + $this->value = $value; + } + + /** + * Sets the start value for the metric. + * + * For metrics where the start value is set, this timestamp will be sent via an optional `start` parameter. + * + * Alternatively to setting the start value directly, the {@see Perflab_Server_Timing_Metric::measure_start()} and + * {@see Perflab_Server_Timing_Metric::measure_end()} methods can be used to further simplify measuring. + * + * @since n.e.x.t + * + * @param int|float|mixed $value The start value to set for the metric, in milliseconds. + */ + public function set_start_value( $value ): void { + if ( ! $this->check_value( $value, __METHOD__ ) ) { return; } @@ -95,11 +105,11 @@ public function set_value( $value ): void { $value = (float) $value; } - $this->value = $value; + $this->start_value = $value; } /** - * Gets the metric value. + * Gets the metric value, if set. * * @since 1.8.0 * @@ -110,36 +120,112 @@ public function get_value() { } /** - * Captures the current time, as a reference point to calculate the duration of a task afterward. + * Gets the metric start value, if set. + * + * @since n.e.x.t + * + * @return int|float|null The metric start value, or null if none set. + */ + public function get_start_value() { + return $this->start_value; + } + + /** + * Captures the current time as metric start time, to calculate the duration of a task afterward. + * + * This should be used in combination with {@see Perflab_Server_Timing_Metric::measure_end()}. Alternatively, + * {@see Perflab_Server_Timing_Metric::set_value()} and {@see Perflab_Server_Timing_Metric::set_start_value()} can + * be used to set a calculated value manually. * - * 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_start(): void { + $this->set_start_value( microtime( true ) * 1000.0 ); + } + + /** + * Captures the current time and compares it to the metric start time to calculate a task's duration. + * + * This should be used in combination with {@see Perflab_Server_Timing_Metric::measure_start()}. Alternatively, + * {@see Perflab_Server_Timing_Metric::set_value()} and {@see Perflab_Server_Timing_Metric::set_start_value()} can + * be used to set a calculated value manually. + * + * @since n.e.x.t + */ + public function measure_end(): void { + if ( null === $this->start_value ) { + _doing_it_wrong( + __METHOD__, + sprintf( + /* translators: 1: PHP method name, 2: alternative PHP method name */ + esc_html__( 'The %1$s method or %2$s method must be called before.', 'performance-lab' ), + __CLASS__ . '::measure_start()', + __CLASS__ . '::set_start_value()' + ), + '' + ); + return; + } + + $this->set_value( microtime( true ) * 1000.0 - $this->start_value ); + } + + /** + * Captures the current time, as a reference point to calculate the duration of a task afterward. * * @since 1.8.0 + * @deprecated n.e.x.t + * @see Perflab_Server_Timing_Metric::measure_start() */ public function measure_before(): void { - $this->before_value = microtime( true ); + _deprecated_function( __METHOD__, 'n.e.x.t (Performance Lab)', __CLASS__ . '::measure_start()' ); + $this->measure_start(); } /** * 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 1.8.0 + * @deprecated n.e.x.t + * @see Perflab_Server_Timing_Metric::measure_end() */ public function measure_after(): void { - if ( null === $this->before_value ) { + _deprecated_function( __METHOD__, 'n.e.x.t (Performance Lab)', __CLASS__ . '::measure_end()' ); + $this->measure_end(); + } + + /** + * Checks whether the passed metric value is valid and whether the timing of the method call is correct. + * + * @since n.e.x.t + * + * @param int|float|mixed $value The value to check, in milliseconds. + * @param string $method The method name originally called (typically passed via `__METHOD__`). + * @return bool True if the method call with the value is valid, false otherwise. + */ + private function check_value( $value, string $method ): bool { + if ( ! is_numeric( $value ) ) { _doing_it_wrong( - __METHOD__, - /* translators: %s: PHP method name */ - sprintf( esc_html__( 'The %s method must be called before.', 'performance-lab' ), __CLASS__ . '::measure_before()' ), + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + $method, + /* translators: %s: PHP parameter name */ + sprintf( esc_html__( 'The %s parameter must be an integer, float, or numeric string.', 'performance-lab' ), '$value' ), '' ); - return; + return false; + } + + if ( 0 !== did_action( 'perflab_server_timing_send_header' ) && ! doing_action( 'perflab_server_timing_send_header' ) ) { + _doing_it_wrong( + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + $method, + /* translators: %s: WordPress action name */ + sprintf( esc_html__( 'The method must be called before or during the %s action.', 'performance-lab' ), 'perflab_server_timing_send_header' ), + '' + ); + return false; } - $this->set_value( ( microtime( true ) - $this->before_value ) * 1000.0 ); + return true; } } diff --git a/plugins/performance-lab/includes/server-timing/class-perflab-server-timing.php b/plugins/performance-lab/includes/server-timing/class-perflab-server-timing.php index 3eed6ad703..bc9b070f2d 100644 --- a/plugins/performance-lab/includes/server-timing/class-perflab-server-timing.php +++ b/plugins/performance-lab/includes/server-timing/class-perflab-server-timing.php @@ -164,6 +164,15 @@ public function send_header(): void { return; } + // Include non-standard metrics to provide timing context to the browser. + if ( $this->use_output_buffer() ) { + $start_time = round( $GLOBALS['timestart'] * 1000.0, 2 ); + $end_time = round( microtime( true ) * 1000.0, 2 ); + + $header_value .= sprintf( ', response-start;start=%s', $start_time ); + $header_value .= sprintf( ', response-end;start=%s', $end_time ); + } + header( sprintf( 'Server-Timing: %s', $header_value ), false ); } @@ -282,6 +291,7 @@ function ( string $output, ?int $phase ): string { * Formats the header segment for a single metric. * * @since 1.8.0 + * @since n.e.x.t Now includes optional `start` parameter if specified for the given metric. * * @param Perflab_Server_Timing_Metric $metric The metric to format. * @return string|null Segment for the Server-Timing header, or null if no value set. @@ -301,6 +311,16 @@ private function format_metric_header_value( Perflab_Server_Timing_Metric $metri // See https://github.com/WordPress/performance/issues/955. $name = preg_replace( '/[^!#$%&\'*+\-.^_`|~0-9a-zA-Z]/', '-', $metric->get_slug() ); - return sprintf( 'wp-%1$s;dur=%2$s', $name, $value ); + $header = sprintf( 'wp-%1$s;dur=%2$s', $name, $value ); + + $start_value = $metric->get_start_value(); + if ( null !== $start_value ) { + if ( is_float( $start_value ) ) { + $start_value = round( $start_value, 2 ); + } + $header .= sprintf( ';start=%s', $start_value ); + } + + return $header; } } diff --git a/plugins/performance-lab/includes/server-timing/defaults.php b/plugins/performance-lab/includes/server-timing/defaults.php index da7c8d3426..10500273df 100644 --- a/plugins/performance-lab/includes/server-timing/defaults.php +++ b/plugins/performance-lab/includes/server-timing/defaults.php @@ -30,7 +30,8 @@ function perflab_register_default_server_timing_before_template_metrics(): void array( 'measure_callback' => static function ( $metric ): void { // The 'timestart' global is set right at the beginning of WordPress execution. - $metric->set_value( ( microtime( true ) - $GLOBALS['timestart'] ) * 1000.0 ); + $metric->set_start_value( $GLOBALS['timestart'] * 1000.0 ); + $metric->measure_end(); }, 'access_cap' => 'exist', ) @@ -113,8 +114,8 @@ static function ( $passthrough = null ) { 'template', array( 'measure_callback' => static function ( Perflab_Server_Timing_Metric $metric ): void { - $metric->measure_before(); - add_action( 'perflab_server_timing_send_header', array( $metric, 'measure_after' ), PHP_INT_MAX ); + $metric->measure_start(); + add_action( 'perflab_server_timing_send_header', array( $metric, 'measure_end' ), PHP_INT_MAX ); }, 'access_cap' => 'exist', ) @@ -134,7 +135,8 @@ static function (): void { array( 'measure_callback' => static function ( $metric ): void { // The 'timestart' global is set right at the beginning of WordPress execution. - $metric->set_value( ( microtime( true ) - $GLOBALS['timestart'] ) * 1000.0 ); + $metric->set_start_value( $GLOBALS['timestart'] * 1000.0 ); + $metric->measure_end(); }, 'access_cap' => 'exist', ) @@ -242,17 +244,17 @@ static function ( $hook_name ) use ( $hooks_to_measure ): void { } $measure_callback = static function ( $metric ) use ( $hook_name, $hook_type ): void { - $metric->measure_before(); + $metric->measure_start(); if ( 'action' === $hook_type ) { $cb = static function () use ( $metric, $hook_name, &$cb ): void { - $metric->measure_after(); + $metric->measure_end(); remove_action( $hook_name, $cb, PHP_INT_MAX ); }; add_action( $hook_name, $cb, PHP_INT_MAX ); } else { $cb = static function ( $passthrough ) use ( $metric, $hook_name, &$cb ) { - $metric->measure_after(); + $metric->measure_end(); remove_filter( $hook_name, $cb, PHP_INT_MAX ); return $passthrough; }; diff --git a/plugins/performance-lab/includes/server-timing/load.php b/plugins/performance-lab/includes/server-timing/load.php index 8769f43aa1..39372d5c42 100644 --- a/plugins/performance-lab/includes/server-timing/load.php +++ b/plugins/performance-lab/includes/server-timing/load.php @@ -132,13 +132,13 @@ function perflab_wrap_server_timing( callable $callback, string $metric_slug, st } // Measure time before the callback. - $server_timing_metric->measure_before(); + $server_timing_metric->measure_start(); // Execute the callback. $result = call_user_func_array( $callback, $callback_args ); // Measure time after the callback and calculate total. - $server_timing_metric->measure_after(); + $server_timing_metric->measure_end(); // Return result (e.g. in case this is a filter callback). return $result; diff --git a/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing-metric.php b/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing-metric.php index 842272f1c5..5198f21df8 100644 --- a/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing-metric.php +++ b/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing-metric.php @@ -48,31 +48,80 @@ public function test_set_value_prevents_late_measurement(): void { $this->setExpectedIncorrectUsage( Perflab_Server_Timing_Metric::class . '::set_value' ); $this->metric->set_value( 2 ); + remove_all_actions( 'perflab_server_timing_send_header' ); do_action( 'perflab_server_timing_send_header' ); $this->metric->set_value( 3 ); $this->assertSame( 2, $this->metric->get_value() ); } + public function test_set_start_value_with_integer(): void { + $this->metric->set_start_value( 123 ); + $this->assertSame( 123, $this->metric->get_start_value() ); + } + + public function test_set_start_value_with_float(): void { + $this->metric->set_start_value( 123.4567 ); + $this->assertSame( 123.4567, $this->metric->get_start_value() ); + } + + public function test_set_start_value_with_numeric_string(): void { + $this->metric->set_start_value( '123.4567' ); + $this->assertSame( 123.4567, $this->metric->get_start_value() ); + } + + public function test_set_start_value_requires_integer_or_float_or_numeric_string(): void { + $this->setExpectedIncorrectUsage( Perflab_Server_Timing_Metric::class . '::set_start_value' ); + + $this->metric->set_start_value( 'not-a-number' ); + $this->assertNull( $this->metric->get_start_value() ); + } + + public function test_set_start_value_prevents_late_measurement(): void { + $this->setExpectedIncorrectUsage( Perflab_Server_Timing_Metric::class . '::set_start_value' ); + + $this->metric->set_start_value( 2 ); + remove_all_actions( 'perflab_server_timing_send_header' ); + do_action( 'perflab_server_timing_send_header' ); + $this->metric->set_start_value( 3 ); + + $this->assertSame( 2, $this->metric->get_start_value() ); + } + public function test_get_value(): void { $this->metric->set_value( 86.42 ); $this->assertSame( 86.42, $this->metric->get_value() ); } - public function test_measure_before_and_after_correctly(): void { - $this->metric->measure_before(); + public function test_get_start_value(): void { + $t = microtime( true ) * 1000.0; + $this->metric->set_start_value( $t ); + $this->assertSame( $t, $this->metric->get_start_value() ); + } + + public function test_measure_start_and_end_correctly(): void { + $this->metric->measure_start(); sleep( 1 ); - $this->metric->measure_after(); + $this->metric->measure_end(); // Loose float comparison with 100ms delta, since measurement won't be exactly 1000ms. $this->assertEqualsWithDelta( 1000.0, $this->metric->get_value(), 100.0 ); } - public function test_measure_after_without_before(): void { - $this->setExpectedIncorrectUsage( Perflab_Server_Timing_Metric::class . '::measure_after' ); + public function test_measure_end_without_start(): void { + $this->setExpectedIncorrectUsage( Perflab_Server_Timing_Metric::class . '::measure_end' ); - $this->metric->measure_after(); + $this->metric->measure_end(); $this->assertNull( $this->metric->get_value() ); } + + public function test_measure_custom_start_and_end_correctly(): void { + $this->metric->set_start_value( microtime( true ) * 1000.0 ); + sleep( 1 ); + $this->metric->measure_end(); + + // Loose float comparison with 100ms delta, since measurement won't be exactly 1000ms. + $this->assertEqualsWithDelta( 1000.0, $this->metric->get_value(), 100.0 ); + } } diff --git a/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing.php b/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing.php index 561c3bb481..be99086fd7 100644 --- a/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing.php +++ b/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing.php @@ -176,6 +176,10 @@ public function data_get_header(): array { $measure_12point345 = static function ( Perflab_Server_Timing_Metric $metric ): void { $metric->set_value( 12.345 ); }; + $measure_with_start = static function ( Perflab_Server_Timing_Metric $metric ): void { + $metric->set_start_value( 100000000.36 ); + $metric->set_value( 100.72 ); + }; return array( 'single metric' => array( @@ -221,6 +225,15 @@ public function data_get_header(): array { ), ), ), + 'metric with start' => array( + 'wp-with-start;dur=100.72;start=100000000.36', + array( + 'with-start' => array( + 'measure_callback' => $measure_with_start, + 'access_cap' => 'exist', + ), + ), + ), ); }