Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add object caching for WP_Theme_JSON::compute_style_properties #5567

Open
wants to merge 8 commits into
base: trunk
Choose a base branch
from

Conversation

pereirinha
Copy link
Member

@pereirinha pereirinha commented Oct 24, 2023

Add object caching to improve the performance of WP_Theme_JSON::compute_style_properties when supported.

Trac ticket: 59595


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

Copy link
Member

@mukeshpanchal27 mukeshpanchal27 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @pereirinha for the PR. Left one feedback.

Comment on lines 1928 to 1934
$args = func_get_args();
$cache_key = 'compute_style_properties_' . md5( wp_json_encode( $args ) );
$cache = wp_cache_get( $cache_key, 'wp-styles' );

if ( $cache ) {
return $cache;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Certainly, you can add the code after line 1944 to avoid creating a cache for empty( $styles ). Please insert the code in the appropriate location to achieve this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch, @mukeshpanchal27.

@mukeshpanchal27
Copy link
Member

@pereirinha I did some benchmarking for TT4 theme home page using XHProf but i can't i can't find any improvement. Did i miss anything?

cc. @felixarntz @joemcgill

@pereirinha
Copy link
Member Author

@pereirinha I did some benchmarking for TT4 theme home page using XHProf but i can't i can't find any improvement. Did i miss anything?

cc. @felixarntz @joemcgill

Thanks, @mukeshpanchal27, for picking it up. Can the reason why you don't see improvements might be that you aren't using Memcached?

Copy link
Member

@joemcgill joemcgill left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for digging into this, @pereirinha. Looking at the code path that generates these style properties, I wonder if we would be better off moving caching of this data to wp_add_global_styles_for_blocks(), which is the main caller of this method. One challenge here is that there are a number of external factors that we might need to account for in order to invalidate the cache (e.g., updating the theme, core version bumps, etc.), so limiting the surface area to a specific use case might be safer. Curious what you think?

@@ -1925,6 +1925,14 @@ protected static function flatten_tree( $tree, $prefix = '', $token = '--' ) {
* @return array Returns the modified $declarations.
*/
protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null, $selector = null, $use_root_padding = null ) {
$args = func_get_args();
$cache_key = 'compute_style_properties_' . md5( wp_json_encode( $args ) );
$cache = wp_cache_get( $cache_key, 'wp-styles' );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You probably need to add a check for development mode before making use of caches, similar to how WP_Theme::get_block_patterns does here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: If we introduce a development mode check, we might end up in a loop of discussions similar to what we had in #59719. Would it be advisable to prioritize and address that ticket first to determine the best way forward, making it easier to adopt the solution in other places? What are your thoughts on this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question—thanks for asking, @mukeshpanchal27. I don't think we need that discussion to block forward progress on this ticket since it's not proposing that we make these caches persistent via transients. In this case, adding a development mode check just ensures that anyone who is developing while using an object cache would still get fresh results.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mukeshpanchal27 @joemcgill, thanks for these inputs.

I just pushed an update to bail processing empty $styles and ensure we don't leverage cache in development mode.

I'm also adding an expiry time for the cache keys — a day is a reasonable value. This will help the system to clean up old data that it's not used often, in case there's a need for resources.

@mukeshpanchal27
Copy link
Member

Thanks, @mukeshpanchal27, for picking it up. Can the reason why you don't see improvements might be that you aren't using Memcached?

I just enabled the Memcached through ENV and now i got the improvement after the changes.

Screenshot 2023-11-01 at 11 51 40 AM

Copy link
Member Author

@pereirinha pereirinha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mukeshpanchal27 @joemcgill

This is ready for you to circle back, as I pushed an update addressing your feedback. And I do have a few thoughts here.

We might not even need to avoid the cache at all. You'll notice when we are generating the cache key, we use the function arguments to generate it — in a single request, we run this function many times with different arguments. When you change a value in any of the arguments, a new cache key will be generated. We won't use cached data if there's no match in the hash, hence, even if you're using cache, this strategy won't impact the development of a theme.

Comment on lines 1928 to 1934
$args = func_get_args();
$cache_key = 'compute_style_properties_' . md5( wp_json_encode( $args ) );
$cache = wp_cache_get( $cache_key, 'wp-styles' );

if ( $cache ) {
return $cache;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch, @mukeshpanchal27.

@@ -1925,6 +1925,14 @@ protected static function flatten_tree( $tree, $prefix = '', $token = '--' ) {
* @return array Returns the modified $declarations.
*/
protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null, $selector = null, $use_root_padding = null ) {
$args = func_get_args();
$cache_key = 'compute_style_properties_' . md5( wp_json_encode( $args ) );
$cache = wp_cache_get( $cache_key, 'wp-styles' );
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mukeshpanchal27 @joemcgill, thanks for these inputs.

I just pushed an update to bail processing empty $styles and ensure we don't leverage cache in development mode.

I'm also adding an expiry time for the cache keys — a day is a reasonable value. This will help the system to clean up old data that it's not used often, in case there's a need for resources.

Comment on lines 1939 to 1941
$args = func_get_args(); // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue.Changed
$cache_key = 'compute_style_properties_' . md5( wp_json_encode( $args ) );
$cache = wp_cache_get( $cache_key, 'wp-styles' );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is still being called if the theme is in development mode. Could you please review the approach taken in this section of the code and consider applying a similar approach here?

@@ -1934,6 +1934,16 @@ protected static function compute_style_properties( $styles, $settings = array()
return $declarations;
}

$can_use_cached = ! wp_is_development_mode( 'theme' );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I can tell, we may be able to avoid the wp_is_development_mode() check here entirely. Here's why:

  • The cache key depends on the given method arguments. This means it'll always be "fresh". For example, if a style changes, a new cache key will be used.
  • The only exception is the call to wp_get_typography_font_size_value() below, which relies on other data (e.g. wp_get_global_settings()). So maybe we can cache everything except that one? Would that still be beneficial for performance?

That would mean that we cache the result of the foreach loop, except for the font-size CSS property we simply store the $value without calling wp_get_typography_font_size_value().

After that loop, even if we got the result from cache, we can cycle through the loop of the results and call wp_get_typography_font_size_value() for any entry that is using the font-size CSS property.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@felixarntz

I agree that the check for the development mode here doesn't bring any value. For that reason, I removed it.

As for your concern around wp_get_global_settings() for the case when users tweak values, I think that we can get away with making that part of the recipe for the cache key. Here's how.

What do you think?

Copy link
Member

@spacedmonkey spacedmonkey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When and how is this cache invalidated? Adding caching is great, but if not correctly cache invalidated when file is change, this will make more problems than it solves.

Copy link

github-actions bot commented Jan 4, 2024

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • The Plugin and Theme Directories cannot be accessed within Playground.
  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@pereirinha
Copy link
Member Author

When and how is this cache invalidated? Adding caching is great, but if not correctly cache invalidated when file is change, this will make more problems than it solves.

@spacedmonkey, the cache is created based on the requested arguments. A single page load will use several different cache entries, and we are using these arguments to create a md5 hash. Now we will also include the wp_get_global_settings(), so that any changes the users make will force a new cache entry.

The cleanup will happen daily so that unused data gets purged. The caveat is that, even if there are no changes, the cache will have to be regenerated once a day.

@pereirinha
Copy link
Member Author

@felixarntz @mukeshpanchal27 this is ready for you to circle back.

I appreciate your patience here.

Copy link
Member

@joemcgill joemcgill left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @pereirinha.

This does make a big impact on performance, and I think we're really close. Even after reviewing this part of the WP_Theme_JSON system for a while, I'm not confident that I understand all of the potential side effects that could cause these values to become stale. Would appreciate thoughts from @tellthemachines or someone who worked on the original system in Gutenberg in case there's something we're missing.

I also still wonder if it would be better to move this caching strategy up to WP_Theme_JSON::get_styles_for_block or further up the call stack, but don't think that's a blocker to getting this improvement in for early testing.

I think this data belongs in the theme_json cache group, but that group is specifically marked to be non-persistent because of the man ways this data can become stale. Curious what @spacedmonkey thinks about alternatives for cache groups. here.

Comment on lines +1938 to +1939
$args = array( func_get_args(), wp_get_global_settings() ); // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue.Changed
$cache_key = 'compute_style_properties_' . md5( wp_json_encode( $args ) );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion/nitpick: rename $args to $cach_args for readability.

Suggested change
$args = array( func_get_args(), wp_get_global_settings() ); // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue.Changed
$cache_key = 'compute_style_properties_' . md5( wp_json_encode( $args ) );
$cache_args = array( func_get_args(), wp_get_global_settings() ); // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue.Changed
$cache_key = 'compute_style_properties_' . md5( wp_json_encode( $cache_args ) );

// The global settings can include dynamic data related to typography. We need evaluate it so that the cache is invalidated when it changes.
$args = array( func_get_args(), wp_get_global_settings() ); // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue.Changed
$cache_key = 'compute_style_properties_' . md5( wp_json_encode( $args ) );
$cache = wp_cache_get( $cache_key, 'wp-styles' );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than introducing a new cache group, I think this should be a part of the existing theme_json group. Would need to change it below where this cache value is set, as well.

Suggested change
$cache = wp_cache_get( $cache_key, 'wp-styles' );
$cache = wp_cache_get( $cache_key, 'theme_json' );

@@ -2000,6 +2009,8 @@ protected static function compute_style_properties( $styles, $settings = array()
}
}

wp_cache_set( $cache_key, $declarations, 'wp-styles', DAY_IN_SECONDS );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure about the best expiration time here. If there is a side effect that we've not accounted for in the cache key value, showing stale styles for a full day without flushing the cache is pretty long. Maybe limiting this to once an hour for now would be safer?

Suggested change
wp_cache_set( $cache_key, $declarations, 'wp-styles', DAY_IN_SECONDS );
wp_cache_set( $cache_key, $declarations, 'theme_json', HOUR_IN_SECONDS );

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theme json is a none presient cache, so there is no need for an expiry here.

@tellthemachines
Copy link
Contributor

Thanks for the ping! I can't see any obvious issues with this approach and changing global styles still works well in my local dev environment. Perhaps @andrewserong or @ramonjd might have additional thoughts as iirc they worked on previous caching-related bugs.

@andrewserong
Copy link
Contributor

Thanks for the ping! I can't see any obvious issues with it, either, but I'm a little cautious about caching in general for these classes just because it can be really hard to predict what sorts of things might go wrong. For example, I see there's no guard against development mode. Will that make it hard for theme and or core developers working locally who are experimenting with making changes in the processing logic (outside of just changes within their theme)?

I agree with this comment about reducing the cache expiry time down to 1 hour: #5567 (comment) that feels safer to me in case there are any issues for folks during a WP version upgrade.

Actually, that'd probably be my main question: would this cache get automatically flushed when a site is upgraded? It's likely that processing logic will change between releases, for example the fluid typography rules can be tweaked, or there could be additions to this function in the future.

Very cool that adding caching here can improve performance! At first I was wondering if it might introduce a performance hit with the extra calls to wp_get_global_settings() and wp_json_encode... interesting that it's cheaper to do both of them to generate a key than to process compute_style_properties with the provided values 🤔

@ramonjd
Copy link
Member

ramonjd commented Jan 11, 2024

Also thanks for the ping!

Love caching stuff when we can, especially huge buckets of style gloop.

I'd lean on @oandregal's experience and opinion here. I believe he worked on caching theme.json output before. E.g., WordPress/gutenberg#45543

@@ -1934,6 +1934,15 @@ protected static function compute_style_properties( $styles, $settings = array()
return $declarations;
}

// The global settings can include dynamic data related to typography. We need evaluate it so that the cache is invalidated when it changes.
$args = array( func_get_args(), wp_get_global_settings() ); // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue.Changed
Copy link
Member

@oandregal oandregal Jan 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way the theme.json code is organized is as follows:

  1. WP_Theme_JSON: private class that deals with a theme.json structure. It doesn't depend on anything else.
  2. WP_Theme_JSON_Resolver: private class that pulls data (theme.json files, theme.json from database) and merges it together appropriately (get_merged_data). It depends on the WP_Theme_JSON class.
  3. Public API: functions such as wp_get_global_settings, wp_get_global_styles, etc. They use and depend on the two previous private classes.

By using a public function such as wp_get_global_settings in the WP_Theme_JSON class, this code is creating a circular dependency. This code is saying: when processing any theme.json structure, read all existing theme.json (core, blocks, theme, user) first.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback @oandregal. This makes perfect sense. I think we would either need to add the caching to the public function that relies on WP_Theme_JSON::compute_style_properties or find a different approach to invalidation within WP_Theme_JSON.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback @oandregal. I'll have a closer look at this

Copy link
Member

@oandregal oandregal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link

@kamranzafar4343 kamranzafar4343 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks fine to me

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants