Make WordPress Core

Changeset 54493


Ignore:
Timestamp:
10/11/2022 05:15:11 PM (20 months ago)
Author:
hellofromTonya
Message:

Editor: Fix performance regression in WP_Theme_JSON_Resolver.

A significant performance regression was added late in WP 6.1 beta cycle when some of the existing caching for theme.json processing was removed. The rationale for removing the caching was this code was now used before all the blocks are registered (aka get template data, etc.) and resulted in stale cache that created issues (see Gutenberg Issue 44434 and Gutenberg Issue 44619). The changes were limited to only reads from the file system. However, it introduced a big impact in performance.

This commit adds caching and checks for blocks with different origins. How? It add caching for the calculated data for core, theme, and user based on the blocks that are registered. If the blocks haven't changed since the last time they were calculated for the origin, the cached data is returned. Otherwise, the data is recalculated and cached.

Essentially, this brings back the previous cache, but refreshing it when the blocks change.

It partially adds unit tests for these changes. Additional tests will be added.

References:

Follow-up to [54251], [54399].

Props aristath, oandregal, bernhard-reiter, spacedmonkey, hellofromTonya.
See #56467.

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-theme-json-resolver.php

    r54491 r54493  
    2222
    2323    /**
     24     * Container for keep track of registered blocks.
     25     *
     26     * @since 6.1.0
     27     * @var array
     28     */
     29    protected static $blocks_cache = array(
     30        'core'   => array(),
     31        'blocks' => array(),
     32        'theme'  => array(),
     33        'user'   => array(),
     34    );
     35
     36    /**
    2437     * Container for data coming from core.
    2538     *
     
    2841     */
    2942    protected static $core = null;
     43
     44    /**
     45     * Container for data coming from the blocks.
     46     *
     47     * @since 6.1.0
     48     * @var WP_Theme_JSON
     49     */
     50    protected static $blocks = null;
    3051
    3152    /**
     
    146167     */
    147168    public static function get_core_data() {
     169        if ( null !== static::$core && static::has_same_registered_blocks( 'core' ) ) {
     170            return static::$core;
     171        }
     172
    148173        $config = static::read_json_file( __DIR__ . '/theme.json' );
    149174        $config = static::translate( $config );
     
    164189
    165190    /**
     191     * Checks whether the registered blocks were already processed for this origin.
     192     *
     193     * @since 6.1.0
     194     *
     195     * @param string $origin Data source for which to cache the blocks.
     196     *                       Valid values are 'core', 'blocks', 'theme', and 'user'.
     197     * @return bool True on success, false otherwise.
     198     */
     199    protected static function has_same_registered_blocks( $origin ) {
     200        // Bail out if the origin is invalid.
     201        if ( ! isset( static::$blocks_cache[ $origin ] ) ) {
     202            return false;
     203        }
     204
     205        $registry = WP_Block_Type_Registry::get_instance();
     206        $blocks   = $registry->get_all_registered();
     207
     208        // Is there metadata for all currently registered blocks?
     209        $block_diff = array_diff_key( $blocks, static::$blocks_cache[ $origin ] );
     210        if ( empty( $block_diff ) ) {
     211            return true;
     212        }
     213
     214        foreach ( $blocks as $block_name => $block_type ) {
     215            static::$blocks_cache[ $origin ][ $block_name ] = true;
     216        }
     217
     218        return false;
     219    }
     220
     221    /**
    166222     * Returns the theme's data.
    167223     *
     
    190246        $options = wp_parse_args( $options, array( 'with_supports' => true ) );
    191247
    192         $theme_json_data = static::read_json_file( static::get_file_path_from_theme( 'theme.json' ) );
    193         $theme_json_data = static::translate( $theme_json_data, wp_get_theme()->get( 'TextDomain' ) );
    194 
    195         /**
    196          * Filters the data provided by the theme for global styles and settings.
    197          *
    198          * @since 6.1.0
    199          *
    200          * @param WP_Theme_JSON_Data Class to access and update the underlying data.
    201          */
    202         $theme_json      = apply_filters( 'theme_json_theme', new WP_Theme_JSON_Data( $theme_json_data, 'theme' ) );
    203         $theme_json_data = $theme_json->get_data();
    204         static::$theme   = new WP_Theme_JSON( $theme_json_data );
     248        if ( null === static::$theme || ! static::has_same_registered_blocks( 'theme' ) ) {
     249            $theme_json_data = static::read_json_file( static::get_file_path_from_theme( 'theme.json' ) );
     250            $theme_json_data = static::translate( $theme_json_data, wp_get_theme()->get( 'TextDomain' ) );
     251
     252            /**
     253             * Filters the data provided by the theme for global styles and settings.
     254             *
     255             * @since 6.1.0
     256             *
     257             * @param WP_Theme_JSON_Data Class to access and update the underlying data.
     258             */
     259            $theme_json      = apply_filters( 'theme_json_theme', new WP_Theme_JSON_Data( $theme_json_data, 'theme' ) );
     260            $theme_json_data = $theme_json->get_data();
     261            static::$theme   = new WP_Theme_JSON( $theme_json_data );
     262        }
    205263
    206264        if ( wp_get_theme()->parent() ) {
     
    259317        $with_theme_supports = new WP_Theme_JSON( $theme_support_data );
    260318        $with_theme_supports->merge( static::$theme );
    261 
    262319        return $with_theme_supports;
    263320    }
     
    273330        $registry = WP_Block_Type_Registry::get_instance();
    274331        $blocks   = $registry->get_all_registered();
    275         $config   = array( 'version' => 2 );
     332
     333        if ( null !== static::$blocks && static::has_same_registered_blocks( 'blocks' ) ) {
     334            return static::$blocks;
     335        }
     336
     337        $config = array( 'version' => 2 );
    276338        foreach ( $blocks as $block_name => $block_type ) {
    277339            if ( isset( $block_type->supports['__experimentalStyle'] ) ) {
     
    299361        $config     = $theme_json->get_data();
    300362
    301         return new WP_Theme_JSON( $config, 'blocks' );
     363        static::$blocks = new WP_Theme_JSON( $config, 'blocks' );
     364        return static::$blocks;
    302365    }
    303366
     
    408471     */
    409472    public static function get_user_data() {
     473        if ( null !== static::$user && static::has_same_registered_blocks( 'user' ) ) {
     474            return static::$user;
     475        }
     476
    410477        $config   = array();
    411478        $user_cpt = static::get_user_data_from_wp_global_styles( wp_get_theme() );
     
    563630     * @since 5.9.0 Added the `$user`, `$user_custom_post_type_id`,
    564631     *              and `$i18n_schema` variables to reset.
     632     * @since 6.1.0 Added the `$blocks` and `$blocks_cache` variables
     633     *              to reset.
    565634     */
    566635    public static function clean_cached_data() {
    567636        static::$core                     = null;
     637        static::$blocks                   = null;
     638        static::$blocks_cache             = array(
     639            'core'   => array(),
     640            'blocks' => array(),
     641            'theme'  => array(),
     642            'user'   => array(),
     643        );
    568644        static::$theme                    = null;
    569645        static::$user                     = null;
  • trunk/tests/phpunit/tests/theme/wpThemeJsonResolver.php

    r54446 r54493  
    3434    private $queries = array();
    3535
     36    /**
     37     * WP_Theme_JSON_Resolver::$blocks_cache property.
     38     *
     39     * @var ReflectionProperty
     40     */
     41    private static $property_blocks_cache;
     42
     43    /**
     44     * Original value of the WP_Theme_JSON_Resolver::$blocks_cache property.
     45     *
     46     * @var array
     47     */
     48    private static $property_blocks_cache_orig_value;
     49
     50    /**
     51     * WP_Theme_JSON_Resolver::$core property.
     52     *
     53     * @var ReflectionProperty
     54     */
     55    private static $property_core;
     56
     57    /**
     58     * Original value of the WP_Theme_JSON_Resolver::$core property.
     59     *
     60     * @var WP_Theme_JSON
     61     */
     62    private static $property_core_orig_value;
     63
     64    public static function set_up_before_class() {
     65        parent::set_up_before_class();
     66
     67        static::$property_blocks_cache = new ReflectionProperty( WP_Theme_JSON_Resolver::class, 'blocks_cache' );
     68        static::$property_blocks_cache->setAccessible( true );
     69        static::$property_blocks_cache_orig_value = static::$property_blocks_cache->getValue();
     70
     71        static::$property_core = new ReflectionProperty( WP_Theme_JSON_Resolver::class, 'core' );
     72        static::$property_core->setAccessible( true );
     73        static::$property_core_orig_value = static::$property_core->getValue();
     74    }
     75
     76    public static function tear_down_after_class() {
     77        static::$property_blocks_cache->setValue( WP_Theme_JSON_Resolver::class, static::$property_blocks_cache_orig_value );
     78        static::$property_core->setValue( WP_Theme_JSON_Resolver::class, static::$property_core_orig_value );
     79        parent::tear_down_after_class();
     80    }
     81
    3682    public function set_up() {
    3783        parent::set_up();
     
    56102        wp_clean_themes_cache();
    57103        unset( $GLOBALS['wp_themes'] );
     104
     105        // Reset data between tests.
     106        WP_Theme_JSON_Resolver::clean_cached_data();
    58107        parent::tear_down();
    59108    }
     
    191240    }
    192241
     242    private function get_registered_block_names( $hard_reset = false ) {
     243        static $expected_block_names;
     244
     245        if ( ! $hard_reset && ! empty( $expected_block_names ) ) {
     246            return $expected_block_names;
     247        }
     248
     249        $expected_block_names = array();
     250        $resolver             = WP_Block_Type_Registry::get_instance();
     251        $blocks               = $resolver->get_all_registered();
     252        foreach ( array_keys( $blocks ) as $block_name ) {
     253            $expected_block_names[ $block_name ] = true;
     254        }
     255
     256        return $expected_block_names;
     257    }
     258
     259    /**
     260     * Tests when WP_Theme_JSON_Resolver::$blocks_cache is empty or does not match
     261     * the all registered blocks.
     262     *
     263     * Though this is a non-public method, it is vital to other functionality.
     264     * Therefore, tests are provided to validate it functions as expected.
     265     *
     266     * @dataProvider data_has_same_registered_blocks_when_all_blocks_not_cached
     267     * @ticket 56467
     268     *
     269     * @param string $origin The origin to test.
     270     */
     271    public function test_has_same_registered_blocks_when_all_blocks_not_cached( $origin, array $cache = array() ) {
     272        $has_same_registered_blocks = new ReflectionMethod( WP_Theme_JSON_Resolver::class, 'has_same_registered_blocks' );
     273        $has_same_registered_blocks->setAccessible( true );
     274        $expected_cache = $this->get_registered_block_names();
     275
     276        // Set up the blocks cache for the origin.
     277        $blocks_cache            = static::$property_blocks_cache->getValue();
     278        $blocks_cache[ $origin ] = $cache;
     279        static::$property_blocks_cache->setValue( null, $blocks_cache );
     280
     281        $this->assertFalse( $has_same_registered_blocks->invoke( null, $origin ), 'WP_Theme_JSON_Resolver::has_same_registered_blocks() should return false when same blocks are not cached' );
     282        $blocks_cache = static::$property_blocks_cache->getValue();
     283        $this->assertSameSets( $expected_cache, $blocks_cache[ $origin ], 'WP_Theme_JSON_Resolver::$blocks_cache should contain all expected block names for the given origin' );
     284    }
     285
     286    /**
     287     * Data provider.
     288     *
     289     * @return array
     290     */
     291    public function data_has_same_registered_blocks_when_all_blocks_not_cached() {
     292        return array(
     293            'origin: core; cache: empty'       => array(
     294                'origin' => 'core',
     295            ),
     296            'origin: blocks; cache: empty'     => array(
     297                'origin' => 'blocks',
     298            ),
     299            'origin: theme; cache: empty'      => array(
     300                'origin' => 'theme',
     301            ),
     302            'origin: user; cache: empty'       => array(
     303                'origin' => 'user',
     304            ),
     305            'origin: core; cache: not empty'   => array(
     306                'origin' => 'core',
     307                'cache'  => array(
     308                    'core/block' => true,
     309                ),
     310            ),
     311            'origin: blocks; cache: not empty' => array(
     312                'origin' => 'blocks',
     313                'cache'  => array(
     314                    'core/block'    => true,
     315                    'core/comments' => true,
     316                ),
     317            ),
     318            'origin: theme; cache: not empty'  => array(
     319                'origin' => 'theme',
     320                'cache'  => array(
     321                    'core/cover' => true,
     322                ),
     323            ),
     324            'origin: user; cache: not empty'   => array(
     325                'origin' => 'user',
     326                'cache'  => array(
     327                    'core/gallery' => true,
     328                ),
     329            ),
     330        );
     331    }
     332
     333    /**
     334     * Tests when WP_Theme_JSON_Resolver::$blocks_cache is empty or does not match
     335     * the all registered blocks.
     336     *
     337     * Though this is a non-public method, it is vital to other functionality.
     338     * Therefore, tests are provided to validate it functions as expected.
     339     *
     340     * @dataProvider data_has_same_registered_blocks_when_all_blocks_are_cached
     341     * @ticket 56467
     342     *
     343     * @param string $origin The origin to test.
     344     */
     345    public function test_has_same_registered_blocks_when_all_blocks_are_cached( $origin ) {
     346        $has_same_registered_blocks = new ReflectionMethod( WP_Theme_JSON_Resolver::class, 'has_same_registered_blocks' );
     347        $has_same_registered_blocks->setAccessible( true );
     348        $expected_cache = $this->get_registered_block_names();
     349
     350        // Set up the cache with all registered blocks.
     351        $blocks_cache            = static::$property_blocks_cache->getValue();
     352        $blocks_cache[ $origin ] = $this->get_registered_block_names();
     353        static::$property_blocks_cache->setValue( null, $blocks_cache );
     354
     355        $this->assertTrue( $has_same_registered_blocks->invoke( null, $origin ), 'WP_Theme_JSON_Resolver::has_same_registered_blocks() should return true when using the cache' );
     356        $this->assertSameSets( $expected_cache, $blocks_cache[ $origin ], 'WP_Theme_JSON_Resolver::$blocks_cache should contain all expected block names for the given origin' );
     357    }
     358
     359    /**
     360     * Data provider.
     361     *
     362     * @return array
     363     */
     364    public function data_has_same_registered_blocks_when_all_blocks_are_cached() {
     365        return array(
     366            'core'   => array( 'core' ),
     367            'blocks' => array( 'blocks' ),
     368            'theme'  => array( 'theme' ),
     369            'user'   => array( 'user' ),
     370        );
     371    }
     372
     373    /**
     374     * @dataProvider data_get_core_data
     375     * @covers WP_Theme_JSON_Resolver::get_core_data
     376     * @ticket 56467
     377     */
     378    public function test_get_core_data( $should_fire_filter, $core_is_cached, $blocks_are_cached ) {
     379        WP_Theme_JSON_Resolver::clean_cached_data();
     380
     381        // If should cache core, then fire the method to cache it before running the tests.
     382        if ( $core_is_cached ) {
     383            WP_Theme_JSON_Resolver::get_core_data();
     384        }
     385
     386        // If should cache registered blocks, then set them up before running the tests.
     387        if ( $blocks_are_cached ) {
     388            $blocks_cache         = static::$property_blocks_cache->getValue();
     389            $blocks_cache['core'] = $this->get_registered_block_names();
     390            static::$property_blocks_cache->setValue( null, $blocks_cache );
     391        }
     392
     393        $expected_filter_count = did_filter( 'theme_json_default' );
     394        $actual                = WP_Theme_JSON_Resolver::get_core_data();
     395        if ( $should_fire_filter ) {
     396            $expected_filter_count++;
     397        }
     398
     399        $this->assertSame( $expected_filter_count, did_filter( 'theme_json_default' ), 'The filter "theme_json_default" should fire the given number of times' );
     400        $this->assertInstanceOf( WP_Theme_JSON::class, $actual, 'WP_Theme_JSON_Resolver::get_core_data() should return instance of WP_Theme_JSON' );
     401        $this->assertSame( static::$property_core->getValue(), $actual, 'WP_Theme_JSON_Resolver::$core property should be the same object as returned from WP_Theme_JSON_Resolver::get_core_data()' );
     402    }
     403
     404    /**
     405     * Data provider.
     406     *
     407     * @return array
     408     */
     409    public function data_get_core_data() {
     410        return array(
     411            'When both caches are empty'     => array(
     412                'should_fire_filter' => true,
     413                'core_is_cached'     => false,
     414                'blocks_are_cached'  => false,
     415            ),
     416            'When the blocks_cache is not empty and matches' => array(
     417                'should_fire_filter' => true,
     418                'core_is_cached'     => false,
     419                'blocks_are_cached'  => true,
     420            ),
     421            'When blocks_cache is empty but core cache is not' => array(
     422                'should_fire_filter' => true,
     423                'core_is_cached'     => true,
     424                'blocks_are_cached'  => false,
     425            ),
     426            'When both caches are not empty' => array(
     427                'should_fire_filter' => true,
     428                'core_is_cached'     => true,
     429                'blocks_are_cached'  => false,
     430            ),
     431        );
     432    }
     433
    193434    /**
    194435     * @ticket 52991
Note: See TracChangeset for help on using the changeset viewer.