Make WordPress Core

Changeset 56765


Ignore:
Timestamp:
10/03/2023 03:16:55 PM (3 years ago)
Author:
spacedmonkey
Message:

Editor: Improve performance of _register_theme_block_patterns function.

The _register_theme_block_patterns function imposed a significant resource overhead. This issue primarily stems from themes, such as TT4, that register a substantial number of block patterns. These patterns necessitate numerous file operations, including file lookups, file reading into memory, and related processes. To provide an overview, the _register_theme_block_patterns function performed the following file operations:

  • is_dir
  • is_readable
  • file_exists
  • glob
  • file_get_contents (utilized via get_file_data)

To address these issues, caching using a transient has been added to a new function call _wp_get_block_patterns. If theme development mode is disabled and theme exists, the block patterns are saved in a transient cache. This cache is used all requests after that, saving file lookups and reading files into memory. Cache invalidation is done, when themes are switched, deleted or updated. Meaning that block patterns are not stored in the cache incorrectly.

Props flixos90, joemcgill, peterwilsoncc, costdev, swissspidy, aristath, westonruter, spacedmonkey.
Fixes #59490

Location:
trunk
Files:
5 added
5 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/includes/theme.php

    r56639 r56765  
    8282        do_action( 'delete_theme', $stylesheet );
    8383
     84        $theme = wp_get_theme( $stylesheet );
     85
    8486        $themes_dir = trailingslashit( $themes_dir );
    8587        $theme_dir  = trailingslashit( $themes_dir . $stylesheet );
     
    125127                WP_Theme::network_disable_theme( $stylesheet );
    126128        }
     129
     130        // Clear theme caches.
     131        $theme->cache_delete();
    127132
    128133        // Force refresh of theme update information.
  • trunk/src/wp-includes/block-patterns.php

    r56738 r56765  
    320320/**
    321321 * Register any patterns that the active theme may provide under its
    322  * `./patterns/` directory. Each pattern is defined as a PHP file and defines
    323  * its metadata using plugin-style headers. The minimum required definition is:
    324  *
    325  *     /**
    326  *      * Title: My Pattern
    327  *      * Slug: my-theme/my-pattern
    328  *      *
    329  *
    330  * The output of the PHP source corresponds to the content of the pattern, e.g.:
    331  *
    332  *     <main><p><?php echo "Hello"; ?></p></main>
    333  *
    334  * If applicable, this will collect from both parent and child theme.
    335  *
    336  * Other settable fields include:
    337  *
    338  *   - Description
    339  *   - Viewport Width
    340  *   - Inserter         (yes/no)
    341  *   - Categories       (comma-separated values)
    342  *   - Keywords         (comma-separated values)
    343  *   - Block Types      (comma-separated values)
    344  *   - Post Types       (comma-separated values)
    345  *   - Template Types   (comma-separated values)
     322 * `./patterns/` directory.
    346323 *
    347324 * @since 6.0.0
    348325 * @since 6.1.0 The `postTypes` property was added.
    349326 * @since 6.2.0 The `templateTypes` property was added.
     327 * @since 6.4.0 Uses the `_wp_get_block_patterns` function.
    350328 * @access private
    351329 */
    352330function _register_theme_block_patterns() {
     331        /*
     332         * Register patterns for the active theme. If the theme is a child theme,
     333         * let it override any patterns from the parent theme that shares the same slug.
     334         */
     335        $themes   = array();
     336        $theme    = wp_get_theme();
     337        $themes[] = $theme;
     338        if ( $theme->parent() ) {
     339                $themes[] = $theme->parent();
     340        }
     341        $registry = WP_Block_Patterns_Registry::get_instance();
     342
     343        foreach ( $themes as $theme ) {
     344                $pattern_data = _wp_get_block_patterns( $theme );
     345                $dirpath      = $theme->get_stylesheet_directory() . '/patterns/';
     346                $text_domain  = $theme->get( 'TextDomain' );
     347
     348                foreach ( $pattern_data['patterns'] as $file => $pattern_data ) {
     349                        if ( $registry->is_registered( $pattern_data['slug'] ) ) {
     350                                continue;
     351                        }
     352
     353                        // The actual pattern content is the output of the file.
     354                        ob_start();
     355                        include $dirpath . $file;
     356                        $pattern_data['content'] = ob_get_clean();
     357                        if ( ! $pattern_data['content'] ) {
     358                                continue;
     359                        }
     360
     361                        // Translate the pattern metadata.
     362                        // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain,WordPress.WP.I18n.LowLevelTranslationFunction
     363                        $pattern_data['title'] = translate_with_gettext_context( $pattern_data['title'], 'Pattern title', $text_domain );
     364                        if ( ! empty( $pattern_data['description'] ) ) {
     365                                // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain,WordPress.WP.I18n.LowLevelTranslationFunction
     366                                $pattern_data['description'] = translate_with_gettext_context( $pattern_data['description'], 'Pattern description', $text_domain );
     367                        }
     368
     369                        register_block_pattern( $pattern_data['slug'], $pattern_data );
     370                }
     371        }
     372}
     373add_action( 'init', '_register_theme_block_patterns' );
     374
     375/**
     376 * Gets block pattern data for a specified theme.
     377 * Each pattern is defined as a PHP file and defines
     378 *  its metadata using plugin-style headers. The minimum required definition is:
     379 *
     380 *      /**
     381 *       * Title: My Pattern
     382 *       * Slug: my-theme/my-pattern
     383 *       *
     384 *
     385 *  The output of the PHP source corresponds to the content of the pattern, e.g.:
     386 *
     387 *      <main><p><?php echo "Hello"; ?></p></main>
     388 *
     389 *  If applicable, this will collect from both parent and child theme.
     390 *
     391 *  Other settable fields include:
     392 *
     393 *    - Description
     394 *    - Viewport Width
     395 *    - Inserter         (yes/no)
     396 *    - Categories       (comma-separated values)
     397 *    - Keywords         (comma-separated values)
     398 *    - Block Types      (comma-separated values)
     399 *    - Post Types       (comma-separated values)
     400 *    - Template Types   (comma-separated values)
     401 *
     402 * @since 6.4.0
     403 * @access private
     404 *
     405 * @param WP_Theme $theme Theme object.
     406 * @return array Block pattern data.
     407 */
     408
     409function _wp_get_block_patterns( WP_Theme $theme ) {
     410        if ( ! $theme->exists() ) {
     411                return array(
     412                        'version'  => false,
     413                        'patterns' => array(),
     414                );
     415        }
     416
     417        $transient_name = 'wp_theme_patterns_' . $theme->get_stylesheet();
     418        $version        = $theme->get( 'Version' );
     419        $can_use_cached = ! wp_is_development_mode( 'theme' );
     420
     421        if ( $can_use_cached ) {
     422                $pattern_data = get_transient( $transient_name );
     423                if ( is_array( $pattern_data ) && $pattern_data['version'] === $version ) {
     424                        return $pattern_data;
     425                }
     426        }
     427
     428        $pattern_data = array(
     429                'version'  => $version,
     430                'patterns' => array(),
     431        );
     432        $dirpath      = $theme->get_stylesheet_directory() . '/patterns/';
     433
     434        if ( ! file_exists( $dirpath ) ) {
     435                if ( $can_use_cached ) {
     436                        set_transient( $transient_name, $pattern_data );
     437                }
     438                return $pattern_data;
     439        }
     440        $files = glob( $dirpath . '*.php' );
     441        if ( ! $files ) {
     442                if ( $can_use_cached ) {
     443                        set_transient( $transient_name, $pattern_data );
     444                }
     445                return $pattern_data;
     446        }
     447
    353448        $default_headers = array(
    354449                'title'         => 'Title',
     
    364459        );
    365460
    366         /*
    367          * Register patterns for the active theme. If the theme is a child theme,
    368          * let it override any patterns from the parent theme that shares the same slug.
    369          */
    370         $themes     = array();
    371         $stylesheet = get_stylesheet();
    372         $template   = get_template();
    373         if ( $stylesheet !== $template ) {
    374                 $themes[] = wp_get_theme( $stylesheet );
    375         }
    376         $themes[] = wp_get_theme( $template );
    377 
    378         foreach ( $themes as $theme ) {
    379                 $dirpath = $theme->get_stylesheet_directory() . '/patterns/';
    380                 if ( ! is_dir( $dirpath ) || ! is_readable( $dirpath ) ) {
     461        $properties_to_parse = array(
     462                'categories',
     463                'keywords',
     464                'blockTypes',
     465                'postTypes',
     466                'templateTypes',
     467        );
     468
     469        foreach ( $files as $file ) {
     470                $pattern = get_file_data( $file, $default_headers );
     471
     472                if ( empty( $pattern['slug'] ) ) {
     473                        _doing_it_wrong(
     474                                __FUNCTION__,
     475                                sprintf(
     476                                        /* translators: %s: file name. */
     477                                        __( 'Could not register file "%s" as a block pattern ("Slug" field missing)' ),
     478                                        $file
     479                                ),
     480                                '6.0.0'
     481                        );
    381482                        continue;
    382483                }
    383                 if ( file_exists( $dirpath ) ) {
    384                         $files = glob( $dirpath . '*.php' );
    385                         if ( $files ) {
    386                                 foreach ( $files as $file ) {
    387                                         $pattern_data = get_file_data( $file, $default_headers );
    388 
    389                                         if ( empty( $pattern_data['slug'] ) ) {
    390                                                 _doing_it_wrong(
    391                                                         '_register_theme_block_patterns',
    392                                                         sprintf(
    393                                                                 /* translators: %s: file name. */
    394                                                                 __( 'Could not register file "%s" as a block pattern ("Slug" field missing)' ),
    395                                                                 $file
    396                                                         ),
    397                                                         '6.0.0'
    398                                                 );
    399                                                 continue;
    400                                         }
    401 
    402                                         if ( ! preg_match( '/^[A-z0-9\/_-]+$/', $pattern_data['slug'] ) ) {
    403                                                 _doing_it_wrong(
    404                                                         '_register_theme_block_patterns',
    405                                                         sprintf(
    406                                                                 /* translators: %1s: file name; %2s: slug value found. */
    407                                                                 __( 'Could not register file "%1$s" as a block pattern (invalid slug "%2$s")' ),
    408                                                                 $file,
    409                                                                 $pattern_data['slug']
    410                                                         ),
    411                                                         '6.0.0'
    412                                                 );
    413                                         }
    414 
    415                                         if ( WP_Block_Patterns_Registry::get_instance()->is_registered( $pattern_data['slug'] ) ) {
    416                                                 continue;
    417                                         }
    418 
    419                                         // Title is a required property.
    420                                         if ( ! $pattern_data['title'] ) {
    421                                                 _doing_it_wrong(
    422                                                         '_register_theme_block_patterns',
    423                                                         sprintf(
    424                                                                 /* translators: %1s: file name; %2s: slug value found. */
    425                                                                 __( 'Could not register file "%s" as a block pattern ("Title" field missing)' ),
    426                                                                 $file
    427                                                         ),
    428                                                         '6.0.0'
    429                                                 );
    430                                                 continue;
    431                                         }
    432 
    433                                         // For properties of type array, parse data as comma-separated.
    434                                         foreach ( array( 'categories', 'keywords', 'blockTypes', 'postTypes', 'templateTypes' ) as $property ) {
    435                                                 if ( ! empty( $pattern_data[ $property ] ) ) {
    436                                                         $pattern_data[ $property ] = array_filter(
    437                                                                 preg_split(
    438                                                                         '/[\s,]+/',
    439                                                                         (string) $pattern_data[ $property ]
    440                                                                 )
    441                                                         );
    442                                                 } else {
    443                                                         unset( $pattern_data[ $property ] );
    444                                                 }
    445                                         }
    446 
    447                                         // Parse properties of type int.
    448                                         foreach ( array( 'viewportWidth' ) as $property ) {
    449                                                 if ( ! empty( $pattern_data[ $property ] ) ) {
    450                                                         $pattern_data[ $property ] = (int) $pattern_data[ $property ];
    451                                                 } else {
    452                                                         unset( $pattern_data[ $property ] );
    453                                                 }
    454                                         }
    455 
    456                                         // Parse properties of type bool.
    457                                         foreach ( array( 'inserter' ) as $property ) {
    458                                                 if ( ! empty( $pattern_data[ $property ] ) ) {
    459                                                         $pattern_data[ $property ] = in_array(
    460                                                                 strtolower( $pattern_data[ $property ] ),
    461                                                                 array( 'yes', 'true' ),
    462                                                                 true
    463                                                         );
    464                                                 } else {
    465                                                         unset( $pattern_data[ $property ] );
    466                                                 }
    467                                         }
    468 
    469                                         // Translate the pattern metadata.
    470                                         $text_domain = $theme->get( 'TextDomain' );
    471                                         // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain,WordPress.WP.I18n.LowLevelTranslationFunction
    472                                         $pattern_data['title'] = translate_with_gettext_context( $pattern_data['title'], 'Pattern title', $text_domain );
    473                                         if ( ! empty( $pattern_data['description'] ) ) {
    474                                                 // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain,WordPress.WP.I18n.LowLevelTranslationFunction
    475                                                 $pattern_data['description'] = translate_with_gettext_context( $pattern_data['description'], 'Pattern description', $text_domain );
    476                                         }
    477 
    478                                         // The actual pattern content is the output of the file.
    479                                         ob_start();
    480                                         include $file;
    481                                         $pattern_data['content'] = ob_get_clean();
    482                                         if ( ! $pattern_data['content'] ) {
    483                                                 continue;
    484                                         }
    485 
    486                                         register_block_pattern( $pattern_data['slug'], $pattern_data );
    487                                 }
     484
     485                if ( ! preg_match( '/^[A-z0-9\/_-]+$/', $pattern['slug'] ) ) {
     486                        _doing_it_wrong(
     487                                __FUNCTION__,
     488                                sprintf(
     489                                        /* translators: %1s: file name; %2s: slug value found. */
     490                                        __( 'Could not register file "%1$s" as a block pattern (invalid slug "%2$s")' ),
     491                                        $file,
     492                                        $pattern['slug']
     493                                ),
     494                                '6.0.0'
     495                        );
     496                }
     497
     498                // Title is a required property.
     499                if ( ! $pattern['title'] ) {
     500                        _doing_it_wrong(
     501                                __FUNCTION__,
     502                                sprintf(
     503                                        /* translators: %1s: file name. */
     504                                        __( 'Could not register file "%s" as a block pattern ("Title" field missing)' ),
     505                                        $file
     506                                ),
     507                                '6.0.0'
     508                        );
     509                        continue;
     510                }
     511
     512                // For properties of type array, parse data as comma-separated.
     513                foreach ( $properties_to_parse as $property ) {
     514                        if ( ! empty( $pattern[ $property ] ) ) {
     515                                $pattern[ $property ] = array_filter( wp_parse_list( (string) $pattern[ $property ] ) );
     516                        } else {
     517                                unset( $pattern[ $property ] );
    488518                        }
    489519                }
    490         }
    491 }
    492 add_action( 'init', '_register_theme_block_patterns' );
     520
     521                // Parse properties of type int.
     522                $property = 'viewportWidth';
     523                if ( ! empty( $pattern[ $property ] ) ) {
     524                        $pattern[ $property ] = (int) $pattern[ $property ];
     525                } else {
     526                        unset( $pattern[ $property ] );
     527                }
     528
     529                // Parse properties of type bool.
     530                $property = 'inserter';
     531                if ( ! empty( $pattern[ $property ] ) ) {
     532                        $pattern[ $property ] = in_array(
     533                                strtolower( $pattern[ $property ] ),
     534                                array( 'yes', 'true' ),
     535                                true
     536                        );
     537                } else {
     538                        unset( $pattern[ $property ] );
     539                }
     540
     541                $key = str_replace( $dirpath, '', $file );
     542
     543                $pattern_data['patterns'][ $key ] = $pattern;
     544        }
     545
     546        if ( $can_use_cached ) {
     547                set_transient( $transient_name, $pattern_data );
     548        }
     549
     550        return $pattern_data;
     551}
  • trunk/src/wp-includes/class-wp-theme.php

    r56727 r56765  
    822822                $this->headers                = array();
    823823                $this->__construct( $this->stylesheet, $this->theme_root );
     824                $this->delete_pattern_cache();
     825        }
     826
     827        /**
     828         * Clear block pattern cache.
     829         *
     830         * @since 6.4.0
     831         */
     832        public function delete_pattern_cache() {
     833                delete_transient( 'wp_theme_patterns_' . $this->stylesheet );
    824834        }
    825835
  • trunk/src/wp-includes/theme.php

    r56748 r56765  
    873873        $wp_stylesheet_path = null;
    874874        $wp_template_path   = null;
     875
     876        // Clear pattern caches.
     877        $new_theme->delete_pattern_cache();
     878        $old_theme->delete_pattern_cache();
    875879
    876880        /**
  • trunk/tests/phpunit/tests/theme/themeDir.php

    r56759 r56765  
    186186                        'Block Theme [1.0.0] in subdirectory',
    187187                        'Block Theme Deprecated Path',
     188                        'Block Theme Patterns',
    188189                        'Block Theme Post Content Default',
    189190                        'Block Theme with defined Typography Fonts',
Note: See TracChangeset for help on using the changeset viewer.