From 68a01be56d4b3e25f3c2a5e9f692543752d9a3f1 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 1 Nov 2023 17:06:00 -0700 Subject: [PATCH 01/62] Add initial REST API endpoint for storing page metrics --- .../image-loading-optimization/detect.js | 17 ++- .../image-loading-optimization/hooks.php | 8 +- .../image-loading-optimization/load.php | 1 + .../image-loading-optimization/rest-api.php | 115 ++++++++++++++++++ 4 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 modules/images/image-loading-optimization/rest-api.php diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detect.js index c279a7ad93..a0e7a3ce8c 100644 --- a/modules/images/image-loading-optimization/detect.js +++ b/modules/images/image-loading-optimization/detect.js @@ -78,11 +78,15 @@ function getBreadcrumbs( leafElement ) { * @param {number} serveTime The serve time of the page in milliseconds from PHP via `ceil( microtime( true ) * 1000 )`. * @param {number} detectionTimeWindow The number of milliseconds between now and when the page was first generated in which detection should proceed. * @param {boolean} isDebug Whether to show debug messages. + * @param {string} restApiEndpoint URL for where to send the detection data. + * @param {string} restApiNonce Nonce for writing to the REST API. */ export default async function detect( serveTime, detectionTimeWindow, - isDebug + isDebug, + restApiEndpoint, + restApiNonce ) { const runTime = new Date().valueOf(); @@ -271,6 +275,17 @@ export default async function detect( pageMetrics.elements.push( elementMetrics ); } + // TODO: Wait until idle. + const response = await fetch( restApiEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-WP-Nonce': restApiNonce, + }, + body: JSON.stringify( pageMetrics ), + } ); + log( 'response:', await response.json() ); + // TODO: Send data to server. log( pageMetrics ); diff --git a/modules/images/image-loading-optimization/hooks.php b/modules/images/image-loading-optimization/hooks.php index 09692aa828..620c780a03 100644 --- a/modules/images/image-loading-optimization/hooks.php +++ b/modules/images/image-loading-optimization/hooks.php @@ -68,7 +68,13 @@ function image_loading_optimization_print_detection_script() { */ $detection_time_window = apply_filters( 'perflab_image_loading_detection_time_window', 5000 ); - $detect_args = array( $serve_time, $detection_time_window, WP_DEBUG ); + $detect_args = array( + $serve_time, + $detection_time_window, + WP_DEBUG, + rest_url( '/perflab/v1/image-loading-optimization/metrics-storage' ), + wp_create_nonce( 'wp_rest' ), + ); wp_print_inline_script_tag( sprintf( 'import detect from %s; detect( ...%s )', diff --git a/modules/images/image-loading-optimization/load.php b/modules/images/image-loading-optimization/load.php index fb6e31c2bf..6f271a96d2 100644 --- a/modules/images/image-loading-optimization/load.php +++ b/modules/images/image-loading-optimization/load.php @@ -15,3 +15,4 @@ require_once __DIR__ . '/helper.php'; require_once __DIR__ . '/hooks.php'; +require_once __DIR__ . '/rest-api.php'; diff --git a/modules/images/image-loading-optimization/rest-api.php b/modules/images/image-loading-optimization/rest-api.php new file mode 100644 index 0000000000..bef2555c2c --- /dev/null +++ b/modules/images/image-loading-optimization/rest-api.php @@ -0,0 +1,115 @@ + 'object', + 'properties' => array( + 'width' => array( + 'type' => 'number', + 'minimum' => 0, + ), + 'height' => array( + 'type' => 'number', + 'minimum' => 0, + ), + // TODO: There are other properties to define if we need them: x, y, top, right, bottom, left. + ), + ); + + register_rest_route( + 'perflab/v1', + '/image-loading-optimization/metrics-storage', + array( + 'methods' => 'POST', + 'callback' => 'image_loading_optimization_handle_rest_request', + 'permission_callback' => '__return_true', // Needs to be available to unauthenticated visitors. + 'args' => array( + 'viewport' => array( + 'description' => __( 'Viewport dimensions', 'performance-lab' ), + 'type' => 'object', + 'required' => true, + 'properties' => array( + 'width' => array( + 'type' => 'int', + 'minimum' => 0, + ), + 'height' => array( + 'type' => 'int', + 'minimum' => 0, + ), + ), + ), + 'elements' => array( + 'description' => __( 'Element metrics', 'performance-lab' ), + 'type' => 'array', + 'items' => array( + // See the ElementMetrics in detect.js. + 'type' => 'object', + 'properties' => array( + 'isLCP' => array( + 'type' => 'bool', + ), + 'isLCPCandidate' => array( + 'type' => 'bool', + ), + 'breadcrumbs' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'tagName' => array( + 'type' => 'string', + // TODO: Pattern? + ), + 'index' => array( + 'type' => 'int', + 'minimum' => 0, + ), + ), + ), + ), + 'intersectionRatio' => array( + 'type' => 'number', + 'minimum' => 0.0, + 'maximum' => 1.0, + ), + 'intersectionRect' => $dom_rect_schema, + 'boundingClientRect' => $dom_rect_schema, + ), + ), + ), + ), + ) + ); +} +add_action( 'rest_api_init', 'image_loading_optimization_register_endpoint' ); + +/** + * Handle REST API request to store metrics. + * + * @param WP_REST_Request $request Request. + * @return WP_REST_Response Response. + */ +function image_loading_optimization_handle_rest_request( WP_REST_Request $request ) { + + return new WP_REST_Response( + array( + 'success' => true, + 'body' => $request->get_json_params(), + ) + ); +} From b4e29d609647c8e23fc4db6532e50ba8692e82c5 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 2 Nov 2023 16:06:26 -0700 Subject: [PATCH 02/62] Include url in PageMetrics and further define schema --- .../image-loading-optimization/detect.js | 2 ++ .../image-loading-optimization/hooks.php | 3 ++ .../image-loading-optimization/rest-api.php | 34 ++++++++++++------- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detect.js index a0e7a3ce8c..129079b4a7 100644 --- a/modules/images/image-loading-optimization/detect.js +++ b/modules/images/image-loading-optimization/detect.js @@ -31,6 +31,7 @@ function warn( ...message ) { /** * @typedef {Object} PageMetrics + * @property {string} url - URL of the page. * @property {Object} viewport - Viewport. * @property {number} viewport.width - Viewport width. * @property {number} viewport.height - Viewport height. @@ -235,6 +236,7 @@ export default async function detect( /** @type {PageMetrics} */ const pageMetrics = { + url: win.location.href, // TODO: Consider sending canonical URL instead. viewport: { width: win.innerWidth, height: win.innerHeight, diff --git a/modules/images/image-loading-optimization/hooks.php b/modules/images/image-loading-optimization/hooks.php index 620c780a03..666c795214 100644 --- a/modules/images/image-loading-optimization/hooks.php +++ b/modules/images/image-loading-optimization/hooks.php @@ -49,6 +49,9 @@ static function ( $output ) { /** * Prints the script for detecting loaded images and the LCP element. + * + * @todo This should eventually only print the script if metrics are needed. + * @todo This script should not be printed if the page was requested with non-removal (non-canonical) query args. */ function image_loading_optimization_print_detection_script() { $serve_time = ceil( microtime( true ) * 1000 ); diff --git a/modules/images/image-loading-optimization/rest-api.php b/modules/images/image-loading-optimization/rest-api.php index bef2555c2c..69519ec562 100644 --- a/modules/images/image-loading-optimization/rest-api.php +++ b/modules/images/image-loading-optimization/rest-api.php @@ -38,18 +38,25 @@ function image_loading_optimization_register_endpoint() { 'callback' => 'image_loading_optimization_handle_rest_request', 'permission_callback' => '__return_true', // Needs to be available to unauthenticated visitors. 'args' => array( + 'url' => array( + 'type' => 'string', + 'required' => true, + 'format' => 'uri', + ), 'viewport' => array( 'description' => __( 'Viewport dimensions', 'performance-lab' ), 'type' => 'object', 'required' => true, 'properties' => array( 'width' => array( - 'type' => 'int', - 'minimum' => 0, + 'type' => 'int', + 'required' => true, + 'minimum' => 0, ), 'height' => array( - 'type' => 'int', - 'minimum' => 0, + 'type' => 'int', + 'required' => true, + 'minimum' => 0, ), ), ), @@ -61,19 +68,21 @@ function image_loading_optimization_register_endpoint() { 'type' => 'object', 'properties' => array( 'isLCP' => array( - 'type' => 'bool', + 'type' => 'bool', + 'required' => true, ), 'isLCPCandidate' => array( 'type' => 'bool', ), 'breadcrumbs' => array( - 'type' => 'array', - 'items' => array( + 'type' => 'array', + 'required' => true, + 'items' => array( 'type' => 'object', 'properties' => array( 'tagName' => array( - 'type' => 'string', - // TODO: Pattern? + 'type' => 'string', + 'pattern' => '^[a-zA-Z0-9-]+$', ), 'index' => array( 'type' => 'int', @@ -83,9 +92,10 @@ function image_loading_optimization_register_endpoint() { ), ), 'intersectionRatio' => array( - 'type' => 'number', - 'minimum' => 0.0, - 'maximum' => 1.0, + 'type' => 'number', + 'required' => true, + 'minimum' => 0.0, + 'maximum' => 1.0, ), 'intersectionRect' => $dom_rect_schema, 'boundingClientRect' => $dom_rect_schema, From ffcae75d01ea982722fa217c336c76be0d2d1ccc Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 2 Nov 2023 16:12:06 -0700 Subject: [PATCH 03/62] Validate that the provided URL is for this site --- .../images/image-loading-optimization/rest-api.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/modules/images/image-loading-optimization/rest-api.php b/modules/images/image-loading-optimization/rest-api.php index 69519ec562..bcb764643a 100644 --- a/modules/images/image-loading-optimization/rest-api.php +++ b/modules/images/image-loading-optimization/rest-api.php @@ -39,9 +39,15 @@ function image_loading_optimization_register_endpoint() { 'permission_callback' => '__return_true', // Needs to be available to unauthenticated visitors. 'args' => array( 'url' => array( - 'type' => 'string', - 'required' => true, - 'format' => 'uri', + 'type' => 'string', + 'required' => true, + 'format' => 'uri', + 'validate_callback' => static function ( $url ) { + if ( ! wp_validate_redirect( $url ) ) { + return new WP_Error( 'non_origin_url', __( 'URL for another site provided.', 'performance-lab' ) ); + } + return true; + }, ), 'viewport' => array( 'description' => __( 'Viewport dimensions', 'performance-lab' ), From 1634fb9a5be0f8e22e4950c086b94e36e52f76cb Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 2 Nov 2023 17:18:32 -0700 Subject: [PATCH 04/62] Ensure both tagName and index are required --- modules/images/image-loading-optimization/rest-api.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/modules/images/image-loading-optimization/rest-api.php b/modules/images/image-loading-optimization/rest-api.php index bcb764643a..335a70aa05 100644 --- a/modules/images/image-loading-optimization/rest-api.php +++ b/modules/images/image-loading-optimization/rest-api.php @@ -87,12 +87,14 @@ function image_loading_optimization_register_endpoint() { 'type' => 'object', 'properties' => array( 'tagName' => array( - 'type' => 'string', - 'pattern' => '^[a-zA-Z0-9-]+$', + 'type' => 'string', + 'required' => true, + 'pattern' => '^[a-zA-Z0-9-]+$', ), 'index' => array( - 'type' => 'int', - 'minimum' => 0, + 'type' => 'int', + 'required' => true, + 'minimum' => 0, ), ), ), From 534eba2d1378970b5a535739190bcc459c6f8fd6 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 2 Nov 2023 17:57:58 -0700 Subject: [PATCH 05/62] Add initial storage locking to protect against flooding --- .../image-loading-optimization/helper.php | 56 +++++++++++++++++++ .../image-loading-optimization/hooks.php | 5 ++ .../image-loading-optimization/rest-api.php | 16 +++++- 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/modules/images/image-loading-optimization/helper.php b/modules/images/image-loading-optimization/helper.php index 77e6b12b10..59e47f937a 100644 --- a/modules/images/image-loading-optimization/helper.php +++ b/modules/images/image-loading-optimization/helper.php @@ -9,3 +9,59 @@ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } + +/** + * Gets the TTL for the metrics storage lock. + * + * @return int TTL. + */ +function image_loading_optimization_get_metrics_storage_lock_ttl() { + + /** + * Filters how long a given IP is locked from submitting another metrics-storage REST API request. + * + * @param int $ttl TTL. + */ + return (int) apply_filters( 'perflab_image_loading_detection_lock_ttl', 10 * MINUTE_IN_SECONDS ); +} + +/** + * Gets transient key for locking metrics storage (for the current IP). + * + * @return string Transient key. + */ +function image_loading_optimization_get_metrics_storage_lock_transient_key() { + $ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR']; + return 'page_metrics_storage_lock_' . wp_hash( $ip_address ); +} + +/** + * Sets metrics storage lock (for the current IP). + */ +function image_loading_optimization_set_metrics_storage_lock() { + $ttl = image_loading_optimization_get_metrics_storage_lock_ttl(); + $key = image_loading_optimization_get_metrics_storage_lock_transient_key(); + if ( 0 === $ttl ) { + delete_transient( $key ); + } else { + set_transient( $key, time(), $ttl ); + } +} + +/** + * Checks whether metrics storage is locked (for the current IP). + * + * @todo This isn't working properly? + * @return bool Whether locked. + */ +function image_loading_optimization_is_metrics_storage_locked() { + $ttl = image_loading_optimization_get_metrics_storage_lock_ttl(); + if ( 0 === $ttl ) { + return false; + } + $transient = (int) get_transient( image_loading_optimization_get_metrics_storage_lock_transient_key() ); + if ( 0 === $transient ) { + return false; + } + return time() - $transient < $ttl; +} diff --git a/modules/images/image-loading-optimization/hooks.php b/modules/images/image-loading-optimization/hooks.php index 666c795214..88dd70635b 100644 --- a/modules/images/image-loading-optimization/hooks.php +++ b/modules/images/image-loading-optimization/hooks.php @@ -54,6 +54,11 @@ static function ( $output ) { * @todo This script should not be printed if the page was requested with non-removal (non-canonical) query args. */ function image_loading_optimization_print_detection_script() { + + if ( image_loading_optimization_is_metrics_storage_locked() ) { + return; + } + $serve_time = ceil( microtime( true ) * 1000 ); /** diff --git a/modules/images/image-loading-optimization/rest-api.php b/modules/images/image-loading-optimization/rest-api.php index 335a70aa05..0746ff1315 100644 --- a/modules/images/image-loading-optimization/rest-api.php +++ b/modules/images/image-loading-optimization/rest-api.php @@ -36,7 +36,17 @@ function image_loading_optimization_register_endpoint() { array( 'methods' => 'POST', 'callback' => 'image_loading_optimization_handle_rest_request', - 'permission_callback' => '__return_true', // Needs to be available to unauthenticated visitors. + 'permission_callback' => static function () { + // Needs to be available to unauthenticated visitors. + if ( image_loading_optimization_is_metrics_storage_locked() ) { + return new WP_Error( + 'metrics_storage_locked', + __( 'Metrics storage is presently locked for the current IP.', 'performance-lab' ), + array( 'status' => 403 ) + ); + } + return true; + }, 'args' => array( 'url' => array( 'type' => 'string', @@ -124,6 +134,10 @@ function image_loading_optimization_register_endpoint() { */ function image_loading_optimization_handle_rest_request( WP_REST_Request $request ) { + // TODO: We need storage. + + image_loading_optimization_set_metrics_storage_lock(); + return new WP_REST_Response( array( 'success' => true, From ba30471f82bc97fae8eb9788375d81d2d2bdabaa Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 3 Nov 2023 11:44:18 -0700 Subject: [PATCH 06/62] Update metrics storage locking --- modules/images/image-loading-optimization/helper.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/modules/images/image-loading-optimization/helper.php b/modules/images/image-loading-optimization/helper.php index 59e47f937a..e55f0bb992 100644 --- a/modules/images/image-loading-optimization/helper.php +++ b/modules/images/image-loading-optimization/helper.php @@ -20,14 +20,17 @@ function image_loading_optimization_get_metrics_storage_lock_ttl() { /** * Filters how long a given IP is locked from submitting another metrics-storage REST API request. * + * Filtering the TTL to zero will disable any metrics storage locking. This is useful during development. + * * @param int $ttl TTL. */ - return (int) apply_filters( 'perflab_image_loading_detection_lock_ttl', 10 * MINUTE_IN_SECONDS ); + return (int) apply_filters( 'perflab_image_loading_optimization_metrics_storage_lock_ttl', MINUTE_IN_SECONDS ); } /** * Gets transient key for locking metrics storage (for the current IP). * + * @todo Should the URL be included in the key? Or should a user only be allowed to store one metric? * @return string Transient key. */ function image_loading_optimization_get_metrics_storage_lock_transient_key() { @@ -51,7 +54,6 @@ function image_loading_optimization_set_metrics_storage_lock() { /** * Checks whether metrics storage is locked (for the current IP). * - * @todo This isn't working properly? * @return bool Whether locked. */ function image_loading_optimization_is_metrics_storage_locked() { @@ -59,9 +61,9 @@ function image_loading_optimization_is_metrics_storage_locked() { if ( 0 === $ttl ) { return false; } - $transient = (int) get_transient( image_loading_optimization_get_metrics_storage_lock_transient_key() ); - if ( 0 === $transient ) { + $locked_time = (int) get_transient( image_loading_optimization_get_metrics_storage_lock_transient_key() ); + if ( 0 === $locked_time ) { return false; } - return time() - $transient < $ttl; + return time() - $locked_time < $ttl; } From 4e0d7de6cc43c0d7bd9e89c4352b754239fcda79 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 3 Nov 2023 11:54:32 -0700 Subject: [PATCH 07/62] Move storage helper functions into storage.php --- .../image-loading-optimization/helper.php | 58 ---------------- .../image-loading-optimization/load.php | 1 + .../image-loading-optimization/storage.php | 69 +++++++++++++++++++ 3 files changed, 70 insertions(+), 58 deletions(-) create mode 100644 modules/images/image-loading-optimization/storage.php diff --git a/modules/images/image-loading-optimization/helper.php b/modules/images/image-loading-optimization/helper.php index e55f0bb992..77e6b12b10 100644 --- a/modules/images/image-loading-optimization/helper.php +++ b/modules/images/image-loading-optimization/helper.php @@ -9,61 +9,3 @@ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } - -/** - * Gets the TTL for the metrics storage lock. - * - * @return int TTL. - */ -function image_loading_optimization_get_metrics_storage_lock_ttl() { - - /** - * Filters how long a given IP is locked from submitting another metrics-storage REST API request. - * - * Filtering the TTL to zero will disable any metrics storage locking. This is useful during development. - * - * @param int $ttl TTL. - */ - return (int) apply_filters( 'perflab_image_loading_optimization_metrics_storage_lock_ttl', MINUTE_IN_SECONDS ); -} - -/** - * Gets transient key for locking metrics storage (for the current IP). - * - * @todo Should the URL be included in the key? Or should a user only be allowed to store one metric? - * @return string Transient key. - */ -function image_loading_optimization_get_metrics_storage_lock_transient_key() { - $ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR']; - return 'page_metrics_storage_lock_' . wp_hash( $ip_address ); -} - -/** - * Sets metrics storage lock (for the current IP). - */ -function image_loading_optimization_set_metrics_storage_lock() { - $ttl = image_loading_optimization_get_metrics_storage_lock_ttl(); - $key = image_loading_optimization_get_metrics_storage_lock_transient_key(); - if ( 0 === $ttl ) { - delete_transient( $key ); - } else { - set_transient( $key, time(), $ttl ); - } -} - -/** - * Checks whether metrics storage is locked (for the current IP). - * - * @return bool Whether locked. - */ -function image_loading_optimization_is_metrics_storage_locked() { - $ttl = image_loading_optimization_get_metrics_storage_lock_ttl(); - if ( 0 === $ttl ) { - return false; - } - $locked_time = (int) get_transient( image_loading_optimization_get_metrics_storage_lock_transient_key() ); - if ( 0 === $locked_time ) { - return false; - } - return time() - $locked_time < $ttl; -} diff --git a/modules/images/image-loading-optimization/load.php b/modules/images/image-loading-optimization/load.php index 6f271a96d2..e760b0a6e9 100644 --- a/modules/images/image-loading-optimization/load.php +++ b/modules/images/image-loading-optimization/load.php @@ -15,4 +15,5 @@ require_once __DIR__ . '/helper.php'; require_once __DIR__ . '/hooks.php'; +require_once __DIR__ . '/storage.php'; require_once __DIR__ . '/rest-api.php'; diff --git a/modules/images/image-loading-optimization/storage.php b/modules/images/image-loading-optimization/storage.php new file mode 100644 index 0000000000..e3032c28bb --- /dev/null +++ b/modules/images/image-loading-optimization/storage.php @@ -0,0 +1,69 @@ + Date: Fri, 3 Nov 2023 12:15:23 -0700 Subject: [PATCH 08/62] Add post type for page metrics storage --- .../image-loading-optimization/storage.php | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/modules/images/image-loading-optimization/storage.php b/modules/images/image-loading-optimization/storage.php index e3032c28bb..49a33ccd75 100644 --- a/modules/images/image-loading-optimization/storage.php +++ b/modules/images/image-loading-optimization/storage.php @@ -10,6 +10,8 @@ exit; // Exit if accessed directly. } +define( 'IMAGE_LOADING_OPTIMIZATION_PAGE_METRICS_POST_TYPE', 'ilo_page_metrics' ); + /** * Gets the TTL for the metrics storage lock. * @@ -67,3 +69,28 @@ function image_loading_optimization_is_metrics_storage_locked() { } return time() - $locked_time < $ttl; } + +/** + * Register post type for metrics storage. + * + * This the configuration for this post type is similar to the oembed_cache in core. + */ +function image_loading_optimization_register_page_metrics_post_type() { + register_post_type( + IMAGE_LOADING_OPTIMIZATION_PAGE_METRICS_POST_TYPE, + array( + 'labels' => array( + 'name' => __( 'Page Metrics', 'performance-lab' ), + 'singular_name' => __( 'Page Metrics', 'performance-lab' ), + ), + 'public' => false, + 'hierarchical' => false, + 'rewrite' => false, + 'query_var' => false, + 'delete_with_user' => false, + 'can_export' => false, + 'supports' => array(), + ) + ); +} +add_action( 'init', 'image_loading_optimization_register_page_metrics_post_type' ); From b7396c8106c5d224bc1853aa14a4a0144ac0da4e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 6 Nov 2023 17:15:32 -0800 Subject: [PATCH 09/62] WIP --- composer.json | 3 +- .../image-loading-optimization/rest-api.php | 2 +- .../image-loading-optimization/storage.php | 189 +++++++++++++++++- 3 files changed, 191 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 8ddca6af09..27cd7d9afc 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,8 @@ }, "require": { "composer/installers": "~1.0", - "php": ">=7|^8" + "php": ">=7|^8", + "ext-json": "*" }, "scripts": { "phpstan": "phpstan analyze --ansi --memory-limit=2048M", diff --git a/modules/images/image-loading-optimization/rest-api.php b/modules/images/image-loading-optimization/rest-api.php index 0746ff1315..a347f75d29 100644 --- a/modules/images/image-loading-optimization/rest-api.php +++ b/modules/images/image-loading-optimization/rest-api.php @@ -32,7 +32,7 @@ function image_loading_optimization_register_endpoint() { register_rest_route( 'perflab/v1', - '/image-loading-optimization/metrics-storage', + '/image-loading-optimization/metrics-storage', // @todo or rather metric-storage? array( 'methods' => 'POST', 'callback' => 'image_loading_optimization_handle_rest_request', diff --git a/modules/images/image-loading-optimization/storage.php b/modules/images/image-loading-optimization/storage.php index 49a33ccd75..c9413609b3 100644 --- a/modules/images/image-loading-optimization/storage.php +++ b/modules/images/image-loading-optimization/storage.php @@ -29,6 +29,22 @@ function image_loading_optimization_get_metrics_storage_lock_ttl() { return (int) apply_filters( 'perflab_image_loading_optimization_metrics_storage_lock_ttl', MINUTE_IN_SECONDS ); } +/** + * Gets the maximum width for a viewport to be considered as a mobile device. + * + * @todo This could instead return an array of thresholds, like [ 320, 480, 576 ] which would add additional buckets for small smartphones and phablets in addition to normal smartphones and desktops. + * @return int Viewport width. + */ +function image_loading_optimization_get_max_mobile_viewport_width() { + + /** + * Filters the maximum width for a viewport to be considered as a mobile device. + * + * @param int $mobile_max_width Mobile max width. + */ + return (int) apply_filters( 'perflab_image_loading_optimization_max_mobile_viewport_with', 480 ); +} + /** * Gets transient key for locking metrics storage (for the current IP). * @@ -89,8 +105,179 @@ function image_loading_optimization_register_page_metrics_post_type() { 'query_var' => false, 'delete_with_user' => false, 'can_export' => false, - 'supports' => array(), + 'supports' => array( 'title' ), // The original URL is stored in the post_title, and the MD5 hash in the post_name. ) ); } add_action( 'init', 'image_loading_optimization_register_page_metrics_post_type' ); + +/** + * Gets desired sample size for a viewport's page metrics. + * + * @return int + */ +function image_loading_optimization_get_page_metrics_viewport_sample_size() { + /** + * Filters desired sample size for a viewport's page metrics. + * + * @param int $sample_size Sample size. + */ + return (int) apply_filters( 'perflab_image_loading_optimization_page_metrics_viewport_sample_size', 10 ); +} + +/** + * Get slug for page metrics post. + * + * @param string $url URL. + * @return string Slug for URL. + */ +function image_loading_optimization_get_page_metrics_slug( $url ) { + return md5( $url ); +} + +/** + * Get page metrics post. + * + * @param string $url URL. + * @return WP_Post|null Post object if exists. + */ +function image_loading_optimization_get_page_metrics_post( $url ) { + $post_query = new WP_Query( + array( + 'post_type' => IMAGE_LOADING_OPTIMIZATION_PAGE_METRICS_POST_TYPE, + 'post_status' => 'publish', + 'name' => image_loading_optimization_get_page_metrics_slug( $url ), + 'posts_per_page' => 1, + 'no_found_rows' => true, + 'cache_results' => true, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + 'lazy_load_term_meta' => false, + ) + ); + + $post = array_shift( $post_query->posts ); + if ( $post instanceof WP_Post ) { + return $post; + } else { + return null; + } +} + +/** + * Store page metrics. + * + * @param WP_Post $post Page metrics post. + * @return array|WP_Error Page metrics when valid, or WP_Error otherwise. + */ +function image_loading_optimization_parse_stored_page_metrics( WP_Post $post ) { + $page_metrics = json_decode( $post->post_content, true ); + if ( json_last_error() ) { + return new WP_Error( + 'page_metrics_json_parse_error', + sprintf( + /* translators: 1: Post type slug, 2: JSON error message */ + __( 'Contents of %1$s post type not valid JSON: %2$s', 'performance-lab' ), + IMAGE_LOADING_OPTIMIZATION_PAGE_METRICS_POST_TYPE, + json_last_error_msg() + ) + ); + } + if ( ! is_array( $page_metrics ) ) { + return new WP_Error( + 'page_metrics_invalid_data_format', + sprintf( + /* translators: %s is post type slug */ + __( 'Contents of %s post type was not a JSON array.', 'performance-lab' ), + IMAGE_LOADING_OPTIMIZATION_PAGE_METRICS_POST_TYPE + ) + ); + } + return $page_metrics; +} + +/** + * + * @todo This needs to take a set of page metrics and segment the individual metrics into breakpoints. + * + * @return void + */ +function image_loading_optimization_segment_stored_page_metrics() { + +} + +/** + * Store page metrics. + * + * The $validated_page_metrics parameter has the following array shape: + * + * { + * 'url': string, + * 'viewport': array{ + * 'width': int, + * 'height': int + * }, + * 'elements': array + * } + * + * @param array $validated_page_metrics Page metrics, already validated by REST API. + * @return true|WP_Error True on success or WP_Error otherwise. + */ +function image_loading_optimization_store_page_metrics( array $validated_page_metrics ) { + $url = $validated_page_metrics['url']; + unset( $validated_page_metrics['url'] ); // Not stored in post_content but rather in post_title/post_name. + + // TODO: What about storing a version identifier? + $post_data = array( + 'post_title' => $url, + ); + + $post = image_loading_optimization_get_page_metrics_post( $url ); + + if ( $post instanceof WP_Post ) { + $post_data['ID'] = $post->ID; + $post_data['post_name'] = $post->post_name; + + $page_metrics = image_loading_optimization_parse_stored_page_metrics( $post ); + if ( $page_metrics instanceof WP_Error ) { + if ( function_exists( 'wp_trigger_error' ) ) { + wp_trigger_error( __FUNCTION__, esc_html( $page_metrics->get_error_message() ) ); + } + $page_metrics = array(); + } + } else { + $post_data['post_name'] = image_loading_optimization_get_page_metrics_slug( $url ); + $page_metrics = array(); + } + + // TODO: Unshift the first metrics entry if we are currently at the max allowed. + $segmented_page_metrics = + + $mobile_max_width = image_loading_optimization_get_max_mobile_viewport_width(); + $viewport_sample_size = image_loading_optimization_get_page_metrics_viewport_sample_size(); + + $viewport_page_metrics = array(); + + $existing_storage[] = $validated_page_metrics; + + + $post_data['post_content'] = wp_json_encode( $validated_page_metrics ); + + $has_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ); + if ( $has_kses ) { + // Prevent KSES from corrupting JSON in post_content. + kses_remove_filters(); + } + + if ( isset( $post_data['ID'] ) ) { + $result = wp_update_post( wp_slash( $post_data ), true ); + } else { + $result = wp_insert_post( wp_slash( $post_data ), true ); + } + + if ( $has_kses ) { + kses_init_filters(); + } + + return $result instanceof WP_Error ? $result : true; +} From 0e5b79f660970bfa1d4dffdf2bcdebfb0099bb3f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 6 Nov 2023 19:53:06 -0800 Subject: [PATCH 10/62] Fix storage and improve naming --- .../image-loading-optimization/hooks.php | 4 +- .../image-loading-optimization/rest-api.php | 31 +++++--- .../image-loading-optimization/storage.php | 74 ++++++++++--------- 3 files changed, 61 insertions(+), 48 deletions(-) diff --git a/modules/images/image-loading-optimization/hooks.php b/modules/images/image-loading-optimization/hooks.php index 88dd70635b..43fa5a1686 100644 --- a/modules/images/image-loading-optimization/hooks.php +++ b/modules/images/image-loading-optimization/hooks.php @@ -55,7 +55,7 @@ static function ( $output ) { */ function image_loading_optimization_print_detection_script() { - if ( image_loading_optimization_is_metrics_storage_locked() ) { + if ( image_loading_optimization_is_page_metric_storage_locked() ) { return; } @@ -80,7 +80,7 @@ function image_loading_optimization_print_detection_script() { $serve_time, $detection_time_window, WP_DEBUG, - rest_url( '/perflab/v1/image-loading-optimization/metrics-storage' ), + rest_url( IMAGE_LOADING_OPTIMIZATION_REST_API_NAMESPACE . IMAGE_LOADING_OPTIMIZATION_PAGE_METRIC_STORAGE_ROUTE ), wp_create_nonce( 'wp_rest' ), ); wp_print_inline_script_tag( diff --git a/modules/images/image-loading-optimization/rest-api.php b/modules/images/image-loading-optimization/rest-api.php index a347f75d29..6f33a8c17b 100644 --- a/modules/images/image-loading-optimization/rest-api.php +++ b/modules/images/image-loading-optimization/rest-api.php @@ -10,8 +10,11 @@ exit; // Exit if accessed directly. } +define( 'IMAGE_LOADING_OPTIMIZATION_REST_API_NAMESPACE', 'image-loading-optimization/v1' ); +define( 'IMAGE_LOADING_OPTIMIZATION_PAGE_METRIC_STORAGE_ROUTE', '/image-loading-optimization/page-metric-storage' ); + /** - * Register endpoint for storage of metrics. + * Register endpoint for storage of page metric. */ function image_loading_optimization_register_endpoint() { @@ -31,17 +34,17 @@ function image_loading_optimization_register_endpoint() { ); register_rest_route( - 'perflab/v1', - '/image-loading-optimization/metrics-storage', // @todo or rather metric-storage? + IMAGE_LOADING_OPTIMIZATION_REST_API_NAMESPACE, + IMAGE_LOADING_OPTIMIZATION_PAGE_METRIC_STORAGE_ROUTE, array( 'methods' => 'POST', 'callback' => 'image_loading_optimization_handle_rest_request', 'permission_callback' => static function () { // Needs to be available to unauthenticated visitors. - if ( image_loading_optimization_is_metrics_storage_locked() ) { + if ( image_loading_optimization_is_page_metric_storage_locked() ) { return new WP_Error( - 'metrics_storage_locked', - __( 'Metrics storage is presently locked for the current IP.', 'performance-lab' ), + 'page_metric_storage_locked', + __( 'Page metric storage is presently locked for the current IP.', 'performance-lab' ), array( 'status' => 403 ) ); } @@ -130,18 +133,26 @@ function image_loading_optimization_register_endpoint() { * Handle REST API request to store metrics. * * @param WP_REST_Request $request Request. - * @return WP_REST_Response Response. + * @return WP_REST_Response|WP_Error Response. */ function image_loading_optimization_handle_rest_request( WP_REST_Request $request ) { // TODO: We need storage. - image_loading_optimization_set_metrics_storage_lock(); + image_loading_optimization_set_page_metric_storage_lock(); + + $result = image_loading_optimization_store_page_metric( $request->get_json_params() ); + + if ( $result instanceof WP_Error ) { + return $result; + } - return new WP_REST_Response( + $response = new WP_REST_Response( array( 'success' => true, - 'body' => $request->get_json_params(), + 'post_id' => $result, ) ); + $response->set_status( 201 ); + return $response; } diff --git a/modules/images/image-loading-optimization/storage.php b/modules/images/image-loading-optimization/storage.php index c9413609b3..9f430b9dd4 100644 --- a/modules/images/image-loading-optimization/storage.php +++ b/modules/images/image-loading-optimization/storage.php @@ -13,16 +13,16 @@ define( 'IMAGE_LOADING_OPTIMIZATION_PAGE_METRICS_POST_TYPE', 'ilo_page_metrics' ); /** - * Gets the TTL for the metrics storage lock. + * Gets the TTL for the page metric storage lock. * * @return int TTL. */ -function image_loading_optimization_get_metrics_storage_lock_ttl() { +function image_loading_optimization_get_page_metric_storage_lock_ttl() { /** - * Filters how long a given IP is locked from submitting another metrics-storage REST API request. + * Filters how long a given IP is locked from submitting another metric-storage REST API request. * - * Filtering the TTL to zero will disable any metrics storage locking. This is useful during development. + * Filtering the TTL to zero will disable any metric storage locking. This is useful during development. * * @param int $ttl TTL. */ @@ -46,22 +46,22 @@ function image_loading_optimization_get_max_mobile_viewport_width() { } /** - * Gets transient key for locking metrics storage (for the current IP). + * Gets transient key for locking page metric storage (for the current IP). * * @todo Should the URL be included in the key? Or should a user only be allowed to store one metric? * @return string Transient key. */ -function image_loading_optimization_get_metrics_storage_lock_transient_key() { +function image_loading_optimization_get_page_metric_storage_lock_transient_key() { $ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR']; return 'page_metrics_storage_lock_' . wp_hash( $ip_address ); } /** - * Sets metrics storage lock (for the current IP). + * Sets page metric storage lock (for the current IP). */ -function image_loading_optimization_set_metrics_storage_lock() { - $ttl = image_loading_optimization_get_metrics_storage_lock_ttl(); - $key = image_loading_optimization_get_metrics_storage_lock_transient_key(); +function image_loading_optimization_set_page_metric_storage_lock() { + $ttl = image_loading_optimization_get_page_metric_storage_lock_ttl(); + $key = image_loading_optimization_get_page_metric_storage_lock_transient_key(); if ( 0 === $ttl ) { delete_transient( $key ); } else { @@ -70,16 +70,16 @@ function image_loading_optimization_set_metrics_storage_lock() { } /** - * Checks whether metrics storage is locked (for the current IP). + * Checks whether page metric storage is locked (for the current IP). * * @return bool Whether locked. */ -function image_loading_optimization_is_metrics_storage_locked() { - $ttl = image_loading_optimization_get_metrics_storage_lock_ttl(); +function image_loading_optimization_is_page_metric_storage_locked() { + $ttl = image_loading_optimization_get_page_metric_storage_lock_ttl(); if ( 0 === $ttl ) { return false; } - $locked_time = (int) get_transient( image_loading_optimization_get_metrics_storage_lock_transient_key() ); + $locked_time = (int) get_transient( image_loading_optimization_get_page_metric_storage_lock_transient_key() ); if ( 0 === $locked_time ) { return false; } @@ -87,7 +87,7 @@ function image_loading_optimization_is_metrics_storage_locked() { } /** - * Register post type for metrics storage. + * Register post type for page metrics storage. * * This the configuration for this post type is similar to the oembed_cache in core. */ @@ -126,7 +126,7 @@ function image_loading_optimization_get_page_metrics_viewport_sample_size() { } /** - * Get slug for page metrics post. + * Gets slug for page metrics post. * * @param string $url URL. * @return string Slug for URL. @@ -165,7 +165,7 @@ function image_loading_optimization_get_page_metrics_post( $url ) { } /** - * Store page metrics. + * Parses post content in page metrics post. * * @param WP_Post $post Page metrics post. * @return array|WP_Error Page metrics when valid, or WP_Error otherwise. @@ -202,14 +202,14 @@ function image_loading_optimization_parse_stored_page_metrics( WP_Post $post ) { * * @return void */ -function image_loading_optimization_segment_stored_page_metrics() { +function image_loading_optimization_segment_stored_page_metrics( array $page_metrics, array $breakpoints ) { } /** - * Store page metrics. + * Stores page metric by merging it with the other page metrics for a given URL. * - * The $validated_page_metrics parameter has the following array shape: + * The $validated_page_metric parameter has the following array shape: * * { * 'url': string, @@ -220,12 +220,14 @@ function image_loading_optimization_segment_stored_page_metrics() { * 'elements': array * } * - * @param array $validated_page_metrics Page metrics, already validated by REST API. - * @return true|WP_Error True on success or WP_Error otherwise. + * @param array $validated_page_metric Page metric, already validated by REST API. + * + * @return int|WP_Error Post ID or WP_Error otherwise. */ -function image_loading_optimization_store_page_metrics( array $validated_page_metrics ) { - $url = $validated_page_metrics['url']; - unset( $validated_page_metrics['url'] ); // Not stored in post_content but rather in post_title/post_name. +function image_loading_optimization_store_page_metric( array $validated_page_metric ) { + $url = $validated_page_metric['url']; + unset( $validated_page_metric['url'] ); // Not stored in post_content but rather in post_title/post_name. + $validated_page_metric['timestamp'] = time(); // TODO: What about storing a version identifier? $post_data = array( @@ -250,18 +252,16 @@ function image_loading_optimization_store_page_metrics( array $validated_page_me $page_metrics = array(); } - // TODO: Unshift the first metrics entry if we are currently at the max allowed. - $segmented_page_metrics = - - $mobile_max_width = image_loading_optimization_get_max_mobile_viewport_width(); + // Add the provided page metric to the page metrics. + // TODO: Need to implement viewport breakpoint segmenting. + // $segmented_page_metrics = + // $mobile_max_width = image_loading_optimization_get_max_mobile_viewport_width(); $viewport_sample_size = image_loading_optimization_get_page_metrics_viewport_sample_size(); + // $viewport_page_metrics = array(); + $page_metrics = array_slice( $page_metrics, 0, $viewport_sample_size - 1 ); // Make room for the additional page metric. + array_unshift( $page_metrics, $validated_page_metric ); - $viewport_page_metrics = array(); - - $existing_storage[] = $validated_page_metrics; - - - $post_data['post_content'] = wp_json_encode( $validated_page_metrics ); + $post_data['post_content'] = wp_json_encode( $page_metrics, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); // TODO: No need for pretty-printing. $has_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ); if ( $has_kses ) { @@ -269,6 +269,8 @@ function image_loading_optimization_store_page_metrics( array $validated_page_me kses_remove_filters(); } + $post_data['post_type'] = IMAGE_LOADING_OPTIMIZATION_PAGE_METRICS_POST_TYPE; + $post_data['post_status'] = 'publish'; if ( isset( $post_data['ID'] ) ) { $result = wp_update_post( wp_slash( $post_data ), true ); } else { @@ -279,5 +281,5 @@ function image_loading_optimization_store_page_metrics( array $validated_page_me kses_init_filters(); } - return $result instanceof WP_Error ? $result : true; + return $result; } From 739a53e55f461cada695d4a24e9dccfc95f6567b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 6 Nov 2023 20:01:40 -0800 Subject: [PATCH 11/62] Use ILO consistently as the prefix --- .../image-loading-optimization/hooks.php | 12 ++-- .../image-loading-optimization/load.php | 6 +- .../image-loading-optimization/rest-api.php | 22 +++---- .../image-loading-optimization/storage.php | 64 +++++++++---------- 4 files changed, 52 insertions(+), 52 deletions(-) diff --git a/modules/images/image-loading-optimization/hooks.php b/modules/images/image-loading-optimization/hooks.php index 43fa5a1686..718cfe8853 100644 --- a/modules/images/image-loading-optimization/hooks.php +++ b/modules/images/image-loading-optimization/hooks.php @@ -31,7 +31,7 @@ * @param mixed $passthrough Optional. Filter value. Default null. * @return mixed Unmodified value of $passthrough. */ -function image_loading_optimization_buffer_output( $passthrough = null ) { +function ilo_buffer_output( $passthrough = null ) { ob_start( static function ( $output ) { /** @@ -45,7 +45,7 @@ static function ( $output ) { ); return $passthrough; } -add_filter( 'template_include', 'image_loading_optimization_buffer_output', PHP_INT_MAX ); +add_filter( 'template_include', 'ilo_buffer_output', PHP_INT_MAX ); /** * Prints the script for detecting loaded images and the LCP element. @@ -53,9 +53,9 @@ static function ( $output ) { * @todo This should eventually only print the script if metrics are needed. * @todo This script should not be printed if the page was requested with non-removal (non-canonical) query args. */ -function image_loading_optimization_print_detection_script() { +function ilo_print_detection_script() { - if ( image_loading_optimization_is_page_metric_storage_locked() ) { + if ( ilo_is_page_metric_storage_locked() ) { return; } @@ -80,7 +80,7 @@ function image_loading_optimization_print_detection_script() { $serve_time, $detection_time_window, WP_DEBUG, - rest_url( IMAGE_LOADING_OPTIMIZATION_REST_API_NAMESPACE . IMAGE_LOADING_OPTIMIZATION_PAGE_METRIC_STORAGE_ROUTE ), + rest_url( ILO_REST_API_NAMESPACE . ILO_PAGE_METRIC_STORAGE_ROUTE ), wp_create_nonce( 'wp_rest' ), ); wp_print_inline_script_tag( @@ -92,4 +92,4 @@ function image_loading_optimization_print_detection_script() { array( 'type' => 'module' ) ); } -add_action( 'wp_print_footer_scripts', 'image_loading_optimization_print_detection_script' ); +add_action( 'wp_print_footer_scripts', 'ilo_print_detection_script' ); diff --git a/modules/images/image-loading-optimization/load.php b/modules/images/image-loading-optimization/load.php index 0828ab47bb..2aaace8afa 100644 --- a/modules/images/image-loading-optimization/load.php +++ b/modules/images/image-loading-optimization/load.php @@ -9,14 +9,14 @@ */ // Define the constant. -if ( defined( 'IMAGE_LOADING_OPTIMIZATION_VERSION' ) ) { +if ( defined( 'ILO_VERSION' ) ) { return; } -define( 'IMAGE_LOADING_OPTIMIZATION_VERSION', 'Performance Lab ' . PERFLAB_VERSION ); +define( 'ILO_VERSION', 'Performance Lab ' . PERFLAB_VERSION ); // Do not load the code if it is already loaded through another means. -if ( function_exists( 'image_loading_optimization_buffer_output' ) ) { +if ( function_exists( 'ilo_buffer_output' ) ) { return; } diff --git a/modules/images/image-loading-optimization/rest-api.php b/modules/images/image-loading-optimization/rest-api.php index 6f33a8c17b..672f5cf9e8 100644 --- a/modules/images/image-loading-optimization/rest-api.php +++ b/modules/images/image-loading-optimization/rest-api.php @@ -10,13 +10,13 @@ exit; // Exit if accessed directly. } -define( 'IMAGE_LOADING_OPTIMIZATION_REST_API_NAMESPACE', 'image-loading-optimization/v1' ); -define( 'IMAGE_LOADING_OPTIMIZATION_PAGE_METRIC_STORAGE_ROUTE', '/image-loading-optimization/page-metric-storage' ); +define( 'ILO_REST_API_NAMESPACE', 'image-loading-optimization/v1' ); +define( 'ILO_PAGE_METRIC_STORAGE_ROUTE', '/image-loading-optimization/page-metric-storage' ); /** * Register endpoint for storage of page metric. */ -function image_loading_optimization_register_endpoint() { +function ilo_register_endpoint() { $dom_rect_schema = array( 'type' => 'object', @@ -34,14 +34,14 @@ function image_loading_optimization_register_endpoint() { ); register_rest_route( - IMAGE_LOADING_OPTIMIZATION_REST_API_NAMESPACE, - IMAGE_LOADING_OPTIMIZATION_PAGE_METRIC_STORAGE_ROUTE, + ILO_REST_API_NAMESPACE, + ILO_PAGE_METRIC_STORAGE_ROUTE, array( 'methods' => 'POST', - 'callback' => 'image_loading_optimization_handle_rest_request', + 'callback' => 'ilo_handle_rest_request', 'permission_callback' => static function () { // Needs to be available to unauthenticated visitors. - if ( image_loading_optimization_is_page_metric_storage_locked() ) { + if ( ilo_is_page_metric_storage_locked() ) { return new WP_Error( 'page_metric_storage_locked', __( 'Page metric storage is presently locked for the current IP.', 'performance-lab' ), @@ -127,7 +127,7 @@ function image_loading_optimization_register_endpoint() { ) ); } -add_action( 'rest_api_init', 'image_loading_optimization_register_endpoint' ); +add_action( 'rest_api_init', 'ilo_register_endpoint' ); /** * Handle REST API request to store metrics. @@ -135,13 +135,13 @@ function image_loading_optimization_register_endpoint() { * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error Response. */ -function image_loading_optimization_handle_rest_request( WP_REST_Request $request ) { +function ilo_handle_rest_request( WP_REST_Request $request ) { // TODO: We need storage. - image_loading_optimization_set_page_metric_storage_lock(); + ilo_set_page_metric_storage_lock(); - $result = image_loading_optimization_store_page_metric( $request->get_json_params() ); + $result = ilo_store_page_metric( $request->get_json_params() ); if ( $result instanceof WP_Error ) { return $result; diff --git a/modules/images/image-loading-optimization/storage.php b/modules/images/image-loading-optimization/storage.php index 9f430b9dd4..8e92fc2e01 100644 --- a/modules/images/image-loading-optimization/storage.php +++ b/modules/images/image-loading-optimization/storage.php @@ -10,14 +10,14 @@ exit; // Exit if accessed directly. } -define( 'IMAGE_LOADING_OPTIMIZATION_PAGE_METRICS_POST_TYPE', 'ilo_page_metrics' ); +define( 'ILO_PAGE_METRICS_POST_TYPE', 'ilo_page_metrics' ); /** * Gets the TTL for the page metric storage lock. * * @return int TTL. */ -function image_loading_optimization_get_page_metric_storage_lock_ttl() { +function ilo_get_page_metric_storage_lock_ttl() { /** * Filters how long a given IP is locked from submitting another metric-storage REST API request. @@ -26,7 +26,7 @@ function image_loading_optimization_get_page_metric_storage_lock_ttl() { * * @param int $ttl TTL. */ - return (int) apply_filters( 'perflab_image_loading_optimization_metrics_storage_lock_ttl', MINUTE_IN_SECONDS ); + return (int) apply_filters( 'ilo_metrics_storage_lock_ttl', MINUTE_IN_SECONDS ); } /** @@ -35,14 +35,14 @@ function image_loading_optimization_get_page_metric_storage_lock_ttl() { * @todo This could instead return an array of thresholds, like [ 320, 480, 576 ] which would add additional buckets for small smartphones and phablets in addition to normal smartphones and desktops. * @return int Viewport width. */ -function image_loading_optimization_get_max_mobile_viewport_width() { +function ilo_get_max_mobile_viewport_width() { /** * Filters the maximum width for a viewport to be considered as a mobile device. * * @param int $mobile_max_width Mobile max width. */ - return (int) apply_filters( 'perflab_image_loading_optimization_max_mobile_viewport_with', 480 ); + return (int) apply_filters( 'ilo_max_mobile_viewport_with', 480 ); } /** @@ -51,7 +51,7 @@ function image_loading_optimization_get_max_mobile_viewport_width() { * @todo Should the URL be included in the key? Or should a user only be allowed to store one metric? * @return string Transient key. */ -function image_loading_optimization_get_page_metric_storage_lock_transient_key() { +function ilo_get_page_metric_storage_lock_transient_key() { $ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR']; return 'page_metrics_storage_lock_' . wp_hash( $ip_address ); } @@ -59,9 +59,9 @@ function image_loading_optimization_get_page_metric_storage_lock_transient_key() /** * Sets page metric storage lock (for the current IP). */ -function image_loading_optimization_set_page_metric_storage_lock() { - $ttl = image_loading_optimization_get_page_metric_storage_lock_ttl(); - $key = image_loading_optimization_get_page_metric_storage_lock_transient_key(); +function ilo_set_page_metric_storage_lock() { + $ttl = ilo_get_page_metric_storage_lock_ttl(); + $key = ilo_get_page_metric_storage_lock_transient_key(); if ( 0 === $ttl ) { delete_transient( $key ); } else { @@ -74,12 +74,12 @@ function image_loading_optimization_set_page_metric_storage_lock() { * * @return bool Whether locked. */ -function image_loading_optimization_is_page_metric_storage_locked() { - $ttl = image_loading_optimization_get_page_metric_storage_lock_ttl(); +function ilo_is_page_metric_storage_locked() { + $ttl = ilo_get_page_metric_storage_lock_ttl(); if ( 0 === $ttl ) { return false; } - $locked_time = (int) get_transient( image_loading_optimization_get_page_metric_storage_lock_transient_key() ); + $locked_time = (int) get_transient( ilo_get_page_metric_storage_lock_transient_key() ); if ( 0 === $locked_time ) { return false; } @@ -91,9 +91,9 @@ function image_loading_optimization_is_page_metric_storage_locked() { * * This the configuration for this post type is similar to the oembed_cache in core. */ -function image_loading_optimization_register_page_metrics_post_type() { +function ilo_register_page_metrics_post_type() { register_post_type( - IMAGE_LOADING_OPTIMIZATION_PAGE_METRICS_POST_TYPE, + ILO_PAGE_METRICS_POST_TYPE, array( 'labels' => array( 'name' => __( 'Page Metrics', 'performance-lab' ), @@ -109,20 +109,20 @@ function image_loading_optimization_register_page_metrics_post_type() { ) ); } -add_action( 'init', 'image_loading_optimization_register_page_metrics_post_type' ); +add_action( 'init', 'ilo_register_page_metrics_post_type' ); /** * Gets desired sample size for a viewport's page metrics. * * @return int */ -function image_loading_optimization_get_page_metrics_viewport_sample_size() { +function ilo_get_page_metrics_viewport_sample_size() { /** * Filters desired sample size for a viewport's page metrics. * * @param int $sample_size Sample size. */ - return (int) apply_filters( 'perflab_image_loading_optimization_page_metrics_viewport_sample_size', 10 ); + return (int) apply_filters( 'ilo_page_metrics_viewport_sample_size', 10 ); } /** @@ -131,7 +131,7 @@ function image_loading_optimization_get_page_metrics_viewport_sample_size() { * @param string $url URL. * @return string Slug for URL. */ -function image_loading_optimization_get_page_metrics_slug( $url ) { +function ilo_get_page_metrics_slug( $url ) { return md5( $url ); } @@ -141,12 +141,12 @@ function image_loading_optimization_get_page_metrics_slug( $url ) { * @param string $url URL. * @return WP_Post|null Post object if exists. */ -function image_loading_optimization_get_page_metrics_post( $url ) { +function ilo_get_page_metrics_post( $url ) { $post_query = new WP_Query( array( - 'post_type' => IMAGE_LOADING_OPTIMIZATION_PAGE_METRICS_POST_TYPE, + 'post_type' => ILO_PAGE_METRICS_POST_TYPE, 'post_status' => 'publish', - 'name' => image_loading_optimization_get_page_metrics_slug( $url ), + 'name' => ilo_get_page_metrics_slug( $url ), 'posts_per_page' => 1, 'no_found_rows' => true, 'cache_results' => true, @@ -170,7 +170,7 @@ function image_loading_optimization_get_page_metrics_post( $url ) { * @param WP_Post $post Page metrics post. * @return array|WP_Error Page metrics when valid, or WP_Error otherwise. */ -function image_loading_optimization_parse_stored_page_metrics( WP_Post $post ) { +function ilo_parse_stored_page_metrics( WP_Post $post ) { $page_metrics = json_decode( $post->post_content, true ); if ( json_last_error() ) { return new WP_Error( @@ -178,7 +178,7 @@ function image_loading_optimization_parse_stored_page_metrics( WP_Post $post ) { sprintf( /* translators: 1: Post type slug, 2: JSON error message */ __( 'Contents of %1$s post type not valid JSON: %2$s', 'performance-lab' ), - IMAGE_LOADING_OPTIMIZATION_PAGE_METRICS_POST_TYPE, + ILO_PAGE_METRICS_POST_TYPE, json_last_error_msg() ) ); @@ -189,7 +189,7 @@ function image_loading_optimization_parse_stored_page_metrics( WP_Post $post ) { sprintf( /* translators: %s is post type slug */ __( 'Contents of %s post type was not a JSON array.', 'performance-lab' ), - IMAGE_LOADING_OPTIMIZATION_PAGE_METRICS_POST_TYPE + ILO_PAGE_METRICS_POST_TYPE ) ); } @@ -202,7 +202,7 @@ function image_loading_optimization_parse_stored_page_metrics( WP_Post $post ) { * * @return void */ -function image_loading_optimization_segment_stored_page_metrics( array $page_metrics, array $breakpoints ) { +function ilo_segment_stored_page_metrics( array $page_metrics, array $breakpoints ) { } @@ -224,7 +224,7 @@ function image_loading_optimization_segment_stored_page_metrics( array $page_met * * @return int|WP_Error Post ID or WP_Error otherwise. */ -function image_loading_optimization_store_page_metric( array $validated_page_metric ) { +function ilo_store_page_metric( array $validated_page_metric ) { $url = $validated_page_metric['url']; unset( $validated_page_metric['url'] ); // Not stored in post_content but rather in post_title/post_name. $validated_page_metric['timestamp'] = time(); @@ -234,13 +234,13 @@ function image_loading_optimization_store_page_metric( array $validated_page_met 'post_title' => $url, ); - $post = image_loading_optimization_get_page_metrics_post( $url ); + $post = ilo_get_page_metrics_post( $url ); if ( $post instanceof WP_Post ) { $post_data['ID'] = $post->ID; $post_data['post_name'] = $post->post_name; - $page_metrics = image_loading_optimization_parse_stored_page_metrics( $post ); + $page_metrics = ilo_parse_stored_page_metrics( $post ); if ( $page_metrics instanceof WP_Error ) { if ( function_exists( 'wp_trigger_error' ) ) { wp_trigger_error( __FUNCTION__, esc_html( $page_metrics->get_error_message() ) ); @@ -248,15 +248,15 @@ function image_loading_optimization_store_page_metric( array $validated_page_met $page_metrics = array(); } } else { - $post_data['post_name'] = image_loading_optimization_get_page_metrics_slug( $url ); + $post_data['post_name'] = ilo_get_page_metrics_slug( $url ); $page_metrics = array(); } // Add the provided page metric to the page metrics. // TODO: Need to implement viewport breakpoint segmenting. // $segmented_page_metrics = - // $mobile_max_width = image_loading_optimization_get_max_mobile_viewport_width(); - $viewport_sample_size = image_loading_optimization_get_page_metrics_viewport_sample_size(); + // $mobile_max_width = ilo_get_max_mobile_viewport_width(); + $viewport_sample_size = ilo_get_page_metrics_viewport_sample_size(); // $viewport_page_metrics = array(); $page_metrics = array_slice( $page_metrics, 0, $viewport_sample_size - 1 ); // Make room for the additional page metric. array_unshift( $page_metrics, $validated_page_metric ); @@ -269,7 +269,7 @@ function image_loading_optimization_store_page_metric( array $validated_page_met kses_remove_filters(); } - $post_data['post_type'] = IMAGE_LOADING_OPTIMIZATION_PAGE_METRICS_POST_TYPE; + $post_data['post_type'] = ILO_PAGE_METRICS_POST_TYPE; $post_data['post_status'] = 'publish'; if ( isset( $post_data['ID'] ) ) { $result = wp_update_post( wp_slash( $post_data ), true ); From e4d1578188f2b2b11f0ea1dc8d2d9868eaf78eae Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 6 Nov 2023 21:04:57 -0800 Subject: [PATCH 12/62] Segment page metrics by breakpoint and constrain sample size --- .../image-loading-optimization/storage.php | 78 ++++++++++++++----- 1 file changed, 59 insertions(+), 19 deletions(-) diff --git a/modules/images/image-loading-optimization/storage.php b/modules/images/image-loading-optimization/storage.php index 8e92fc2e01..fd49729f28 100644 --- a/modules/images/image-loading-optimization/storage.php +++ b/modules/images/image-loading-optimization/storage.php @@ -30,19 +30,35 @@ function ilo_get_page_metric_storage_lock_ttl() { } /** - * Gets the maximum width for a viewport to be considered as a mobile device. + * Gets the breakpoint max widths to group page metrics for various viewports. * - * @todo This could instead return an array of thresholds, like [ 320, 480, 576 ] which would add additional buckets for small smartphones and phablets in addition to normal smartphones and desktops. - * @return int Viewport width. + * Each max with represents the maximum width (inclusive) for a given breakpoint. So if there is one number, 480, then + * this means there will be two viewport groupings, one for 0<=480, and another >480. If instead there were three + * provided breakpoints (320, 480, 576) then this means there will be four viewport groupings: + * + * 1. 0-320 (small smartphone) + * 2. 321-480 (normal smartphone) + * 3. 481-576 (phablets) + * 4. >576 (desktop) + * + * @return int[] Breakpoint max widths, sorted in ascending order. */ -function ilo_get_max_mobile_viewport_width() { +function ilo_get_breakpoint_max_widths() { /** - * Filters the maximum width for a viewport to be considered as a mobile device. + * Filters the breakpoint max widths to group page metrics for various viewports. * - * @param int $mobile_max_width Mobile max width. + * @param int[] $breakpoint_max_widths Max widths for viewport breakpoints. */ - return (int) apply_filters( 'ilo_max_mobile_viewport_with', 480 ); + $breakpoint_max_widths = array_map( + static function ( $breakpoint_max_width ) { + return (int) $breakpoint_max_width; + }, + (array) apply_filters( 'ilo_viewport_breakpoint_max_widths', array( 480 ) ) + ); + + sort( $breakpoint_max_widths ); + return $breakpoint_max_widths; } /** @@ -116,7 +132,7 @@ function ilo_register_page_metrics_post_type() { * * @return int */ -function ilo_get_page_metrics_viewport_sample_size() { +function ilo_get_page_metrics_breakpoint_sample_size() { /** * Filters desired sample size for a viewport's page metrics. * @@ -197,13 +213,31 @@ function ilo_parse_stored_page_metrics( WP_Post $post ) { } /** + * Groups page metrics by breakpoint. * - * @todo This needs to take a set of page metrics and segment the individual metrics into breakpoints. - * - * @return void + * @param array $page_metrics Page metrics. + * @param int[] $breakpoints Viewport breakpoint max widths, sorted in ascending order. + * @return array Grouped page metrics. */ -function ilo_segment_stored_page_metrics( array $page_metrics, array $breakpoints ) { - +function ilo_group_page_metrics_by_breakpoint( array $page_metrics, array $breakpoints ) { + $max_index = count( $breakpoints ); + $groups = array_fill( 0, $max_index + 1, array() ); + $largest_breakpoint = $breakpoints[ $max_index - 1 ]; + foreach ( $page_metrics as $page_metric ) { + if ( ! isset( $page_metric['viewport']['width'] ) ) { + continue; + } + $viewport_width = $page_metric['viewport']['width']; + if ( $viewport_width > $largest_breakpoint ) { + $groups[ $max_index ][] = $page_metric; + } + foreach ( $breakpoints as $group => $breakpoint ) { + if ( $viewport_width <= $breakpoint ) { + $groups[ $group ][] = $page_metric; + } + } + } + return $groups; } /** @@ -253,14 +287,20 @@ function ilo_store_page_metric( array $validated_page_metric ) { } // Add the provided page metric to the page metrics. - // TODO: Need to implement viewport breakpoint segmenting. - // $segmented_page_metrics = - // $mobile_max_width = ilo_get_max_mobile_viewport_width(); - $viewport_sample_size = ilo_get_page_metrics_viewport_sample_size(); - // $viewport_page_metrics = array(); - $page_metrics = array_slice( $page_metrics, 0, $viewport_sample_size - 1 ); // Make room for the additional page metric. array_unshift( $page_metrics, $validated_page_metric ); + $breakpoints = ilo_get_breakpoint_max_widths(); + $sample_size = ilo_get_page_metrics_breakpoint_sample_size(); + $grouped_page_metrics = ilo_group_page_metrics_by_breakpoint( $page_metrics, $breakpoints ); + + foreach ( $grouped_page_metrics as &$breakpoint_page_metrics ) { + if ( count( $breakpoint_page_metrics ) > $sample_size ) { + $breakpoint_page_metrics = array_slice( $breakpoint_page_metrics, 0, $sample_size ); + } + } + + $page_metrics = array_merge( ...$grouped_page_metrics ); + // TODO: Also need to capture the current theme and template which can be used to invalidate the cached page metrics. $post_data['post_content'] = wp_json_encode( $page_metrics, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); // TODO: No need for pretty-printing. $has_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ); From 2b1a15b519d5e62883c5161a7d8c72b1fb9828e6 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 7 Nov 2023 11:26:37 -0800 Subject: [PATCH 13/62] Improve function ordering --- .../image-loading-optimization/detect.js | 5 +- .../image-loading-optimization/rest-api.php | 3 - .../image-loading-optimization/storage.php | 62 +++++++++---------- 3 files changed, 33 insertions(+), 37 deletions(-) diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detect.js index 1bc18b8db2..5971637f13 100644 --- a/modules/images/image-loading-optimization/detect.js +++ b/modules/images/image-loading-optimization/detect.js @@ -289,6 +289,8 @@ export default async function detect( pageMetrics.elements.push( elementMetrics ); } + log( pageMetrics ); + // TODO: Wait until idle. const response = await fetch( restApiEndpoint, { method: 'POST', @@ -300,9 +302,6 @@ export default async function detect( } ); log( 'response:', await response.json() ); - // TODO: Send data to server. - log( pageMetrics ); - // Clean up. breadcrumbedElementsMap.clear(); } diff --git a/modules/images/image-loading-optimization/rest-api.php b/modules/images/image-loading-optimization/rest-api.php index 672f5cf9e8..74967a0331 100644 --- a/modules/images/image-loading-optimization/rest-api.php +++ b/modules/images/image-loading-optimization/rest-api.php @@ -136,9 +136,6 @@ function ilo_register_endpoint() { * @return WP_REST_Response|WP_Error Response. */ function ilo_handle_rest_request( WP_REST_Request $request ) { - - // TODO: We need storage. - ilo_set_page_metric_storage_lock(); $result = ilo_store_page_metric( $request->get_json_params() ); diff --git a/modules/images/image-loading-optimization/storage.php b/modules/images/image-loading-optimization/storage.php index fd49729f28..fd7ea9fd88 100644 --- a/modules/images/image-loading-optimization/storage.php +++ b/modules/images/image-loading-optimization/storage.php @@ -12,23 +12,6 @@ define( 'ILO_PAGE_METRICS_POST_TYPE', 'ilo_page_metrics' ); -/** - * Gets the TTL for the page metric storage lock. - * - * @return int TTL. - */ -function ilo_get_page_metric_storage_lock_ttl() { - - /** - * Filters how long a given IP is locked from submitting another metric-storage REST API request. - * - * Filtering the TTL to zero will disable any metric storage locking. This is useful during development. - * - * @param int $ttl TTL. - */ - return (int) apply_filters( 'ilo_metrics_storage_lock_ttl', MINUTE_IN_SECONDS ); -} - /** * Gets the breakpoint max widths to group page metrics for various viewports. * @@ -61,6 +44,37 @@ static function ( $breakpoint_max_width ) { return $breakpoint_max_widths; } +/** + * Gets desired sample size for a viewport's page metrics. + * + * @return int Sample size. + */ +function ilo_get_page_metrics_breakpoint_sample_size() { + /** + * Filters desired sample size for a viewport's page metrics. + * + * @param int $sample_size Sample size. + */ + return (int) apply_filters( 'ilo_page_metrics_viewport_sample_size', 10 ); +} + +/** + * Gets the TTL for the page metric storage lock. + * + * @return int TTL. + */ +function ilo_get_page_metric_storage_lock_ttl() { + + /** + * Filters how long a given IP is locked from submitting another metric-storage REST API request. + * + * Filtering the TTL to zero will disable any metric storage locking. This is useful during development. + * + * @param int $ttl TTL. + */ + return (int) apply_filters( 'ilo_metrics_storage_lock_ttl', MINUTE_IN_SECONDS ); +} + /** * Gets transient key for locking page metric storage (for the current IP). * @@ -127,20 +141,6 @@ function ilo_register_page_metrics_post_type() { } add_action( 'init', 'ilo_register_page_metrics_post_type' ); -/** - * Gets desired sample size for a viewport's page metrics. - * - * @return int - */ -function ilo_get_page_metrics_breakpoint_sample_size() { - /** - * Filters desired sample size for a viewport's page metrics. - * - * @param int $sample_size Sample size. - */ - return (int) apply_filters( 'ilo_page_metrics_viewport_sample_size', 10 ); -} - /** * Gets slug for page metrics post. * From f516af85f463f36cf009a76ab8075475599a268a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 7 Nov 2023 20:03:46 -0800 Subject: [PATCH 14/62] Add ilo_get_page_metric_ttl --- .../image-loading-optimization/hooks.php | 1 + .../image-loading-optimization/storage.php | 25 ++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/modules/images/image-loading-optimization/hooks.php b/modules/images/image-loading-optimization/hooks.php index 718cfe8853..59b98e61a7 100644 --- a/modules/images/image-loading-optimization/hooks.php +++ b/modules/images/image-loading-optimization/hooks.php @@ -55,6 +55,7 @@ static function ( $output ) { */ function ilo_print_detection_script() { + // TODO: Also abort if we don't need any new page metrics due to the sample size being full. if ( ilo_is_page_metric_storage_locked() ) { return; } diff --git a/modules/images/image-loading-optimization/storage.php b/modules/images/image-loading-optimization/storage.php index fd7ea9fd88..606200553a 100644 --- a/modules/images/image-loading-optimization/storage.php +++ b/modules/images/image-loading-optimization/storage.php @@ -37,7 +37,7 @@ function ilo_get_breakpoint_max_widths() { static function ( $breakpoint_max_width ) { return (int) $breakpoint_max_width; }, - (array) apply_filters( 'ilo_viewport_breakpoint_max_widths', array( 480 ) ) + (array) apply_filters( 'ilo_breakpoint_max_widths', array( 480 ) ) ); sort( $breakpoint_max_widths ); @@ -45,7 +45,7 @@ static function ( $breakpoint_max_width ) { } /** - * Gets desired sample size for a viewport's page metrics. + * Gets desired sample size for a breakpoint's page metrics. * * @return int Sample size. */ @@ -55,7 +55,25 @@ function ilo_get_page_metrics_breakpoint_sample_size() { * * @param int $sample_size Sample size. */ - return (int) apply_filters( 'ilo_page_metrics_viewport_sample_size', 10 ); + return (int) apply_filters( 'ilo_page_metrics_breakpoint_sample_size', 10 ); +} + +/** + * Gets the expiration age for a given page metric. + * + * When a page metric expires it is eligible to be replaced by a newer one. + * + * TODO: However, we keep viewport-specific page metrics regardless of TTL. + * + * @return int Expiration age in seconds. + */ +function ilo_get_page_metric_ttl() { + /** + * Filters the expiration age for a given page metric. + * + * @param int $ttl TTL. + */ + return (int) apply_filters( 'ilo_page_metric_ttl', MONTH_IN_SECONDS ); } /** @@ -300,7 +318,6 @@ function ilo_store_page_metric( array $validated_page_metric ) { $page_metrics = array_merge( ...$grouped_page_metrics ); - // TODO: Also need to capture the current theme and template which can be used to invalidate the cached page metrics. $post_data['post_content'] = wp_json_encode( $page_metrics, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); // TODO: No need for pretty-printing. $has_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ); From 2f804613a2dbdfd6a23746bf4d770b5d675de24e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 7 Nov 2023 20:08:12 -0800 Subject: [PATCH 15/62] Move lock functions into separate file --- .../image-loading-optimization/storage.php | 60 +--------------- .../storage/lock.php | 69 +++++++++++++++++++ 2 files changed, 71 insertions(+), 58 deletions(-) create mode 100644 modules/images/image-loading-optimization/storage/lock.php diff --git a/modules/images/image-loading-optimization/storage.php b/modules/images/image-loading-optimization/storage.php index 606200553a..b5d1d37be8 100644 --- a/modules/images/image-loading-optimization/storage.php +++ b/modules/images/image-loading-optimization/storage.php @@ -10,6 +10,8 @@ exit; // Exit if accessed directly. } +require_once __DIR__ . '/storage/lock.php'; + define( 'ILO_PAGE_METRICS_POST_TYPE', 'ilo_page_metrics' ); /** @@ -76,64 +78,6 @@ function ilo_get_page_metric_ttl() { return (int) apply_filters( 'ilo_page_metric_ttl', MONTH_IN_SECONDS ); } -/** - * Gets the TTL for the page metric storage lock. - * - * @return int TTL. - */ -function ilo_get_page_metric_storage_lock_ttl() { - - /** - * Filters how long a given IP is locked from submitting another metric-storage REST API request. - * - * Filtering the TTL to zero will disable any metric storage locking. This is useful during development. - * - * @param int $ttl TTL. - */ - return (int) apply_filters( 'ilo_metrics_storage_lock_ttl', MINUTE_IN_SECONDS ); -} - -/** - * Gets transient key for locking page metric storage (for the current IP). - * - * @todo Should the URL be included in the key? Or should a user only be allowed to store one metric? - * @return string Transient key. - */ -function ilo_get_page_metric_storage_lock_transient_key() { - $ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR']; - return 'page_metrics_storage_lock_' . wp_hash( $ip_address ); -} - -/** - * Sets page metric storage lock (for the current IP). - */ -function ilo_set_page_metric_storage_lock() { - $ttl = ilo_get_page_metric_storage_lock_ttl(); - $key = ilo_get_page_metric_storage_lock_transient_key(); - if ( 0 === $ttl ) { - delete_transient( $key ); - } else { - set_transient( $key, time(), $ttl ); - } -} - -/** - * Checks whether page metric storage is locked (for the current IP). - * - * @return bool Whether locked. - */ -function ilo_is_page_metric_storage_locked() { - $ttl = ilo_get_page_metric_storage_lock_ttl(); - if ( 0 === $ttl ) { - return false; - } - $locked_time = (int) get_transient( ilo_get_page_metric_storage_lock_transient_key() ); - if ( 0 === $locked_time ) { - return false; - } - return time() - $locked_time < $ttl; -} - /** * Register post type for page metrics storage. * diff --git a/modules/images/image-loading-optimization/storage/lock.php b/modules/images/image-loading-optimization/storage/lock.php new file mode 100644 index 0000000000..efbb89424c --- /dev/null +++ b/modules/images/image-loading-optimization/storage/lock.php @@ -0,0 +1,69 @@ + Date: Tue, 7 Nov 2023 20:22:11 -0800 Subject: [PATCH 16/62] Reorganize functions into files --- .../image-loading-optimization/load.php | 1 - .../image-loading-optimization/storage.php | 276 +----------------- .../storage/data.php | 125 ++++++++ .../storage/lock.php | 2 +- .../storage/post-type.php | 180 ++++++++++++ .../{ => storage}/rest-api.php | 3 +- 6 files changed, 311 insertions(+), 276 deletions(-) create mode 100644 modules/images/image-loading-optimization/storage/data.php create mode 100644 modules/images/image-loading-optimization/storage/post-type.php rename modules/images/image-loading-optimization/{ => storage}/rest-api.php (97%) diff --git a/modules/images/image-loading-optimization/load.php b/modules/images/image-loading-optimization/load.php index 2aaace8afa..68f6287195 100644 --- a/modules/images/image-loading-optimization/load.php +++ b/modules/images/image-loading-optimization/load.php @@ -23,4 +23,3 @@ require_once __DIR__ . '/helper.php'; require_once __DIR__ . '/hooks.php'; require_once __DIR__ . '/storage.php'; -require_once __DIR__ . '/rest-api.php'; diff --git a/modules/images/image-loading-optimization/storage.php b/modules/images/image-loading-optimization/storage.php index b5d1d37be8..5ac9fb9220 100644 --- a/modules/images/image-loading-optimization/storage.php +++ b/modules/images/image-loading-optimization/storage.php @@ -11,276 +11,6 @@ } require_once __DIR__ . '/storage/lock.php'; - -define( 'ILO_PAGE_METRICS_POST_TYPE', 'ilo_page_metrics' ); - -/** - * Gets the breakpoint max widths to group page metrics for various viewports. - * - * Each max with represents the maximum width (inclusive) for a given breakpoint. So if there is one number, 480, then - * this means there will be two viewport groupings, one for 0<=480, and another >480. If instead there were three - * provided breakpoints (320, 480, 576) then this means there will be four viewport groupings: - * - * 1. 0-320 (small smartphone) - * 2. 321-480 (normal smartphone) - * 3. 481-576 (phablets) - * 4. >576 (desktop) - * - * @return int[] Breakpoint max widths, sorted in ascending order. - */ -function ilo_get_breakpoint_max_widths() { - - /** - * Filters the breakpoint max widths to group page metrics for various viewports. - * - * @param int[] $breakpoint_max_widths Max widths for viewport breakpoints. - */ - $breakpoint_max_widths = array_map( - static function ( $breakpoint_max_width ) { - return (int) $breakpoint_max_width; - }, - (array) apply_filters( 'ilo_breakpoint_max_widths', array( 480 ) ) - ); - - sort( $breakpoint_max_widths ); - return $breakpoint_max_widths; -} - -/** - * Gets desired sample size for a breakpoint's page metrics. - * - * @return int Sample size. - */ -function ilo_get_page_metrics_breakpoint_sample_size() { - /** - * Filters desired sample size for a viewport's page metrics. - * - * @param int $sample_size Sample size. - */ - return (int) apply_filters( 'ilo_page_metrics_breakpoint_sample_size', 10 ); -} - -/** - * Gets the expiration age for a given page metric. - * - * When a page metric expires it is eligible to be replaced by a newer one. - * - * TODO: However, we keep viewport-specific page metrics regardless of TTL. - * - * @return int Expiration age in seconds. - */ -function ilo_get_page_metric_ttl() { - /** - * Filters the expiration age for a given page metric. - * - * @param int $ttl TTL. - */ - return (int) apply_filters( 'ilo_page_metric_ttl', MONTH_IN_SECONDS ); -} - -/** - * Register post type for page metrics storage. - * - * This the configuration for this post type is similar to the oembed_cache in core. - */ -function ilo_register_page_metrics_post_type() { - register_post_type( - ILO_PAGE_METRICS_POST_TYPE, - array( - 'labels' => array( - 'name' => __( 'Page Metrics', 'performance-lab' ), - 'singular_name' => __( 'Page Metrics', 'performance-lab' ), - ), - 'public' => false, - 'hierarchical' => false, - 'rewrite' => false, - 'query_var' => false, - 'delete_with_user' => false, - 'can_export' => false, - 'supports' => array( 'title' ), // The original URL is stored in the post_title, and the MD5 hash in the post_name. - ) - ); -} -add_action( 'init', 'ilo_register_page_metrics_post_type' ); - -/** - * Gets slug for page metrics post. - * - * @param string $url URL. - * @return string Slug for URL. - */ -function ilo_get_page_metrics_slug( $url ) { - return md5( $url ); -} - -/** - * Get page metrics post. - * - * @param string $url URL. - * @return WP_Post|null Post object if exists. - */ -function ilo_get_page_metrics_post( $url ) { - $post_query = new WP_Query( - array( - 'post_type' => ILO_PAGE_METRICS_POST_TYPE, - 'post_status' => 'publish', - 'name' => ilo_get_page_metrics_slug( $url ), - 'posts_per_page' => 1, - 'no_found_rows' => true, - 'cache_results' => true, - 'update_post_meta_cache' => false, - 'update_post_term_cache' => false, - 'lazy_load_term_meta' => false, - ) - ); - - $post = array_shift( $post_query->posts ); - if ( $post instanceof WP_Post ) { - return $post; - } else { - return null; - } -} - -/** - * Parses post content in page metrics post. - * - * @param WP_Post $post Page metrics post. - * @return array|WP_Error Page metrics when valid, or WP_Error otherwise. - */ -function ilo_parse_stored_page_metrics( WP_Post $post ) { - $page_metrics = json_decode( $post->post_content, true ); - if ( json_last_error() ) { - return new WP_Error( - 'page_metrics_json_parse_error', - sprintf( - /* translators: 1: Post type slug, 2: JSON error message */ - __( 'Contents of %1$s post type not valid JSON: %2$s', 'performance-lab' ), - ILO_PAGE_METRICS_POST_TYPE, - json_last_error_msg() - ) - ); - } - if ( ! is_array( $page_metrics ) ) { - return new WP_Error( - 'page_metrics_invalid_data_format', - sprintf( - /* translators: %s is post type slug */ - __( 'Contents of %s post type was not a JSON array.', 'performance-lab' ), - ILO_PAGE_METRICS_POST_TYPE - ) - ); - } - return $page_metrics; -} - -/** - * Groups page metrics by breakpoint. - * - * @param array $page_metrics Page metrics. - * @param int[] $breakpoints Viewport breakpoint max widths, sorted in ascending order. - * @return array Grouped page metrics. - */ -function ilo_group_page_metrics_by_breakpoint( array $page_metrics, array $breakpoints ) { - $max_index = count( $breakpoints ); - $groups = array_fill( 0, $max_index + 1, array() ); - $largest_breakpoint = $breakpoints[ $max_index - 1 ]; - foreach ( $page_metrics as $page_metric ) { - if ( ! isset( $page_metric['viewport']['width'] ) ) { - continue; - } - $viewport_width = $page_metric['viewport']['width']; - if ( $viewport_width > $largest_breakpoint ) { - $groups[ $max_index ][] = $page_metric; - } - foreach ( $breakpoints as $group => $breakpoint ) { - if ( $viewport_width <= $breakpoint ) { - $groups[ $group ][] = $page_metric; - } - } - } - return $groups; -} - -/** - * Stores page metric by merging it with the other page metrics for a given URL. - * - * The $validated_page_metric parameter has the following array shape: - * - * { - * 'url': string, - * 'viewport': array{ - * 'width': int, - * 'height': int - * }, - * 'elements': array - * } - * - * @param array $validated_page_metric Page metric, already validated by REST API. - * - * @return int|WP_Error Post ID or WP_Error otherwise. - */ -function ilo_store_page_metric( array $validated_page_metric ) { - $url = $validated_page_metric['url']; - unset( $validated_page_metric['url'] ); // Not stored in post_content but rather in post_title/post_name. - $validated_page_metric['timestamp'] = time(); - - // TODO: What about storing a version identifier? - $post_data = array( - 'post_title' => $url, - ); - - $post = ilo_get_page_metrics_post( $url ); - - if ( $post instanceof WP_Post ) { - $post_data['ID'] = $post->ID; - $post_data['post_name'] = $post->post_name; - - $page_metrics = ilo_parse_stored_page_metrics( $post ); - if ( $page_metrics instanceof WP_Error ) { - if ( function_exists( 'wp_trigger_error' ) ) { - wp_trigger_error( __FUNCTION__, esc_html( $page_metrics->get_error_message() ) ); - } - $page_metrics = array(); - } - } else { - $post_data['post_name'] = ilo_get_page_metrics_slug( $url ); - $page_metrics = array(); - } - - // Add the provided page metric to the page metrics. - array_unshift( $page_metrics, $validated_page_metric ); - $breakpoints = ilo_get_breakpoint_max_widths(); - $sample_size = ilo_get_page_metrics_breakpoint_sample_size(); - $grouped_page_metrics = ilo_group_page_metrics_by_breakpoint( $page_metrics, $breakpoints ); - - foreach ( $grouped_page_metrics as &$breakpoint_page_metrics ) { - if ( count( $breakpoint_page_metrics ) > $sample_size ) { - $breakpoint_page_metrics = array_slice( $breakpoint_page_metrics, 0, $sample_size ); - } - } - - $page_metrics = array_merge( ...$grouped_page_metrics ); - - $post_data['post_content'] = wp_json_encode( $page_metrics, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); // TODO: No need for pretty-printing. - - $has_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ); - if ( $has_kses ) { - // Prevent KSES from corrupting JSON in post_content. - kses_remove_filters(); - } - - $post_data['post_type'] = ILO_PAGE_METRICS_POST_TYPE; - $post_data['post_status'] = 'publish'; - if ( isset( $post_data['ID'] ) ) { - $result = wp_update_post( wp_slash( $post_data ), true ); - } else { - $result = wp_insert_post( wp_slash( $post_data ), true ); - } - - if ( $has_kses ) { - kses_init_filters(); - } - - return $result; -} +require_once __DIR__ . '/storage/post-type.php'; +require_once __DIR__ . '/storage/data.php'; +require_once __DIR__ . '/storage/rest-api.php'; diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php new file mode 100644 index 0000000000..f042422c4b --- /dev/null +++ b/modules/images/image-loading-optimization/storage/data.php @@ -0,0 +1,125 @@ + $sample_size ) { + $breakpoint_page_metrics = array_slice( $breakpoint_page_metrics, 0, $sample_size ); + } + } + + return array_merge( ...$grouped_page_metrics ); +} + +/** + * Gets the breakpoint max widths to group page metrics for various viewports. + * + * Each max with represents the maximum width (inclusive) for a given breakpoint. So if there is one number, 480, then + * this means there will be two viewport groupings, one for 0<=480, and another >480. If instead there were three + * provided breakpoints (320, 480, 576) then this means there will be four viewport groupings: + * + * 1. 0-320 (small smartphone) + * 2. 321-480 (normal smartphone) + * 3. 481-576 (phablets) + * 4. >576 (desktop) + * + * @return int[] Breakpoint max widths, sorted in ascending order. + */ +function ilo_get_breakpoint_max_widths() { + + /** + * Filters the breakpoint max widths to group page metrics for various viewports. + * + * @param int[] $breakpoint_max_widths Max widths for viewport breakpoints. + */ + $breakpoint_max_widths = array_map( + static function ( $breakpoint_max_width ) { + return (int) $breakpoint_max_width; + }, + (array) apply_filters( 'ilo_breakpoint_max_widths', array( 480 ) ) + ); + + sort( $breakpoint_max_widths ); + return $breakpoint_max_widths; +} + +/** + * Gets desired sample size for a breakpoint's page metrics. + * + * @return int Sample size. + */ +function ilo_get_page_metrics_breakpoint_sample_size() { + /** + * Filters desired sample size for a viewport's page metrics. + * + * @param int $sample_size Sample size. + */ + return (int) apply_filters( 'ilo_page_metrics_breakpoint_sample_size', 10 ); +} + +/** + * Groups page metrics by breakpoint. + * + * @param array $page_metrics Page metrics. + * @param int[] $breakpoints Viewport breakpoint max widths, sorted in ascending order. + * @return array Grouped page metrics. + */ +function ilo_group_page_metrics_by_breakpoint( array $page_metrics, array $breakpoints ) { + $max_index = count( $breakpoints ); + $groups = array_fill( 0, $max_index + 1, array() ); + $largest_breakpoint = $breakpoints[ $max_index - 1 ]; + foreach ( $page_metrics as $page_metric ) { + if ( ! isset( $page_metric['viewport']['width'] ) ) { + continue; + } + $viewport_width = $page_metric['viewport']['width']; + if ( $viewport_width > $largest_breakpoint ) { + $groups[ $max_index ][] = $page_metric; + } + foreach ( $breakpoints as $group => $breakpoint ) { + if ( $viewport_width <= $breakpoint ) { + $groups[ $group ][] = $page_metric; + } + } + } + return $groups; +} diff --git a/modules/images/image-loading-optimization/storage/lock.php b/modules/images/image-loading-optimization/storage/lock.php index efbb89424c..1ae536397b 100644 --- a/modules/images/image-loading-optimization/storage/lock.php +++ b/modules/images/image-loading-optimization/storage/lock.php @@ -1,6 +1,6 @@ array( + 'name' => __( 'Page Metrics', 'performance-lab' ), + 'singular_name' => __( 'Page Metrics', 'performance-lab' ), + ), + 'public' => false, + 'hierarchical' => false, + 'rewrite' => false, + 'query_var' => false, + 'delete_with_user' => false, + 'can_export' => false, + 'supports' => array( 'title' ), // The original URL is stored in the post_title, and the MD5 hash in the post_name. + ) + ); +} +add_action( 'init', 'ilo_register_page_metrics_post_type' ); + +/** + * Gets slug for page metrics post. + * + * @param string $url URL. + * @return string Slug for URL. + */ +function ilo_get_page_metrics_slug( $url ) { + return md5( $url ); +} + +/** + * Get page metrics post. + * + * @param string $url URL. + * @return WP_Post|null Post object if exists. + */ +function ilo_get_page_metrics_post( $url ) { + $post_query = new WP_Query( + array( + 'post_type' => ILO_PAGE_METRICS_POST_TYPE, + 'post_status' => 'publish', + 'name' => ilo_get_page_metrics_slug( $url ), + 'posts_per_page' => 1, + 'no_found_rows' => true, + 'cache_results' => true, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + 'lazy_load_term_meta' => false, + ) + ); + + $post = array_shift( $post_query->posts ); + if ( $post instanceof WP_Post ) { + return $post; + } else { + return null; + } +} + +/** + * Parses post content in page metrics post. + * + * @param WP_Post $post Page metrics post. + * @return array|WP_Error Page metrics when valid, or WP_Error otherwise. + */ +function ilo_parse_stored_page_metrics( WP_Post $post ) { + $page_metrics = json_decode( $post->post_content, true ); + if ( json_last_error() ) { + return new WP_Error( + 'page_metrics_json_parse_error', + sprintf( + /* translators: 1: Post type slug, 2: JSON error message */ + __( 'Contents of %1$s post type not valid JSON: %2$s', 'performance-lab' ), + ILO_PAGE_METRICS_POST_TYPE, + json_last_error_msg() + ) + ); + } + if ( ! is_array( $page_metrics ) ) { + return new WP_Error( + 'page_metrics_invalid_data_format', + sprintf( + /* translators: %s is post type slug */ + __( 'Contents of %s post type was not a JSON array.', 'performance-lab' ), + ILO_PAGE_METRICS_POST_TYPE + ) + ); + } + return $page_metrics; +} + +/** + * Stores page metric by merging it with the other page metrics for a given URL. + * + * The $validated_page_metric parameter has the following array shape: + * + * { + * 'url': string, + * 'viewport': array{ + * 'width': int, + * 'height': int + * }, + * 'elements': array + * } + * + * @param array $validated_page_metric Page metric, already validated by REST API. + * + * @return int|WP_Error Post ID or WP_Error otherwise. + */ +function ilo_store_page_metric( array $validated_page_metric ) { + $url = $validated_page_metric['url']; + unset( $validated_page_metric['url'] ); // Not stored in post_content but rather in post_title/post_name. + $validated_page_metric['timestamp'] = time(); + + // TODO: What about storing a version identifier? + $post_data = array( + 'post_title' => $url, + ); + + $post = ilo_get_page_metrics_post( $url ); + + if ( $post instanceof WP_Post ) { + $post_data['ID'] = $post->ID; + $post_data['post_name'] = $post->post_name; + + $page_metrics = ilo_parse_stored_page_metrics( $post ); + if ( $page_metrics instanceof WP_Error ) { + if ( function_exists( 'wp_trigger_error' ) ) { + wp_trigger_error( __FUNCTION__, esc_html( $page_metrics->get_error_message() ) ); + } + $page_metrics = array(); + } + } else { + $post_data['post_name'] = ilo_get_page_metrics_slug( $url ); + $page_metrics = array(); + } + + $page_metrics = ilo_unshift_page_metrics( $page_metrics, $validated_page_metric ); + + $post_data['post_content'] = wp_json_encode( $page_metrics, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); // TODO: No need for pretty-printing. + + $has_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ); + if ( $has_kses ) { + // Prevent KSES from corrupting JSON in post_content. + kses_remove_filters(); + } + + $post_data['post_type'] = ILO_PAGE_METRICS_POST_TYPE; + $post_data['post_status'] = 'publish'; + if ( isset( $post_data['ID'] ) ) { + $result = wp_update_post( wp_slash( $post_data ), true ); + } else { + $result = wp_insert_post( wp_slash( $post_data ), true ); + } + + if ( $has_kses ) { + kses_init_filters(); + } + + return $result; +} diff --git a/modules/images/image-loading-optimization/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php similarity index 97% rename from modules/images/image-loading-optimization/rest-api.php rename to modules/images/image-loading-optimization/storage/rest-api.php index 74967a0331..42c7d6af74 100644 --- a/modules/images/image-loading-optimization/rest-api.php +++ b/modules/images/image-loading-optimization/storage/rest-api.php @@ -138,7 +138,8 @@ function ilo_register_endpoint() { function ilo_handle_rest_request( WP_REST_Request $request ) { ilo_set_page_metric_storage_lock(); - $result = ilo_store_page_metric( $request->get_json_params() ); + $page_metric = $request->get_json_params(); + $result = ilo_store_page_metric( $page_metric ); if ( $result instanceof WP_Error ) { return $result; From d2455e9c4dfe843e4ed8d4a8f3c80bd52f1a49f0 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 7 Nov 2023 20:26:05 -0800 Subject: [PATCH 17/62] Use PHP 7 const --- .../images/image-loading-optimization/storage/post-type.php | 6 +++--- .../images/image-loading-optimization/storage/rest-api.php | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php index c0f6788d87..de9f10d5be 100644 --- a/modules/images/image-loading-optimization/storage/post-type.php +++ b/modules/images/image-loading-optimization/storage/post-type.php @@ -10,7 +10,7 @@ exit; // Exit if accessed directly. } -define( 'ILO_PAGE_METRICS_POST_TYPE', 'ilo_page_metrics' ); +const ILO_PAGE_METRICS_POST_TYPE = 'ilo_page_metrics'; /** * Register post type for page metrics storage. @@ -88,7 +88,7 @@ function ilo_parse_stored_page_metrics( WP_Post $post ) { return new WP_Error( 'page_metrics_json_parse_error', sprintf( - /* translators: 1: Post type slug, 2: JSON error message */ + /* translators: 1: Post type slug, 2: JSON error message */ __( 'Contents of %1$s post type not valid JSON: %2$s', 'performance-lab' ), ILO_PAGE_METRICS_POST_TYPE, json_last_error_msg() @@ -99,7 +99,7 @@ function ilo_parse_stored_page_metrics( WP_Post $post ) { return new WP_Error( 'page_metrics_invalid_data_format', sprintf( - /* translators: %s is post type slug */ + /* translators: %s is post type slug */ __( 'Contents of %s post type was not a JSON array.', 'performance-lab' ), ILO_PAGE_METRICS_POST_TYPE ) diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php index 42c7d6af74..87c6e77da4 100644 --- a/modules/images/image-loading-optimization/storage/rest-api.php +++ b/modules/images/image-loading-optimization/storage/rest-api.php @@ -10,8 +10,9 @@ exit; // Exit if accessed directly. } -define( 'ILO_REST_API_NAMESPACE', 'image-loading-optimization/v1' ); -define( 'ILO_PAGE_METRIC_STORAGE_ROUTE', '/image-loading-optimization/page-metric-storage' ); +const ILO_REST_API_NAMESPACE = 'image-loading-optimization/v1'; + +const ILO_PAGE_METRIC_STORAGE_ROUTE = '/image-loading-optimization/page-metric-storage'; /** * Register endpoint for storage of page metric. From 79bafac29a0882a1f1f577aab133ee4846a5f0c1 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 7 Nov 2023 20:32:56 -0800 Subject: [PATCH 18/62] Improve static analysis --- .../images/image-loading-optimization/storage/rest-api.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php index 87c6e77da4..facd678dd8 100644 --- a/modules/images/image-loading-optimization/storage/rest-api.php +++ b/modules/images/image-loading-optimization/storage/rest-api.php @@ -39,7 +39,9 @@ function ilo_register_endpoint() { ILO_PAGE_METRIC_STORAGE_ROUTE, array( 'methods' => 'POST', - 'callback' => 'ilo_handle_rest_request', + 'callback' => static function ( WP_REST_Request $request ) { + return ilo_handle_rest_request( $request ); + }, 'permission_callback' => static function () { // Needs to be available to unauthenticated visitors. if ( ilo_is_page_metric_storage_locked() ) { From c01649442959849de3796239d4092a864bcfdac8 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 8 Nov 2023 11:32:55 -0800 Subject: [PATCH 19/62] Split out detection logic into separate include --- .../image-loading-optimization/detection.php | 59 +++++++++++++++++++ .../{ => detection}/detect.js | 0 .../image-loading-optimization/hooks.php | 48 --------------- .../image-loading-optimization/load.php | 1 + .../image-loading-optimization/storage.php | 2 +- 5 files changed, 61 insertions(+), 49 deletions(-) create mode 100644 modules/images/image-loading-optimization/detection.php rename modules/images/image-loading-optimization/{ => detection}/detect.js (100%) diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php new file mode 100644 index 0000000000..1cc0567559 --- /dev/null +++ b/modules/images/image-loading-optimization/detection.php @@ -0,0 +1,59 @@ + 'module' ) + ); +} +add_action( 'wp_print_footer_scripts', 'ilo_print_detection_script' ); diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detection/detect.js similarity index 100% rename from modules/images/image-loading-optimization/detect.js rename to modules/images/image-loading-optimization/detection/detect.js diff --git a/modules/images/image-loading-optimization/hooks.php b/modules/images/image-loading-optimization/hooks.php index 59b98e61a7..1f002dab9d 100644 --- a/modules/images/image-loading-optimization/hooks.php +++ b/modules/images/image-loading-optimization/hooks.php @@ -46,51 +46,3 @@ static function ( $output ) { return $passthrough; } add_filter( 'template_include', 'ilo_buffer_output', PHP_INT_MAX ); - -/** - * Prints the script for detecting loaded images and the LCP element. - * - * @todo This should eventually only print the script if metrics are needed. - * @todo This script should not be printed if the page was requested with non-removal (non-canonical) query args. - */ -function ilo_print_detection_script() { - - // TODO: Also abort if we don't need any new page metrics due to the sample size being full. - if ( ilo_is_page_metric_storage_locked() ) { - return; - } - - $serve_time = ceil( microtime( true ) * 1000 ); - - /** - * Filters the time window between serve time and run time in which loading detection is allowed to run. - * - * Allow this amount of milliseconds between when the page was first generated (and perhaps cached) and when the - * detect function on the page is allowed to perform its detection logic and submit the request to store the results. - * This avoids situations in which there is missing detection metrics in which case a site with page caching which - * also has a lot of traffic could result in a cache stampede. - * - * @since n.e.x.t - * @todo The value should probably be something like the 99th percentile of Time To Last Byte (TTLB) for WordPress sites in CrUX. - * - * @param int $detection_time_window Detection time window in milliseconds. - */ - $detection_time_window = apply_filters( 'perflab_image_loading_detection_time_window', 5000 ); - - $detect_args = array( - $serve_time, - $detection_time_window, - WP_DEBUG, - rest_url( ILO_REST_API_NAMESPACE . ILO_PAGE_METRIC_STORAGE_ROUTE ), - wp_create_nonce( 'wp_rest' ), - ); - wp_print_inline_script_tag( - sprintf( - 'import detect from %s; detect( ...%s )', - wp_json_encode( add_query_arg( 'ver', PERFLAB_VERSION, plugin_dir_url( __FILE__ ) . 'detect.js' ) ), - wp_json_encode( $detect_args ) - ), - array( 'type' => 'module' ) - ); -} -add_action( 'wp_print_footer_scripts', 'ilo_print_detection_script' ); diff --git a/modules/images/image-loading-optimization/load.php b/modules/images/image-loading-optimization/load.php index 68f6287195..118b22d91b 100644 --- a/modules/images/image-loading-optimization/load.php +++ b/modules/images/image-loading-optimization/load.php @@ -23,3 +23,4 @@ require_once __DIR__ . '/helper.php'; require_once __DIR__ . '/hooks.php'; require_once __DIR__ . '/storage.php'; +require_once __DIR__ . '/detection.php'; diff --git a/modules/images/image-loading-optimization/storage.php b/modules/images/image-loading-optimization/storage.php index 5ac9fb9220..52bbf3b344 100644 --- a/modules/images/image-loading-optimization/storage.php +++ b/modules/images/image-loading-optimization/storage.php @@ -1,6 +1,6 @@ Date: Wed, 8 Nov 2023 11:33:41 -0800 Subject: [PATCH 20/62] Remove unused helper.php --- modules/images/image-loading-optimization/helper.php | 11 ----------- modules/images/image-loading-optimization/load.php | 1 - 2 files changed, 12 deletions(-) delete mode 100644 modules/images/image-loading-optimization/helper.php diff --git a/modules/images/image-loading-optimization/helper.php b/modules/images/image-loading-optimization/helper.php deleted file mode 100644 index 77e6b12b10..0000000000 --- a/modules/images/image-loading-optimization/helper.php +++ /dev/null @@ -1,11 +0,0 @@ - Date: Wed, 8 Nov 2023 11:44:05 -0800 Subject: [PATCH 21/62] Improve logging --- .../detection/detect.js | 49 ++++++++++++++----- .../storage/rest-api.php | 5 +- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/modules/images/image-loading-optimization/detection/detect.js b/modules/images/image-loading-optimization/detection/detect.js index 5971637f13..0fa5d7f534 100644 --- a/modules/images/image-loading-optimization/detection/detect.js +++ b/modules/images/image-loading-optimization/detection/detect.js @@ -25,6 +25,16 @@ function warn( ...message ) { console.warn( consoleLogPrefix, ...message ); } +/** + * Log an error. + * + * @param {...*} message + */ +function error( ...message ) { + // eslint-disable-next-line no-console + console.error( consoleLogPrefix, ...message ); +} + /** * @typedef {Object} Breadcrumb * @property {number} index - Index of element among sibling elements. @@ -264,7 +274,7 @@ export default async function detect( ); if ( ! breadcrumbs ) { if ( isDebug ) { - warn( 'Unable to look up breadcrumbs for element' ); + error( 'Unable to look up breadcrumbs for element' ); } continue; } @@ -289,18 +299,33 @@ export default async function detect( pageMetrics.elements.push( elementMetrics ); } - log( pageMetrics ); + if ( isDebug ) { + log( 'Page metrics:', pageMetrics ); + } - // TODO: Wait until idle. - const response = await fetch( restApiEndpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-WP-Nonce': restApiNonce, - }, - body: JSON.stringify( pageMetrics ), - } ); - log( 'response:', await response.json() ); + // TODO: Wait until idle? Yield to main? + try { + const response = await fetch( restApiEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-WP-Nonce': restApiNonce, + }, + body: JSON.stringify( pageMetrics ), + } ); + if ( isDebug ) { + const body = await response.json(); + if ( response.status === 200 ) { + log( 'Response:', body ); + } else { + error( 'Failure:', body ); + } + } + } catch ( err ) { + if ( isDebug ) { + error( err ); + } + } // Clean up. breadcrumbedElementsMap.clear(); diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php index facd678dd8..000c00e688 100644 --- a/modules/images/image-loading-optimization/storage/rest-api.php +++ b/modules/images/image-loading-optimization/storage/rest-api.php @@ -148,12 +148,11 @@ function ilo_handle_rest_request( WP_REST_Request $request ) { return $result; } - $response = new WP_REST_Response( + return new WP_REST_Response( array( 'success' => true, 'post_id' => $result, + 'data' => ilo_parse_stored_page_metrics( ilo_get_page_metrics_post( $page_metric['url'] ) ), // TODO: Remove this debug data. ) ); - $response->set_status( 201 ); - return $response; } From a65d0be988ad313f3c851c5bb66e662a1a63d126 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 8 Nov 2023 21:07:53 -0800 Subject: [PATCH 22/62] Restore version constant --- modules/images/image-loading-optimization/load.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/images/image-loading-optimization/load.php b/modules/images/image-loading-optimization/load.php index 283d45b2ad..90aac38d17 100644 --- a/modules/images/image-loading-optimization/load.php +++ b/modules/images/image-loading-optimization/load.php @@ -9,11 +9,11 @@ */ // Define the constant. -if ( defined( 'ILO_VERSION' ) ) { +if ( defined( 'IMAGE_LOADING_OPTIMIZATION_VERSION' ) ) { return; } -define( 'ILO_VERSION', 'Performance Lab ' . PERFLAB_VERSION ); +define( 'IMAGE_LOADING_OPTIMIZATION_VERSION', 'Performance Lab ' . PERFLAB_VERSION ); // Do not load the code if it is already loaded through another means. if ( function_exists( 'ilo_buffer_output' ) ) { From a17c9a267fe4a31d071f0e59b957ff2f51d1199d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 8 Nov 2023 21:08:23 -0800 Subject: [PATCH 23/62] Remove redundant plugin short-circuit --- modules/images/image-loading-optimization/load.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/modules/images/image-loading-optimization/load.php b/modules/images/image-loading-optimization/load.php index 90aac38d17..6004979d30 100644 --- a/modules/images/image-loading-optimization/load.php +++ b/modules/images/image-loading-optimization/load.php @@ -15,11 +15,6 @@ define( 'IMAGE_LOADING_OPTIMIZATION_VERSION', 'Performance Lab ' . PERFLAB_VERSION ); -// Do not load the code if it is already loaded through another means. -if ( function_exists( 'ilo_buffer_output' ) ) { - return; -} - require_once __DIR__ . '/hooks.php'; require_once __DIR__ . '/storage.php'; require_once __DIR__ . '/detection.php'; From 7f6cc2be72512be94acb2781152fde530e656ef7 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 8 Nov 2023 21:13:43 -0800 Subject: [PATCH 24/62] Remove superflous include file --- .../images/image-loading-optimization/load.php | 8 +++++++- .../image-loading-optimization/storage.php | 16 ---------------- 2 files changed, 7 insertions(+), 17 deletions(-) delete mode 100644 modules/images/image-loading-optimization/storage.php diff --git a/modules/images/image-loading-optimization/load.php b/modules/images/image-loading-optimization/load.php index 6004979d30..1086e0ed9d 100644 --- a/modules/images/image-loading-optimization/load.php +++ b/modules/images/image-loading-optimization/load.php @@ -16,5 +16,11 @@ define( 'IMAGE_LOADING_OPTIMIZATION_VERSION', 'Performance Lab ' . PERFLAB_VERSION ); require_once __DIR__ . '/hooks.php'; -require_once __DIR__ . '/storage.php'; + +// Storage logic. +require_once __DIR__ . '/storage/lock.php'; +require_once __DIR__ . '/storage/post-type.php'; +require_once __DIR__ . '/storage/data.php'; +require_once __DIR__ . '/storage/rest-api.php'; + require_once __DIR__ . '/detection.php'; diff --git a/modules/images/image-loading-optimization/storage.php b/modules/images/image-loading-optimization/storage.php deleted file mode 100644 index 52bbf3b344..0000000000 --- a/modules/images/image-loading-optimization/storage.php +++ /dev/null @@ -1,16 +0,0 @@ - Date: Wed, 8 Nov 2023 21:16:14 -0800 Subject: [PATCH 25/62] Improve page metrics route --- modules/images/image-loading-optimization/detection.php | 2 +- .../images/image-loading-optimization/storage/rest-api.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php index 1cc0567559..76dd045198 100644 --- a/modules/images/image-loading-optimization/detection.php +++ b/modules/images/image-loading-optimization/detection.php @@ -44,7 +44,7 @@ function ilo_print_detection_script() { $serve_time, $detection_time_window, WP_DEBUG, - rest_url( ILO_REST_API_NAMESPACE . ILO_PAGE_METRIC_STORAGE_ROUTE ), + rest_url( ILO_REST_API_NAMESPACE . ILO_PAGE_METRICS_ROUTE ), wp_create_nonce( 'wp_rest' ), ); wp_print_inline_script_tag( diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php index 000c00e688..4844392d3a 100644 --- a/modules/images/image-loading-optimization/storage/rest-api.php +++ b/modules/images/image-loading-optimization/storage/rest-api.php @@ -12,7 +12,7 @@ const ILO_REST_API_NAMESPACE = 'image-loading-optimization/v1'; -const ILO_PAGE_METRIC_STORAGE_ROUTE = '/image-loading-optimization/page-metric-storage'; +const ILO_PAGE_METRICS_ROUTE = '/page-metrics'; /** * Register endpoint for storage of page metric. @@ -36,7 +36,7 @@ function ilo_register_endpoint() { register_rest_route( ILO_REST_API_NAMESPACE, - ILO_PAGE_METRIC_STORAGE_ROUTE, + ILO_PAGE_METRICS_ROUTE, array( 'methods' => 'POST', 'callback' => static function ( WP_REST_Request $request ) { From fb8837b11999f96d9a07c07661767452ad885982 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 8 Nov 2023 21:29:27 -0800 Subject: [PATCH 26/62] Update composer lockfile --- composer.lock | 73 +++++++++++++++++++++++++++------------------------ 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/composer.lock b/composer.lock index 59e7347f06..6a9ae7127c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8afb8511538e46c6875a017b72ad8711", + "content-hash": "2dcd132f2c017c64da30a4a4b6f78f29", "packages": [ { "name": "composer/installers", @@ -236,30 +236,30 @@ }, { "name": "doctrine/instantiator", - "version": "2.0.0", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", "shasum": "" }, "require": { - "php": "^8.1" + "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^11", + "doctrine/coding-standard": "^9 || ^11", "ext-pdo": "*", "ext-phar": "*", - "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.9.4", - "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^9.5.27", - "vimeo/psalm": "^5.4" + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.30 || ^5.4" }, "type": "library", "autoload": { @@ -286,7 +286,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" }, "funding": [ { @@ -302,7 +302,7 @@ "type": "tidelift" } ], - "time": "2022-12-30T00:23:10+00:00" + "time": "2022-12-30T00:15:36+00:00" }, { "name": "myclabs/deep-copy", @@ -532,16 +532,16 @@ }, { "name": "php-stubs/wordpress-stubs", - "version": "v6.3.0", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/php-stubs/wordpress-stubs.git", - "reference": "adda7609e71d5f4dc7b87c74f8ec9e3437d2e92c" + "reference": "286d42eeb44c6808633cc59b8dbb9aa75fe41264" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/adda7609e71d5f4dc7b87c74f8ec9e3437d2e92c", - "reference": "adda7609e71d5f4dc7b87c74f8ec9e3437d2e92c", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/286d42eeb44c6808633cc59b8dbb9aa75fe41264", + "reference": "286d42eeb44c6808633cc59b8dbb9aa75fe41264", "shasum": "" }, "require-dev": { @@ -554,6 +554,7 @@ }, "suggest": { "paragonie/sodium_compat": "Pure PHP implementation of libsodium", + "symfony/polyfill-php80": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" }, "type": "library", @@ -570,9 +571,9 @@ ], "support": { "issues": "https://github.com/php-stubs/wordpress-stubs/issues", - "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.3.0" + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.4.0" }, - "time": "2023-08-10T16:34:11+00:00" + "time": "2023-11-08T07:02:08+00:00" }, { "name": "phpcompatibility/php-compatibility", @@ -865,16 +866,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.38", + "version": "1.10.41", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "5302bb402c57f00fb3c2c015bac86e0827e4b691" + "reference": "c6174523c2a69231df55bdc65b61655e72876d76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/5302bb402c57f00fb3c2c015bac86e0827e4b691", - "reference": "5302bb402c57f00fb3c2c015bac86e0827e4b691", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c6174523c2a69231df55bdc65b61655e72876d76", + "reference": "c6174523c2a69231df55bdc65b61655e72876d76", "shasum": "" }, "require": { @@ -923,7 +924,7 @@ "type": "tidelift" } ], - "time": "2023-10-06T14:19:14+00:00" + "time": "2023-11-05T12:57:57+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -2614,22 +2615,22 @@ }, { "name": "szepeviktor/phpstan-wordpress", - "version": "v1.3.0", + "version": "v1.3.2", "source": { "type": "git", "url": "https://github.com/szepeviktor/phpstan-wordpress.git", - "reference": "5b5cc77ed51fdaf64efe3f00b5aae4b709d2cfa9" + "reference": "b8516ed6bab7ec50aae981698ce3f67f1be2e45a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/5b5cc77ed51fdaf64efe3f00b5aae4b709d2cfa9", - "reference": "5b5cc77ed51fdaf64efe3f00b5aae4b709d2cfa9", + "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/b8516ed6bab7ec50aae981698ce3f67f1be2e45a", + "reference": "b8516ed6bab7ec50aae981698ce3f67f1be2e45a", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", "php-stubs/wordpress-stubs": "^4.7 || ^5.0 || ^6.0", - "phpstan/phpstan": "^1.10.0", + "phpstan/phpstan": "^1.10.30", "symfony/polyfill-php73": "^1.12.0" }, "require-dev": { @@ -2640,6 +2641,9 @@ "phpunit/phpunit": "^8.0 || ^9.0", "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^0.8" }, + "suggest": { + "swissspidy/phpstan-no-private": "Detect usage of internal core functions, classes and methods" + }, "type": "phpstan-extension", "extra": { "phpstan": { @@ -2667,9 +2671,9 @@ ], "support": { "issues": "https://github.com/szepeviktor/phpstan-wordpress/issues", - "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v1.3.0" + "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v1.3.2" }, - "time": "2023-04-23T06:15:06+00:00" + "time": "2023-10-16T17:23:56+00:00" }, { "name": "theseer/tokenizer", @@ -2902,8 +2906,9 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=7|^8" + "php": ">=7|^8", + "ext-json": "*" }, "platform-dev": [], - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.2.0" } From 80c6827277ebcad35c8ebdfe173c773fc8d23d71 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 9 Nov 2023 10:36:29 -0800 Subject: [PATCH 27/62] Pass object to detect() instead of positional args --- .../image-loading-optimization/detection.php | 12 ++++++------ .../detection/detect.js | 17 +++++++++-------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php index 76dd045198..3dcd945e91 100644 --- a/modules/images/image-loading-optimization/detection.php +++ b/modules/images/image-loading-optimization/detection.php @@ -41,15 +41,15 @@ function ilo_print_detection_script() { $detection_time_window = apply_filters( 'perflab_image_loading_detection_time_window', 5000 ); $detect_args = array( - $serve_time, - $detection_time_window, - WP_DEBUG, - rest_url( ILO_REST_API_NAMESPACE . ILO_PAGE_METRICS_ROUTE ), - wp_create_nonce( 'wp_rest' ), + 'serveTime' => $serve_time, + 'detectionTimeWindow' => $detection_time_window, + 'isDebug' => WP_DEBUG, + 'restApiEndpoint' => rest_url( ILO_REST_API_NAMESPACE . ILO_PAGE_METRICS_ROUTE ), + 'restApiNonce' => wp_create_nonce( 'wp_rest' ), ); wp_print_inline_script_tag( sprintf( - 'import detect from %s; detect( ...%s )', + 'import detect from %s; detect( %s )', wp_json_encode( add_query_arg( 'ver', PERFLAB_VERSION, plugin_dir_url( __FILE__ ) . 'detection/detect.js' ) ), wp_json_encode( $detect_args ) ), diff --git a/modules/images/image-loading-optimization/detection/detect.js b/modules/images/image-loading-optimization/detection/detect.js index 0fa5d7f534..7498e3d81d 100644 --- a/modules/images/image-loading-optimization/detection/detect.js +++ b/modules/images/image-loading-optimization/detection/detect.js @@ -98,19 +98,20 @@ function getBreadcrumbs( leafElement ) { /** * Detects the LCP element, loaded images, client viewport and store for future optimizations. * - * @param {number} serveTime The serve time of the page in milliseconds from PHP via `ceil( microtime( true ) * 1000 )`. - * @param {number} detectionTimeWindow The number of milliseconds between now and when the page was first generated in which detection should proceed. - * @param {boolean} isDebug Whether to show debug messages. - * @param {string} restApiEndpoint URL for where to send the detection data. - * @param {string} restApiNonce Nonce for writing to the REST API. + * @param {Object} args Args. + * @param {number} args.serveTime The serve time of the page in milliseconds from PHP via `ceil( microtime( true ) * 1000 )`. + * @param {number} args.detectionTimeWindow The number of milliseconds between now and when the page was first generated in which detection should proceed. + * @param {boolean} args.isDebug Whether to show debug messages. + * @param {string} args.restApiEndpoint URL for where to send the detection data. + * @param {string} args.restApiNonce Nonce for writing to the REST API. */ -export default async function detect( +export default async function detect( { serveTime, detectionTimeWindow, isDebug, restApiEndpoint, - restApiNonce -) { + restApiNonce, +} ) { const runTime = new Date().valueOf(); // Abort running detection logic if it was served in a cached page. From f6208c20ba799d74b2eced8a161145d68cb5c551 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 9 Nov 2023 10:50:11 -0800 Subject: [PATCH 28/62] Add description to ilo_get_page_metrics_breakpoint_sample_size and reduce default size to 3 --- .../images/image-loading-optimization/storage/data.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php index f042422c4b..8bb8b3fb68 100644 --- a/modules/images/image-loading-optimization/storage/data.php +++ b/modules/images/image-loading-optimization/storage/data.php @@ -83,17 +83,21 @@ static function ( $breakpoint_max_width ) { } /** - * Gets desired sample size for a breakpoint's page metrics. + * Gets the sample size for a breakpoint's page metrics on a given URL. + * + * A breakpoint divides page metrics for viewports which are smaller and those which are larger. Given the default + * sample size of 3 and there being just a single breakpoint (480) by default, for a given URL, there would be a maximum + * total of 6 page metrics stored for a given URL: 3 for mobile and 3 for desktop. * * @return int Sample size. */ function ilo_get_page_metrics_breakpoint_sample_size() { /** - * Filters desired sample size for a viewport's page metrics. + * Filters the sample size for a breakpoint's page metrics on a given URL. * * @param int $sample_size Sample size. */ - return (int) apply_filters( 'ilo_page_metrics_breakpoint_sample_size', 10 ); + return (int) apply_filters( 'ilo_page_metrics_breakpoint_sample_size', 3 ); } /** From fcf9acd3cdbfd33a940c75ef9088d5f265a0181a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 9 Nov 2023 15:23:39 -0800 Subject: [PATCH 29/62] Add initial function to get normalized current URL --- .../storage/data.php | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php index 8bb8b3fb68..5de95f4049 100644 --- a/modules/images/image-loading-optimization/storage/data.php +++ b/modules/images/image-loading-optimization/storage/data.php @@ -28,6 +28,78 @@ function ilo_get_page_metric_ttl() { return (int) apply_filters( 'ilo_page_metric_ttl', MONTH_IN_SECONDS ); } +/** + * Gets the normalized current URL. + * + * TODO: This will need to be made more robust for non-singular URLs. What about multi-faceted archives with multiple taxonomies and date parameters? + * + * @return string Normalized current URL. + */ +function ilo_get_normalized_current_url() { + if ( is_singular() ) { + $url = wp_get_canonical_url(); + if ( $url ) { + return $url; + } + } + + $home_path = wp_parse_url( home_url( '/' ), PHP_URL_PATH ); + + $scheme = is_ssl() ? 'https' : 'http'; + $host = strtok( $_SERVER['HTTP_HOST'], ':' ); // Use of strtok() since wp-env erroneously includes port in host. + $port = (int) $_SERVER['SERVER_PORT']; + $path = ''; + $query = ''; + if ( preg_match( '%(^.+?)(?:\?([^#]+))?%', wp_unslash( $_SERVER['REQUEST_URI'] ), $matches ) ) { + if ( ! empty( $matches[1] ) ) { + $path = $matches[1]; + } + if ( ! empty( $matches[2] ) ) { + $query = $matches[2]; + } + } + if ( $query ) { + $removable_query_args = wp_removable_query_args(); + $removable_query_args[] = 'fbclid'; + + $old_query_args = array(); + $new_query_args = array(); + wp_parse_str( $query, $old_query_args ); + foreach ( $old_query_args as $key => $value ) { + if ( + str_starts_with( 'utm_', $key ) || + in_array( $key, $removable_query_args, true ) + ) { + continue; + } + $new_query_args[ $key ] = $value; + } + asort( $new_query_args ); + $query = build_query( $new_query_args ); + } + + // Normalize open-ended URLs. + if ( is_404() ) { + $path = $home_path; + $query = 'error=404'; + } elseif ( is_search() ) { + $path = $home_path; + $query = 's={}'; + } + + // Rebuild the URL. + $url = $scheme . '://' . $host; + if ( 80 !== $port && 443 !== $port ) { + $url .= ":{$port}"; + } + $url .= $path; + if ( $query ) { + $url .= "?{$query}"; + } + + return $url; +} + /** * Unshift a new page metric onto an array of page metrics. * From 224ea516dd0ff470f51f9905d43865c2ee67b9ea Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 10 Nov 2023 11:22:40 -0800 Subject: [PATCH 30/62] Add function for normalizing query vars and getting current URL --- .../storage/data.php | 106 +++++++++--------- 1 file changed, 51 insertions(+), 55 deletions(-) diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php index 5de95f4049..c2ac6b38e4 100644 --- a/modules/images/image-loading-optimization/storage/data.php +++ b/modules/images/image-loading-optimization/storage/data.php @@ -29,75 +29,71 @@ function ilo_get_page_metric_ttl() { } /** - * Gets the normalized current URL. + * Get the URL for the current request. * - * TODO: This will need to be made more robust for non-singular URLs. What about multi-faceted archives with multiple taxonomies and date parameters? + * This is essentially the REQUEST_URI prefixed by the scheme and host for the home URL. + * This is needed in particular due to subdirectory installs. * - * @return string Normalized current URL. + * @return string Current URL. */ -function ilo_get_normalized_current_url() { - if ( is_singular() ) { - $url = wp_get_canonical_url(); - if ( $url ) { - return $url; - } +function ilo_get_current_url() { + $parsed_url = wp_parse_url( home_url() ); + + if ( ! is_array( $parsed_url ) ) { + $parsed_url = array(); } - $home_path = wp_parse_url( home_url( '/' ), PHP_URL_PATH ); + if ( empty( $parsed_url['scheme'] ) ) { + $parsed_url['scheme'] = is_ssl() ? 'https' : 'http'; + } + if ( ! isset( $parsed_url['host'] ) ) { + $parsed_url['host'] = isset( $_SERVER['HTTP_HOST'] ) ? wp_unslash( $_SERVER['HTTP_HOST'] ) : 'localhost'; + } - $scheme = is_ssl() ? 'https' : 'http'; - $host = strtok( $_SERVER['HTTP_HOST'], ':' ); // Use of strtok() since wp-env erroneously includes port in host. - $port = (int) $_SERVER['SERVER_PORT']; - $path = ''; - $query = ''; - if ( preg_match( '%(^.+?)(?:\?([^#]+))?%', wp_unslash( $_SERVER['REQUEST_URI'] ), $matches ) ) { - if ( ! empty( $matches[1] ) ) { - $path = $matches[1]; - } - if ( ! empty( $matches[2] ) ) { - $query = $matches[2]; + $current_url = $parsed_url['scheme'] . '://'; + if ( isset( $parsed_url['user'] ) ) { + $current_url .= $parsed_url['user']; + if ( isset( $parsed_url['pass'] ) ) { + $current_url .= ':' . $parsed_url['pass']; } + $current_url .= '@'; } - if ( $query ) { - $removable_query_args = wp_removable_query_args(); - $removable_query_args[] = 'fbclid'; - - $old_query_args = array(); - $new_query_args = array(); - wp_parse_str( $query, $old_query_args ); - foreach ( $old_query_args as $key => $value ) { - if ( - str_starts_with( 'utm_', $key ) || - in_array( $key, $removable_query_args, true ) - ) { - continue; - } - $new_query_args[ $key ] = $value; - } - asort( $new_query_args ); - $query = build_query( $new_query_args ); + $current_url .= $parsed_url['host']; + if ( isset( $parsed_url['port'] ) ) { + $current_url .= ':' . $parsed_url['port']; } + $current_url .= '/'; - // Normalize open-ended URLs. - if ( is_404() ) { - $path = $home_path; - $query = 'error=404'; - } elseif ( is_search() ) { - $path = $home_path; - $query = 's={}'; + if ( isset( $_SERVER['REQUEST_URI'] ) ) { + $current_url .= ltrim( wp_unslash( $_SERVER['REQUEST_URI'] ), '/' ); } + return esc_url_raw( $current_url ); +} - // Rebuild the URL. - $url = $scheme . '://' . $host; - if ( 80 !== $port && 443 !== $port ) { - $url .= ":{$port}"; - } - $url .= $path; - if ( $query ) { - $url .= "?{$query}"; +/** + * Gets the normalized query vars for the current request. + * + * This is used as a cache key for stored page metrics. + * + * @return array Normalized query vars. + */ +function ilo_get_normalized_query_vars() { + global $wp; + + // Note that the order of this array is naturally normalized since it is + // assembled by iterating over public_query_vars. + $normalized_query_vars = $wp->query_vars; + + // Normalize unbounded query vars. + if ( is_404() ) { + $normalized_query_vars = array( + 'error' => 404, + ); + } elseif ( array_key_exists( 's', $normalized_query_vars ) ) { + $normalized_query_vars['s'] = '...'; } - return $url; + return $normalized_query_vars; } /** From dd9b3754dc3d3aed99dada94656e9e1988ae1b07 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 10 Nov 2023 11:23:35 -0800 Subject: [PATCH 31/62] Use current() instead of array_shift() --- modules/images/image-loading-optimization/storage/post-type.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php index de9f10d5be..578dcc2db2 100644 --- a/modules/images/image-loading-optimization/storage/post-type.php +++ b/modules/images/image-loading-optimization/storage/post-type.php @@ -68,7 +68,7 @@ function ilo_get_page_metrics_post( $url ) { ) ); - $post = array_shift( $post_query->posts ); + $post = current( $post_query->posts ); if ( $post instanceof WP_Post ) { return $post; } else { From f9a14939d935e37a360dae450a4a19fff9f92c43 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 10 Nov 2023 12:39:11 -0800 Subject: [PATCH 32/62] Use query vars instead of URL for computing slug; add HMAC --- .../image-loading-optimization/detection.php | 5 +++ .../detection/detect.js | 8 ++++- .../storage/data.php | 24 ++++++++++++++ .../storage/post-type.php | 32 ++++++------------- .../storage/rest-api.php | 20 ++++++++++-- 5 files changed, 64 insertions(+), 25 deletions(-) diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php index 3dcd945e91..00f5e10c72 100644 --- a/modules/images/image-loading-optimization/detection.php +++ b/modules/images/image-loading-optimization/detection.php @@ -40,12 +40,17 @@ function ilo_print_detection_script() { */ $detection_time_window = apply_filters( 'perflab_image_loading_detection_time_window', 5000 ); + $query_vars = ilo_get_normalized_query_vars(); + $slug = ilo_get_page_metrics_slug( $query_vars ); + $detect_args = array( 'serveTime' => $serve_time, 'detectionTimeWindow' => $detection_time_window, 'isDebug' => WP_DEBUG, 'restApiEndpoint' => rest_url( ILO_REST_API_NAMESPACE . ILO_PAGE_METRICS_ROUTE ), 'restApiNonce' => wp_create_nonce( 'wp_rest' ), + 'pageMetricsSlug' => $slug, + 'pageMetricsHmac' => ilo_get_slug_hmac( $slug ), // TODO: Or would a nonce make more sense with the $slug being the action? ); wp_print_inline_script_tag( sprintf( diff --git a/modules/images/image-loading-optimization/detection/detect.js b/modules/images/image-loading-optimization/detection/detect.js index 7498e3d81d..dec3879ba3 100644 --- a/modules/images/image-loading-optimization/detection/detect.js +++ b/modules/images/image-loading-optimization/detection/detect.js @@ -104,6 +104,8 @@ function getBreadcrumbs( leafElement ) { * @param {boolean} args.isDebug Whether to show debug messages. * @param {string} args.restApiEndpoint URL for where to send the detection data. * @param {string} args.restApiNonce Nonce for writing to the REST API. + * @param {string} args.pageMetricsSlug Slug for page metrics. + * @param {string} args.pageMetricsHmac HMAC for the page metric slug. */ export default async function detect( { serveTime, @@ -111,6 +113,8 @@ export default async function detect( { isDebug, restApiEndpoint, restApiNonce, + pageMetricsSlug, + pageMetricsHmac, } ) { const runTime = new Date().valueOf(); @@ -259,7 +263,9 @@ export default async function detect( { /** @type {PageMetrics} */ const pageMetrics = { - url: win.location.href, // TODO: Consider sending canonical URL instead. + url: win.location.href, + slug: pageMetricsSlug, + hmac: pageMetricsHmac, viewport: { width: win.innerWidth, height: win.innerHeight, diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php index c2ac6b38e4..63e5ccc3b7 100644 --- a/modules/images/image-loading-optimization/storage/data.php +++ b/modules/images/image-loading-optimization/storage/data.php @@ -96,6 +96,30 @@ function ilo_get_normalized_query_vars() { return $normalized_query_vars; } +/** + * Gets slug for page metrics. + * + * @see ilo_get_normalized_query_vars() + * + * @param array $query_vars Normalized query vars. + * @return string Slug. + */ +function ilo_get_page_metrics_slug( $query_vars ) { + return md5( wp_json_encode( $query_vars ) ); +} + +/** + * Compute HMAC for page metrics slug. + * + * This is used in the REST API to authenticate the storage of new page metrics from a given URL. + * + * @param string $slug Page metrics slug. + * @return false HMAC. + */ +function ilo_get_slug_hmac( $slug ) { + return hash_hmac( 'sha1', $slug, wp_salt() ); +} + /** * Unshift a new page metric onto an array of page metrics. * diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php index 578dcc2db2..d3bfefdc32 100644 --- a/modules/images/image-loading-optimization/storage/post-type.php +++ b/modules/images/image-loading-optimization/storage/post-type.php @@ -37,28 +37,18 @@ function ilo_register_page_metrics_post_type() { } add_action( 'init', 'ilo_register_page_metrics_post_type' ); -/** - * Gets slug for page metrics post. - * - * @param string $url URL. - * @return string Slug for URL. - */ -function ilo_get_page_metrics_slug( $url ) { - return md5( $url ); -} - /** * Get page metrics post. * - * @param string $url URL. + * @param string $slug Page metrics slug. * @return WP_Post|null Post object if exists. */ -function ilo_get_page_metrics_post( $url ) { +function ilo_get_page_metrics_post( $slug ) { $post_query = new WP_Query( array( 'post_type' => ILO_PAGE_METRICS_POST_TYPE, 'post_status' => 'publish', - 'name' => ilo_get_page_metrics_slug( $url ), + 'name' => $slug, 'posts_per_page' => 1, 'no_found_rows' => true, 'cache_results' => true, @@ -114,7 +104,6 @@ function ilo_parse_stored_page_metrics( WP_Post $post ) { * The $validated_page_metric parameter has the following array shape: * * { - * 'url': string, * 'viewport': array{ * 'width': int, * 'height': int @@ -122,21 +111,20 @@ function ilo_parse_stored_page_metrics( WP_Post $post ) { * 'elements': array * } * - * @param array $validated_page_metric Page metric, already validated by REST API. - * + * @param string $url URL for the page metrics. This is used purely as metadata. + * @param string $slug Page metrics slug (computed from query vars). + * @param array $validated_page_metric Page metric, already validated by REST API. * @return int|WP_Error Post ID or WP_Error otherwise. */ -function ilo_store_page_metric( array $validated_page_metric ) { - $url = $validated_page_metric['url']; - unset( $validated_page_metric['url'] ); // Not stored in post_content but rather in post_title/post_name. +function ilo_store_page_metric( $url, $slug, array $validated_page_metric ) { $validated_page_metric['timestamp'] = time(); // TODO: What about storing a version identifier? $post_data = array( - 'post_title' => $url, + 'post_title' => $url, // TODO: Should we keep this? It can help with debugging. ); - $post = ilo_get_page_metrics_post( $url ); + $post = ilo_get_page_metrics_post( $slug ); if ( $post instanceof WP_Post ) { $post_data['ID'] = $post->ID; @@ -150,7 +138,7 @@ function ilo_store_page_metric( array $validated_page_metric ) { $page_metrics = array(); } } else { - $post_data['post_name'] = ilo_get_page_metrics_slug( $url ); + $post_data['post_name'] = $slug; $page_metrics = array(); } diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php index 4844392d3a..fb2c2c9868 100644 --- a/modules/images/image-loading-optimization/storage/rest-api.php +++ b/modules/images/image-loading-optimization/storage/rest-api.php @@ -65,6 +65,22 @@ function ilo_register_endpoint() { return true; }, ), + 'slug' => array( + 'type' => 'string', + 'required' => true, + 'pattern' => '^[0-9a-f]{32}$', + ), + 'hmac' => array( + 'type' => 'string', + 'required' => true, + 'pattern' => '^[0-9a-f]+$', + 'validate_callback' => static function ( $hmac, WP_REST_Request $request ) { + if ( ! hash_equals( $hmac, ilo_get_slug_hmac( $request->get_param( 'slug' ) ) ) ) { + return new WP_Error( 'invalid_hmac', __( 'HMAC comparison failure.', 'performance-lab' ) ); + } + return true; + }, + ), 'viewport' => array( 'description' => __( 'Viewport dimensions', 'performance-lab' ), 'type' => 'object', @@ -142,7 +158,7 @@ function ilo_handle_rest_request( WP_REST_Request $request ) { ilo_set_page_metric_storage_lock(); $page_metric = $request->get_json_params(); - $result = ilo_store_page_metric( $page_metric ); + $result = ilo_store_page_metric( $page_metric['url'], $page_metric['slug'], $request->get_json_params() ); if ( $result instanceof WP_Error ) { return $result; @@ -152,7 +168,7 @@ function ilo_handle_rest_request( WP_REST_Request $request ) { array( 'success' => true, 'post_id' => $result, - 'data' => ilo_parse_stored_page_metrics( ilo_get_page_metrics_post( $page_metric['url'] ) ), // TODO: Remove this debug data. + 'data' => ilo_parse_stored_page_metrics( ilo_get_page_metrics_post( $page_metric['slug'] ) ), // TODO: Remove this debug data. ) ); } From 5dc6828bd39683c931f75e0a60250316e145fdaa Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 10 Nov 2023 13:03:35 -0800 Subject: [PATCH 33/62] Use nonce instead of hmac --- .../image-loading-optimization/detection.php | 2 +- .../detection/detect.js | 6 ++--- .../storage/data.php | 27 ++++++++++++++++--- .../storage/rest-api.php | 8 +++--- 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php index 00f5e10c72..113577ffb0 100644 --- a/modules/images/image-loading-optimization/detection.php +++ b/modules/images/image-loading-optimization/detection.php @@ -50,7 +50,7 @@ function ilo_print_detection_script() { 'restApiEndpoint' => rest_url( ILO_REST_API_NAMESPACE . ILO_PAGE_METRICS_ROUTE ), 'restApiNonce' => wp_create_nonce( 'wp_rest' ), 'pageMetricsSlug' => $slug, - 'pageMetricsHmac' => ilo_get_slug_hmac( $slug ), // TODO: Or would a nonce make more sense with the $slug being the action? + 'pageMetricsNonce' => ilo_get_page_metrics_storage_nonce( $slug ), ); wp_print_inline_script_tag( sprintf( diff --git a/modules/images/image-loading-optimization/detection/detect.js b/modules/images/image-loading-optimization/detection/detect.js index dec3879ba3..6379cf7856 100644 --- a/modules/images/image-loading-optimization/detection/detect.js +++ b/modules/images/image-loading-optimization/detection/detect.js @@ -105,7 +105,7 @@ function getBreadcrumbs( leafElement ) { * @param {string} args.restApiEndpoint URL for where to send the detection data. * @param {string} args.restApiNonce Nonce for writing to the REST API. * @param {string} args.pageMetricsSlug Slug for page metrics. - * @param {string} args.pageMetricsHmac HMAC for the page metric slug. + * @param {string} args.pageMetricsNonce Nonce for page metrics storage. */ export default async function detect( { serveTime, @@ -114,7 +114,7 @@ export default async function detect( { restApiEndpoint, restApiNonce, pageMetricsSlug, - pageMetricsHmac, + pageMetricsNonce, } ) { const runTime = new Date().valueOf(); @@ -265,7 +265,7 @@ export default async function detect( { const pageMetrics = { url: win.location.href, slug: pageMetricsSlug, - hmac: pageMetricsHmac, + nonce: pageMetricsNonce, viewport: { width: win.innerWidth, height: win.innerHeight, diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php index 63e5ccc3b7..f3813143b6 100644 --- a/modules/images/image-loading-optimization/storage/data.php +++ b/modules/images/image-loading-optimization/storage/data.php @@ -109,15 +109,34 @@ function ilo_get_page_metrics_slug( $query_vars ) { } /** - * Compute HMAC for page metrics slug. + * Compute nonce for storing page metrics for a specific slug. * * This is used in the REST API to authenticate the storage of new page metrics from a given URL. * + * @see wp_create_nonce() + * @see ilo_verify_page_metrics_storage_nonce() + * * @param string $slug Page metrics slug. - * @return false HMAC. + * @return string Nonce. + */ +function ilo_get_page_metrics_storage_nonce( $slug ) { + return wp_create_nonce( "store_page_metrics:{$slug}" ); +} + +/** + * Verify nonce for storing page metrics for a specific slug. + * + * @see wp_verify_nonce() + * @see ilo_get_page_metrics_storage_nonce() + * + * @param string $nonce Page metrics storage nonce. + * @param string $slug Page metrics slug. + * @return int|false 1 if the nonce is valid and generated between 0-12 hours ago, + * 2 if the nonce is valid and generated between 12-24 hours ago. + * False if the nonce is invalid. */ -function ilo_get_slug_hmac( $slug ) { - return hash_hmac( 'sha1', $slug, wp_salt() ); +function ilo_verify_page_metrics_storage_nonce( $nonce, $slug ) { + return wp_verify_nonce( $nonce, "store_page_metrics:{$slug}" ); } /** diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php index fb2c2c9868..2c2816c9a3 100644 --- a/modules/images/image-loading-optimization/storage/rest-api.php +++ b/modules/images/image-loading-optimization/storage/rest-api.php @@ -70,13 +70,13 @@ function ilo_register_endpoint() { 'required' => true, 'pattern' => '^[0-9a-f]{32}$', ), - 'hmac' => array( + 'nonce' => array( 'type' => 'string', 'required' => true, 'pattern' => '^[0-9a-f]+$', - 'validate_callback' => static function ( $hmac, WP_REST_Request $request ) { - if ( ! hash_equals( $hmac, ilo_get_slug_hmac( $request->get_param( 'slug' ) ) ) ) { - return new WP_Error( 'invalid_hmac', __( 'HMAC comparison failure.', 'performance-lab' ) ); + 'validate_callback' => static function ( $nonce, WP_REST_Request $request ) { + if ( ! ilo_verify_page_metrics_storage_nonce( $nonce, $request->get_param( 'slug' ) ) ) { + return new WP_Error( 'invalid_nonce', __( 'Page metrics nonce verification failure.', 'performance-lab' ) ); } return true; }, From e0f2b6826ee48502a6ba3594f93f82b4e3c0c877 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 10 Nov 2023 15:19:04 -0800 Subject: [PATCH 34/62] Prevent collecting page metrics when sample size is full for all breakpoints --- .../image-loading-optimization/detection.php | 40 ++++++++++++++++--- .../storage/data.php | 40 ++++++++++++------- .../storage/post-type.php | 20 +++++++++- .../storage/rest-api.php | 10 +++-- 4 files changed, 87 insertions(+), 23 deletions(-) diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php index 113577ffb0..a8a7c3c234 100644 --- a/modules/images/image-loading-optimization/detection.php +++ b/modules/images/image-loading-optimization/detection.php @@ -17,8 +17,41 @@ * @todo This script should not be printed if the page was requested with non-removal (non-canonical) query args. */ function ilo_print_detection_script() { + $query_vars = ilo_get_normalized_query_vars(); + $slug = ilo_get_page_metrics_slug( $query_vars ); + $data = ilo_get_page_metrics_data( $slug ); + if ( ! is_array( $data ) ) { + $data = $data; + } + + $metrics_by_breakpoint = ilo_group_page_metrics_by_breakpoint( $data, ilo_get_breakpoint_max_widths() ); + $sample_size = ilo_get_page_metrics_breakpoint_sample_size(); + $freshness_ttl = ilo_get_page_metric_freshness_ttl(); + + // TODO: This same logic needs to be in the endpoint so that we can reject requests when not needed. + $current_time = time(); + $needed_minimum_viewport_widths = array(); + foreach ( $metrics_by_breakpoint as $minimum_viewport_width => $page_metrics ) { + $needs_page_metrics = false; + if ( count( $page_metrics ) < $sample_size ) { + $needs_page_metrics = true; + } else { + foreach ( $page_metrics as $page_metric ) { + if ( isset( $page_metric['timestamp'] ) && $page_metric['timestamp'] + $freshness_ttl < $current_time ) { + $needs_page_metrics = true; + break; + } + } + } + $needed_minimum_viewport_widths[ $minimum_viewport_width ] = $needs_page_metrics; + } - // TODO: Also abort if we don't need any new page metrics due to the sample size being full. + // Abort if we already have all the sample size we need for all breakpoints. + if ( count( array_filter( $needed_minimum_viewport_widths ) ) === 0 ) { + return; + } + + // Abort if storage is locked. if ( ilo_is_page_metric_storage_locked() ) { return; } @@ -40,9 +73,6 @@ function ilo_print_detection_script() { */ $detection_time_window = apply_filters( 'perflab_image_loading_detection_time_window', 5000 ); - $query_vars = ilo_get_normalized_query_vars(); - $slug = ilo_get_page_metrics_slug( $query_vars ); - $detect_args = array( 'serveTime' => $serve_time, 'detectionTimeWindow' => $detection_time_window, @@ -54,7 +84,7 @@ function ilo_print_detection_script() { ); wp_print_inline_script_tag( sprintf( - 'import detect from %s; detect( %s )', + 'import detect from %s; detect( %s );', wp_json_encode( add_query_arg( 'ver', PERFLAB_VERSION, plugin_dir_url( __FILE__ ) . 'detection/detect.js' ) ), wp_json_encode( $detect_args ) ), diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php index f3813143b6..1eda44737b 100644 --- a/modules/images/image-loading-optimization/storage/data.php +++ b/modules/images/image-loading-optimization/storage/data.php @@ -15,11 +15,9 @@ * * When a page metric expires it is eligible to be replaced by a newer one. * - * TODO: However, we keep viewport-specific page metrics regardless of TTL. - * * @return int Expiration age in seconds. */ -function ilo_get_page_metric_ttl() { +function ilo_get_page_metric_freshness_ttl() { /** * Filters the expiration age for a given page metric. * @@ -216,25 +214,39 @@ function ilo_get_page_metrics_breakpoint_sample_size() { * * @param array $page_metrics Page metrics. * @param int[] $breakpoints Viewport breakpoint max widths, sorted in ascending order. - * @return array Grouped page metrics. + * @return array Page metrics grouped by breakpoint. The array keys are the minimum widths for a viewport to lie within + * the breakpoint. The returned array is always one larger than the provided array of breakpoints, since + * the breakpoints reflect the max inclusive boundaries whereas the return value is the groups of page + * metrics with viewports on either side of the breakpoint boundaries. */ function ilo_group_page_metrics_by_breakpoint( array $page_metrics, array $breakpoints ) { - $max_index = count( $breakpoints ); - $groups = array_fill( 0, $max_index + 1, array() ); - $largest_breakpoint = $breakpoints[ $max_index - 1 ]; + + // Convert breakpoint max widths into viewport minimum widths. + $viewport_minimum_widths = array_map( + static function ( $breakpoint ) { + return $breakpoint + 1; + }, + $breakpoints + ); + + $grouped = array_fill_keys( array_merge( array( 0 ), $viewport_minimum_widths ), array() ); + foreach ( $page_metrics as $page_metric ) { if ( ! isset( $page_metric['viewport']['width'] ) ) { continue; } $viewport_width = $page_metric['viewport']['width']; - if ( $viewport_width > $largest_breakpoint ) { - $groups[ $max_index ][] = $page_metric; - } - foreach ( $breakpoints as $group => $breakpoint ) { - if ( $viewport_width <= $breakpoint ) { - $groups[ $group ][] = $page_metric; + + $current_minimum_viewport = 0; + foreach ( $viewport_minimum_widths as $viewport_minimum_width ) { + if ( $viewport_width > $viewport_minimum_width ) { + $current_minimum_viewport = $viewport_minimum_width; + } else { + break; } } + + $grouped[ $current_minimum_viewport ][] = $page_metric; } - return $groups; + return $grouped; } diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php index d3bfefdc32..c9a0c173ba 100644 --- a/modules/images/image-loading-optimization/storage/post-type.php +++ b/modules/images/image-loading-optimization/storage/post-type.php @@ -31,7 +31,7 @@ function ilo_register_page_metrics_post_type() { 'query_var' => false, 'delete_with_user' => false, 'can_export' => false, - 'supports' => array( 'title' ), // The original URL is stored in the post_title, and the MD5 hash in the post_name. + 'supports' => array( 'title' ), // The original URL is stored in the post_title, and the post_name is a hash of the query vars. ) ); } @@ -98,6 +98,24 @@ function ilo_parse_stored_page_metrics( WP_Post $post ) { return $page_metrics; } +/** + * Parses post content in page metrics post. + * + * @param string $slug Page metrics slug. + * @return array Page metrics data, or null if invalid. + */ +function ilo_get_page_metrics_data( $slug ) { + $post = ilo_get_page_metrics_post( $slug ); + if ( ! ( $post instanceof WP_Post ) ) { + return null; + } + $data = ilo_parse_stored_page_metrics( $post ); + if ( ! is_array( $data ) ) { + return null; + } + return $data; +} + /** * Stores page metric by merging it with the other page metrics for a given URL. * diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php index 2c2816c9a3..7d48ced227 100644 --- a/modules/images/image-loading-optimization/storage/rest-api.php +++ b/modules/images/image-loading-optimization/storage/rest-api.php @@ -157,8 +157,12 @@ function ilo_register_endpoint() { function ilo_handle_rest_request( WP_REST_Request $request ) { ilo_set_page_metric_storage_lock(); - $page_metric = $request->get_json_params(); - $result = ilo_store_page_metric( $page_metric['url'], $page_metric['slug'], $request->get_json_params() ); + $page_metric = wp_array_slice_assoc( $request->get_json_params(), array( 'viewport', 'elements' ) ); + $result = ilo_store_page_metric( + $request->get_param( 'url' ), + $request->get_param( 'slug' ), + $page_metric + ); if ( $result instanceof WP_Error ) { return $result; @@ -168,7 +172,7 @@ function ilo_handle_rest_request( WP_REST_Request $request ) { array( 'success' => true, 'post_id' => $result, - 'data' => ilo_parse_stored_page_metrics( ilo_get_page_metrics_post( $page_metric['slug'] ) ), // TODO: Remove this debug data. + 'data' => ilo_parse_stored_page_metrics( ilo_get_page_metrics_post( $request->get_param( 'slug' ) ) ), // TODO: Remove this debug data. ) ); } From a6b7760bcbe4c44acadb31f656e06993fecb50bc Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 13 Nov 2023 10:58:27 -0800 Subject: [PATCH 35/62] Fix function prefix in tests and self-assignment --- admin/server-timing.php | 4 ++-- modules/images/image-loading-optimization/detection.php | 2 +- server-timing/class-perflab-server-timing.php | 2 +- tests/admin/server-timing-tests.php | 2 +- .../images/image-loading-optimization/load-tests.php | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/admin/server-timing.php b/admin/server-timing.php index 46f28df890..85daaf1464 100644 --- a/admin/server-timing.php +++ b/admin/server-timing.php @@ -43,7 +43,7 @@ function perflab_add_server_timing_page() { * @since 2.6.0 */ function perflab_load_server_timing_page() { - if ( ! has_filter( 'template_include', 'image_loading_optimization_buffer_output' ) ) { + if ( ! has_filter( 'template_include', 'ilo_buffer_output' ) ) { /* * This settings section technically includes a field, however it is directly rendered as part of the section * callback due to requiring custom markup. @@ -95,7 +95,7 @@ static function () { ); ?>

- +

assertArrayHasKey( PERFLAB_SERVER_TIMING_SCREEN, $wp_settings_sections ); $expected_sections = array( 'benchmarking' ); - if ( ! has_filter( 'template_include', 'image_loading_optimization_buffer_output' ) ) { + if ( ! has_filter( 'template_include', 'ilo_buffer_output' ) ) { $expected_sections[] = 'output-buffering'; } $this->assertEqualSets( diff --git a/tests/modules/images/image-loading-optimization/load-tests.php b/tests/modules/images/image-loading-optimization/load-tests.php index a1a9333c05..3fb443947d 100644 --- a/tests/modules/images/image-loading-optimization/load-tests.php +++ b/tests/modules/images/image-loading-optimization/load-tests.php @@ -16,14 +16,14 @@ class Image_Loading_Optimization_Load_Tests extends ImagesTestCase { * @test */ public function it_is_hooking_output_buffering_at_template_include() { - $this->assertEquals( PHP_INT_MAX, has_filter( 'template_include', 'image_loading_optimization_buffer_output' ) ); + $this->assertEquals( PHP_INT_MAX, has_filter( 'template_include', 'ilo_buffer_output' ) ); } /** * Make output is buffered and that it is also filtered. * * @test - * @covers ::image_loading_optimization_buffer_output + * @covers ::ilo_buffer_output */ public function it_buffers_and_filters_output() { $original = 'Hello World!'; @@ -42,7 +42,7 @@ function ( $buffer ) use ( $original, $expected ) { ); $original_ob_level = ob_get_level(); - image_loading_optimization_buffer_output(); + ilo_buffer_output(); $this->assertSame( $original_ob_level + 1, ob_get_level(), 'Expected call to ob_start().' ); echo $original; From f7aa73b4ee4ec342d933e5a405376e4725371aee Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 13 Nov 2023 11:03:10 -0800 Subject: [PATCH 36/62] Fix filter for ilo_get_page_metric_freshness_ttl; reduce TTL from month to day --- .../image-loading-optimization/storage/data.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php index 1eda44737b..91d20d46a7 100644 --- a/modules/images/image-loading-optimization/storage/data.php +++ b/modules/images/image-loading-optimization/storage/data.php @@ -11,19 +11,19 @@ } /** - * Gets the expiration age for a given page metric. + * Gets the freshness age (TTL) for a given page metric. * - * When a page metric expires it is eligible to be replaced by a newer one. + * When a page metric expires it is eligible to be replaced by a newer one if its viewport lies within the same breakpoint. * - * @return int Expiration age in seconds. + * @return int Expiration TTL in seconds. */ function ilo_get_page_metric_freshness_ttl() { /** - * Filters the expiration age for a given page metric. + * Filters the freshness age (TTL) for a given page metric. * - * @param int $ttl TTL. + * @param int $ttl Expiration TTL in seconds. */ - return (int) apply_filters( 'ilo_page_metric_ttl', MONTH_IN_SECONDS ); + return (int) apply_filters( 'ilo_page_metric_freshness_ttl', DAY_IN_SECONDS ); } /** From 8eb0dbef72f7a7957f9fc5660601901b19760d3e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 13 Nov 2023 17:16:39 -0800 Subject: [PATCH 37/62] Add client-side and server-side checks for whether page metrics needed for breakpoints --- .../image-loading-optimization/detection.php | 45 ++++----------- .../detection/detect.js | 47 +++++++++++++--- .../storage/data.php | 55 +++++++++++++++++++ .../storage/rest-api.php | 15 ++++- 4 files changed, 116 insertions(+), 46 deletions(-) diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php index 91f336793d..2f96406e0f 100644 --- a/modules/images/image-loading-optimization/detection.php +++ b/modules/images/image-loading-optimization/detection.php @@ -13,41 +13,15 @@ /** * Prints the script for detecting loaded images and the LCP element. * - * @todo This should eventually only print the script if metrics are needed. * @todo This script should not be printed if the page was requested with non-removal (non-canonical) query args. */ function ilo_print_detection_script() { $query_vars = ilo_get_normalized_query_vars(); $slug = ilo_get_page_metrics_slug( $query_vars ); - $data = ilo_get_page_metrics_data( $slug ); - if ( ! is_array( $data ) ) { - $data = array(); - } - - $metrics_by_breakpoint = ilo_group_page_metrics_by_breakpoint( $data, ilo_get_breakpoint_max_widths() ); - $sample_size = ilo_get_page_metrics_breakpoint_sample_size(); - $freshness_ttl = ilo_get_page_metric_freshness_ttl(); - - // TODO: This same logic needs to be in the endpoint so that we can reject requests when not needed. - $current_time = time(); - $needed_minimum_viewport_widths = array(); - foreach ( $metrics_by_breakpoint as $minimum_viewport_width => $page_metrics ) { - $needs_page_metrics = false; - if ( count( $page_metrics ) < $sample_size ) { - $needs_page_metrics = true; - } else { - foreach ( $page_metrics as $page_metric ) { - if ( isset( $page_metric['timestamp'] ) && $page_metric['timestamp'] + $freshness_ttl < $current_time ) { - $needs_page_metrics = true; - break; - } - } - } - $needed_minimum_viewport_widths[ $minimum_viewport_width ] = $needs_page_metrics; - } // Abort if we already have all the sample size we need for all breakpoints. - if ( count( array_filter( $needed_minimum_viewport_widths ) ) === 0 ) { + $needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths( $slug ); + if ( ! ilo_needs_page_metric_for_breakpoint( $needed_minimum_viewport_widths ) ) { return; } @@ -74,13 +48,14 @@ function ilo_print_detection_script() { $detection_time_window = apply_filters( 'perflab_image_loading_detection_time_window', 5000 ); $detect_args = array( - 'serveTime' => $serve_time, - 'detectionTimeWindow' => $detection_time_window, - 'isDebug' => WP_DEBUG, - 'restApiEndpoint' => rest_url( ILO_REST_API_NAMESPACE . ILO_PAGE_METRICS_ROUTE ), - 'restApiNonce' => wp_create_nonce( 'wp_rest' ), - 'pageMetricsSlug' => $slug, - 'pageMetricsNonce' => ilo_get_page_metrics_storage_nonce( $slug ), + 'serveTime' => $serve_time, + 'detectionTimeWindow' => $detection_time_window, + 'isDebug' => WP_DEBUG, + 'restApiEndpoint' => rest_url( ILO_REST_API_NAMESPACE . ILO_PAGE_METRICS_ROUTE ), + 'restApiNonce' => wp_create_nonce( 'wp_rest' ), + 'pageMetricsSlug' => $slug, + 'pageMetricsNonce' => ilo_get_page_metrics_storage_nonce( $slug ), + 'neededMinimumViewportWidths' => $needed_minimum_viewport_widths, ); wp_print_inline_script_tag( sprintf( diff --git a/modules/images/image-loading-optimization/detection/detect.js b/modules/images/image-loading-optimization/detection/detect.js index 6379cf7856..5101f127df 100644 --- a/modules/images/image-loading-optimization/detection/detect.js +++ b/modules/images/image-loading-optimization/detection/detect.js @@ -95,17 +95,40 @@ function getBreadcrumbs( leafElement ) { return breadcrumbs; } +/** + * Checks whether the page metric(s) for the provided viewport width is needed. + * + * @param {number} viewportWidth - Current viewport width. + * @param {Array[]} neededMinimumViewportWidths - Needed minimum viewport widths, in ascending order. + * @return {boolean} Whether page metrics are needed. + */ +function isViewportNeeded( viewportWidth, neededMinimumViewportWidths ) { + let lastWasNeeded = false; + for ( const [ + minimumViewportWidth, + isNeeded, + ] of neededMinimumViewportWidths ) { + if ( viewportWidth >= minimumViewportWidth ) { + lastWasNeeded = isNeeded; + } else { + break; + } + } + return lastWasNeeded; +} + /** * Detects the LCP element, loaded images, client viewport and store for future optimizations. * - * @param {Object} args Args. - * @param {number} args.serveTime The serve time of the page in milliseconds from PHP via `ceil( microtime( true ) * 1000 )`. - * @param {number} args.detectionTimeWindow The number of milliseconds between now and when the page was first generated in which detection should proceed. - * @param {boolean} args.isDebug Whether to show debug messages. - * @param {string} args.restApiEndpoint URL for where to send the detection data. - * @param {string} args.restApiNonce Nonce for writing to the REST API. - * @param {string} args.pageMetricsSlug Slug for page metrics. - * @param {string} args.pageMetricsNonce Nonce for page metrics storage. + * @param {Object} args Args. + * @param {number} args.serveTime The serve time of the page in milliseconds from PHP via `ceil( microtime( true ) * 1000 )`. + * @param {number} args.detectionTimeWindow The number of milliseconds between now and when the page was first generated in which detection should proceed. + * @param {boolean} args.isDebug Whether to show debug messages. + * @param {string} args.restApiEndpoint URL for where to send the detection data. + * @param {string} args.restApiNonce Nonce for writing to the REST API. + * @param {string} args.pageMetricsSlug Slug for page metrics. + * @param {string} args.pageMetricsNonce Nonce for page metrics storage. + * @param {Array} args.neededMinimumViewportWidths Needed minimum viewport widths for page metrics. */ export default async function detect( { serveTime, @@ -115,6 +138,7 @@ export default async function detect( { restApiNonce, pageMetricsSlug, pageMetricsNonce, + neededMinimumViewportWidths, // TODO: The name is not great here. } ) { const runTime = new Date().valueOf(); @@ -139,6 +163,13 @@ export default async function detect( { return; } + if ( ! isViewportNeeded( win.innerWidth, neededMinimumViewportWidths ) ) { + if ( isDebug ) { + log( 'No need for page metrics from the current viewport.' ); + } + return; + } + if ( isDebug ) { log( 'Proceeding with detection' ); } diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php index 91d20d46a7..1088de68b1 100644 --- a/modules/images/image-loading-optimization/storage/data.php +++ b/modules/images/image-loading-optimization/storage/data.php @@ -250,3 +250,58 @@ static function ( $breakpoint ) { } return $grouped; } + +/** + * Get needed minimum viewport widths. + * + * @param string $slug Page metric slug. + * @return array Array of tuples mapping minimum viewport width to whether page metric(s) are needed. + */ +function ilo_get_needed_minimum_viewport_widths( $slug ) { + $data = ilo_get_page_metrics_data( $slug ); + if ( ! is_array( $data ) ) { + $data = array(); + } + + $metrics_by_breakpoint = ilo_group_page_metrics_by_breakpoint( $data, ilo_get_breakpoint_max_widths() ); + $sample_size = ilo_get_page_metrics_breakpoint_sample_size(); + $freshness_ttl = ilo_get_page_metric_freshness_ttl(); + + $current_time = time(); + $needed_minimum_viewport_widths = array(); + foreach ( $metrics_by_breakpoint as $minimum_viewport_width => $viewport_page_metrics ) { + $needs_page_metrics = false; + if ( count( $viewport_page_metrics ) < $sample_size ) { + $needs_page_metrics = true; + } else { + foreach ( $viewport_page_metrics as $page_metric ) { + if ( isset( $page_metric['timestamp'] ) && $page_metric['timestamp'] + $freshness_ttl < $current_time ) { + $needs_page_metrics = true; + break; + } + } + } + $needed_minimum_viewport_widths[] = array( + $minimum_viewport_width, + $needs_page_metrics, + ); + } + + return $needed_minimum_viewport_widths; +} + + +/** + * Checks whether there is a page metric needed for one of the breakpoints. + * + * @param array $needed_minimum_viewport_widths Array of tuples mapping minimum viewport width to whether page metric(s) are needed. + * @return bool Whether a page metric is needed. + */ +function ilo_needs_page_metric_for_breakpoint( $needed_minimum_viewport_widths ) { + foreach ( $needed_minimum_viewport_widths as list( $minimum_viewport_width, $is_needed ) ) { + if ( $is_needed ) { + return true; + } + } + return false; +} diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php index 7d48ced227..bcd90347a0 100644 --- a/modules/images/image-loading-optimization/storage/rest-api.php +++ b/modules/images/image-loading-optimization/storage/rest-api.php @@ -155,13 +155,22 @@ function ilo_register_endpoint() { * @return WP_REST_Response|WP_Error Response. */ function ilo_handle_rest_request( WP_REST_Request $request ) { + $needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths( $request->get_param( 'slug' ) ); + if ( ! ilo_needs_page_metric_for_breakpoint( $needed_minimum_viewport_widths ) ) { + return new WP_Error( + 'no_page_metric_needed', + __( 'No page metric needed for any of the breakpoints.', 'performance-lab' ), + array( 'status' => 403 ) + ); + } + ilo_set_page_metric_storage_lock(); + $new_page_metric = wp_array_slice_assoc( $request->get_json_params(), array( 'viewport', 'elements' ) ); - $page_metric = wp_array_slice_assoc( $request->get_json_params(), array( 'viewport', 'elements' ) ); - $result = ilo_store_page_metric( + $result = ilo_store_page_metric( $request->get_param( 'url' ), $request->get_param( 'slug' ), - $page_metric + $new_page_metric ); if ( $result instanceof WP_Error ) { From 8c6b55984b7604ac01886bf8c87e8720748f7354 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 13 Nov 2023 17:21:49 -0800 Subject: [PATCH 38/62] Remove unused ilo_get_current_url() --- .../storage/data.php | 43 ------------------- 1 file changed, 43 deletions(-) diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php index 1088de68b1..a544363d34 100644 --- a/modules/images/image-loading-optimization/storage/data.php +++ b/modules/images/image-loading-optimization/storage/data.php @@ -26,48 +26,6 @@ function ilo_get_page_metric_freshness_ttl() { return (int) apply_filters( 'ilo_page_metric_freshness_ttl', DAY_IN_SECONDS ); } -/** - * Get the URL for the current request. - * - * This is essentially the REQUEST_URI prefixed by the scheme and host for the home URL. - * This is needed in particular due to subdirectory installs. - * - * @return string Current URL. - */ -function ilo_get_current_url() { - $parsed_url = wp_parse_url( home_url() ); - - if ( ! is_array( $parsed_url ) ) { - $parsed_url = array(); - } - - if ( empty( $parsed_url['scheme'] ) ) { - $parsed_url['scheme'] = is_ssl() ? 'https' : 'http'; - } - if ( ! isset( $parsed_url['host'] ) ) { - $parsed_url['host'] = isset( $_SERVER['HTTP_HOST'] ) ? wp_unslash( $_SERVER['HTTP_HOST'] ) : 'localhost'; - } - - $current_url = $parsed_url['scheme'] . '://'; - if ( isset( $parsed_url['user'] ) ) { - $current_url .= $parsed_url['user']; - if ( isset( $parsed_url['pass'] ) ) { - $current_url .= ':' . $parsed_url['pass']; - } - $current_url .= '@'; - } - $current_url .= $parsed_url['host']; - if ( isset( $parsed_url['port'] ) ) { - $current_url .= ':' . $parsed_url['port']; - } - $current_url .= '/'; - - if ( isset( $_SERVER['REQUEST_URI'] ) ) { - $current_url .= ltrim( wp_unslash( $_SERVER['REQUEST_URI'] ), '/' ); - } - return esc_url_raw( $current_url ); -} - /** * Gets the normalized query vars for the current request. * @@ -290,7 +248,6 @@ function ilo_get_needed_minimum_viewport_widths( $slug ) { return $needed_minimum_viewport_widths; } - /** * Checks whether there is a page metric needed for one of the breakpoints. * From 7e4064ab11f2befa4e8987e1149a7052019160bb Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 13 Nov 2023 17:40:14 -0800 Subject: [PATCH 39/62] Improve testability of ilo_get_needed_minimum_viewport_widths() --- .../image-loading-optimization/detection.php | 8 +++++++- .../storage/data.php | 19 +++++++------------ .../storage/post-type.php | 13 +++++++++---- .../storage/rest-api.php | 8 +++++++- 4 files changed, 30 insertions(+), 18 deletions(-) diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php index 2f96406e0f..8bc5bd373f 100644 --- a/modules/images/image-loading-optimization/detection.php +++ b/modules/images/image-loading-optimization/detection.php @@ -20,7 +20,13 @@ function ilo_print_detection_script() { $slug = ilo_get_page_metrics_slug( $query_vars ); // Abort if we already have all the sample size we need for all breakpoints. - $needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths( $slug ); + $needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths( + ilo_get_page_metrics_data( $slug ), + time(), + ilo_get_breakpoint_max_widths(), + ilo_get_page_metrics_breakpoint_sample_size(), + ilo_get_page_metric_freshness_ttl() + ); if ( ! ilo_needs_page_metric_for_breakpoint( $needed_minimum_viewport_widths ) ) { return; } diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php index a544363d34..8a8573b7c6 100644 --- a/modules/images/image-loading-optimization/storage/data.php +++ b/modules/images/image-loading-optimization/storage/data.php @@ -212,20 +212,15 @@ static function ( $breakpoint ) { /** * Get needed minimum viewport widths. * - * @param string $slug Page metric slug. + * @param array $page_metrics Page metrics. + * @param int $current_time Current time. + * @param int[] $breakpoint_max_widths Breakpoint max widths. + * @param int $sample_size Sample size for viewports in a breakpoint. + * @param int $freshness_ttl Freshness TTL for a page metric. * @return array Array of tuples mapping minimum viewport width to whether page metric(s) are needed. */ -function ilo_get_needed_minimum_viewport_widths( $slug ) { - $data = ilo_get_page_metrics_data( $slug ); - if ( ! is_array( $data ) ) { - $data = array(); - } - - $metrics_by_breakpoint = ilo_group_page_metrics_by_breakpoint( $data, ilo_get_breakpoint_max_widths() ); - $sample_size = ilo_get_page_metrics_breakpoint_sample_size(); - $freshness_ttl = ilo_get_page_metric_freshness_ttl(); - - $current_time = time(); +function ilo_get_needed_minimum_viewport_widths( $page_metrics, $current_time, $breakpoint_max_widths, $sample_size, $freshness_ttl ) { + $metrics_by_breakpoint = ilo_group_page_metrics_by_breakpoint( $page_metrics, $breakpoint_max_widths ); $needed_minimum_viewport_widths = array(); foreach ( $metrics_by_breakpoint as $minimum_viewport_width => $viewport_page_metrics ) { $needs_page_metrics = false; diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php index c9a0c173ba..b5b5a6db29 100644 --- a/modules/images/image-loading-optimization/storage/post-type.php +++ b/modules/images/image-loading-optimization/storage/post-type.php @@ -99,19 +99,24 @@ function ilo_parse_stored_page_metrics( WP_Post $post ) { } /** - * Parses post content in page metrics post. + * Gets page metrics for a slug. + * + * This is a convenience abstractions for lower-level functions. + * + * @see ilo_get_page_metrics_post() + * @see ilo_parse_stored_page_metrics() * * @param string $slug Page metrics slug. - * @return array Page metrics data, or null if invalid. + * @return array Page metrics data, or empty array if invalid. */ function ilo_get_page_metrics_data( $slug ) { $post = ilo_get_page_metrics_post( $slug ); if ( ! ( $post instanceof WP_Post ) ) { - return null; + return array(); } $data = ilo_parse_stored_page_metrics( $post ); if ( ! is_array( $data ) ) { - return null; + return array(); } return $data; } diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php index bcd90347a0..2e8a0518dd 100644 --- a/modules/images/image-loading-optimization/storage/rest-api.php +++ b/modules/images/image-loading-optimization/storage/rest-api.php @@ -155,7 +155,13 @@ function ilo_register_endpoint() { * @return WP_REST_Response|WP_Error Response. */ function ilo_handle_rest_request( WP_REST_Request $request ) { - $needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths( $request->get_param( 'slug' ) ); + $needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths( + ilo_get_page_metrics_data( $request->get_param( 'slug' ) ), + time(), + ilo_get_breakpoint_max_widths(), + ilo_get_page_metrics_breakpoint_sample_size(), + ilo_get_page_metric_freshness_ttl() + ); if ( ! ilo_needs_page_metric_for_breakpoint( $needed_minimum_viewport_widths ) ) { return new WP_Error( 'no_page_metric_needed', From a1f1aaa9e9261740380558af5c9901661465e6b5 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 13 Nov 2023 18:16:54 -0800 Subject: [PATCH 40/62] Opt for sessionStorage for storage lock for aborting --- .../image-loading-optimization/detection.php | 6 +- .../detection/detect.js | 87 ++++++++++++++++--- 2 files changed, 76 insertions(+), 17 deletions(-) diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php index 8bc5bd373f..287ef79425 100644 --- a/modules/images/image-loading-optimization/detection.php +++ b/modules/images/image-loading-optimization/detection.php @@ -31,11 +31,6 @@ function ilo_print_detection_script() { return; } - // Abort if storage is locked. - if ( ilo_is_page_metric_storage_locked() ) { - return; - } - $serve_time = ceil( microtime( true ) * 1000 ); /** @@ -62,6 +57,7 @@ function ilo_print_detection_script() { 'pageMetricsSlug' => $slug, 'pageMetricsNonce' => ilo_get_page_metrics_storage_nonce( $slug ), 'neededMinimumViewportWidths' => $needed_minimum_viewport_widths, + 'storageLockTTL' => ilo_get_page_metric_storage_lock_ttl(), ); wp_print_inline_script_tag( sprintf( diff --git a/modules/images/image-loading-optimization/detection/detect.js b/modules/images/image-loading-optimization/detection/detect.js index 5101f127df..11bc7d0c3b 100644 --- a/modules/images/image-loading-optimization/detection/detect.js +++ b/modules/images/image-loading-optimization/detection/detect.js @@ -5,6 +5,43 @@ const doc = win.document; const consoleLogPrefix = '[Image Loading Optimization]'; +const storageLockTimeSessionKey = 'iloStorageLockTime'; + +/** + * Checks whether storage is locked. + * + * @param {number} currentTime - Current time in milliseconds. + * @param {number} storageLockTTL - Storage lock TTL in seconds. + * @return {boolean} Whether storage is locked. + */ +function isStorageLocked( currentTime, storageLockTTL ) { + try { + const storageLockTime = parseInt( + sessionStorage.getItem( storageLockTimeSessionKey ) + ); + return ( + ! isNaN( storageLockTime ) && + currentTime < storageLockTime + storageLockTTL * 1000 + ); + } catch ( e ) { + return false; + } +} + +/** + * Set the storage lock. + * + * @param {number} currentTime - Current time in milliseconds. + */ +function setStorageLock( currentTime ) { + try { + sessionStorage.setItem( + storageLockTimeSessionKey, + String( currentTime ) + ); + } catch ( e ) {} +} + /** * Log a message. * @@ -117,18 +154,28 @@ function isViewportNeeded( viewportWidth, neededMinimumViewportWidths ) { return lastWasNeeded; } +/** + * Gets the current time in milliseconds. + * + * @return {number} Current time in milliseconds. + */ +function getCurrentTime() { + return new Date().valueOf(); +} + /** * Detects the LCP element, loaded images, client viewport and store for future optimizations. * - * @param {Object} args Args. - * @param {number} args.serveTime The serve time of the page in milliseconds from PHP via `ceil( microtime( true ) * 1000 )`. - * @param {number} args.detectionTimeWindow The number of milliseconds between now and when the page was first generated in which detection should proceed. - * @param {boolean} args.isDebug Whether to show debug messages. - * @param {string} args.restApiEndpoint URL for where to send the detection data. - * @param {string} args.restApiNonce Nonce for writing to the REST API. - * @param {string} args.pageMetricsSlug Slug for page metrics. - * @param {string} args.pageMetricsNonce Nonce for page metrics storage. - * @param {Array} args.neededMinimumViewportWidths Needed minimum viewport widths for page metrics. + * @param {Object} args Args. + * @param {number} args.serveTime The serve time of the page in milliseconds from PHP via `ceil( microtime( true ) * 1000 )`. + * @param {number} args.detectionTimeWindow The number of milliseconds between now and when the page was first generated in which detection should proceed. + * @param {boolean} args.isDebug Whether to show debug messages. + * @param {string} args.restApiEndpoint URL for where to send the detection data. + * @param {string} args.restApiNonce Nonce for writing to the REST API. + * @param {string} args.pageMetricsSlug Slug for page metrics. + * @param {string} args.pageMetricsNonce Nonce for page metrics storage. + * @param {Array[]} args.neededMinimumViewportWidths Needed minimum viewport widths for page metrics. + * @param {number} args.storageLockTTL The TTL (in seconds) for the page metric storage lock. */ export default async function detect( { serveTime, @@ -138,12 +185,23 @@ export default async function detect( { restApiNonce, pageMetricsSlug, pageMetricsNonce, - neededMinimumViewportWidths, // TODO: The name is not great here. + neededMinimumViewportWidths, + storageLockTTL, } ) { - const runTime = new Date().valueOf(); + const currentTime = getCurrentTime(); + + // As an alternative to this, the ilo_print_detection_script() function can short-circuit if the + // ilo_is_page_metric_storage_locked() function returns true. However, the downside with that is page caching could + // result in metrics being missed being gathered when a user navigates around a site and primes the page cache. + if ( isStorageLocked( currentTime, storageLockTTL ) ) { + if ( isDebug ) { + warn( 'Aborted detection due to storage being locked.' ); + } + return; + } // Abort running detection logic if it was served in a cached page. - if ( runTime - serveTime > detectionTimeWindow ) { + if ( currentTime - serveTime > detectionTimeWindow ) { if ( isDebug ) { warn( 'Aborted detection due to being outside detection time window.' @@ -351,6 +409,11 @@ export default async function detect( { }, body: JSON.stringify( pageMetrics ), } ); + + if ( response.status === 200 ) { + setStorageLock( getCurrentTime() ); + } + if ( isDebug ) { const body = await response.json(); if ( response.status === 200 ) { From acf17f97e6ef66a1a499fa04f47efc2d471a02a4 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 14 Nov 2023 10:33:22 -0800 Subject: [PATCH 41/62] Ensure grouped page metrics are sorted by timestamp before unshifting --- .../image-loading-optimization/storage/data.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php index 8a8573b7c6..e5294c7380 100644 --- a/modules/images/image-loading-optimization/storage/data.php +++ b/modules/images/image-loading-optimization/storage/data.php @@ -110,6 +110,18 @@ function ilo_unshift_page_metrics( $page_metrics, $validated_page_metric ) { foreach ( $grouped_page_metrics as &$breakpoint_page_metrics ) { if ( count( $breakpoint_page_metrics ) > $sample_size ) { + + // Sort page metrics in descending order by timestamp. + usort( + $breakpoint_page_metrics, + static function ( $a, $b ) { + if ( ! isset( $a['timestamp'] ) || ! isset( $b['timestamp'] ) ) { + return 0; + } + return $b['timestamp'] <=> $a['timestamp']; + } + ); + $breakpoint_page_metrics = array_slice( $breakpoint_page_metrics, 0, $sample_size ); } } From 6cd8198532b7076770d93b2003364949d0d3030f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 14 Nov 2023 10:36:18 -0800 Subject: [PATCH 42/62] Use microtime(true) instead of time() --- .../images/image-loading-optimization/detection.php | 7 +++---- .../image-loading-optimization/detection/detect.js | 4 ++-- .../image-loading-optimization/storage/data.php | 2 +- .../image-loading-optimization/storage/lock.php | 12 ++++++------ .../image-loading-optimization/storage/post-type.php | 2 +- .../image-loading-optimization/storage/rest-api.php | 2 +- 6 files changed, 14 insertions(+), 15 deletions(-) diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php index 287ef79425..ac251f83b9 100644 --- a/modules/images/image-loading-optimization/detection.php +++ b/modules/images/image-loading-optimization/detection.php @@ -18,11 +18,12 @@ function ilo_print_detection_script() { $query_vars = ilo_get_normalized_query_vars(); $slug = ilo_get_page_metrics_slug( $query_vars ); + $microtime = microtime( true ); // Abort if we already have all the sample size we need for all breakpoints. $needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths( ilo_get_page_metrics_data( $slug ), - time(), + $microtime, ilo_get_breakpoint_max_widths(), ilo_get_page_metrics_breakpoint_sample_size(), ilo_get_page_metric_freshness_ttl() @@ -31,8 +32,6 @@ function ilo_print_detection_script() { return; } - $serve_time = ceil( microtime( true ) * 1000 ); - /** * Filters the time window between serve time and run time in which loading detection is allowed to run. * @@ -49,7 +48,7 @@ function ilo_print_detection_script() { $detection_time_window = apply_filters( 'perflab_image_loading_detection_time_window', 5000 ); $detect_args = array( - 'serveTime' => $serve_time, + 'serveTime' => $microtime * 1000, // In milliseconds for comparison with `Date.now()` in JavaScript. 'detectionTimeWindow' => $detection_time_window, 'isDebug' => WP_DEBUG, 'restApiEndpoint' => rest_url( ILO_REST_API_NAMESPACE . ILO_PAGE_METRICS_ROUTE ), diff --git a/modules/images/image-loading-optimization/detection/detect.js b/modules/images/image-loading-optimization/detection/detect.js index 11bc7d0c3b..803919f28c 100644 --- a/modules/images/image-loading-optimization/detection/detect.js +++ b/modules/images/image-loading-optimization/detection/detect.js @@ -160,14 +160,14 @@ function isViewportNeeded( viewportWidth, neededMinimumViewportWidths ) { * @return {number} Current time in milliseconds. */ function getCurrentTime() { - return new Date().valueOf(); + return Date.now(); } /** * Detects the LCP element, loaded images, client viewport and store for future optimizations. * * @param {Object} args Args. - * @param {number} args.serveTime The serve time of the page in milliseconds from PHP via `ceil( microtime( true ) * 1000 )`. + * @param {number} args.serveTime The serve time of the page in milliseconds from PHP via `microtime( true ) * 1000`. * @param {number} args.detectionTimeWindow The number of milliseconds between now and when the page was first generated in which detection should proceed. * @param {boolean} args.isDebug Whether to show debug messages. * @param {string} args.restApiEndpoint URL for where to send the detection data. diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php index e5294c7380..e1a747310f 100644 --- a/modules/images/image-loading-optimization/storage/data.php +++ b/modules/images/image-loading-optimization/storage/data.php @@ -225,7 +225,7 @@ static function ( $breakpoint ) { * Get needed minimum viewport widths. * * @param array $page_metrics Page metrics. - * @param int $current_time Current time. + * @param float $current_time Current time as returned by microtime(true). * @param int[] $breakpoint_max_widths Breakpoint max widths. * @param int $sample_size Sample size for viewports in a breakpoint. * @param int $freshness_ttl Freshness TTL for a page metric. diff --git a/modules/images/image-loading-optimization/storage/lock.php b/modules/images/image-loading-optimization/storage/lock.php index 1ae536397b..f0084b9d38 100644 --- a/modules/images/image-loading-optimization/storage/lock.php +++ b/modules/images/image-loading-optimization/storage/lock.php @@ -11,9 +11,9 @@ } /** - * Gets the TTL for the page metric storage lock. + * Gets the TTL (in seconds) for the page metric storage lock. * - * @return int TTL. + * @return int TTL in seconds. */ function ilo_get_page_metric_storage_lock_ttl() { @@ -47,7 +47,7 @@ function ilo_set_page_metric_storage_lock() { if ( 0 === $ttl ) { delete_transient( $key ); } else { - set_transient( $key, time(), $ttl ); + set_transient( $key, microtime( true ), $ttl ); } } @@ -61,9 +61,9 @@ function ilo_is_page_metric_storage_locked() { if ( 0 === $ttl ) { return false; } - $locked_time = (int) get_transient( ilo_get_page_metric_storage_lock_transient_key() ); - if ( 0 === $locked_time ) { + $locked_time = get_transient( ilo_get_page_metric_storage_lock_transient_key() ); + if ( false === $locked_time ) { return false; } - return time() - $locked_time < $ttl; + return microtime( true ) - floatval( $locked_time ) < $ttl; } diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php index b5b5a6db29..3654cccd1e 100644 --- a/modules/images/image-loading-optimization/storage/post-type.php +++ b/modules/images/image-loading-optimization/storage/post-type.php @@ -140,7 +140,7 @@ function ilo_get_page_metrics_data( $slug ) { * @return int|WP_Error Post ID or WP_Error otherwise. */ function ilo_store_page_metric( $url, $slug, array $validated_page_metric ) { - $validated_page_metric['timestamp'] = time(); + $validated_page_metric['timestamp'] = microtime( true ); // TODO: What about storing a version identifier? $post_data = array( diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php index 2e8a0518dd..0c84c79f51 100644 --- a/modules/images/image-loading-optimization/storage/rest-api.php +++ b/modules/images/image-loading-optimization/storage/rest-api.php @@ -157,7 +157,7 @@ function ilo_register_endpoint() { function ilo_handle_rest_request( WP_REST_Request $request ) { $needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths( ilo_get_page_metrics_data( $request->get_param( 'slug' ) ), - time(), + microtime( true ), ilo_get_breakpoint_max_widths(), ilo_get_page_metrics_breakpoint_sample_size(), ilo_get_page_metric_freshness_ttl() From 9d6707c54ed3e04cd6378f49187e30b54e14337e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 14 Nov 2023 10:42:03 -0800 Subject: [PATCH 43/62] Reduce code duplication with helper function --- .../image-loading-optimization/detection.php | 8 +------ .../storage/data.php | 21 +++++++++++++++++++ .../storage/rest-api.php | 8 +------ 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php index ac251f83b9..f7e929480d 100644 --- a/modules/images/image-loading-optimization/detection.php +++ b/modules/images/image-loading-optimization/detection.php @@ -21,13 +21,7 @@ function ilo_print_detection_script() { $microtime = microtime( true ); // Abort if we already have all the sample size we need for all breakpoints. - $needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths( - ilo_get_page_metrics_data( $slug ), - $microtime, - ilo_get_breakpoint_max_widths(), - ilo_get_page_metrics_breakpoint_sample_size(), - ilo_get_page_metric_freshness_ttl() - ); + $needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths_now_for_slug( $slug ); if ( ! ilo_needs_page_metric_for_breakpoint( $needed_minimum_viewport_widths ) ) { return; } diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php index e1a747310f..4ca3081001 100644 --- a/modules/images/image-loading-optimization/storage/data.php +++ b/modules/images/image-loading-optimization/storage/data.php @@ -255,6 +255,27 @@ function ilo_get_needed_minimum_viewport_widths( $page_metrics, $current_time, $ return $needed_minimum_viewport_widths; } + +/** + * Get needed minimum viewport widths by slug for the current time. + * + * This is a convenience wrapper on top of ilo_get_needed_minimum_viewport_widths() to reduce code duplication. + * + * @see ilo_get_needed_minimum_viewport_widths() + * + * @param string $slug Page metrics slug. + * @return array Array of tuples mapping minimum viewport width to whether page metric(s) are needed. + */ +function ilo_get_needed_minimum_viewport_widths_now_for_slug( $slug ) { + return ilo_get_needed_minimum_viewport_widths( + ilo_get_page_metrics_data( $slug ), + microtime( true ), + ilo_get_breakpoint_max_widths(), + ilo_get_page_metrics_breakpoint_sample_size(), + ilo_get_page_metric_freshness_ttl() + ); +} + /** * Checks whether there is a page metric needed for one of the breakpoints. * diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php index 0c84c79f51..9feea484df 100644 --- a/modules/images/image-loading-optimization/storage/rest-api.php +++ b/modules/images/image-loading-optimization/storage/rest-api.php @@ -155,13 +155,7 @@ function ilo_register_endpoint() { * @return WP_REST_Response|WP_Error Response. */ function ilo_handle_rest_request( WP_REST_Request $request ) { - $needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths( - ilo_get_page_metrics_data( $request->get_param( 'slug' ) ), - microtime( true ), - ilo_get_breakpoint_max_widths(), - ilo_get_page_metrics_breakpoint_sample_size(), - ilo_get_page_metric_freshness_ttl() - ); + $needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths_now_for_slug( $request->get_param( 'slug' ) ); if ( ! ilo_needs_page_metric_for_breakpoint( $needed_minimum_viewport_widths ) ) { return new WP_Error( 'no_page_metric_needed', From 99ef2996caf805cf2ca0d253374ee66e901cbf84 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 14 Nov 2023 11:28:41 -0800 Subject: [PATCH 44/62] Prevent optimizing search results and add ilo_can_optimize_response filter --- .../image-loading-optimization/detection.php | 6 +++-- .../storage/data.php | 23 ++++++++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php index f7e929480d..a8e014e0f5 100644 --- a/modules/images/image-loading-optimization/detection.php +++ b/modules/images/image-loading-optimization/detection.php @@ -12,10 +12,12 @@ /** * Prints the script for detecting loaded images and the LCP element. - * - * @todo This script should not be printed if the page was requested with non-removal (non-canonical) query args. */ function ilo_print_detection_script() { + if ( ! ilo_can_optimize_response() ) { + return; + } + $query_vars = ilo_get_normalized_query_vars(); $slug = ilo_get_page_metrics_slug( $query_vars ); $microtime = microtime( true ); diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php index 4ca3081001..7c4c97b6b3 100644 --- a/modules/images/image-loading-optimization/storage/data.php +++ b/modules/images/image-loading-optimization/storage/data.php @@ -26,6 +26,26 @@ function ilo_get_page_metric_freshness_ttl() { return (int) apply_filters( 'ilo_page_metric_freshness_ttl', DAY_IN_SECONDS ); } +/** + * Determines whether the current response can be optimized. + * + * Only search results are not eligible by default for optimization. This is because there is no predictability in + * whether posts in the loop will have featured images assigned or not. If a theme template for search results doesn't + * even show featured images, then this isn't an issue. + * + * @return bool Whether response can be optimized. + */ +function ilo_can_optimize_response() { + $able = ! is_search(); + + /** + * Filters whether the current response can be optimized. + * + * @param bool $able Whether response can be optimized. + */ + return (bool) apply_filters( 'ilo_can_optimize_response', $able ); +} + /** * Gets the normalized query vars for the current request. * @@ -45,8 +65,6 @@ function ilo_get_normalized_query_vars() { $normalized_query_vars = array( 'error' => 404, ); - } elseif ( array_key_exists( 's', $normalized_query_vars ) ) { - $normalized_query_vars['s'] = '...'; } return $normalized_query_vars; @@ -255,7 +273,6 @@ function ilo_get_needed_minimum_viewport_widths( $page_metrics, $current_time, $ return $needed_minimum_viewport_widths; } - /** * Get needed minimum viewport widths by slug for the current time. * From 8cb2dcf73ad67a21733f668c8f8554b0bc97baf0 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 14 Nov 2023 11:34:11 -0800 Subject: [PATCH 45/62] Add TODO for ilo_get_normalized_query_vars --- modules/images/image-loading-optimization/storage/data.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php index 7c4c97b6b3..b0c2662e38 100644 --- a/modules/images/image-loading-optimization/storage/data.php +++ b/modules/images/image-loading-optimization/storage/data.php @@ -51,6 +51,8 @@ function ilo_can_optimize_response() { * * This is used as a cache key for stored page metrics. * + * TODO: For non-singular requests, consider adding the post IDs from The Loop to ensure publishing a new post will invalidate the cache. + * * @return array Normalized query vars. */ function ilo_get_normalized_query_vars() { From a4ffbaef191b7d2b859c0a8bf08834fd57defedf Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 14 Nov 2023 12:55:45 -0800 Subject: [PATCH 46/62] Reference JSON Schema for defintion of function arg array shape --- .../image-loading-optimization/storage/data.php | 2 +- .../image-loading-optimization/storage/post-type.php | 12 +----------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php index b0c2662e38..804452bf24 100644 --- a/modules/images/image-loading-optimization/storage/data.php +++ b/modules/images/image-loading-optimization/storage/data.php @@ -119,7 +119,7 @@ function ilo_verify_page_metrics_storage_nonce( $nonce, $slug ) { * Unshift a new page metric onto an array of page metrics. * * @param array $page_metrics Page metrics. - * @param array $validated_page_metric Validated page metric. + * @param array $validated_page_metric Validated page metric. See JSON Schema defined in ilo_register_endpoint(). * @return array Updated page metrics. */ function ilo_unshift_page_metrics( $page_metrics, $validated_page_metric ) { diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php index 3654cccd1e..5c030941c3 100644 --- a/modules/images/image-loading-optimization/storage/post-type.php +++ b/modules/images/image-loading-optimization/storage/post-type.php @@ -124,19 +124,9 @@ function ilo_get_page_metrics_data( $slug ) { /** * Stores page metric by merging it with the other page metrics for a given URL. * - * The $validated_page_metric parameter has the following array shape: - * - * { - * 'viewport': array{ - * 'width': int, - * 'height': int - * }, - * 'elements': array - * } - * * @param string $url URL for the page metrics. This is used purely as metadata. * @param string $slug Page metrics slug (computed from query vars). - * @param array $validated_page_metric Page metric, already validated by REST API. + * @param array $validated_page_metric Validated page metric. See JSON Schema defined in ilo_register_endpoint(). * @return int|WP_Error Post ID or WP_Error otherwise. */ function ilo_store_page_metric( $url, $slug, array $validated_page_metric ) { From ec6e17757c51b558f5bda72c56361af89e52ce4c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 14 Nov 2023 13:17:42 -0800 Subject: [PATCH 47/62] Add array type declarations --- .../images/image-loading-optimization/storage/data.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php index 804452bf24..73283a4066 100644 --- a/modules/images/image-loading-optimization/storage/data.php +++ b/modules/images/image-loading-optimization/storage/data.php @@ -80,7 +80,7 @@ function ilo_get_normalized_query_vars() { * @param array $query_vars Normalized query vars. * @return string Slug. */ -function ilo_get_page_metrics_slug( $query_vars ) { +function ilo_get_page_metrics_slug( array $query_vars ) { return md5( wp_json_encode( $query_vars ) ); } @@ -122,7 +122,7 @@ function ilo_verify_page_metrics_storage_nonce( $nonce, $slug ) { * @param array $validated_page_metric Validated page metric. See JSON Schema defined in ilo_register_endpoint(). * @return array Updated page metrics. */ -function ilo_unshift_page_metrics( $page_metrics, $validated_page_metric ) { +function ilo_unshift_page_metrics( array $page_metrics, array $validated_page_metric ) { array_unshift( $page_metrics, $validated_page_metric ); $breakpoints = ilo_get_breakpoint_max_widths(); $sample_size = ilo_get_page_metrics_breakpoint_sample_size(); @@ -251,7 +251,7 @@ static function ( $breakpoint ) { * @param int $freshness_ttl Freshness TTL for a page metric. * @return array Array of tuples mapping minimum viewport width to whether page metric(s) are needed. */ -function ilo_get_needed_minimum_viewport_widths( $page_metrics, $current_time, $breakpoint_max_widths, $sample_size, $freshness_ttl ) { +function ilo_get_needed_minimum_viewport_widths( array $page_metrics, $current_time, array $breakpoint_max_widths, $sample_size, $freshness_ttl ) { $metrics_by_breakpoint = ilo_group_page_metrics_by_breakpoint( $page_metrics, $breakpoint_max_widths ); $needed_minimum_viewport_widths = array(); foreach ( $metrics_by_breakpoint as $minimum_viewport_width => $viewport_page_metrics ) { @@ -301,7 +301,7 @@ function ilo_get_needed_minimum_viewport_widths_now_for_slug( $slug ) { * @param array $needed_minimum_viewport_widths Array of tuples mapping minimum viewport width to whether page metric(s) are needed. * @return bool Whether a page metric is needed. */ -function ilo_needs_page_metric_for_breakpoint( $needed_minimum_viewport_widths ) { +function ilo_needs_page_metric_for_breakpoint( array $needed_minimum_viewport_widths ) { foreach ( $needed_minimum_viewport_widths as list( $minimum_viewport_width, $is_needed ) ) { if ( $is_needed ) { return true; From 817d6825b6542f6bb2e0cbfeefaf4e93d00dd504 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 14 Nov 2023 14:11:15 -0800 Subject: [PATCH 48/62] Add PHP type declarations --- .../image-loading-optimization/detection.php | 2 +- .../image-loading-optimization/hooks.php | 10 +++--- .../storage/data.php | 34 +++++++++---------- .../storage/lock.php | 8 ++--- .../storage/post-type.php | 10 +++--- .../storage/rest-api.php | 4 +-- .../image-loading-optimization/load-tests.php | 2 +- 7 files changed, 35 insertions(+), 35 deletions(-) diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php index a8e014e0f5..d445b1a230 100644 --- a/modules/images/image-loading-optimization/detection.php +++ b/modules/images/image-loading-optimization/detection.php @@ -13,7 +13,7 @@ /** * Prints the script for detecting loaded images and the LCP element. */ -function ilo_print_detection_script() { +function ilo_print_detection_script() /*: void (in PHP 7.1) */ { if ( ! ilo_can_optimize_response() ) { return; } diff --git a/modules/images/image-loading-optimization/hooks.php b/modules/images/image-loading-optimization/hooks.php index 1f002dab9d..60565a6119 100644 --- a/modules/images/image-loading-optimization/hooks.php +++ b/modules/images/image-loading-optimization/hooks.php @@ -28,19 +28,19 @@ * @since n.e.x.t * @link https://core.trac.wordpress.org/ticket/43258 * - * @param mixed $passthrough Optional. Filter value. Default null. - * @return mixed Unmodified value of $passthrough. + * @param string $passthrough Optional. Filter value. Default null. + * @return string Unmodified value of $passthrough. */ -function ilo_buffer_output( $passthrough = null ) { +function ilo_buffer_output( string $passthrough ): string { ob_start( - static function ( $output ) { + static function ( string $output ): string { /** * Filters the template output buffer prior to sending to the client. * * @param string $output Output buffer. * @return string Filtered output buffer. */ - return apply_filters( 'perflab_template_output_buffer', $output ); + return (string) apply_filters( 'perflab_template_output_buffer', $output ); } ); return $passthrough; diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php index 73283a4066..e91dc17f2a 100644 --- a/modules/images/image-loading-optimization/storage/data.php +++ b/modules/images/image-loading-optimization/storage/data.php @@ -17,7 +17,7 @@ * * @return int Expiration TTL in seconds. */ -function ilo_get_page_metric_freshness_ttl() { +function ilo_get_page_metric_freshness_ttl(): int { /** * Filters the freshness age (TTL) for a given page metric. * @@ -35,7 +35,7 @@ function ilo_get_page_metric_freshness_ttl() { * * @return bool Whether response can be optimized. */ -function ilo_can_optimize_response() { +function ilo_can_optimize_response(): bool { $able = ! is_search(); /** @@ -55,7 +55,7 @@ function ilo_can_optimize_response() { * * @return array Normalized query vars. */ -function ilo_get_normalized_query_vars() { +function ilo_get_normalized_query_vars(): array { global $wp; // Note that the order of this array is naturally normalized since it is @@ -80,7 +80,7 @@ function ilo_get_normalized_query_vars() { * @param array $query_vars Normalized query vars. * @return string Slug. */ -function ilo_get_page_metrics_slug( array $query_vars ) { +function ilo_get_page_metrics_slug( array $query_vars ): string { return md5( wp_json_encode( $query_vars ) ); } @@ -95,7 +95,7 @@ function ilo_get_page_metrics_slug( array $query_vars ) { * @param string $slug Page metrics slug. * @return string Nonce. */ -function ilo_get_page_metrics_storage_nonce( $slug ) { +function ilo_get_page_metrics_storage_nonce( string $slug ): string { return wp_create_nonce( "store_page_metrics:{$slug}" ); } @@ -107,12 +107,12 @@ function ilo_get_page_metrics_storage_nonce( $slug ) { * * @param string $nonce Page metrics storage nonce. * @param string $slug Page metrics slug. - * @return int|false 1 if the nonce is valid and generated between 0-12 hours ago, - * 2 if the nonce is valid and generated between 12-24 hours ago. - * False if the nonce is invalid. + * @return int 1 if the nonce is valid and generated between 0-12 hours ago, + * 2 if the nonce is valid and generated between 12-24 hours ago. + * 0 if the nonce is invalid. */ -function ilo_verify_page_metrics_storage_nonce( $nonce, $slug ) { - return wp_verify_nonce( $nonce, "store_page_metrics:{$slug}" ); +function ilo_verify_page_metrics_storage_nonce( string $nonce, string $slug ): int { + return (int) wp_verify_nonce( $nonce, "store_page_metrics:{$slug}" ); } /** @@ -122,7 +122,7 @@ function ilo_verify_page_metrics_storage_nonce( $nonce, $slug ) { * @param array $validated_page_metric Validated page metric. See JSON Schema defined in ilo_register_endpoint(). * @return array Updated page metrics. */ -function ilo_unshift_page_metrics( array $page_metrics, array $validated_page_metric ) { +function ilo_unshift_page_metrics( array $page_metrics, array $validated_page_metric ): array { array_unshift( $page_metrics, $validated_page_metric ); $breakpoints = ilo_get_breakpoint_max_widths(); $sample_size = ilo_get_page_metrics_breakpoint_sample_size(); @@ -163,7 +163,7 @@ static function ( $a, $b ) { * * @return int[] Breakpoint max widths, sorted in ascending order. */ -function ilo_get_breakpoint_max_widths() { +function ilo_get_breakpoint_max_widths(): array { /** * Filters the breakpoint max widths to group page metrics for various viewports. @@ -190,7 +190,7 @@ static function ( $breakpoint_max_width ) { * * @return int Sample size. */ -function ilo_get_page_metrics_breakpoint_sample_size() { +function ilo_get_page_metrics_breakpoint_sample_size(): int { /** * Filters the sample size for a breakpoint's page metrics on a given URL. * @@ -209,7 +209,7 @@ function ilo_get_page_metrics_breakpoint_sample_size() { * the breakpoints reflect the max inclusive boundaries whereas the return value is the groups of page * metrics with viewports on either side of the breakpoint boundaries. */ -function ilo_group_page_metrics_by_breakpoint( array $page_metrics, array $breakpoints ) { +function ilo_group_page_metrics_by_breakpoint( array $page_metrics, array $breakpoints ): array { // Convert breakpoint max widths into viewport minimum widths. $viewport_minimum_widths = array_map( @@ -251,7 +251,7 @@ static function ( $breakpoint ) { * @param int $freshness_ttl Freshness TTL for a page metric. * @return array Array of tuples mapping minimum viewport width to whether page metric(s) are needed. */ -function ilo_get_needed_minimum_viewport_widths( array $page_metrics, $current_time, array $breakpoint_max_widths, $sample_size, $freshness_ttl ) { +function ilo_get_needed_minimum_viewport_widths( array $page_metrics, float $current_time, array $breakpoint_max_widths, int $sample_size, int $freshness_ttl ): array { $metrics_by_breakpoint = ilo_group_page_metrics_by_breakpoint( $page_metrics, $breakpoint_max_widths ); $needed_minimum_viewport_widths = array(); foreach ( $metrics_by_breakpoint as $minimum_viewport_width => $viewport_page_metrics ) { @@ -285,7 +285,7 @@ function ilo_get_needed_minimum_viewport_widths( array $page_metrics, $current_t * @param string $slug Page metrics slug. * @return array Array of tuples mapping minimum viewport width to whether page metric(s) are needed. */ -function ilo_get_needed_minimum_viewport_widths_now_for_slug( $slug ) { +function ilo_get_needed_minimum_viewport_widths_now_for_slug( string $slug ): array { return ilo_get_needed_minimum_viewport_widths( ilo_get_page_metrics_data( $slug ), microtime( true ), @@ -301,7 +301,7 @@ function ilo_get_needed_minimum_viewport_widths_now_for_slug( $slug ) { * @param array $needed_minimum_viewport_widths Array of tuples mapping minimum viewport width to whether page metric(s) are needed. * @return bool Whether a page metric is needed. */ -function ilo_needs_page_metric_for_breakpoint( array $needed_minimum_viewport_widths ) { +function ilo_needs_page_metric_for_breakpoint( array $needed_minimum_viewport_widths ): bool { foreach ( $needed_minimum_viewport_widths as list( $minimum_viewport_width, $is_needed ) ) { if ( $is_needed ) { return true; diff --git a/modules/images/image-loading-optimization/storage/lock.php b/modules/images/image-loading-optimization/storage/lock.php index f0084b9d38..b9794394f5 100644 --- a/modules/images/image-loading-optimization/storage/lock.php +++ b/modules/images/image-loading-optimization/storage/lock.php @@ -15,7 +15,7 @@ * * @return int TTL in seconds. */ -function ilo_get_page_metric_storage_lock_ttl() { +function ilo_get_page_metric_storage_lock_ttl(): int { /** * Filters how long a given IP is locked from submitting another metric-storage REST API request. @@ -33,7 +33,7 @@ function ilo_get_page_metric_storage_lock_ttl() { * @todo Should the URL be included in the key? Or should a user only be allowed to store one metric? * @return string Transient key. */ -function ilo_get_page_metric_storage_lock_transient_key() { +function ilo_get_page_metric_storage_lock_transient_key(): string { $ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR']; return 'page_metrics_storage_lock_' . wp_hash( $ip_address ); } @@ -41,7 +41,7 @@ function ilo_get_page_metric_storage_lock_transient_key() { /** * Sets page metric storage lock (for the current IP). */ -function ilo_set_page_metric_storage_lock() { +function ilo_set_page_metric_storage_lock() /*: void (in PHP 7.1) */ { $ttl = ilo_get_page_metric_storage_lock_ttl(); $key = ilo_get_page_metric_storage_lock_transient_key(); if ( 0 === $ttl ) { @@ -56,7 +56,7 @@ function ilo_set_page_metric_storage_lock() { * * @return bool Whether locked. */ -function ilo_is_page_metric_storage_locked() { +function ilo_is_page_metric_storage_locked(): bool { $ttl = ilo_get_page_metric_storage_lock_ttl(); if ( 0 === $ttl ) { return false; diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php index 5c030941c3..1e0be232e6 100644 --- a/modules/images/image-loading-optimization/storage/post-type.php +++ b/modules/images/image-loading-optimization/storage/post-type.php @@ -17,7 +17,7 @@ * * This the configuration for this post type is similar to the oembed_cache in core. */ -function ilo_register_page_metrics_post_type() { +function ilo_register_page_metrics_post_type() /*: void (in PHP 7.1) */ { register_post_type( ILO_PAGE_METRICS_POST_TYPE, array( @@ -43,7 +43,7 @@ function ilo_register_page_metrics_post_type() { * @param string $slug Page metrics slug. * @return WP_Post|null Post object if exists. */ -function ilo_get_page_metrics_post( $slug ) { +function ilo_get_page_metrics_post( string $slug ) /*: ?WP_Post (in PHP 7.1) */ { $post_query = new WP_Query( array( 'post_type' => ILO_PAGE_METRICS_POST_TYPE, @@ -72,7 +72,7 @@ function ilo_get_page_metrics_post( $slug ) { * @param WP_Post $post Page metrics post. * @return array|WP_Error Page metrics when valid, or WP_Error otherwise. */ -function ilo_parse_stored_page_metrics( WP_Post $post ) { +function ilo_parse_stored_page_metrics( WP_Post $post ) /*: array|WP_Error (in PHP 8) */ { $page_metrics = json_decode( $post->post_content, true ); if ( json_last_error() ) { return new WP_Error( @@ -109,7 +109,7 @@ function ilo_parse_stored_page_metrics( WP_Post $post ) { * @param string $slug Page metrics slug. * @return array Page metrics data, or empty array if invalid. */ -function ilo_get_page_metrics_data( $slug ) { +function ilo_get_page_metrics_data( string $slug ): array { $post = ilo_get_page_metrics_post( $slug ); if ( ! ( $post instanceof WP_Post ) ) { return array(); @@ -129,7 +129,7 @@ function ilo_get_page_metrics_data( $slug ) { * @param array $validated_page_metric Validated page metric. See JSON Schema defined in ilo_register_endpoint(). * @return int|WP_Error Post ID or WP_Error otherwise. */ -function ilo_store_page_metric( $url, $slug, array $validated_page_metric ) { +function ilo_store_page_metric( string $url, string $slug, array $validated_page_metric ) /*: int|WP_Error (in PHP 8) */ { $validated_page_metric['timestamp'] = microtime( true ); // TODO: What about storing a version identifier? diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php index 9feea484df..71c1f5300f 100644 --- a/modules/images/image-loading-optimization/storage/rest-api.php +++ b/modules/images/image-loading-optimization/storage/rest-api.php @@ -17,7 +17,7 @@ /** * Register endpoint for storage of page metric. */ -function ilo_register_endpoint() { +function ilo_register_endpoint() /*: void (in PHP 7.1) */ { $dom_rect_schema = array( 'type' => 'object', @@ -154,7 +154,7 @@ function ilo_register_endpoint() { * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error Response. */ -function ilo_handle_rest_request( WP_REST_Request $request ) { +function ilo_handle_rest_request( WP_REST_Request $request ) /*: WP_REST_Response|WP_Error (in PHP 8) */ { $needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths_now_for_slug( $request->get_param( 'slug' ) ); if ( ! ilo_needs_page_metric_for_breakpoint( $needed_minimum_viewport_widths ) ) { return new WP_Error( diff --git a/tests/modules/images/image-loading-optimization/load-tests.php b/tests/modules/images/image-loading-optimization/load-tests.php index 3fb443947d..1015d12b7d 100644 --- a/tests/modules/images/image-loading-optimization/load-tests.php +++ b/tests/modules/images/image-loading-optimization/load-tests.php @@ -42,7 +42,7 @@ function ( $buffer ) use ( $original, $expected ) { ); $original_ob_level = ob_get_level(); - ilo_buffer_output(); + ilo_buffer_output( '' ); $this->assertSame( $original_ob_level + 1, ob_get_level(), 'Expected call to ob_start().' ); echo $original; From caaef872dbd05595a730a91ad9efee2ad47caefd Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 14 Nov 2023 18:10:24 -0800 Subject: [PATCH 49/62] Fix placement of ilo_breakpoint_max_widths filter --- .../images/image-loading-optimization/storage/data.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php index e91dc17f2a..96f642c0a9 100644 --- a/modules/images/image-loading-optimization/storage/data.php +++ b/modules/images/image-loading-optimization/storage/data.php @@ -165,15 +165,15 @@ static function ( $a, $b ) { */ function ilo_get_breakpoint_max_widths(): array { - /** - * Filters the breakpoint max widths to group page metrics for various viewports. - * - * @param int[] $breakpoint_max_widths Max widths for viewport breakpoints. - */ $breakpoint_max_widths = array_map( static function ( $breakpoint_max_width ) { return (int) $breakpoint_max_width; }, + /** + * Filters the breakpoint max widths to group page metrics for various viewports. + * + * @param int[] $breakpoint_max_widths Max widths for viewport breakpoints. + */ (array) apply_filters( 'ilo_breakpoint_max_widths', array( 480 ) ) ); From eadec5592a8b103e3522fa9f29b75e5b8dcea2e9 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 14 Nov 2023 18:11:43 -0800 Subject: [PATCH 50/62] Use 3rd person singular for function phpdoc Co-authored-by: Felix Arntz --- modules/images/image-loading-optimization/storage/data.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php index 96f642c0a9..b90953e45d 100644 --- a/modules/images/image-loading-optimization/storage/data.php +++ b/modules/images/image-loading-optimization/storage/data.php @@ -85,7 +85,7 @@ function ilo_get_page_metrics_slug( array $query_vars ): string { } /** - * Compute nonce for storing page metrics for a specific slug. + * Computes nonce for storing page metrics for a specific slug. * * This is used in the REST API to authenticate the storage of new page metrics from a given URL. * @@ -100,7 +100,7 @@ function ilo_get_page_metrics_storage_nonce( string $slug ): string { } /** - * Verify nonce for storing page metrics for a specific slug. + * Verifies nonce for storing page metrics for a specific slug. * * @see wp_verify_nonce() * @see ilo_get_page_metrics_storage_nonce() @@ -116,7 +116,7 @@ function ilo_verify_page_metrics_storage_nonce( string $nonce, string $slug ): i } /** - * Unshift a new page metric onto an array of page metrics. + * Unshifts a new page metric onto an array of page metrics. * * @param array $page_metrics Page metrics. * @param array $validated_page_metric Validated page metric. See JSON Schema defined in ilo_register_endpoint(). From 02dbb359fa726ed889475f9cad66bb362f9b36e8 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 14 Nov 2023 18:15:25 -0800 Subject: [PATCH 51/62] Use 3rd person singular in phpdoc for additional functions --- modules/images/image-loading-optimization/storage/data.php | 4 ++-- .../images/image-loading-optimization/storage/post-type.php | 4 ++-- .../images/image-loading-optimization/storage/rest-api.php | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php index b90953e45d..25d3bca571 100644 --- a/modules/images/image-loading-optimization/storage/data.php +++ b/modules/images/image-loading-optimization/storage/data.php @@ -242,7 +242,7 @@ static function ( $breakpoint ) { } /** - * Get needed minimum viewport widths. + * Gets needed minimum viewport widths. * * @param array $page_metrics Page metrics. * @param float $current_time Current time as returned by microtime(true). @@ -276,7 +276,7 @@ function ilo_get_needed_minimum_viewport_widths( array $page_metrics, float $cur } /** - * Get needed minimum viewport widths by slug for the current time. + * Gets needed minimum viewport widths by slug for the current time. * * This is a convenience wrapper on top of ilo_get_needed_minimum_viewport_widths() to reduce code duplication. * diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php index 1e0be232e6..c3a840e240 100644 --- a/modules/images/image-loading-optimization/storage/post-type.php +++ b/modules/images/image-loading-optimization/storage/post-type.php @@ -13,7 +13,7 @@ const ILO_PAGE_METRICS_POST_TYPE = 'ilo_page_metrics'; /** - * Register post type for page metrics storage. + * Registers post type for page metrics storage. * * This the configuration for this post type is similar to the oembed_cache in core. */ @@ -38,7 +38,7 @@ function ilo_register_page_metrics_post_type() /*: void (in PHP 7.1) */ { add_action( 'init', 'ilo_register_page_metrics_post_type' ); /** - * Get page metrics post. + * Gets page metrics post. * * @param string $slug Page metrics slug. * @return WP_Post|null Post object if exists. diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php index 71c1f5300f..9726f04087 100644 --- a/modules/images/image-loading-optimization/storage/rest-api.php +++ b/modules/images/image-loading-optimization/storage/rest-api.php @@ -15,7 +15,7 @@ const ILO_PAGE_METRICS_ROUTE = '/page-metrics'; /** - * Register endpoint for storage of page metric. + * Registers endpoint for storage of page metric. */ function ilo_register_endpoint() /*: void (in PHP 7.1) */ { @@ -149,7 +149,7 @@ function ilo_register_endpoint() /*: void (in PHP 7.1) */ { add_action( 'rest_api_init', 'ilo_register_endpoint' ); /** - * Handle REST API request to store metrics. + * Handles REST API request to store metrics. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error Response. From 06d7fad7f75e02a0f3b2e572ba6d289065515fd1 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 14 Nov 2023 18:17:39 -0800 Subject: [PATCH 52/62] Account for isStorageLocked with storageLockTTL of 0 Co-authored-by: Felix Arntz --- modules/images/image-loading-optimization/detection/detect.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/images/image-loading-optimization/detection/detect.js b/modules/images/image-loading-optimization/detection/detect.js index 803919f28c..58a0034376 100644 --- a/modules/images/image-loading-optimization/detection/detect.js +++ b/modules/images/image-loading-optimization/detection/detect.js @@ -15,6 +15,10 @@ const storageLockTimeSessionKey = 'iloStorageLockTime'; * @return {boolean} Whether storage is locked. */ function isStorageLocked( currentTime, storageLockTTL ) { + if ( ! storageLockTTL ) { + return false; + } + try { const storageLockTime = parseInt( sessionStorage.getItem( storageLockTimeSessionKey ) From da0b4208b73694d1e1800cb04178140b9837b094 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 14 Nov 2023 19:55:14 -0800 Subject: [PATCH 53/62] Ensure storage lock TTL is at least zero and add filter example --- .../detection/detect.js | 2 +- .../image-loading-optimization/storage/lock.php | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/modules/images/image-loading-optimization/detection/detect.js b/modules/images/image-loading-optimization/detection/detect.js index 58a0034376..96d0587a4b 100644 --- a/modules/images/image-loading-optimization/detection/detect.js +++ b/modules/images/image-loading-optimization/detection/detect.js @@ -15,7 +15,7 @@ const storageLockTimeSessionKey = 'iloStorageLockTime'; * @return {boolean} Whether storage is locked. */ function isStorageLocked( currentTime, storageLockTTL ) { - if ( ! storageLockTTL ) { + if ( storageLockTTL === 0 ) { return false; } diff --git a/modules/images/image-loading-optimization/storage/lock.php b/modules/images/image-loading-optimization/storage/lock.php index b9794394f5..eb6b21ca68 100644 --- a/modules/images/image-loading-optimization/storage/lock.php +++ b/modules/images/image-loading-optimization/storage/lock.php @@ -13,18 +13,24 @@ /** * Gets the TTL (in seconds) for the page metric storage lock. * - * @return int TTL in seconds. + * @return int TTL in seconds, greater than or equal to zero. */ function ilo_get_page_metric_storage_lock_ttl(): int { /** * Filters how long a given IP is locked from submitting another metric-storage REST API request. * - * Filtering the TTL to zero will disable any metric storage locking. This is useful during development. + * Filtering the TTL to zero will disable any metric storage locking. This is useful, for example, to disable + * locking when a user is logged-in with code like the following: + * + * add_filter( 'ilo_metrics_storage_lock_ttl', static function ( $ttl ) { + * return is_user_logged_in() ? 0 : $ttl; + * } ); * * @param int $ttl TTL. */ - return (int) apply_filters( 'ilo_metrics_storage_lock_ttl', MINUTE_IN_SECONDS ); + $ttl = (int) apply_filters( 'ilo_metrics_storage_lock_ttl', MINUTE_IN_SECONDS ); + return max( 0, $ttl ); } /** @@ -40,6 +46,9 @@ function ilo_get_page_metric_storage_lock_transient_key(): string { /** * Sets page metric storage lock (for the current IP). + * + * If the storage lock TTL is greater than zero, then a transient is set with the current timestamp and expiring at TTL + * seconds. Otherwise, if the current TTL is zero, then any transient is deleted. */ function ilo_set_page_metric_storage_lock() /*: void (in PHP 7.1) */ { $ttl = ilo_get_page_metric_storage_lock_ttl(); From 19ce75252cd1ef2a414e421d1c8a71024a5d1a32 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 14 Nov 2023 20:23:08 -0800 Subject: [PATCH 54/62] Add since and access private tags --- .../image-loading-optimization/detection.php | 3 ++ .../image-loading-optimization/hooks.php | 5 ++- .../storage/data.php | 45 +++++++++++++++++++ .../storage/lock.php | 11 +++++ .../storage/post-type.php | 15 +++++++ .../storage/rest-api.php | 6 +++ 6 files changed, 83 insertions(+), 2 deletions(-) diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php index d445b1a230..db8b3a658a 100644 --- a/modules/images/image-loading-optimization/detection.php +++ b/modules/images/image-loading-optimization/detection.php @@ -12,6 +12,9 @@ /** * Prints the script for detecting loaded images and the LCP element. + * + * @since n.e.x.t + * @access private */ function ilo_print_detection_script() /*: void (in PHP 7.1) */ { if ( ! ilo_can_optimize_response() ) { diff --git a/modules/images/image-loading-optimization/hooks.php b/modules/images/image-loading-optimization/hooks.php index 60565a6119..426f20b646 100644 --- a/modules/images/image-loading-optimization/hooks.php +++ b/modules/images/image-loading-optimization/hooks.php @@ -17,15 +17,14 @@ * * This is a hack which would eventually be replaced with something like this in wp-includes/template-loader.php: * - * ``` * $template = apply_filters( 'template_include', $template ); * + ob_start( 'wp_template_output_buffer_callback' ); * if ( $template ) { * include $template; * } elseif ( current_user_can( 'switch_themes' ) ) { - * ``` * * @since n.e.x.t + * @access private * @link https://core.trac.wordpress.org/ticket/43258 * * @param string $passthrough Optional. Filter value. Default null. @@ -37,6 +36,8 @@ static function ( string $output ): string { /** * Filters the template output buffer prior to sending to the client. * + * @since n.e.x.t + * * @param string $output Output buffer. * @return string Filtered output buffer. */ diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php index 25d3bca571..b562d2784e 100644 --- a/modules/images/image-loading-optimization/storage/data.php +++ b/modules/images/image-loading-optimization/storage/data.php @@ -15,12 +15,17 @@ * * When a page metric expires it is eligible to be replaced by a newer one if its viewport lies within the same breakpoint. * + * @since n.e.x.t + * @access private + * * @return int Expiration TTL in seconds. */ function ilo_get_page_metric_freshness_ttl(): int { /** * Filters the freshness age (TTL) for a given page metric. * + * @since n.e.x.t + * * @param int $ttl Expiration TTL in seconds. */ return (int) apply_filters( 'ilo_page_metric_freshness_ttl', DAY_IN_SECONDS ); @@ -33,6 +38,9 @@ function ilo_get_page_metric_freshness_ttl(): int { * whether posts in the loop will have featured images assigned or not. If a theme template for search results doesn't * even show featured images, then this isn't an issue. * + * @since n.e.x.t + * @access private + * * @return bool Whether response can be optimized. */ function ilo_can_optimize_response(): bool { @@ -41,6 +49,8 @@ function ilo_can_optimize_response(): bool { /** * Filters whether the current response can be optimized. * + * @since n.e.x.t + * * @param bool $able Whether response can be optimized. */ return (bool) apply_filters( 'ilo_can_optimize_response', $able ); @@ -53,6 +63,9 @@ function ilo_can_optimize_response(): bool { * * TODO: For non-singular requests, consider adding the post IDs from The Loop to ensure publishing a new post will invalidate the cache. * + * @since n.e.x.t + * @access private + * * @return array Normalized query vars. */ function ilo_get_normalized_query_vars(): array { @@ -75,6 +88,9 @@ function ilo_get_normalized_query_vars(): array { /** * Gets slug for page metrics. * + * @since n.e.x.t + * @access private + * * @see ilo_get_normalized_query_vars() * * @param array $query_vars Normalized query vars. @@ -89,6 +105,9 @@ function ilo_get_page_metrics_slug( array $query_vars ): string { * * This is used in the REST API to authenticate the storage of new page metrics from a given URL. * + * @since n.e.x.t + * @access private + * * @see wp_create_nonce() * @see ilo_verify_page_metrics_storage_nonce() * @@ -102,6 +121,9 @@ function ilo_get_page_metrics_storage_nonce( string $slug ): string { /** * Verifies nonce for storing page metrics for a specific slug. * + * @since n.e.x.t + * @access private + * * @see wp_verify_nonce() * @see ilo_get_page_metrics_storage_nonce() * @@ -118,6 +140,9 @@ function ilo_verify_page_metrics_storage_nonce( string $nonce, string $slug ): i /** * Unshifts a new page metric onto an array of page metrics. * + * @since n.e.x.t + * @access private + * * @param array $page_metrics Page metrics. * @param array $validated_page_metric Validated page metric. See JSON Schema defined in ilo_register_endpoint(). * @return array Updated page metrics. @@ -161,6 +186,9 @@ static function ( $a, $b ) { * 3. 481-576 (phablets) * 4. >576 (desktop) * + * @since n.e.x.t + * @access private + * * @return int[] Breakpoint max widths, sorted in ascending order. */ function ilo_get_breakpoint_max_widths(): array { @@ -188,12 +216,17 @@ static function ( $breakpoint_max_width ) { * sample size of 3 and there being just a single breakpoint (480) by default, for a given URL, there would be a maximum * total of 6 page metrics stored for a given URL: 3 for mobile and 3 for desktop. * + * @since n.e.x.t + * @access private + * * @return int Sample size. */ function ilo_get_page_metrics_breakpoint_sample_size(): int { /** * Filters the sample size for a breakpoint's page metrics on a given URL. * + * @since n.e.x.t + * * @param int $sample_size Sample size. */ return (int) apply_filters( 'ilo_page_metrics_breakpoint_sample_size', 3 ); @@ -202,6 +235,9 @@ function ilo_get_page_metrics_breakpoint_sample_size(): int { /** * Groups page metrics by breakpoint. * + * @since n.e.x.t + * @access private + * * @param array $page_metrics Page metrics. * @param int[] $breakpoints Viewport breakpoint max widths, sorted in ascending order. * @return array Page metrics grouped by breakpoint. The array keys are the minimum widths for a viewport to lie within @@ -244,6 +280,9 @@ static function ( $breakpoint ) { /** * Gets needed minimum viewport widths. * + * @since n.e.x.t + * @access private + * * @param array $page_metrics Page metrics. * @param float $current_time Current time as returned by microtime(true). * @param int[] $breakpoint_max_widths Breakpoint max widths. @@ -280,6 +319,9 @@ function ilo_get_needed_minimum_viewport_widths( array $page_metrics, float $cur * * This is a convenience wrapper on top of ilo_get_needed_minimum_viewport_widths() to reduce code duplication. * + * @since n.e.x.t + * @access private + * * @see ilo_get_needed_minimum_viewport_widths() * * @param string $slug Page metrics slug. @@ -298,6 +340,9 @@ function ilo_get_needed_minimum_viewport_widths_now_for_slug( string $slug ): ar /** * Checks whether there is a page metric needed for one of the breakpoints. * + * @since n.e.x.t + * @access private + * * @param array $needed_minimum_viewport_widths Array of tuples mapping minimum viewport width to whether page metric(s) are needed. * @return bool Whether a page metric is needed. */ diff --git a/modules/images/image-loading-optimization/storage/lock.php b/modules/images/image-loading-optimization/storage/lock.php index eb6b21ca68..5b9307fdad 100644 --- a/modules/images/image-loading-optimization/storage/lock.php +++ b/modules/images/image-loading-optimization/storage/lock.php @@ -13,6 +13,9 @@ /** * Gets the TTL (in seconds) for the page metric storage lock. * + * @since n.e.x.t + * @access private + * * @return int TTL in seconds, greater than or equal to zero. */ function ilo_get_page_metric_storage_lock_ttl(): int { @@ -27,6 +30,8 @@ function ilo_get_page_metric_storage_lock_ttl(): int { * return is_user_logged_in() ? 0 : $ttl; * } ); * + * @since n.e.x.t + * * @param int $ttl TTL. */ $ttl = (int) apply_filters( 'ilo_metrics_storage_lock_ttl', MINUTE_IN_SECONDS ); @@ -49,6 +54,9 @@ function ilo_get_page_metric_storage_lock_transient_key(): string { * * If the storage lock TTL is greater than zero, then a transient is set with the current timestamp and expiring at TTL * seconds. Otherwise, if the current TTL is zero, then any transient is deleted. + * + * @since n.e.x.t + * @access private */ function ilo_set_page_metric_storage_lock() /*: void (in PHP 7.1) */ { $ttl = ilo_get_page_metric_storage_lock_ttl(); @@ -63,6 +71,9 @@ function ilo_set_page_metric_storage_lock() /*: void (in PHP 7.1) */ { /** * Checks whether page metric storage is locked (for the current IP). * + * @since n.e.x.t + * @access private + * * @return bool Whether locked. */ function ilo_is_page_metric_storage_locked(): bool { diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php index c3a840e240..fce053b38d 100644 --- a/modules/images/image-loading-optimization/storage/post-type.php +++ b/modules/images/image-loading-optimization/storage/post-type.php @@ -16,6 +16,9 @@ * Registers post type for page metrics storage. * * This the configuration for this post type is similar to the oembed_cache in core. + * + * @since n.e.x.t + * @access private */ function ilo_register_page_metrics_post_type() /*: void (in PHP 7.1) */ { register_post_type( @@ -40,6 +43,9 @@ function ilo_register_page_metrics_post_type() /*: void (in PHP 7.1) */ { /** * Gets page metrics post. * + * @since n.e.x.t + * @access private + * * @param string $slug Page metrics slug. * @return WP_Post|null Post object if exists. */ @@ -69,6 +75,9 @@ function ilo_get_page_metrics_post( string $slug ) /*: ?WP_Post (in PHP 7.1) */ /** * Parses post content in page metrics post. * + * @since n.e.x.t + * @access private + * * @param WP_Post $post Page metrics post. * @return array|WP_Error Page metrics when valid, or WP_Error otherwise. */ @@ -103,6 +112,9 @@ function ilo_parse_stored_page_metrics( WP_Post $post ) /*: array|WP_Error (in P * * This is a convenience abstractions for lower-level functions. * + * @since n.e.x.t + * @access private + * * @see ilo_get_page_metrics_post() * @see ilo_parse_stored_page_metrics() * @@ -124,6 +136,9 @@ function ilo_get_page_metrics_data( string $slug ): array { /** * Stores page metric by merging it with the other page metrics for a given URL. * + * @since n.e.x.t + * @access private + * * @param string $url URL for the page metrics. This is used purely as metadata. * @param string $slug Page metrics slug (computed from query vars). * @param array $validated_page_metric Validated page metric. See JSON Schema defined in ilo_register_endpoint(). diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php index 9726f04087..8cd9848c22 100644 --- a/modules/images/image-loading-optimization/storage/rest-api.php +++ b/modules/images/image-loading-optimization/storage/rest-api.php @@ -16,6 +16,9 @@ /** * Registers endpoint for storage of page metric. + * + * @since n.e.x.t + * @access private */ function ilo_register_endpoint() /*: void (in PHP 7.1) */ { @@ -151,6 +154,9 @@ function ilo_register_endpoint() /*: void (in PHP 7.1) */ { /** * Handles REST API request to store metrics. * + * @since n.e.x.t + * @access private + * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error Response. */ From 19a2c3b274b553074f212b053c66f13fe665a148 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 14 Nov 2023 20:26:13 -0800 Subject: [PATCH 55/62] Use IMAGE_LOADING_OPTIMIZATION_VERSION constant for script ver arg --- modules/images/image-loading-optimization/detection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php index db8b3a658a..47e770e9a9 100644 --- a/modules/images/image-loading-optimization/detection.php +++ b/modules/images/image-loading-optimization/detection.php @@ -60,7 +60,7 @@ function ilo_print_detection_script() /*: void (in PHP 7.1) */ { wp_print_inline_script_tag( sprintf( 'import detect from %s; detect( %s );', - wp_json_encode( add_query_arg( 'ver', PERFLAB_VERSION, plugin_dir_url( __FILE__ ) . 'detection/detect.js' ) ), + wp_json_encode( add_query_arg( 'ver', IMAGE_LOADING_OPTIMIZATION_VERSION, plugin_dir_url( __FILE__ ) . 'detection/detect.js' ) ), wp_json_encode( $detect_args ) ), array( 'type' => 'module' ) From f566663483409664c0776989df918a9a08635dff Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 15 Nov 2023 09:52:00 -0800 Subject: [PATCH 56/62] Move error emitting to ilo_parse_stored_page_metrics() --- .../storage/data.php | 3 +- .../storage/post-type.php | 57 +++++-------------- 2 files changed, 17 insertions(+), 43 deletions(-) diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php index b562d2784e..211d1210f0 100644 --- a/modules/images/image-loading-optimization/storage/data.php +++ b/modules/images/image-loading-optimization/storage/data.php @@ -328,8 +328,9 @@ function ilo_get_needed_minimum_viewport_widths( array $page_metrics, float $cur * @return array Array of tuples mapping minimum viewport width to whether page metric(s) are needed. */ function ilo_get_needed_minimum_viewport_widths_now_for_slug( string $slug ): array { + $post = ilo_get_page_metrics_post( $slug ); return ilo_get_needed_minimum_viewport_widths( - ilo_get_page_metrics_data( $slug ), + $post instanceof WP_Post ? ilo_parse_stored_page_metrics( $post ) : array(), microtime( true ), ilo_get_breakpoint_max_widths(), ilo_get_page_metrics_breakpoint_sample_size(), diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php index fce053b38d..e712678d2b 100644 --- a/modules/images/image-loading-optimization/storage/post-type.php +++ b/modules/images/image-loading-optimization/storage/post-type.php @@ -79,13 +79,19 @@ function ilo_get_page_metrics_post( string $slug ) /*: ?WP_Post (in PHP 7.1) */ * @access private * * @param WP_Post $post Page metrics post. - * @return array|WP_Error Page metrics when valid, or WP_Error otherwise. + * @return array Page metrics. */ -function ilo_parse_stored_page_metrics( WP_Post $post ) /*: array|WP_Error (in PHP 8) */ { +function ilo_parse_stored_page_metrics( WP_Post $post ): array { + $this_function = __FUNCTION__; + $trigger_error = static function ( $error ) use ( $this_function ) { + if ( function_exists( 'wp_trigger_error' ) ) { + wp_trigger_error( $this_function, esc_html( $error ), E_USER_WARNING ); + } + }; + $page_metrics = json_decode( $post->post_content, true ); if ( json_last_error() ) { - return new WP_Error( - 'page_metrics_json_parse_error', + $trigger_error( sprintf( /* translators: 1: Post type slug, 2: JSON error message */ __( 'Contents of %1$s post type not valid JSON: %2$s', 'performance-lab' ), @@ -93,46 +99,20 @@ function ilo_parse_stored_page_metrics( WP_Post $post ) /*: array|WP_Error (in P json_last_error_msg() ) ); - } - if ( ! is_array( $page_metrics ) ) { - return new WP_Error( - 'page_metrics_invalid_data_format', + $page_metrics = array(); + } elseif ( ! is_array( $page_metrics ) ) { + $trigger_error( sprintf( /* translators: %s is post type slug */ __( 'Contents of %s post type was not a JSON array.', 'performance-lab' ), ILO_PAGE_METRICS_POST_TYPE ) ); + $page_metrics = array(); } return $page_metrics; } -/** - * Gets page metrics for a slug. - * - * This is a convenience abstractions for lower-level functions. - * - * @since n.e.x.t - * @access private - * - * @see ilo_get_page_metrics_post() - * @see ilo_parse_stored_page_metrics() - * - * @param string $slug Page metrics slug. - * @return array Page metrics data, or empty array if invalid. - */ -function ilo_get_page_metrics_data( string $slug ): array { - $post = ilo_get_page_metrics_post( $slug ); - if ( ! ( $post instanceof WP_Post ) ) { - return array(); - } - $data = ilo_parse_stored_page_metrics( $post ); - if ( ! is_array( $data ) ) { - return array(); - } - return $data; -} - /** * Stores page metric by merging it with the other page metrics for a given URL. * @@ -157,14 +137,7 @@ function ilo_store_page_metric( string $url, string $slug, array $validated_page if ( $post instanceof WP_Post ) { $post_data['ID'] = $post->ID; $post_data['post_name'] = $post->post_name; - - $page_metrics = ilo_parse_stored_page_metrics( $post ); - if ( $page_metrics instanceof WP_Error ) { - if ( function_exists( 'wp_trigger_error' ) ) { - wp_trigger_error( __FUNCTION__, esc_html( $page_metrics->get_error_message() ) ); - } - $page_metrics = array(); - } + $page_metrics = ilo_parse_stored_page_metrics( $post ); } else { $post_data['post_name'] = $slug; $page_metrics = array(); From b228934d938040485671559145b5044c2b656497 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 15 Nov 2023 11:24:18 -0800 Subject: [PATCH 57/62] Elaborate on return value phpdoc for ilo_get_page_metric_storage_lock_ttl() Co-authored-by: Felix Arntz --- modules/images/image-loading-optimization/storage/lock.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/images/image-loading-optimization/storage/lock.php b/modules/images/image-loading-optimization/storage/lock.php index 5b9307fdad..97902d4a30 100644 --- a/modules/images/image-loading-optimization/storage/lock.php +++ b/modules/images/image-loading-optimization/storage/lock.php @@ -16,7 +16,7 @@ * @since n.e.x.t * @access private * - * @return int TTL in seconds, greater than or equal to zero. + * @return int TTL in seconds, greater than or equal to zero. A value of zero means that the storage lock should be disabled and thus that transients must not be used. */ function ilo_get_page_metric_storage_lock_ttl(): int { From 95d940603cecfeacbdddc64d04384acce303cc54 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 15 Nov 2023 11:27:11 -0800 Subject: [PATCH 58/62] Remove overkill PHP 7.1+ type declaration comments --- modules/images/image-loading-optimization/detection.php | 2 +- modules/images/image-loading-optimization/storage/lock.php | 2 +- .../images/image-loading-optimization/storage/post-type.php | 6 +++--- .../images/image-loading-optimization/storage/rest-api.php | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php index 47e770e9a9..90a1ffc461 100644 --- a/modules/images/image-loading-optimization/detection.php +++ b/modules/images/image-loading-optimization/detection.php @@ -16,7 +16,7 @@ * @since n.e.x.t * @access private */ -function ilo_print_detection_script() /*: void (in PHP 7.1) */ { +function ilo_print_detection_script() { if ( ! ilo_can_optimize_response() ) { return; } diff --git a/modules/images/image-loading-optimization/storage/lock.php b/modules/images/image-loading-optimization/storage/lock.php index 97902d4a30..298689243c 100644 --- a/modules/images/image-loading-optimization/storage/lock.php +++ b/modules/images/image-loading-optimization/storage/lock.php @@ -58,7 +58,7 @@ function ilo_get_page_metric_storage_lock_transient_key(): string { * @since n.e.x.t * @access private */ -function ilo_set_page_metric_storage_lock() /*: void (in PHP 7.1) */ { +function ilo_set_page_metric_storage_lock() { $ttl = ilo_get_page_metric_storage_lock_ttl(); $key = ilo_get_page_metric_storage_lock_transient_key(); if ( 0 === $ttl ) { diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php index e712678d2b..bb7f4e9b75 100644 --- a/modules/images/image-loading-optimization/storage/post-type.php +++ b/modules/images/image-loading-optimization/storage/post-type.php @@ -20,7 +20,7 @@ * @since n.e.x.t * @access private */ -function ilo_register_page_metrics_post_type() /*: void (in PHP 7.1) */ { +function ilo_register_page_metrics_post_type() { register_post_type( ILO_PAGE_METRICS_POST_TYPE, array( @@ -49,7 +49,7 @@ function ilo_register_page_metrics_post_type() /*: void (in PHP 7.1) */ { * @param string $slug Page metrics slug. * @return WP_Post|null Post object if exists. */ -function ilo_get_page_metrics_post( string $slug ) /*: ?WP_Post (in PHP 7.1) */ { +function ilo_get_page_metrics_post( string $slug ) { $post_query = new WP_Query( array( 'post_type' => ILO_PAGE_METRICS_POST_TYPE, @@ -124,7 +124,7 @@ function ilo_parse_stored_page_metrics( WP_Post $post ): array { * @param array $validated_page_metric Validated page metric. See JSON Schema defined in ilo_register_endpoint(). * @return int|WP_Error Post ID or WP_Error otherwise. */ -function ilo_store_page_metric( string $url, string $slug, array $validated_page_metric ) /*: int|WP_Error (in PHP 8) */ { +function ilo_store_page_metric( string $url, string $slug, array $validated_page_metric ) { $validated_page_metric['timestamp'] = microtime( true ); // TODO: What about storing a version identifier? diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php index 8cd9848c22..a9c68ef84d 100644 --- a/modules/images/image-loading-optimization/storage/rest-api.php +++ b/modules/images/image-loading-optimization/storage/rest-api.php @@ -20,7 +20,7 @@ * @since n.e.x.t * @access private */ -function ilo_register_endpoint() /*: void (in PHP 7.1) */ { +function ilo_register_endpoint() { $dom_rect_schema = array( 'type' => 'object', @@ -160,7 +160,7 @@ function ilo_register_endpoint() /*: void (in PHP 7.1) */ { * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error Response. */ -function ilo_handle_rest_request( WP_REST_Request $request ) /*: WP_REST_Response|WP_Error (in PHP 8) */ { +function ilo_handle_rest_request( WP_REST_Request $request ) { $needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths_now_for_slug( $request->get_param( 'slug' ) ); if ( ! ilo_needs_page_metric_for_breakpoint( $needed_minimum_viewport_widths ) ) { return new WP_Error( From 9876aa44911a17fe5cae2843a34f5eb5c15368ef Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 15 Nov 2023 12:03:32 -0800 Subject: [PATCH 59/62] Rename 'page metrics' to 'URL metrics' --- .../image-loading-optimization/detection.php | 12 +- .../detection/detect.js | 36 ++--- .../storage/data.php | 148 +++++++++--------- .../storage/lock.php | 26 +-- .../storage/post-type.php | 68 ++++---- .../storage/rest-api.php | 32 ++-- 6 files changed, 161 insertions(+), 161 deletions(-) diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php index 90a1ffc461..c54229fc86 100644 --- a/modules/images/image-loading-optimization/detection.php +++ b/modules/images/image-loading-optimization/detection.php @@ -22,12 +22,12 @@ function ilo_print_detection_script() { } $query_vars = ilo_get_normalized_query_vars(); - $slug = ilo_get_page_metrics_slug( $query_vars ); + $slug = ilo_get_url_metrics_slug( $query_vars ); $microtime = microtime( true ); // Abort if we already have all the sample size we need for all breakpoints. $needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths_now_for_slug( $slug ); - if ( ! ilo_needs_page_metric_for_breakpoint( $needed_minimum_viewport_widths ) ) { + if ( ! ilo_needs_url_metric_for_breakpoint( $needed_minimum_viewport_widths ) ) { return; } @@ -50,12 +50,12 @@ function ilo_print_detection_script() { 'serveTime' => $microtime * 1000, // In milliseconds for comparison with `Date.now()` in JavaScript. 'detectionTimeWindow' => $detection_time_window, 'isDebug' => WP_DEBUG, - 'restApiEndpoint' => rest_url( ILO_REST_API_NAMESPACE . ILO_PAGE_METRICS_ROUTE ), + 'restApiEndpoint' => rest_url( ILO_REST_API_NAMESPACE . ILO_URL_METRICS_ROUTE ), 'restApiNonce' => wp_create_nonce( 'wp_rest' ), - 'pageMetricsSlug' => $slug, - 'pageMetricsNonce' => ilo_get_page_metrics_storage_nonce( $slug ), + 'urlMetricsSlug' => $slug, + 'urlMetricsNonce' => ilo_get_url_metrics_storage_nonce( $slug ), 'neededMinimumViewportWidths' => $needed_minimum_viewport_widths, - 'storageLockTTL' => ilo_get_page_metric_storage_lock_ttl(), + 'storageLockTTL' => ilo_get_url_metric_storage_lock_ttl(), ); wp_print_inline_script_tag( sprintf( diff --git a/modules/images/image-loading-optimization/detection/detect.js b/modules/images/image-loading-optimization/detection/detect.js index 96d0587a4b..512832e7f2 100644 --- a/modules/images/image-loading-optimization/detection/detect.js +++ b/modules/images/image-loading-optimization/detection/detect.js @@ -93,7 +93,7 @@ function error( ...message ) { */ /** - * @typedef {Object} PageMetrics + * @typedef {Object} URLMetrics * @property {string} url - URL of the page. * @property {Object} viewport - Viewport. * @property {number} viewport.width - Viewport width. @@ -137,11 +137,11 @@ function getBreadcrumbs( leafElement ) { } /** - * Checks whether the page metric(s) for the provided viewport width is needed. + * Checks whether the URL metric(s) for the provided viewport width is needed. * * @param {number} viewportWidth - Current viewport width. * @param {Array[]} neededMinimumViewportWidths - Needed minimum viewport widths, in ascending order. - * @return {boolean} Whether page metrics are needed. + * @return {boolean} Whether URL metrics are needed. */ function isViewportNeeded( viewportWidth, neededMinimumViewportWidths ) { let lastWasNeeded = false; @@ -176,10 +176,10 @@ function getCurrentTime() { * @param {boolean} args.isDebug Whether to show debug messages. * @param {string} args.restApiEndpoint URL for where to send the detection data. * @param {string} args.restApiNonce Nonce for writing to the REST API. - * @param {string} args.pageMetricsSlug Slug for page metrics. - * @param {string} args.pageMetricsNonce Nonce for page metrics storage. - * @param {Array[]} args.neededMinimumViewportWidths Needed minimum viewport widths for page metrics. - * @param {number} args.storageLockTTL The TTL (in seconds) for the page metric storage lock. + * @param {string} args.urlMetricsSlug Slug for URL metrics. + * @param {string} args.urlMetricsNonce Nonce for URL metrics storage. + * @param {Array[]} args.neededMinimumViewportWidths Needed minimum viewport widths for URL metrics. + * @param {number} args.storageLockTTL The TTL (in seconds) for the URL metric storage lock. */ export default async function detect( { serveTime, @@ -187,15 +187,15 @@ export default async function detect( { isDebug, restApiEndpoint, restApiNonce, - pageMetricsSlug, - pageMetricsNonce, + urlMetricsSlug, + urlMetricsNonce, neededMinimumViewportWidths, storageLockTTL, } ) { const currentTime = getCurrentTime(); // As an alternative to this, the ilo_print_detection_script() function can short-circuit if the - // ilo_is_page_metric_storage_locked() function returns true. However, the downside with that is page caching could + // ilo_is_url_metric_storage_locked() function returns true. However, the downside with that is page caching could // result in metrics being missed being gathered when a user navigates around a site and primes the page cache. if ( isStorageLocked( currentTime, storageLockTTL ) ) { if ( isDebug ) { @@ -227,7 +227,7 @@ export default async function detect( { if ( ! isViewportNeeded( win.innerWidth, neededMinimumViewportWidths ) ) { if ( isDebug ) { - log( 'No need for page metrics from the current viewport.' ); + log( 'No need for URL metrics from the current viewport.' ); } return; } @@ -354,11 +354,11 @@ export default async function detect( { log( 'Detection is stopping.' ); } - /** @type {PageMetrics} */ - const pageMetrics = { + /** @type {URLMetrics} */ + const urlMetrics = { url: win.location.href, - slug: pageMetricsSlug, - nonce: pageMetricsNonce, + slug: urlMetricsSlug, + nonce: urlMetricsNonce, viewport: { width: win.innerWidth, height: win.innerHeight, @@ -396,11 +396,11 @@ export default async function detect( { boundingClientRect: elementIntersection.boundingClientRect, }; - pageMetrics.elements.push( elementMetrics ); + urlMetrics.elements.push( elementMetrics ); } if ( isDebug ) { - log( 'Page metrics:', pageMetrics ); + log( 'URL metrics:', urlMetrics ); } // TODO: Wait until idle? Yield to main? @@ -411,7 +411,7 @@ export default async function detect( { 'Content-Type': 'application/json', 'X-WP-Nonce': restApiNonce, }, - body: JSON.stringify( pageMetrics ), + body: JSON.stringify( urlMetrics ), } ); if ( response.status === 200 ) { diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php index 211d1210f0..731275d3ea 100644 --- a/modules/images/image-loading-optimization/storage/data.php +++ b/modules/images/image-loading-optimization/storage/data.php @@ -11,24 +11,24 @@ } /** - * Gets the freshness age (TTL) for a given page metric. + * Gets the freshness age (TTL) for a given URL metric. * - * When a page metric expires it is eligible to be replaced by a newer one if its viewport lies within the same breakpoint. + * When a URL metric expires it is eligible to be replaced by a newer one if its viewport lies within the same breakpoint. * * @since n.e.x.t * @access private * * @return int Expiration TTL in seconds. */ -function ilo_get_page_metric_freshness_ttl(): int { +function ilo_get_url_metric_freshness_ttl(): int { /** - * Filters the freshness age (TTL) for a given page metric. + * Filters the freshness age (TTL) for a given URL metric. * * @since n.e.x.t * * @param int $ttl Expiration TTL in seconds. */ - return (int) apply_filters( 'ilo_page_metric_freshness_ttl', DAY_IN_SECONDS ); + return (int) apply_filters( 'ilo_url_metric_freshness_ttl', DAY_IN_SECONDS ); } /** @@ -59,7 +59,7 @@ function ilo_can_optimize_response(): bool { /** * Gets the normalized query vars for the current request. * - * This is used as a cache key for stored page metrics. + * This is used as a cache key for stored URL metrics. * * TODO: For non-singular requests, consider adding the post IDs from The Loop to ensure publishing a new post will invalidate the cache. * @@ -86,7 +86,7 @@ function ilo_get_normalized_query_vars(): array { } /** - * Gets slug for page metrics. + * Gets slug for URL metrics. * * @since n.e.x.t * @access private @@ -96,69 +96,69 @@ function ilo_get_normalized_query_vars(): array { * @param array $query_vars Normalized query vars. * @return string Slug. */ -function ilo_get_page_metrics_slug( array $query_vars ): string { +function ilo_get_url_metrics_slug( array $query_vars ): string { return md5( wp_json_encode( $query_vars ) ); } /** - * Computes nonce for storing page metrics for a specific slug. + * Computes nonce for storing URL metrics for a specific slug. * - * This is used in the REST API to authenticate the storage of new page metrics from a given URL. + * This is used in the REST API to authenticate the storage of new URL metrics from a given URL. * * @since n.e.x.t * @access private * * @see wp_create_nonce() - * @see ilo_verify_page_metrics_storage_nonce() + * @see ilo_verify_url_metrics_storage_nonce() * - * @param string $slug Page metrics slug. + * @param string $slug URL metrics slug. * @return string Nonce. */ -function ilo_get_page_metrics_storage_nonce( string $slug ): string { - return wp_create_nonce( "store_page_metrics:{$slug}" ); +function ilo_get_url_metrics_storage_nonce( string $slug ): string { + return wp_create_nonce( "store_url_metrics:{$slug}" ); } /** - * Verifies nonce for storing page metrics for a specific slug. + * Verifies nonce for storing URL metrics for a specific slug. * * @since n.e.x.t * @access private * * @see wp_verify_nonce() - * @see ilo_get_page_metrics_storage_nonce() + * @see ilo_get_url_metrics_storage_nonce() * - * @param string $nonce Page metrics storage nonce. - * @param string $slug Page metrics slug. + * @param string $nonce URL metrics storage nonce. + * @param string $slug URL metrics slug. * @return int 1 if the nonce is valid and generated between 0-12 hours ago, * 2 if the nonce is valid and generated between 12-24 hours ago. * 0 if the nonce is invalid. */ -function ilo_verify_page_metrics_storage_nonce( string $nonce, string $slug ): int { - return (int) wp_verify_nonce( $nonce, "store_page_metrics:{$slug}" ); +function ilo_verify_url_metrics_storage_nonce( string $nonce, string $slug ): int { + return (int) wp_verify_nonce( $nonce, "store_url_metrics:{$slug}" ); } /** - * Unshifts a new page metric onto an array of page metrics. + * Unshifts a new URL metric onto an array of URL metrics. * * @since n.e.x.t * @access private * - * @param array $page_metrics Page metrics. - * @param array $validated_page_metric Validated page metric. See JSON Schema defined in ilo_register_endpoint(). - * @return array Updated page metrics. + * @param array $url_metrics URL metrics. + * @param array $validated_url_metric Validated URL metric. See JSON Schema defined in ilo_register_endpoint(). + * @return array Updated URL metrics. */ -function ilo_unshift_page_metrics( array $page_metrics, array $validated_page_metric ): array { - array_unshift( $page_metrics, $validated_page_metric ); - $breakpoints = ilo_get_breakpoint_max_widths(); - $sample_size = ilo_get_page_metrics_breakpoint_sample_size(); - $grouped_page_metrics = ilo_group_page_metrics_by_breakpoint( $page_metrics, $breakpoints ); +function ilo_unshift_url_metrics( array $url_metrics, array $validated_url_metric ): array { + array_unshift( $url_metrics, $validated_url_metric ); + $breakpoints = ilo_get_breakpoint_max_widths(); + $sample_size = ilo_get_url_metrics_breakpoint_sample_size(); + $grouped_url_metrics = ilo_group_url_metrics_by_breakpoint( $url_metrics, $breakpoints ); - foreach ( $grouped_page_metrics as &$breakpoint_page_metrics ) { - if ( count( $breakpoint_page_metrics ) > $sample_size ) { + foreach ( $grouped_url_metrics as &$breakpoint_url_metrics ) { + if ( count( $breakpoint_url_metrics ) > $sample_size ) { - // Sort page metrics in descending order by timestamp. + // Sort URL metrics in descending order by timestamp. usort( - $breakpoint_page_metrics, + $breakpoint_url_metrics, static function ( $a, $b ) { if ( ! isset( $a['timestamp'] ) || ! isset( $b['timestamp'] ) ) { return 0; @@ -167,15 +167,15 @@ static function ( $a, $b ) { } ); - $breakpoint_page_metrics = array_slice( $breakpoint_page_metrics, 0, $sample_size ); + $breakpoint_url_metrics = array_slice( $breakpoint_url_metrics, 0, $sample_size ); } } - return array_merge( ...$grouped_page_metrics ); + return array_merge( ...$grouped_url_metrics ); } /** - * Gets the breakpoint max widths to group page metrics for various viewports. + * Gets the breakpoint max widths to group URL metrics for various viewports. * * Each max with represents the maximum width (inclusive) for a given breakpoint. So if there is one number, 480, then * this means there will be two viewport groupings, one for 0<=480, and another >480. If instead there were three @@ -198,7 +198,7 @@ static function ( $breakpoint_max_width ) { return (int) $breakpoint_max_width; }, /** - * Filters the breakpoint max widths to group page metrics for various viewports. + * Filters the breakpoint max widths to group URL metrics for various viewports. * * @param int[] $breakpoint_max_widths Max widths for viewport breakpoints. */ @@ -210,42 +210,42 @@ static function ( $breakpoint_max_width ) { } /** - * Gets the sample size for a breakpoint's page metrics on a given URL. + * Gets the sample size for a breakpoint's URL metrics on a given URL. * - * A breakpoint divides page metrics for viewports which are smaller and those which are larger. Given the default + * A breakpoint divides URL metrics for viewports which are smaller and those which are larger. Given the default * sample size of 3 and there being just a single breakpoint (480) by default, for a given URL, there would be a maximum - * total of 6 page metrics stored for a given URL: 3 for mobile and 3 for desktop. + * total of 6 URL metrics stored for a given URL: 3 for mobile and 3 for desktop. * * @since n.e.x.t * @access private * * @return int Sample size. */ -function ilo_get_page_metrics_breakpoint_sample_size(): int { +function ilo_get_url_metrics_breakpoint_sample_size(): int { /** - * Filters the sample size for a breakpoint's page metrics on a given URL. + * Filters the sample size for a breakpoint's URL metrics on a given URL. * * @since n.e.x.t * * @param int $sample_size Sample size. */ - return (int) apply_filters( 'ilo_page_metrics_breakpoint_sample_size', 3 ); + return (int) apply_filters( 'ilo_url_metrics_breakpoint_sample_size', 3 ); } /** - * Groups page metrics by breakpoint. + * Groups URL metrics by breakpoint. * * @since n.e.x.t * @access private * - * @param array $page_metrics Page metrics. + * @param array $url_metrics URL metrics. * @param int[] $breakpoints Viewport breakpoint max widths, sorted in ascending order. - * @return array Page metrics grouped by breakpoint. The array keys are the minimum widths for a viewport to lie within + * @return array URL metrics grouped by breakpoint. The array keys are the minimum widths for a viewport to lie within * the breakpoint. The returned array is always one larger than the provided array of breakpoints, since * the breakpoints reflect the max inclusive boundaries whereas the return value is the groups of page * metrics with viewports on either side of the breakpoint boundaries. */ -function ilo_group_page_metrics_by_breakpoint( array $page_metrics, array $breakpoints ): array { +function ilo_group_url_metrics_by_breakpoint( array $url_metrics, array $breakpoints ): array { // Convert breakpoint max widths into viewport minimum widths. $viewport_minimum_widths = array_map( @@ -257,11 +257,11 @@ static function ( $breakpoint ) { $grouped = array_fill_keys( array_merge( array( 0 ), $viewport_minimum_widths ), array() ); - foreach ( $page_metrics as $page_metric ) { - if ( ! isset( $page_metric['viewport']['width'] ) ) { + foreach ( $url_metrics as $url_metric ) { + if ( ! isset( $url_metric['viewport']['width'] ) ) { continue; } - $viewport_width = $page_metric['viewport']['width']; + $viewport_width = $url_metric['viewport']['width']; $current_minimum_viewport = 0; foreach ( $viewport_minimum_widths as $viewport_minimum_width ) { @@ -272,7 +272,7 @@ static function ( $breakpoint ) { } } - $grouped[ $current_minimum_viewport ][] = $page_metric; + $grouped[ $current_minimum_viewport ][] = $url_metric; } return $grouped; } @@ -283,31 +283,31 @@ static function ( $breakpoint ) { * @since n.e.x.t * @access private * - * @param array $page_metrics Page metrics. + * @param array $url_metrics URL metrics. * @param float $current_time Current time as returned by microtime(true). * @param int[] $breakpoint_max_widths Breakpoint max widths. * @param int $sample_size Sample size for viewports in a breakpoint. - * @param int $freshness_ttl Freshness TTL for a page metric. - * @return array Array of tuples mapping minimum viewport width to whether page metric(s) are needed. + * @param int $freshness_ttl Freshness TTL for a URL metric. + * @return array Array of tuples mapping minimum viewport width to whether URL metric(s) are needed. */ -function ilo_get_needed_minimum_viewport_widths( array $page_metrics, float $current_time, array $breakpoint_max_widths, int $sample_size, int $freshness_ttl ): array { - $metrics_by_breakpoint = ilo_group_page_metrics_by_breakpoint( $page_metrics, $breakpoint_max_widths ); +function ilo_get_needed_minimum_viewport_widths( array $url_metrics, float $current_time, array $breakpoint_max_widths, int $sample_size, int $freshness_ttl ): array { + $metrics_by_breakpoint = ilo_group_url_metrics_by_breakpoint( $url_metrics, $breakpoint_max_widths ); $needed_minimum_viewport_widths = array(); - foreach ( $metrics_by_breakpoint as $minimum_viewport_width => $viewport_page_metrics ) { - $needs_page_metrics = false; - if ( count( $viewport_page_metrics ) < $sample_size ) { - $needs_page_metrics = true; + foreach ( $metrics_by_breakpoint as $minimum_viewport_width => $viewport_url_metrics ) { + $needs_url_metrics = false; + if ( count( $viewport_url_metrics ) < $sample_size ) { + $needs_url_metrics = true; } else { - foreach ( $viewport_page_metrics as $page_metric ) { - if ( isset( $page_metric['timestamp'] ) && $page_metric['timestamp'] + $freshness_ttl < $current_time ) { - $needs_page_metrics = true; + foreach ( $viewport_url_metrics as $url_metric ) { + if ( isset( $url_metric['timestamp'] ) && $url_metric['timestamp'] + $freshness_ttl < $current_time ) { + $needs_url_metrics = true; break; } } } $needed_minimum_viewport_widths[] = array( $minimum_viewport_width, - $needs_page_metrics, + $needs_url_metrics, ); } @@ -324,30 +324,30 @@ function ilo_get_needed_minimum_viewport_widths( array $page_metrics, float $cur * * @see ilo_get_needed_minimum_viewport_widths() * - * @param string $slug Page metrics slug. - * @return array Array of tuples mapping minimum viewport width to whether page metric(s) are needed. + * @param string $slug URL metrics slug. + * @return array Array of tuples mapping minimum viewport width to whether URL metric(s) are needed. */ function ilo_get_needed_minimum_viewport_widths_now_for_slug( string $slug ): array { - $post = ilo_get_page_metrics_post( $slug ); + $post = ilo_get_url_metrics_post( $slug ); return ilo_get_needed_minimum_viewport_widths( - $post instanceof WP_Post ? ilo_parse_stored_page_metrics( $post ) : array(), + $post instanceof WP_Post ? ilo_parse_stored_url_metrics( $post ) : array(), microtime( true ), ilo_get_breakpoint_max_widths(), - ilo_get_page_metrics_breakpoint_sample_size(), - ilo_get_page_metric_freshness_ttl() + ilo_get_url_metrics_breakpoint_sample_size(), + ilo_get_url_metric_freshness_ttl() ); } /** - * Checks whether there is a page metric needed for one of the breakpoints. + * Checks whether there is a URL metric needed for one of the breakpoints. * * @since n.e.x.t * @access private * - * @param array $needed_minimum_viewport_widths Array of tuples mapping minimum viewport width to whether page metric(s) are needed. - * @return bool Whether a page metric is needed. + * @param array $needed_minimum_viewport_widths Array of tuples mapping minimum viewport width to whether URL metric(s) are needed. + * @return bool Whether a URL metric is needed. */ -function ilo_needs_page_metric_for_breakpoint( array $needed_minimum_viewport_widths ): bool { +function ilo_needs_url_metric_for_breakpoint( array $needed_minimum_viewport_widths ): bool { foreach ( $needed_minimum_viewport_widths as list( $minimum_viewport_width, $is_needed ) ) { if ( $is_needed ) { return true; diff --git a/modules/images/image-loading-optimization/storage/lock.php b/modules/images/image-loading-optimization/storage/lock.php index 298689243c..d30fe8d75c 100644 --- a/modules/images/image-loading-optimization/storage/lock.php +++ b/modules/images/image-loading-optimization/storage/lock.php @@ -11,14 +11,14 @@ } /** - * Gets the TTL (in seconds) for the page metric storage lock. + * Gets the TTL (in seconds) for the URL metric storage lock. * * @since n.e.x.t * @access private * * @return int TTL in seconds, greater than or equal to zero. A value of zero means that the storage lock should be disabled and thus that transients must not be used. */ -function ilo_get_page_metric_storage_lock_ttl(): int { +function ilo_get_url_metric_storage_lock_ttl(): int { /** * Filters how long a given IP is locked from submitting another metric-storage REST API request. @@ -39,18 +39,18 @@ function ilo_get_page_metric_storage_lock_ttl(): int { } /** - * Gets transient key for locking page metric storage (for the current IP). + * Gets transient key for locking URL metric storage (for the current IP). * * @todo Should the URL be included in the key? Or should a user only be allowed to store one metric? * @return string Transient key. */ -function ilo_get_page_metric_storage_lock_transient_key(): string { +function ilo_get_url_metric_storage_lock_transient_key(): string { $ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR']; - return 'page_metrics_storage_lock_' . wp_hash( $ip_address ); + return 'url_metrics_storage_lock_' . wp_hash( $ip_address ); } /** - * Sets page metric storage lock (for the current IP). + * Sets URL metric storage lock (for the current IP). * * If the storage lock TTL is greater than zero, then a transient is set with the current timestamp and expiring at TTL * seconds. Otherwise, if the current TTL is zero, then any transient is deleted. @@ -58,9 +58,9 @@ function ilo_get_page_metric_storage_lock_transient_key(): string { * @since n.e.x.t * @access private */ -function ilo_set_page_metric_storage_lock() { - $ttl = ilo_get_page_metric_storage_lock_ttl(); - $key = ilo_get_page_metric_storage_lock_transient_key(); +function ilo_set_url_metric_storage_lock() { + $ttl = ilo_get_url_metric_storage_lock_ttl(); + $key = ilo_get_url_metric_storage_lock_transient_key(); if ( 0 === $ttl ) { delete_transient( $key ); } else { @@ -69,19 +69,19 @@ function ilo_set_page_metric_storage_lock() { } /** - * Checks whether page metric storage is locked (for the current IP). + * Checks whether URL metric storage is locked (for the current IP). * * @since n.e.x.t * @access private * * @return bool Whether locked. */ -function ilo_is_page_metric_storage_locked(): bool { - $ttl = ilo_get_page_metric_storage_lock_ttl(); +function ilo_is_url_metric_storage_locked(): bool { + $ttl = ilo_get_url_metric_storage_lock_ttl(); if ( 0 === $ttl ) { return false; } - $locked_time = get_transient( ilo_get_page_metric_storage_lock_transient_key() ); + $locked_time = get_transient( ilo_get_url_metric_storage_lock_transient_key() ); if ( false === $locked_time ) { return false; } diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php index bb7f4e9b75..a95a71176d 100644 --- a/modules/images/image-loading-optimization/storage/post-type.php +++ b/modules/images/image-loading-optimization/storage/post-type.php @@ -10,23 +10,23 @@ exit; // Exit if accessed directly. } -const ILO_PAGE_METRICS_POST_TYPE = 'ilo_page_metrics'; +const ILO_URL_METRICS_POST_TYPE = 'ilo_url_metrics'; /** - * Registers post type for page metrics storage. + * Registers post type for URL metrics storage. * * This the configuration for this post type is similar to the oembed_cache in core. * * @since n.e.x.t * @access private */ -function ilo_register_page_metrics_post_type() { +function ilo_register_url_metrics_post_type() { register_post_type( - ILO_PAGE_METRICS_POST_TYPE, + ILO_URL_METRICS_POST_TYPE, array( 'labels' => array( - 'name' => __( 'Page Metrics', 'performance-lab' ), - 'singular_name' => __( 'Page Metrics', 'performance-lab' ), + 'name' => __( 'URL Metrics', 'performance-lab' ), + 'singular_name' => __( 'URL Metrics', 'performance-lab' ), ), 'public' => false, 'hierarchical' => false, @@ -38,21 +38,21 @@ function ilo_register_page_metrics_post_type() { ) ); } -add_action( 'init', 'ilo_register_page_metrics_post_type' ); +add_action( 'init', 'ilo_register_url_metrics_post_type' ); /** - * Gets page metrics post. + * Gets URL metrics post. * * @since n.e.x.t * @access private * - * @param string $slug Page metrics slug. + * @param string $slug URL metrics slug. * @return WP_Post|null Post object if exists. */ -function ilo_get_page_metrics_post( string $slug ) { +function ilo_get_url_metrics_post( string $slug ) { $post_query = new WP_Query( array( - 'post_type' => ILO_PAGE_METRICS_POST_TYPE, + 'post_type' => ILO_URL_METRICS_POST_TYPE, 'post_status' => 'publish', 'name' => $slug, 'posts_per_page' => 1, @@ -73,15 +73,15 @@ function ilo_get_page_metrics_post( string $slug ) { } /** - * Parses post content in page metrics post. + * Parses post content in URL metrics post. * * @since n.e.x.t * @access private * - * @param WP_Post $post Page metrics post. - * @return array Page metrics. + * @param WP_Post $post URL metrics post. + * @return array URL metrics. */ -function ilo_parse_stored_page_metrics( WP_Post $post ): array { +function ilo_parse_stored_url_metrics( WP_Post $post ): array { $this_function = __FUNCTION__; $trigger_error = static function ( $error ) use ( $this_function ) { if ( function_exists( 'wp_trigger_error' ) ) { @@ -89,63 +89,63 @@ function ilo_parse_stored_page_metrics( WP_Post $post ): array { } }; - $page_metrics = json_decode( $post->post_content, true ); + $url_metrics = json_decode( $post->post_content, true ); if ( json_last_error() ) { $trigger_error( sprintf( /* translators: 1: Post type slug, 2: JSON error message */ __( 'Contents of %1$s post type not valid JSON: %2$s', 'performance-lab' ), - ILO_PAGE_METRICS_POST_TYPE, + ILO_URL_METRICS_POST_TYPE, json_last_error_msg() ) ); - $page_metrics = array(); - } elseif ( ! is_array( $page_metrics ) ) { + $url_metrics = array(); + } elseif ( ! is_array( $url_metrics ) ) { $trigger_error( sprintf( /* translators: %s is post type slug */ __( 'Contents of %s post type was not a JSON array.', 'performance-lab' ), - ILO_PAGE_METRICS_POST_TYPE + ILO_URL_METRICS_POST_TYPE ) ); - $page_metrics = array(); + $url_metrics = array(); } - return $page_metrics; + return $url_metrics; } /** - * Stores page metric by merging it with the other page metrics for a given URL. + * Stores URL metric by merging it with the other URL metrics for a given URL. * * @since n.e.x.t * @access private * - * @param string $url URL for the page metrics. This is used purely as metadata. - * @param string $slug Page metrics slug (computed from query vars). - * @param array $validated_page_metric Validated page metric. See JSON Schema defined in ilo_register_endpoint(). + * @param string $url URL for the URL metrics. This is used purely as metadata. + * @param string $slug URL metrics slug (computed from query vars). + * @param array $validated_url_metric Validated URL metric. See JSON Schema defined in ilo_register_endpoint(). * @return int|WP_Error Post ID or WP_Error otherwise. */ -function ilo_store_page_metric( string $url, string $slug, array $validated_page_metric ) { - $validated_page_metric['timestamp'] = microtime( true ); +function ilo_store_url_metric( string $url, string $slug, array $validated_url_metric ) { + $validated_url_metric['timestamp'] = microtime( true ); // TODO: What about storing a version identifier? $post_data = array( 'post_title' => $url, // TODO: Should we keep this? It can help with debugging. ); - $post = ilo_get_page_metrics_post( $slug ); + $post = ilo_get_url_metrics_post( $slug ); if ( $post instanceof WP_Post ) { $post_data['ID'] = $post->ID; $post_data['post_name'] = $post->post_name; - $page_metrics = ilo_parse_stored_page_metrics( $post ); + $url_metrics = ilo_parse_stored_url_metrics( $post ); } else { $post_data['post_name'] = $slug; - $page_metrics = array(); + $url_metrics = array(); } - $page_metrics = ilo_unshift_page_metrics( $page_metrics, $validated_page_metric ); + $url_metrics = ilo_unshift_url_metrics( $url_metrics, $validated_url_metric ); - $post_data['post_content'] = wp_json_encode( $page_metrics, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); // TODO: No need for pretty-printing. + $post_data['post_content'] = wp_json_encode( $url_metrics, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); // TODO: No need for pretty-printing. $has_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ); if ( $has_kses ) { @@ -153,7 +153,7 @@ function ilo_store_page_metric( string $url, string $slug, array $validated_page kses_remove_filters(); } - $post_data['post_type'] = ILO_PAGE_METRICS_POST_TYPE; + $post_data['post_type'] = ILO_URL_METRICS_POST_TYPE; $post_data['post_status'] = 'publish'; if ( isset( $post_data['ID'] ) ) { $result = wp_update_post( wp_slash( $post_data ), true ); diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php index a9c68ef84d..423355f344 100644 --- a/modules/images/image-loading-optimization/storage/rest-api.php +++ b/modules/images/image-loading-optimization/storage/rest-api.php @@ -12,10 +12,10 @@ const ILO_REST_API_NAMESPACE = 'image-loading-optimization/v1'; -const ILO_PAGE_METRICS_ROUTE = '/page-metrics'; +const ILO_URL_METRICS_ROUTE = '/url-metrics'; /** - * Registers endpoint for storage of page metric. + * Registers endpoint for storage of URL metric. * * @since n.e.x.t * @access private @@ -39,7 +39,7 @@ function ilo_register_endpoint() { register_rest_route( ILO_REST_API_NAMESPACE, - ILO_PAGE_METRICS_ROUTE, + ILO_URL_METRICS_ROUTE, array( 'methods' => 'POST', 'callback' => static function ( WP_REST_Request $request ) { @@ -47,10 +47,10 @@ function ilo_register_endpoint() { }, 'permission_callback' => static function () { // Needs to be available to unauthenticated visitors. - if ( ilo_is_page_metric_storage_locked() ) { + if ( ilo_is_url_metric_storage_locked() ) { return new WP_Error( - 'page_metric_storage_locked', - __( 'Page metric storage is presently locked for the current IP.', 'performance-lab' ), + 'url_metric_storage_locked', + __( 'URL metric storage is presently locked for the current IP.', 'performance-lab' ), array( 'status' => 403 ) ); } @@ -78,8 +78,8 @@ function ilo_register_endpoint() { 'required' => true, 'pattern' => '^[0-9a-f]+$', 'validate_callback' => static function ( $nonce, WP_REST_Request $request ) { - if ( ! ilo_verify_page_metrics_storage_nonce( $nonce, $request->get_param( 'slug' ) ) ) { - return new WP_Error( 'invalid_nonce', __( 'Page metrics nonce verification failure.', 'performance-lab' ) ); + if ( ! ilo_verify_url_metrics_storage_nonce( $nonce, $request->get_param( 'slug' ) ) ) { + return new WP_Error( 'invalid_nonce', __( 'URL metrics nonce verification failure.', 'performance-lab' ) ); } return true; }, @@ -162,21 +162,21 @@ function ilo_register_endpoint() { */ function ilo_handle_rest_request( WP_REST_Request $request ) { $needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths_now_for_slug( $request->get_param( 'slug' ) ); - if ( ! ilo_needs_page_metric_for_breakpoint( $needed_minimum_viewport_widths ) ) { + if ( ! ilo_needs_url_metric_for_breakpoint( $needed_minimum_viewport_widths ) ) { return new WP_Error( - 'no_page_metric_needed', - __( 'No page metric needed for any of the breakpoints.', 'performance-lab' ), + 'no_url_metric_needed', + __( 'No URL metric needed for any of the breakpoints.', 'performance-lab' ), array( 'status' => 403 ) ); } - ilo_set_page_metric_storage_lock(); - $new_page_metric = wp_array_slice_assoc( $request->get_json_params(), array( 'viewport', 'elements' ) ); + ilo_set_url_metric_storage_lock(); + $new_url_metric = wp_array_slice_assoc( $request->get_json_params(), array( 'viewport', 'elements' ) ); - $result = ilo_store_page_metric( + $result = ilo_store_url_metric( $request->get_param( 'url' ), $request->get_param( 'slug' ), - $new_page_metric + $new_url_metric ); if ( $result instanceof WP_Error ) { @@ -187,7 +187,7 @@ function ilo_handle_rest_request( WP_REST_Request $request ) { array( 'success' => true, 'post_id' => $result, - 'data' => ilo_parse_stored_page_metrics( ilo_get_page_metrics_post( $request->get_param( 'slug' ) ) ), // TODO: Remove this debug data. + 'data' => ilo_parse_stored_url_metrics( ilo_get_url_metrics_post( $request->get_param( 'slug' ) ) ), // TODO: Remove this debug data. ) ); } From 82b6210d897dfec56dbe6d07abf30a023e454d11 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 15 Nov 2023 12:04:12 -0800 Subject: [PATCH 60/62] Remove unnecessary curly braces --- modules/images/image-loading-optimization/storage/data.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php index 731275d3ea..0f091a324b 100644 --- a/modules/images/image-loading-optimization/storage/data.php +++ b/modules/images/image-loading-optimization/storage/data.php @@ -115,7 +115,7 @@ function ilo_get_url_metrics_slug( array $query_vars ): string { * @return string Nonce. */ function ilo_get_url_metrics_storage_nonce( string $slug ): string { - return wp_create_nonce( "store_url_metrics:{$slug}" ); + return wp_create_nonce( "store_url_metrics:$slug" ); } /** @@ -134,7 +134,7 @@ function ilo_get_url_metrics_storage_nonce( string $slug ): string { * 0 if the nonce is invalid. */ function ilo_verify_url_metrics_storage_nonce( string $nonce, string $slug ): int { - return (int) wp_verify_nonce( $nonce, "store_url_metrics:{$slug}" ); + return (int) wp_verify_nonce( $nonce, "store_url_metrics:$slug" ); } /** From 1168a5a4b383be56b03398492dc5773e669e54c2 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 15 Nov 2023 12:06:50 -0800 Subject: [PATCH 61/62] Rename filter to ilo_url_metric_storage_lock_ttl --- modules/images/image-loading-optimization/storage/lock.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/images/image-loading-optimization/storage/lock.php b/modules/images/image-loading-optimization/storage/lock.php index d30fe8d75c..2d8992e31f 100644 --- a/modules/images/image-loading-optimization/storage/lock.php +++ b/modules/images/image-loading-optimization/storage/lock.php @@ -34,7 +34,7 @@ function ilo_get_url_metric_storage_lock_ttl(): int { * * @param int $ttl TTL. */ - $ttl = (int) apply_filters( 'ilo_metrics_storage_lock_ttl', MINUTE_IN_SECONDS ); + $ttl = (int) apply_filters( 'ilo_url_metric_storage_lock_ttl', MINUTE_IN_SECONDS ); return max( 0, $ttl ); } From 698d89f6b8ce3a0cb90eec3f35eced3b4c22b7ca Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 15 Nov 2023 12:57:22 -0800 Subject: [PATCH 62/62] Follow AIP-136 --- .../storage/rest-api.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php index 423355f344..aaa7c98500 100644 --- a/modules/images/image-loading-optimization/storage/rest-api.php +++ b/modules/images/image-loading-optimization/storage/rest-api.php @@ -10,9 +10,24 @@ exit; // Exit if accessed directly. } +/** + * Namespace for image-loading-optimization. + * + * @var string + */ const ILO_REST_API_NAMESPACE = 'image-loading-optimization/v1'; -const ILO_URL_METRICS_ROUTE = '/url-metrics'; +/** + * Route for storing a URL metric. + * + * Note the `:store` art of the endpoint follows Google's guidance in AIP-136 for the use of the POST method in a way + * that does not strictly follow the standard usage. Namely, submitting a POST request to this endpoint will either + * create a new `ilo_url_metrics` post, or it will update an existing post if one already exists for the provided slug. + * + * @link https://google.aip.dev/136 + * @var string + */ +const ILO_URL_METRICS_ROUTE = '/url-metrics:store'; /** * Registers endpoint for storage of URL metric.