From de06f5352d19b7b18699aa44f0afb8eb9b0b2850 Mon Sep 17 00:00:00 2001 From: Jonny Harrus Date: Tue, 25 Feb 2025 21:50:39 -0500 Subject: [PATCH 1/6] Add lazy-loading support for user metadata. Introduce the `wp_lazyload_user_meta` function to queue user metadata for lazy-loading, optimizing user meta retrieval. Update related functions, tests, and metadata lazyloader to support this functionality, ensuring improved performance and consistency. --- .../class-wp-metadata-lazyloader.php | 4 ++ src/wp-includes/class-wp-user-query.php | 7 +++- src/wp-includes/pluggable.php | 2 +- src/wp-includes/user.php | 15 ++++++++ tests/phpunit/includes/abstract-testcase.php | 1 + tests/phpunit/tests/user/lazyLoadMeta.php | 37 +++++++++++++++++++ 6 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 tests/phpunit/tests/user/lazyLoadMeta.php diff --git a/src/wp-includes/class-wp-metadata-lazyloader.php b/src/wp-includes/class-wp-metadata-lazyloader.php index f9c02391d2b1d..ae3cca4823167 100644 --- a/src/wp-includes/class-wp-metadata-lazyloader.php +++ b/src/wp-includes/class-wp-metadata-lazyloader.php @@ -65,6 +65,10 @@ public function __construct() { 'filter' => 'get_blog_metadata', 'callback' => array( $this, 'lazyload_meta_callback' ), ), + 'user' => array( + 'filter' => 'get_user_metadata', + 'callback' => array( $this, 'lazyload_meta_callback' ), + ), ); } diff --git a/src/wp-includes/class-wp-user-query.php b/src/wp-includes/class-wp-user-query.php index fd35182eab9f5..cb6d0d95be534 100644 --- a/src/wp-includes/class-wp-user-query.php +++ b/src/wp-includes/class-wp-user-query.php @@ -882,12 +882,15 @@ public function query() { $result->id = $result->ID; } } elseif ( 'all_with_meta' === $qv['fields'] || 'all' === $qv['fields'] ) { + $user_ids = array_map( 'intval', $this->results ); if ( function_exists( 'cache_users' ) ) { - cache_users( $this->results ); + cache_users( $user_ids ); } + wp_lazyload_user_meta( $user_ids ); + $r = array(); - foreach ( $this->results as $userid ) { + foreach ( $user_ids as $userid ) { if ( 'all_with_meta' === $qv['fields'] ) { $r[ $userid ] = new WP_User( $userid, '', $qv['blog_id'] ); } else { diff --git a/src/wp-includes/pluggable.php b/src/wp-includes/pluggable.php index 3a6190beffd0c..dccf7140e52fc 100644 --- a/src/wp-includes/pluggable.php +++ b/src/wp-includes/pluggable.php @@ -125,7 +125,7 @@ function get_user_by( $field, $value ) { function cache_users( $user_ids ) { global $wpdb; - update_meta_cache( 'user', $user_ids ); + wp_lazyload_user_meta( $user_ids ); $clean = _get_non_cached_ids( $user_ids, 'users' ); diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index fe6f18568032c..e56cba63bfda4 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -1284,6 +1284,21 @@ function update_user_meta( $user_id, $meta_key, $meta_value, $prev_value = '' ) return update_metadata( 'user', $user_id, $meta_key, $meta_value, $prev_value ); } +/** + * Queue user meta for lazy-loading. + * + * @since 6.8.0 + * + * @param array $user_ids List of user IDs. + */ +function wp_lazyload_user_meta( array $user_ids ) { + if ( empty( $user_ids ) ) { + return; + } + $lazyloader = wp_metadata_lazyloader(); + $lazyloader->queue_objects( 'user', $user_ids ); +} + /** * Counts number of users who have each of the user roles. * diff --git a/tests/phpunit/includes/abstract-testcase.php b/tests/phpunit/includes/abstract-testcase.php index b6c82cc066efd..813fe2548e84f 100644 --- a/tests/phpunit/includes/abstract-testcase.php +++ b/tests/phpunit/includes/abstract-testcase.php @@ -288,6 +288,7 @@ protected function reset_lazyload_queue() { $lazyloader->reset_queue( 'term' ); $lazyloader->reset_queue( 'comment' ); $lazyloader->reset_queue( 'blog' ); + $lazyloader->reset_queue( 'user' ); } /** diff --git a/tests/phpunit/tests/user/lazyLoadMeta.php b/tests/phpunit/tests/user/lazyLoadMeta.php new file mode 100644 index 0000000000000..6eebbb2b055d0 --- /dev/null +++ b/tests/phpunit/tests/user/lazyLoadMeta.php @@ -0,0 +1,37 @@ +user->create_many( 5 ); + wp_cache_delete_multiple( $user_ids, 'user_meta' ); + wp_lazyload_user_meta( $user_ids ); + $filter = new MockAction(); + add_filter( 'update_user_metadata_cache', array( $filter, 'filter' ), 10, 2 ); + get_user_meta( $user_ids[0] ); + + $args = $filter->get_args(); + $first = reset( $args ); + $load_user_ids = end( $first ); + $this->assertSameSets( $user_ids, $load_user_ids ); + } + public function test_lazy_load_meta_sets() { + $user_ids1 = self::factory()->user->create_many( 5 ); + wp_cache_delete_multiple( $user_ids1, 'user_meta' ); + $user_ids2 = self::factory()->user->create_many( 5 ); + wp_cache_delete_multiple( $user_ids2, 'user_meta' ); + $user_ids = array_merge( $user_ids1, $user_ids2 ); + wp_lazyload_user_meta( $user_ids ); + $filter = new MockAction(); + add_filter( 'update_user_metadata_cache', array( $filter, 'filter' ), 10, 2 ); + get_user_meta( $user_ids[0] ); + + $args = $filter->get_args(); + $first = reset( $args ); + $load_user_ids = end( $first ); + $this->assertSameSets( $user_ids, $load_user_ids ); + } +} From a21fc99ccb1cd61983e26482044c29b28e3186a5 Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Fri, 19 Sep 2025 11:39:05 +0100 Subject: [PATCH 2/6] Apply suggestions from code review Co-authored-by: Weston Ruter --- src/wp-includes/user.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index e56cba63bfda4..fd8921a306128 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -1287,9 +1287,9 @@ function update_user_meta( $user_id, $meta_key, $meta_value, $prev_value = '' ) /** * Queue user meta for lazy-loading. * - * @since 6.8.0 + * @since 6.9.0 * - * @param array $user_ids List of user IDs. + * @param int[] $user_ids List of user IDs. */ function wp_lazyload_user_meta( array $user_ids ) { if ( empty( $user_ids ) ) { From bf504853a1c32444f647db480bef2fa42f4829d6 Mon Sep 17 00:00:00 2001 From: Jonny Harrus Date: Mon, 29 Sep 2025 21:33:32 +0100 Subject: [PATCH 3/6] Revert changes to user query. --- src/wp-includes/class-wp-user-query.php | 7 ++----- src/wp-includes/pluggable.php | 1 + 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/class-wp-user-query.php b/src/wp-includes/class-wp-user-query.php index 9ce2b41651385..014cfb2ac56d9 100644 --- a/src/wp-includes/class-wp-user-query.php +++ b/src/wp-includes/class-wp-user-query.php @@ -884,15 +884,12 @@ public function query() { $result->id = $result->ID; } } elseif ( 'all_with_meta' === $qv['fields'] || 'all' === $qv['fields'] ) { - $user_ids = array_map( 'intval', $this->results ); if ( function_exists( 'cache_users' ) ) { - cache_users( $user_ids ); + cache_users( $this->results ); } - wp_lazyload_user_meta( $user_ids ); - $r = array(); - foreach ( $user_ids as $userid ) { + foreach ( $this->results as $userid ) { if ( 'all_with_meta' === $qv['fields'] ) { $r[ $userid ] = new WP_User( $userid, '', $qv['blog_id'] ); } else { diff --git a/src/wp-includes/pluggable.php b/src/wp-includes/pluggable.php index e4091162e2916..786b9b6522f3d 100644 --- a/src/wp-includes/pluggable.php +++ b/src/wp-includes/pluggable.php @@ -125,6 +125,7 @@ function get_user_by( $field, $value ) { function cache_users( $user_ids ) { global $wpdb; + $user_ids = array_unique( array_map( 'intval', $user_ids ), SORT_NUMERIC ); wp_lazyload_user_meta( $user_ids ); $clean = _get_non_cached_ids( $user_ids, 'users' ); From dc91d12f9556fa20d7a4ffd3953d0a90ac4cecc2 Mon Sep 17 00:00:00 2001 From: Jonny Harrus Date: Thu, 9 Oct 2025 19:25:15 +0100 Subject: [PATCH 4/6] Update tests to reflect lazy-loading behavior for user metadata. --- tests/phpunit/tests/post/updatePostAuthorCaches.php | 2 +- tests/phpunit/tests/query/cacheResults.php | 7 +++---- tests/phpunit/tests/query/thePost.php | 8 ++++---- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/phpunit/tests/post/updatePostAuthorCaches.php b/tests/phpunit/tests/post/updatePostAuthorCaches.php index 0c5823561bf3a..e90f7b8cdf7a9 100644 --- a/tests/phpunit/tests/post/updatePostAuthorCaches.php +++ b/tests/phpunit/tests/post/updatePostAuthorCaches.php @@ -70,6 +70,6 @@ public function test_update_post_author_caches() { $args = $action->get_args(); $last_args = end( $args ); - $this->assertSameSets( self::$user_ids, $last_args[1], 'Ensure that user IDs are primed' ); + $this->assertNull( $last_args[1], 'Ensure that user IDs are not primed' ); } } diff --git a/tests/phpunit/tests/query/cacheResults.php b/tests/phpunit/tests/query/cacheResults.php index 02bb702a2b6aa..76cb51d1367dc 100644 --- a/tests/phpunit/tests/query/cacheResults.php +++ b/tests/phpunit/tests/query/cacheResults.php @@ -1982,11 +1982,10 @@ public function test_author_cache_warmed_by_the_loop( $fields ) { $query_1->the_post(); $num_loop_queries = get_num_queries() - $start_loop_queries; /* - * Two expected queries: - * 1: User meta data, - * 2: User data. + * One expected query: + * 1: User data. */ - $this->assertSame( 2, $num_loop_queries, 'Unexpected number of queries while initializing the loop.' ); + $this->assertSame( 1, $num_loop_queries, 'Unexpected number of queries while initializing the loop.' ); $start_author_queries = get_num_queries(); get_user_by( 'ID', self::$author_id ); diff --git a/tests/phpunit/tests/query/thePost.php b/tests/phpunit/tests/query/thePost.php index 6032a01dbeb1b..c07523a3552b3 100644 --- a/tests/phpunit/tests/query/thePost.php +++ b/tests/phpunit/tests/query/thePost.php @@ -265,10 +265,10 @@ public function test_the_loop_primes_the_author_cache( $fields, $expected_querie */ public function data_the_loop_fields() { return array( - 'all fields' => array( 'all', 2 ), - 'all fields (empty fields)' => array( '', 2 ), - 'post IDs' => array( 'ids', 4 ), - 'post ids and parent' => array( 'id=>parent', 4 ), + 'all fields' => array( 'all', 1 ), + 'all fields (empty fields)' => array( '', 1 ), + 'post IDs' => array( 'ids', 3 ), + 'post ids and parent' => array( 'id=>parent', 3 ), ); } From ea11dabc8efa6dac09b7ef851df68e5ca71f7754 Mon Sep 17 00:00:00 2001 From: Jonny Harrus Date: Thu, 9 Oct 2025 21:30:20 +0100 Subject: [PATCH 5/6] Add test coverage for forced loading of author metadata Enhance tests to verify behavior when author metadata is explicitly loaded using `get_the_author_meta`, ensuring user IDs are properly primed. --- .../tests/post/updatePostAuthorCaches.php | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/post/updatePostAuthorCaches.php b/tests/phpunit/tests/post/updatePostAuthorCaches.php index e90f7b8cdf7a9..0a43f04c92eff 100644 --- a/tests/phpunit/tests/post/updatePostAuthorCaches.php +++ b/tests/phpunit/tests/post/updatePostAuthorCaches.php @@ -67,9 +67,31 @@ public function test_update_post_author_caches() { $q->the_post(); } + $this->assertSame( 0, $action->get_call_count(), 'Ensure that user meta are not primed' ); + } + + /** + * @ticket 63021 + */ + public function test_update_post_author_caches_force_load_meta() { + $action = new MockAction(); + add_filter( 'update_user_metadata_cache', array( $action, 'filter' ), 10, 2 ); + + $q = new WP_Query( + array( + 'post_type' => 'post', + 'posts_per_page' => self::$post_author_count, + ) + ); + + while ( $q->have_posts() ) { + $q->the_post(); + get_the_author_meta(); // Force loading of author meta. + } + $args = $action->get_args(); $last_args = end( $args ); - $this->assertNull( $last_args[1], 'Ensure that user IDs are not primed' ); + $this->assertSameSets( self::$user_ids, $last_args[1], 'Ensure that user IDs are primed' ); } } From 8911583d0d4bed6eddc2e11170d1b66eed082386 Mon Sep 17 00:00:00 2001 From: Jonny Harrus Date: Thu, 9 Oct 2025 21:46:12 +0100 Subject: [PATCH 6/6] Expand test coverage for `wp_lazyload_user_meta` Add new tests to handle various scenarios, including users not in the lazy load queue. Update existing tests to reduce created user count and include cache clearing, ensuring more robust and efficient validation of lazy-loading behavior. --- tests/phpunit/tests/user/lazyLoadMeta.php | 51 +++++++++++++++++++---- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/tests/phpunit/tests/user/lazyLoadMeta.php b/tests/phpunit/tests/user/lazyLoadMeta.php index 6eebbb2b055d0..050153c40cb3b 100644 --- a/tests/phpunit/tests/user/lazyLoadMeta.php +++ b/tests/phpunit/tests/user/lazyLoadMeta.php @@ -4,9 +4,14 @@ * @group user * @covers ::wp_lazyload_user_meta */ -class TestslazyLoadMeta extends WP_UnitTestCase { +class Tests_User_Lazy_Load_Meta extends WP_UnitTestCase { + + /** + * @ticket 63021 + */ public function test_lazy_load_meta() { - $user_ids = self::factory()->user->create_many( 5 ); + $user_ids = self::factory()->user->create_many( 3 ); + // Clear any existing cache. wp_cache_delete_multiple( $user_ids, 'user_meta' ); wp_lazyload_user_meta( $user_ids ); $filter = new MockAction(); @@ -16,14 +21,18 @@ public function test_lazy_load_meta() { $args = $filter->get_args(); $first = reset( $args ); $load_user_ids = end( $first ); - $this->assertSameSets( $user_ids, $load_user_ids ); + $this->assertSameSets( $user_ids, $load_user_ids, 'Ensure all user IDs are loaded in a single batch' ); } + + /** + * @ticket 63021 + */ public function test_lazy_load_meta_sets() { - $user_ids1 = self::factory()->user->create_many( 5 ); - wp_cache_delete_multiple( $user_ids1, 'user_meta' ); - $user_ids2 = self::factory()->user->create_many( 5 ); - wp_cache_delete_multiple( $user_ids2, 'user_meta' ); - $user_ids = array_merge( $user_ids1, $user_ids2 ); + $user_ids1 = self::factory()->user->create_many( 3 ); + $user_ids2 = self::factory()->user->create_many( 3 ); + $user_ids = array_merge( $user_ids1, $user_ids2 ); + // Clear any existing cache. + wp_cache_delete_multiple( $user_ids, 'user_meta' ); wp_lazyload_user_meta( $user_ids ); $filter = new MockAction(); add_filter( 'update_user_metadata_cache', array( $filter, 'filter' ), 10, 2 ); @@ -32,6 +41,30 @@ public function test_lazy_load_meta_sets() { $args = $filter->get_args(); $first = reset( $args ); $load_user_ids = end( $first ); - $this->assertSameSets( $user_ids, $load_user_ids ); + $this->assertSameSets( $user_ids, $load_user_ids, 'Ensure all user IDs are loaded in a single batch' ); + } + + /** + * @ticket 63021 + */ + public function test_lazy_load_meta_not_in_queue() { + $user_ids1 = self::factory()->user->create_many( 3 ); + $user_ids2 = self::factory()->user->create_many( 3 ); + $user_ids = array_merge( $user_ids1, $user_ids2 ); + $new_user_id = self::factory()->user->create(); + wp_lazyload_user_meta( $user_ids ); + // Add a user not in the lazy load queue. + $user_ids[] = $new_user_id; + // Clear any existing cache including the new user not in the queue. + wp_cache_delete_multiple( $user_ids, 'user_meta' ); + + $filter = new MockAction(); + add_filter( 'update_user_metadata_cache', array( $filter, 'filter' ), 10, 2 ); + get_user_meta( $new_user_id ); + + $args = $filter->get_args(); + $first = reset( $args ); + $load_user_ids = end( $first ); + $this->assertSameSets( $user_ids, $load_user_ids, 'Ensure all user IDs are loaded, including the one not in the queue' ); } }