Make WordPress Core

Changeset 36566


Ignore:
Timestamp:
02/17/2016 10:57:33 PM (11 years ago)
Author:
boonebgorges
Message:

More performance improvements to metadata lazyloading.

Comment and term meta lazyloading for WP_Query loops, introduced in 4.4,
depended on filter callback methods belonging to WP_Query objects. This meant
storing WP_Query objects in the $wp_filter global (via add_filter()),
requiring that PHP retain the objects in memory, even when the local variables
would typically be expunged during normal garbage collection. In cases where a
large number of WP_Query objects were instantiated on a single pageload,
and/or where the contents of the WP_Query objects were quite large, serious
performance issues could result.

We skirt this problem by moving metadata lazyloading out of WP_Query. The
new WP_Metadata_Lazyloader class acts as a lazyload queue. Query instances
register items whose metadata should be lazyloaded - such as post terms, or
comments - and a WP_Metadata_Lazyloader method will intercept comment and
term meta requests to perform the cache priming. Since WP_Metadata_Lazyloader
instances are far smaller than WP_Query (containing only object IDs), and
clean up after themselves far better than the previous WP_Query methods (bp
only running their callbacks a single time for a given set of queued objects),
the resource use is decreased dramatically.

See [36525] for an earlier step in this direction.

Props lpawlik, stevegrunwell, boonebgorges.
Fixes #35816.

