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
Built from https://develop.svn.wordpress.org/trunk@56765


git-svn-id: http://core.svn.wordpress.org/trunk@56277 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
spacedmonkey 2023-10-03 15:18:19 +00:00
parent 67161cc1dd
commit b0872b005d
5 changed files with 220 additions and 142 deletions

View File

@ -81,6 +81,8 @@ function delete_theme( $stylesheet, $redirect = '' ) {
*/
do_action( 'delete_theme', $stylesheet );
$theme = wp_get_theme( $stylesheet );
$themes_dir = trailingslashit( $themes_dir );
$theme_dir = trailingslashit( $themes_dir . $stylesheet );
$deleted = $wp_filesystem->delete( $theme_dir, true );
@ -125,6 +127,9 @@ function delete_theme( $stylesheet, $redirect = '' ) {
WP_Theme::network_disable_theme( $stylesheet );
}
// Clear theme caches.
$theme->cache_delete();
// Force refresh of theme update information.
delete_site_transient( 'update_themes' );

View File

@ -319,7 +319,62 @@ function _register_remote_theme_patterns() {
/**
* Register any patterns that the active theme may provide under its
* `./patterns/` directory. Each pattern is defined as a PHP file and defines
* `./patterns/` directory.
*
* @since 6.0.0
* @since 6.1.0 The `postTypes` property was added.
* @since 6.2.0 The `templateTypes` property was added.
* @since 6.4.0 Uses the `_wp_get_block_patterns` function.
* @access private
*/
function _register_theme_block_patterns() {
/*
* Register patterns for the active theme. If the theme is a child theme,
* let it override any patterns from the parent theme that shares the same slug.
*/
$themes = array();
$theme = wp_get_theme();
$themes[] = $theme;
if ( $theme->parent() ) {
$themes[] = $theme->parent();
}
$registry = WP_Block_Patterns_Registry::get_instance();
foreach ( $themes as $theme ) {
$pattern_data = _wp_get_block_patterns( $theme );
$dirpath = $theme->get_stylesheet_directory() . '/patterns/';
$text_domain = $theme->get( 'TextDomain' );
foreach ( $pattern_data['patterns'] as $file => $pattern_data ) {
if ( $registry->is_registered( $pattern_data['slug'] ) ) {
continue;
}
// The actual pattern content is the output of the file.
ob_start();
include $dirpath . $file;
$pattern_data['content'] = ob_get_clean();
if ( ! $pattern_data['content'] ) {
continue;
}
// Translate the pattern metadata.
// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain,WordPress.WP.I18n.LowLevelTranslationFunction
$pattern_data['title'] = translate_with_gettext_context( $pattern_data['title'], 'Pattern title', $text_domain );
if ( ! empty( $pattern_data['description'] ) ) {
// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain,WordPress.WP.I18n.LowLevelTranslationFunction
$pattern_data['description'] = translate_with_gettext_context( $pattern_data['description'], 'Pattern description', $text_domain );
}
register_block_pattern( $pattern_data['slug'], $pattern_data );
}
}
}
add_action( 'init', '_register_theme_block_patterns' );
/**
* Gets block pattern data for a specified theme.
* Each pattern is defined as a PHP file and defines
* its metadata using plugin-style headers. The minimum required definition is:
*
* /**
@ -344,12 +399,52 @@ function _register_remote_theme_patterns() {
* - Post Types (comma-separated values)
* - Template Types (comma-separated values)
*
* @since 6.0.0
* @since 6.1.0 The `postTypes` property was added.
* @since 6.2.0 The `templateTypes` property was added.
* @since 6.4.0
* @access private
*
* @param WP_Theme $theme Theme object.
* @return array Block pattern data.
*/
function _register_theme_block_patterns() {
function _wp_get_block_patterns( WP_Theme $theme ) {
if ( ! $theme->exists() ) {
return array(
'version' => false,
'patterns' => array(),
);
}
$transient_name = 'wp_theme_patterns_' . $theme->get_stylesheet();
$version = $theme->get( 'Version' );
$can_use_cached = ! wp_is_development_mode( 'theme' );
if ( $can_use_cached ) {
$pattern_data = get_transient( $transient_name );
if ( is_array( $pattern_data ) && $pattern_data['version'] === $version ) {
return $pattern_data;
}
}
$pattern_data = array(
'version' => $version,
'patterns' => array(),
);
$dirpath = $theme->get_stylesheet_directory() . '/patterns/';
if ( ! file_exists( $dirpath ) ) {
if ( $can_use_cached ) {
set_transient( $transient_name, $pattern_data );
}
return $pattern_data;
}
$files = glob( $dirpath . '*.php' );
if ( ! $files ) {
if ( $can_use_cached ) {
set_transient( $transient_name, $pattern_data );
}
return $pattern_data;
}
$default_headers = array(
'title' => 'Title',
'slug' => 'Slug',
@ -363,32 +458,20 @@ function _register_theme_block_patterns() {
'templateTypes' => 'Template Types',
);
/*
* Register patterns for the active theme. If the theme is a child theme,
* let it override any patterns from the parent theme that shares the same slug.
*/
$themes = array();
$stylesheet = get_stylesheet();
$template = get_template();
if ( $stylesheet !== $template ) {
$themes[] = wp_get_theme( $stylesheet );
}
$themes[] = wp_get_theme( $template );
$properties_to_parse = array(
'categories',
'keywords',
'blockTypes',
'postTypes',
'templateTypes',
);
foreach ( $themes as $theme ) {
$dirpath = $theme->get_stylesheet_directory() . '/patterns/';
if ( ! is_dir( $dirpath ) || ! is_readable( $dirpath ) ) {
continue;
}
if ( file_exists( $dirpath ) ) {
$files = glob( $dirpath . '*.php' );
if ( $files ) {
foreach ( $files as $file ) {
$pattern_data = get_file_data( $file, $default_headers );
$pattern = get_file_data( $file, $default_headers );
if ( empty( $pattern_data['slug'] ) ) {
if ( empty( $pattern['slug'] ) ) {
_doing_it_wrong(
'_register_theme_block_patterns',
__FUNCTION__,
sprintf(
/* translators: %s: file name. */
__( 'Could not register file "%s" as a block pattern ("Slug" field missing)' ),
@ -399,29 +482,25 @@ function _register_theme_block_patterns() {
continue;
}
if ( ! preg_match( '/^[A-z0-9\/_-]+$/', $pattern_data['slug'] ) ) {
if ( ! preg_match( '/^[A-z0-9\/_-]+$/', $pattern['slug'] ) ) {
_doing_it_wrong(
'_register_theme_block_patterns',
__FUNCTION__,
sprintf(
/* translators: %1s: file name; %2s: slug value found. */
__( 'Could not register file "%1$s" as a block pattern (invalid slug "%2$s")' ),
$file,
$pattern_data['slug']
$pattern['slug']
),
'6.0.0'
);
}
if ( WP_Block_Patterns_Registry::get_instance()->is_registered( $pattern_data['slug'] ) ) {
continue;
}
// Title is a required property.
if ( ! $pattern_data['title'] ) {
if ( ! $pattern['title'] ) {
_doing_it_wrong(
'_register_theme_block_patterns',
__FUNCTION__,
sprintf(
/* translators: %1s: file name; %2s: slug value found. */
/* translators: %1s: file name. */
__( 'Could not register file "%s" as a block pattern ("Title" field missing)' ),
$file
),
@ -431,62 +510,42 @@ function _register_theme_block_patterns() {
}
// For properties of type array, parse data as comma-separated.
foreach ( array( 'categories', 'keywords', 'blockTypes', 'postTypes', 'templateTypes' ) as $property ) {
if ( ! empty( $pattern_data[ $property ] ) ) {
$pattern_data[ $property ] = array_filter(
preg_split(
'/[\s,]+/',
(string) $pattern_data[ $property ]
)
);
foreach ( $properties_to_parse as $property ) {
if ( ! empty( $pattern[ $property ] ) ) {
$pattern[ $property ] = array_filter( wp_parse_list( (string) $pattern[ $property ] ) );
} else {
unset( $pattern_data[ $property ] );
unset( $pattern[ $property ] );
}
}
// Parse properties of type int.
foreach ( array( 'viewportWidth' ) as $property ) {
if ( ! empty( $pattern_data[ $property ] ) ) {
$pattern_data[ $property ] = (int) $pattern_data[ $property ];
$property = 'viewportWidth';
if ( ! empty( $pattern[ $property ] ) ) {
$pattern[ $property ] = (int) $pattern[ $property ];
} else {
unset( $pattern_data[ $property ] );
}
unset( $pattern[ $property ] );
}
// Parse properties of type bool.
foreach ( array( 'inserter' ) as $property ) {
if ( ! empty( $pattern_data[ $property ] ) ) {
$pattern_data[ $property ] = in_array(
strtolower( $pattern_data[ $property ] ),
$property = 'inserter';
if ( ! empty( $pattern[ $property ] ) ) {
$pattern[ $property ] = in_array(
strtolower( $pattern[ $property ] ),
array( 'yes', 'true' ),
true
);
} else {
unset( $pattern_data[ $property ] );
}
unset( $pattern[ $property ] );
}
// Translate the pattern metadata.
$text_domain = $theme->get( 'TextDomain' );
// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain,WordPress.WP.I18n.LowLevelTranslationFunction
$pattern_data['title'] = translate_with_gettext_context( $pattern_data['title'], 'Pattern title', $text_domain );
if ( ! empty( $pattern_data['description'] ) ) {
// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain,WordPress.WP.I18n.LowLevelTranslationFunction
$pattern_data['description'] = translate_with_gettext_context( $pattern_data['description'], 'Pattern description', $text_domain );
$key = str_replace( $dirpath, '', $file );
$pattern_data['patterns'][ $key ] = $pattern;
}
// The actual pattern content is the output of the file.
ob_start();
include $file;
$pattern_data['content'] = ob_get_clean();
if ( ! $pattern_data['content'] ) {
continue;
if ( $can_use_cached ) {
set_transient( $transient_name, $pattern_data );
}
register_block_pattern( $pattern_data['slug'], $pattern_data );
}
}
}
}
return $pattern_data;
}
add_action( 'init', '_register_theme_block_patterns' );

View File

@ -821,6 +821,16 @@ final class WP_Theme implements ArrayAccess {
$this->block_template_folders = null;
$this->headers = array();
$this->__construct( $this->stylesheet, $this->theme_root );
$this->delete_pattern_cache();
}
/**
* Clear block pattern cache.
*
* @since 6.4.0
*/
public function delete_pattern_cache() {
delete_transient( 'wp_theme_patterns_' . $this->stylesheet );
}
/**

View File

@ -873,6 +873,10 @@ function switch_theme( $stylesheet ) {
$wp_stylesheet_path = null;
$wp_template_path = null;
// Clear pattern caches.
$new_theme->delete_pattern_cache();
$old_theme->delete_pattern_cache();
/**
* Fires after the theme is switched.
*

View File

@ -16,7 +16,7 @@
*
* @global string $wp_version
*/
$wp_version = '6.4-beta1-56764';
$wp_version = '6.4-beta1-56765';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.