Ticket #35816: 35816.3.diff
File 35816.3.diff, 20.2 KB (added by , 9 years ago) |
---|
-
new file src/wp-includes/class-wp-metadata-lazyloader.php
diff --git src/wp-includes/class-wp-metadata-lazyloader.php src/wp-includes/class-wp-metadata-lazyloader.php new file mode 100644 index 0000000..881b834
- + 1 <?php 2 3 /** 4 * Lazyloader for object metadata. 5 * 6 * When loading many objects of a given type, such as posts in a WP_Query loop, it often makes 7 * sense to prime various metadata caches at the beginning of the loop. This means fetching all 8 * relevant metadata with a single database query, a technique that has the potential to improve 9 * performance dramatically in some cases. 10 * 11 * In cases where the given metadata may not even be used in the loop, we can improve performance 12 * even more by only priming the metadata cache for affected items the first time a piece of metadata 13 * is requested - ie, by lazyloading it. So, for example, comment meta may not be loaded into the 14 * cache in the comments section of a post until the first time get_comment_meta() is called in the 15 * context of the comment loop. 16 * 17 * WP uses the WP_Metadata_Lazyloader class to queue objects for metadata cache priming. The class 18 * then detects the relevant get_*_meta() function call, and queries the metadata of all queued objects. 19 * 20 * Do not access this class directly. Use the wp_metadata_lazyloader() function. 21 * 22 * @since 4.5.0 23 */ 24 class WP_Metadata_Lazyloader { 25 /** 26 * Pending objects queue. 27 * 28 * @since 4.5.0 29 * @var array 30 */ 31 protected $pending_objects; 32 33 /** 34 * Settings for supported object types. 35 * 36 * @since 4.5.0 37 * @var array 38 */ 39 protected $settings = array(); 40 41 /** 42 * Constructor. 43 * 44 * @since 4.5.0 45 */ 46 public function __construct() { 47 $this->settings = array( 48 'term' => array( 49 'filter' => 'get_term_metadata', 50 'callback' => array( $this, 'lazyload_term_meta' ), 51 ), 52 'comment' => array( 53 'filter' => 'get_comment_metadata', 54 'callback' => array( $this, 'lazyload_comment_meta' ), 55 ), 56 ); 57 } 58 59 /** 60 * Add objects to the metadata lazyload queue. 61 * 62 * @since 4.5.0 63 * 64 * @param string $object_type Type of object whose meta is to be lazyloaded. Accepts 'term' or 'comment'. 65 * @param array $object_ids Array of object IDs. 66 * @return bool|WP_Error True on success, WP_Error on failure. 67 */ 68 public function queue_objects( $object_type, $object_ids ) { 69 if ( ! isset( $this->settings[ $object_type ] ) ) { 70 return new WP_Error( 'invalid_object_type', __( 'Invalid object type' ) ); 71 } 72 73 $type_settings = $this->settings[ $object_type ]; 74 75 if ( ! isset( $this->pending_objects[ $object_type ] ) ) { 76 $this->pending_objects[ $object_type ] = array(); 77 } 78 79 foreach ( $object_ids as $object_id ) { 80 // Keyed by ID for faster lookup. 81 if ( ! isset( $this->pending_objects[ $object_type ][ $object_id ] ) ) { 82 $this->pending_objects[ $object_type ][ $object_id ] = 1; 83 } 84 } 85 86 add_filter( $type_settings['filter'], $type_settings['callback'] ); 87 88 /** 89 * Fires after objects are added to the metadata lazyload queue. 90 * 91 * @since 4.5.0 92 * 93 * @param array $object_ids Object IDs. 94 * @param string $object_type Type of object being queued. 95 * @param WP_Metadata_Lazyloader $lazyloader The lazyloader object. 96 */ 97 do_action( 'metadata_lazyloader_queued_objects', $object_ids, $object_type, $this ); 98 } 99 100 /** 101 * Reset lazyload queue for a given object type. 102 * 103 * @since 4.5.0 104 * 105 * @param string $object_type Object type. Accepts 'comment' or 'term'. 106 * @return bool|WP_Error True on success, WP_Error on failure. 107 */ 108 public function reset_queue( $object_type ) { 109 if ( ! isset( $this->settings[ $object_type ] ) ) { 110 return new WP_Error( 'invalid_object_type', __( 'Invalid object type' ) ); 111 } 112 113 $type_settings = $this->settings[ $object_type ]; 114 115 $this->pending_objects[ $object_type ] = array(); 116 remove_filter( $type_settings['filter'], $type_settings['callback'] ); 117 } 118 119 /** 120 * Lazyloads term meta for queued terms. 121 * 122 * This method is public so that it can be used as a filter callback. As a rule, there 123 * is no need to invoke it directly. 124 * 125 * @since 4.5.0 126 * @access public 127 * 128 * @param mixed $check The `$check` param passed from the 'get_term_metadata' hook. 129 * @return mixed In order not to short-circuit `get_metadata()`. Generally, this is `null`, but it could be 130 * another value if filtered by a plugin. 131 */ 132 public function lazyload_term_meta( $check ) { 133 if ( ! empty( $this->pending_objects['term'] ) ) { 134 update_termmeta_cache( array_keys( $this->pending_objects['term'] ) ); 135 136 // No need to run again for this set of terms. 137 $this->reset_queue( 'term' ); 138 } 139 140 return $check; 141 } 142 143 /** 144 * Lazyload comment meta for queued comments. 145 * 146 * This method is public so that it can be used as a filter callback. As a rule, there is no need to invoke it 147 * directly, from either inside or outside the `WP_Query` object. 148 * 149 * @since 4.5.0 150 * 151 * @param mixed $check The `$check` param passed from the 'get_comment_metadata' hook. 152 * @return mixed The original value of `$check`, so as not to short-circuit `get_comment_metadata()`. 153 */ 154 public function lazyload_comment_meta( $check ) { 155 if ( ! empty( $this->pending_objects['comment'] ) ) { 156 update_meta_cache( 'comment', array_keys( $this->pending_objects['comment'] ) ); 157 158 // No need to run again for this set of comments. 159 $this->reset_queue( 'comment' ); 160 } 161 162 return $check; 163 } 164 } -
src/wp-includes/comment-template.php
diff --git src/wp-includes/comment-template.php src/wp-includes/comment-template.php index 4738dec..54df8b9 100644
function comments_template( $file = '/comments.php', $separate_comments = false 1393 1393 */ 1394 1394 $wp_query->comments = apply_filters( 'comments_array', $comments_flat, $post->ID ); 1395 1395 1396 // Set up lazy-loading for comment metadata.1397 add_action( 'get_comment_metadata', array( $wp_query, 'lazyload_comment_meta' ), 10, 2 );1398 1399 1396 $comments = &$wp_query->comments; 1400 1397 $wp_query->comment_count = count($wp_query->comments); 1401 1398 $wp_query->max_num_comment_pages = $comment_query->max_num_pages; … … function wp_list_comments( $args = array(), $comments = null ) { 2030 2027 if ( null === $r['reverse_top_level'] ) 2031 2028 $r['reverse_top_level'] = ( 'desc' == get_option('comment_order') ); 2032 2029 2030 wp_queue_comments_for_comment_meta_lazyload( $_comments ); 2031 2033 2032 if ( empty( $r['walker'] ) ) { 2034 2033 $walker = new Walker_Comment; 2035 2034 } else { -
src/wp-includes/comment.php
diff --git src/wp-includes/comment.php src/wp-includes/comment.php index 6d8f6e8..e3cabe5 100644
function update_comment_meta($comment_id, $meta_key, $meta_value, $prev_value = 469 469 } 470 470 471 471 /** 472 * Queue comments for metadata lazyloading. 473 * 474 * @since 4.5.0 475 * 476 * @param array $comments Array of comment objects. 477 */ 478 function wp_queue_comments_for_comment_meta_lazyload( $comments ) { 479 // Don't use `wp_list_pluck()` to avoid by-reference manipulation. 480 $comment_ids = array(); 481 if ( is_array( $comments ) ) { 482 foreach ( $comments as $comment ) { 483 if ( $comment instanceof WP_Comment ) { 484 $comment_ids[] = $comment->comment_ID; 485 } 486 } 487 } 488 489 if ( $comment_ids ) { 490 $lazyloader = wp_metadata_lazyloader(); 491 $lazyloader->queue_objects( 'comment', $comment_ids ); 492 } 493 } 494 495 /** 472 496 * Sets the cookies used to store an unauthenticated commentator's identity. Typically used 473 497 * to recall previous comments by this commentator that are still held in moderation. 474 498 * -
src/wp-includes/meta.php
diff --git src/wp-includes/meta.php src/wp-includes/meta.php index 86cc324..9ed3091 100644
function update_meta_cache($meta_type, $object_ids) { 852 852 } 853 853 854 854 /** 855 * Get the metadata lazyloading queue. 856 * 857 * @since 4.5.0 858 * 859 * @return WP_Metadata_Lazyloader $lazyloader Metadata lazyloader queue. 860 */ 861 function wp_metadata_lazyloader() { 862 static $wp_metadata_lazyloader; 863 864 if ( null === $wp_metadata_lazyloader ) { 865 $wp_metadata_lazyloader = new WP_Metadata_Lazyloader(); 866 } 867 868 return $wp_metadata_lazyloader; 869 } 870 871 /** 855 872 * Given a meta query, generates SQL clauses to be appended to a main query. 856 873 * 857 874 * @since 3.2.0 -
src/wp-includes/post.php
diff --git src/wp-includes/post.php src/wp-includes/post.php index 43361d4..3154fa7 100644
function wp_delete_auto_drafts() { 5948 5948 } 5949 5949 5950 5950 /** 5951 * Queue posts for lazyloading of term meta. 5952 * 5953 * @since 4.5.0 5954 * 5955 * @param array $posts Array of WP_Post objects. 5956 */ 5957 function wp_queue_posts_for_term_meta_lazyload( $posts ) { 5958 $post_type_taxonomies = $term_ids = array(); 5959 foreach ( $posts as $post ) { 5960 if ( ! ( $post instanceof WP_Post ) ) { 5961 continue; 5962 } 5963 5964 if ( ! isset( $post_type_taxonomies[ $post->post_type ] ) ) { 5965 $post_type_taxonomies[ $post->post_type ] = get_object_taxonomies( $post->post_type ); 5966 } 5967 5968 foreach ( $post_type_taxonomies[ $post->post_type ] as $taxonomy ) { 5969 // Term cache should already be primed by `update_post_term_cache()`. 5970 $terms = get_object_term_cache( $post->ID, $taxonomy ); 5971 if ( false !== $terms ) { 5972 foreach ( $terms as $term ) { 5973 if ( ! isset( $term_ids[ $term->term_id ] ) ) { 5974 $term_ids[] = $term->term_id; 5975 } 5976 } 5977 } 5978 } 5979 } 5980 5981 if ( $term_ids ) { 5982 $lazyloader = wp_metadata_lazyloader(); 5983 $lazyloader->queue_objects( 'term', $term_ids ); 5984 } 5985 } 5986 5987 /** 5951 5988 * Update the custom taxonomies' term counts when a post's status is changed. 5952 5989 * 5953 5990 * For example, default posts term counts (for custom taxonomies) don't include -
src/wp-includes/query.php
diff --git src/wp-includes/query.php src/wp-includes/query.php index a8fbbc5..053f4e9 100644
class WP_Query { 3605 3605 if ( $this->posts ) 3606 3606 $this->posts = array_map( 'get_post', $this->posts ); 3607 3607 3608 3609 if ( $q['update_post_term_cache'] ) {3610 add_filter( 'get_term_metadata', array( $this, 'lazyload_term_meta' ), 10, 2 );3611 }3612 3613 3608 if ( ! $q['suppress_filters'] ) { 3614 3609 /** 3615 3610 * Filter the raw post results array, prior to status checks. … … class WP_Query { 3738 3733 3739 3734 // If comments have been fetched as part of the query, make sure comment meta lazy-loading is set up. 3740 3735 if ( ! empty( $this->comments ) ) { 3741 add_filter( 'get_comment_metadata', array( $this, 'lazyload_comment_meta' ), 10, 2);3736 wp_queue_comments_for_comment_meta_lazyload( $this->comments ); 3742 3737 } 3743 3738 3744 3739 if ( ! $q['suppress_filters'] ) { … … class WP_Query { 3770 3765 $this->posts = array(); 3771 3766 } 3772 3767 3768 if ( $q['update_post_term_cache'] ) { 3769 wp_queue_posts_for_term_meta_lazyload( $this->posts ); 3770 } 3771 3773 3772 return $this->posts; 3774 3773 } 3775 3774 … … class WP_Query { 4834 4833 } 4835 4834 4836 4835 /** 4837 * Lazy-loads termmeta for located posts. 4838 * 4839 * As a rule, term queries (`get_terms()` and `wp_get_object_terms()`) prime the metadata cache for matched 4840 * terms by default. However, this can cause a slight performance penalty, especially when that metadata is 4841 * not actually used. In the context of a `WP_Query` instance, we're able to avoid this potential penalty. 4842 * `update_object_term_cache()`, called from `update_post_caches()`, does not 'update_term_meta_cache'. 4843 * Instead, the first time `get_term_meta()` is called from within a `WP_Query` loop, the current method 4844 * detects the fact, and then primes the metadata cache for all terms attached to all posts in the loop, 4845 * with a single database query. 4846 * 4847 * This method is public so that it can be used as a filter callback. As a rule, there is no need to invoke it 4848 * directly, from either inside or outside the `WP_Query` object. 4836 * Lazyload term meta for posts in the loop. 4849 4837 * 4850 4838 * @since 4.4.0 4851 * @ access public4839 * @deprecated 4.5.0 See wp_queue_posts_for_term_meta_lazyload(). 4852 4840 * 4853 * @param mixed $check The `$check` param passed from the 'get_term_metadata' hook. 4854 * @param int $term_id ID of the term whose metadata is being cached. 4855 * @return mixed In order not to short-circuit `get_metadata()`. Generally, this is `null`, but it could be 4856 * another value if filtered by a plugin. 4841 * @param mixed $check 4842 * @param int $term_id 4843 * @return mixed 4857 4844 */ 4858 4845 public function lazyload_term_meta( $check, $term_id ) { 4859 // We can only lazyload if the entire post object is present. 4860 $posts = array(); 4861 foreach ( $this->posts as $post ) { 4862 if ( $post instanceof WP_Post ) { 4863 $posts[] = $post; 4864 } 4865 } 4866 4867 if ( ! empty( $posts ) ) { 4868 // Fetch cached term_ids for each post. Keyed by term_id for faster lookup. 4869 $term_ids = array(); 4870 foreach ( $posts as $post ) { 4871 $taxonomies = get_object_taxonomies( $post->post_type ); 4872 foreach ( $taxonomies as $taxonomy ) { 4873 // Term cache should already be primed by 'update_post_term_cache'. 4874 $terms = get_object_term_cache( $post->ID, $taxonomy ); 4875 if ( false !== $terms ) { 4876 foreach ( $terms as $term ) { 4877 if ( ! isset( $term_ids[ $term->term_id ] ) ) { 4878 $term_ids[ $term->term_id ] = 1; 4879 } 4880 } 4881 } 4882 } 4883 } 4884 4885 /* 4886 * Only update the metadata cache for terms belonging to these posts if the term_id passed 4887 * to `get_term_meta()` matches one of those terms. This prevents a single call to 4888 * `get_term_meta()` from priming metadata for all `WP_Query` objects. 4889 */ 4890 if ( isset( $term_ids[ $term_id ] ) ) { 4891 update_termmeta_cache( array_keys( $term_ids ) ); 4892 remove_filter( 'get_term_metadata', array( $this, 'lazyload_term_meta' ), 10, 2 ); 4893 } 4894 } 4895 4896 // If no terms were found, there's no need to run this again. 4897 if ( empty( $term_ids ) ) { 4898 remove_filter( 'get_term_metadata', array( $this, 'lazyload_term_meta' ), 10, 2 ); 4899 } 4900 4846 _deprecated_function( __METHOD__, '4.5.0' ); 4901 4847 return $check; 4902 4848 } 4903 4849 4904 4850 /** 4905 * Lazy-load comment meta when inside of a `WP_Query` loop. 4906 * 4907 * This method is public so that it can be used as a filter callback. As a rule, there is no need to invoke it 4908 * directly, from either inside or outside the `WP_Query` object. 4851 * Lazyload comment meta for comments in the loop. 4909 4852 * 4910 4853 * @since 4.4.0 4854 * @deprecated 4.5.0 See wp_queue_comments_for_comment_meta_lazyload(). 4911 4855 * 4912 * @param mixed $check The `$check` param passed from the 'get_comment_metadata' hook.4913 * @param int $comment_id ID of the comment whose metadata is being cached.4914 * @return mixed The original value of `$check`, to not affect 'get_comment_metadata'.4856 * @param mixed $check 4857 * @param int $comment_id 4858 * @return mixed 4915 4859 */ 4916 4860 public function lazyload_comment_meta( $check, $comment_id ) { 4917 // Don't use `wp_list_pluck()` to avoid by-reference manipulation. 4918 $comment_ids = array(); 4919 if ( is_array( $this->comments ) ) { 4920 foreach ( $this->comments as $comment ) { 4921 $comment_ids[] = $comment->comment_ID; 4922 } 4923 } 4924 4925 /* 4926 * Only update the metadata cache for comments belonging to these posts if the comment_id passed 4927 * to `get_comment_meta()` matches one of those comments. This prevents a single call to 4928 * `get_comment_meta()` from priming metadata for all `WP_Query` objects. 4929 */ 4930 if ( in_array( $comment_id, $comment_ids ) ) { 4931 update_meta_cache( 'comment', $comment_ids ); 4932 remove_filter( 'get_comment_metadata', array( $this, 'lazyload_comment_meta' ), 10, 2 ); 4933 } elseif ( empty( $comment_ids ) ) { 4934 remove_filter( 'get_comment_metadata', array( $this, 'lazyload_comment_meta' ), 10, 2 ); 4935 } 4936 4861 _deprecated_function( __METHOD__, '4.5.0' ); 4937 4862 return $check; 4938 4863 } 4939 4864 } -
src/wp-settings.php
diff --git src/wp-settings.php src/wp-settings.php index ef1d2cd..3c21597 100644
require( ABSPATH . WPINC . '/class-wp-roles.php' ); 126 126 require( ABSPATH . WPINC . '/class-wp-role.php' ); 127 127 require( ABSPATH . WPINC . '/class-wp-user.php' ); 128 128 require( ABSPATH . WPINC . '/query.php' ); 129 require( ABSPATH . WPINC . '/class-wp-metadata-lazyloader.php' ); 129 130 require( ABSPATH . WPINC . '/date.php' ); 130 131 require( ABSPATH . WPINC . '/theme.php' ); 131 132 require( ABSPATH . WPINC . '/class-wp-theme.php' ); -
tests/phpunit/tests/term/meta.php
diff --git tests/phpunit/tests/term/meta.php tests/phpunit/tests/term/meta.php index f6c7b90..1024881 100644
class Tests_Term_Meta extends WP_UnitTestCase { 134 134 // First request will hit the database. 135 135 $num_queries = $wpdb->num_queries; 136 136 $this->assertSame( 'bar', get_term_meta( $terms[0], 'foo', true ) ); 137 $this->assertSame( $num_queries + 1, $wpdb->num_queries ); 137 $num_queries++; 138 $this->assertSame( $num_queries, $wpdb->num_queries ); 138 139 139 140 // Second and third requests should be in cache. 140 141 $this->assertSame( 'bar', get_term_meta( $terms[1], 'foo', true ) ); 141 142 $this->assertSame( 'bar', get_term_meta( $terms[2], 'foo', true ) ); 142 $this->assertSame( $num_queries + 1, $wpdb->num_queries );143 $this->assertSame( $num_queries, $wpdb->num_queries ); 143 144 144 145 // Querying a term not primed should result in a hit. 146 $num_queries++; 145 147 $this->assertSame( 'bar', get_term_meta( $orphan_term, 'foo', true ) ); 146 $this->assertSame( $num_queries + 2, $wpdb->num_queries );148 $this->assertSame( $num_queries, $wpdb->num_queries ); 147 149 } 148 150 } 149 151 } 150 152 151 /**152 * @ticket 34073153 */154 public function test_term_meta_should_be_lazy_loaded_only_for_the_queries_in_which_the_term_has_posts() {155 global $wpdb;156 157 $posts = self::factory()->post->create_many( 3, array( 'post_status' => 'publish' ) );158 register_taxonomy( 'wptests_tax', 'post' );159 $terms = self::factory()->term->create_many( 6, array( 'taxonomy' => 'wptests_tax' ) );160 161 wp_set_object_terms( $posts[0], array( $terms[0], $terms[1] ), 'wptests_tax' );162 wp_set_object_terms( $posts[1], array( $terms[2], $terms[3] ), 'wptests_tax' );163 wp_set_object_terms( $posts[2], array( $terms[0], $terms[4], $terms[5] ), 'wptests_tax' );164 165 foreach ( $terms as $t ) {166 add_term_meta( $t, 'foo', 'bar' );167 }168 169 $q0 = new WP_Query( array( 'p' => $posts[0], 'cache_results' => true ) );170 $q1 = new WP_Query( array( 'p' => $posts[1], 'cache_results' => true ) );171 $q2 = new WP_Query( array( 'p' => $posts[2], 'cache_results' => true ) );172 173 /*174 * $terms[0] belongs to both $posts[0] and $posts[2], so `get_term_meta( $terms[0] )` should prime175 * the cache for term matched by $q0 and $q2.176 */177 178 // First request will hit the database.179 $num_queries = $wpdb->num_queries;180 181 // Prime caches.182 $this->assertSame( 'bar', get_term_meta( $terms[0], 'foo', true ) );183 184 // Two queries: one for $q0 and one for $q2.185 $num_queries += 2;186 $this->assertSame( $num_queries, $wpdb->num_queries );187 188 // Next requests should be in cache.189 $this->assertSame( 'bar', get_term_meta( $terms[1], 'foo', true ) );190 $this->assertSame( 'bar', get_term_meta( $terms[4], 'foo', true ) );191 $this->assertSame( 'bar', get_term_meta( $terms[5], 'foo', true ) );192 $this->assertSame( $num_queries, $wpdb->num_queries );193 194 // Querying for $terms[2] will prime $terms[3] as well.195 $this->assertSame( 'bar', get_term_meta( $terms[2], 'foo', true ) );196 $num_queries++;197 $this->assertSame( $num_queries, $wpdb->num_queries );198 199 $this->assertSame( 'bar', get_term_meta( $terms[3], 'foo', true ) );200 $this->assertSame( $num_queries, $wpdb->num_queries );201 }202 203 public function test_adding_term_meta_should_bust_get_terms_cache() {204 $terms = self::factory()->term->create_many( 2, array( 'taxonomy' => 'wptests_tax' ) );205 206 add_term_meta( $terms[0], 'foo', 'bar' );207 208 // Prime cache.209 $found = get_terms( 'wptests_tax', array(210 'hide_empty' => false,211 'fields' => 'ids',212 'meta_query' => array(213 array(214 'key' => 'foo',215 'value' => 'bar',216 ),217 ),218 ) );219 220 $this->assertEqualSets( array( $terms[0] ), $found );221 222 add_term_meta( $terms[1], 'foo', 'bar' );223 224 $found = get_terms( 'wptests_tax', array(225 'hide_empty' => false,226 'fields' => 'ids',227 'meta_query' => array(228 array(229 'key' => 'foo',230 'value' => 'bar',231 ),232 ),233 ) );234 235 $this->assertEqualSets( array( $terms[0], $terms[1] ), $found );236 }237 238 153 public function test_updating_term_meta_should_bust_get_terms_cache() { 239 154 $terms = self::factory()->term->create_many( 2, array( 'taxonomy' => 'wptests_tax' ) ); 240 155