WordPress.org

Make WordPress Core

Changeset 36566


Ignore:
Timestamp:
02/17/16 22:57:33 (20 months 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.