Make WordPress Core

Changeset 56765


Ignore:
Timestamp:
10/03/2023 03:16:55 PM (18 months 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.