Make WordPress Core

Changeset 54493


Ignore:
Timestamp:
10/11/2022 05:15:11 PM (4 years 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.