Location:
trunk
Files:
1 added
7 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/comment-template.php

    r36527 r36566  
    13931393         */
    13941394        $wp_query->comments = apply_filters( 'comments_array', $comments_flat, $post->ID );
    1395 
    1396         // Set up lazy-loading for comment metadata.
    1397         add_action( 'get_comment_metadata', array( $wp_query, 'lazyload_comment_meta' ), 10, 2 );
    13981395
    13991396        $comments = &$wp_query->comments;
     
    20302027        if ( null === $r['reverse_top_level'] )
    20312028                $r['reverse_top_level'] = ( 'desc' == get_option('comment_order') );
     2029
     2030        wp_queue_comments_for_comment_meta_lazyload( $_comments );
    20322031
    20332032        if ( empty( $r['walker'] ) ) {
  • trunk/src/wp-includes/comment.php

    r36542 r36566  
    470470
    471471/**
     472 * Queue comments for metadata lazyloading.
     473 *
     474 * @since 4.5.0
     475 *
     476 * @param array $comments Array of comment objects.
     477 */
     478function 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/**
    472496 * Sets the cookies used to store an unauthenticated commentator's identity. Typically used
    473497 * to recall previous comments by this commentator that are still held in moderation.
  • trunk/src/wp-includes/meta.php

    r36511 r36566  
    853853
    854854/**
     855 * Get the metadata lazyloading queue.
     856 *
     857 * @since 4.5.0
     858 *
     859 * @return WP_Metadata_Lazyloader $lazyloader Metadata lazyloader queue.
     860 */
     861function 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/**
    855872 * Given a meta query, generates SQL clauses to be appended to a main query.
    856873 *
  • trunk/src/wp-includes/post.php

    r36498 r36566  
    59495949
    59505950/**
     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 */
     5957function 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/**
    59515988 * Update the custom taxonomies' term counts when a post's status is changed.
    59525989 *
  • trunk/src/wp-includes/query.php

    r36524 r36566  
    36063606                        $this->posts = array_map( 'get_post', $this->posts );
    36073607
    3608 
    3609                 if ( $q['update_post_term_cache'] ) {
    3610                         add_filter( 'get_term_metadata', array( $this, 'lazyload_term_meta' ), 10, 2 );
    3611                 }
    3612 
    36133608                if ( ! $q['suppress_filters'] ) {
    36143609                        /**
     
    37393734                // If comments have been fetched as part of the query, make sure comment meta lazy-loading is set up.
    37403735                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 );
    37423737                }
    37433738
     
    37693764                        $this->post_count = 0;
    37703765                        $this->posts = array();
     3766                }
     3767
     3768                if ( $q['update_post_term_cache'] ) {
     3769                        wp_queue_posts_for_term_meta_lazyload( $this->posts );
    37713770                }
    37723771
     
    48354834
    48364835        /**
    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.
    48494837         *
    48504838         * @since 4.4.0
    4851          * @access public
    4852          *
    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.
     4839         * @deprecated 4.5.0 See wp_queue_posts_for_term_meta_lazyload().
     4840         *
     4841         * @param mixed $check
     4842         * @param int   $term_id
     4843         * @return mixed
    48574844         */
    48584845        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' );
    49014847                return $check;
    49024848        }
    49034849
    49044850        /**
    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.
    49094852         *
    49104853         * @since 4.4.0
    4911          *
    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'.
     4854         * @deprecated 4.5.0 See wp_queue_comments_for_comment_meta_lazyload().
     4855         *
     4856         * @param mixed $check
     4857         * @param int   $comment_id
     4858         * @return mixed
    49154859         */
    49164860        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' );
    49374862                return $check;
    49384863        }
  • trunk/src/wp-settings.php

    r36557 r36566  
    136136require( ABSPATH . WPINC . '/meta.php' );
    137137require( ABSPATH . WPINC . '/class-wp-meta-query.php' );
     138require( ABSPATH . WPINC . '/class-wp-metadata-lazyloader.php' );
    138139require( ABSPATH . WPINC . '/general-template.php' );
    139140require( ABSPATH . WPINC . '/link-template.php' );
  • trunk/tests/phpunit/tests/term/meta.php

    r35585 r36566  
    135135                                $num_queries = $wpdb->num_queries;
    136136                                $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 );
    138139
    139140                                // Second and third requests should be in cache.
    140141                                $this->assertSame( 'bar', get_term_meta( $terms[1], 'foo', true ) );
    141142                                $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 );
    143144
    144145                                // Querying a term not primed should result in a hit.
     146                                $num_queries++;
    145147                                $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 );
    147149                        }
    148150                }
    149151        }
    150152
    151         /**
    152          * @ticket 34073
    153          */
    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 prime
    175                  * 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() {
     153        public function test_updating_term_meta_should_bust_get_terms_cache() {
    204154                $terms = self::factory()->term->create_many( 2, array( 'taxonomy' => 'wptests_tax' ) );
    205155
    206156                add_term_meta( $terms[0], 'foo', 'bar' );
     157                add_term_meta( $terms[1], 'foo', 'baz' );
    207158
    208159                // Prime cache.
     
    220171                $this->assertEqualSets( array( $terms[0] ), $found );
    221172
    222                 add_term_meta( $terms[1], 'foo', 'bar' );
     173                update_term_meta( $terms[1], 'foo', 'bar' );
    223174
    224175                $found = get_terms( 'wptests_tax', array(
     
    236187        }
    237188
    238         public function test_updating_term_meta_should_bust_get_terms_cache() {
     189        public function test_deleting_term_meta_should_bust_get_terms_cache() {
    239190                $terms = self::factory()->term->create_many( 2, array( 'taxonomy' => 'wptests_tax' ) );
    240191
    241192                add_term_meta( $terms[0], 'foo', 'bar' );
    242                 add_term_meta( $terms[1], 'foo', 'baz' );
     193                add_term_meta( $terms[1], 'foo', 'bar' );
    243194
    244195                // Prime cache.
     
    254205                ) );
    255206
    256                 $this->assertEqualSets( array( $terms[0] ), $found );
    257 
    258                 update_term_meta( $terms[1], 'foo', 'bar' );
     207                $this->assertEqualSets( array( $terms[0], $terms[1] ), $found );
     208
     209                delete_term_meta( $terms[1], 'foo', 'bar' );
    259210
    260211                $found = get_terms( 'wptests_tax', array(
     
    269220                ) );
    270221
    271                 $this->assertEqualSets( array( $terms[0], $terms[1] ), $found );
    272         }
    273 
    274         public function test_deleting_term_meta_should_bust_get_terms_cache() {
    275                 $terms = self::factory()->term->create_many( 2, array( 'taxonomy' => 'wptests_tax' ) );
    276 
    277                 add_term_meta( $terms[0], 'foo', 'bar' );
    278                 add_term_meta( $terms[1], 'foo', 'bar' );
    279 
    280                 // Prime cache.
    281                 $found = get_terms( 'wptests_tax', array(
    282                         'hide_empty' => false,
    283                         'fields' => 'ids',
    284                         'meta_query' => array(
    285                                 array(
    286                                         'key' => 'foo',
    287                                         'value' => 'bar',
    288                                 ),
    289                         ),
    290                 ) );
    291 
    292                 $this->assertEqualSets( array( $terms[0], $terms[1] ), $found );
    293 
    294                 delete_term_meta( $terms[1], 'foo', 'bar' );
    295 
    296                 $found = get_terms( 'wptests_tax', array(
    297                         'hide_empty' => false,
    298                         'fields' => 'ids',
    299                         'meta_query' => array(
    300                                 array(
    301                                         'key' => 'foo',
    302                                         'value' => 'bar',
    303                                 ),
    304                         ),
    305                 ) );
    306 
    307222                $this->assertEqualSets( array( $terms[0] ), $found );
    308223        }
Note: See TracChangeset for help on using the changeset viewer.