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

Use a cache for block templates and block template parts content #5463

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

Conversation

felixarntz
Copy link
Member

Using a cache for these file content lookups results in a major performance boost for block themes. Here are some Server-Timing benchmarks, using 100 runs with the benchmark-server-timing tool (with the transient cache populated):

  • TT4 theme: 95.31ms (PR) vs 104.89ms (trunk) → 9.1% faster
  • TT3 theme: 72.8ms (PR) vs 80.37ms (trunk) → 9.4% faster
  • TT2 theme: 72.21ms (PR) vs 79.31ms (trunk) → 9.0% faster
  • testing with a few third-party block themes:
    • Frost theme: 71.13ms (PR) vs 75.9ms (trunk) → 6.3% faster
    • Neve theme: 94.97ms (PR) vs 102.74ms (trunk) → 7.6% faster
    • Ollie theme: 99.52ms (PR) vs 105.31ms (trunk) → 5.5% faster

The approach used here has already been established for block patterns (see https://core.trac.wordpress.org/ticket/59490 / https://core.trac.wordpress.org/changeset/56765), and it makes sense to apply it here as well.

Trac ticket: https://core.trac.wordpress.org/ticket/59600


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

@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.

This seems like a good thing to do. I wonder if we would be better off in the long run saving these as Post Types rather than transients, to be consistent with the way these files get saved if they are manually edited in the site editor. Definitely out of scope for this fix however.

Ideally, I think we should add some intentional expiry to these transients to ensure they get cleaned up during transient garbage collection. We should also try to set them to not autoload, since we don't need all of this data on every pageload, only the template data that is in play for that pageload.

I'm also wondering if there is a way we can incorporate these enhancements with _get_block_templates_files, which is a previously noted performance botte neck (see https://core.trac.wordpress.org/ticket/58196#comment:26)

$theme_dir = wp_normalize_path( $theme->get_stylesheet_directory() );

if ( str_starts_with( $template_file_path, $theme_dir ) ) {
$relative_path = substr( $template_file_path, strlen( $theme_dir ) );
Copy link
Member

Choose a reason for hiding this comment

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

Could use str_replace() here and below, similarly to how we're doing it in register_core_block_style_handles():

Suggested change
$relative_path = substr( $template_file_path, strlen( $theme_dir ) );
$relative_path = str_replace( $theme_dir, '', $template_file_path );

Copy link
Member Author

Choose a reason for hiding this comment

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

For this purpose, str_replace() is less precise than substr(), since we want to replace the beginning of the string only, not the partial path wherever it is included in the string.

This is obviously an edge case consideration, but has been pointed out to me in the past, and it makes sense. We shouldn't use str_replace() if we want to only replace a single specific instance of the substring.

Copy link
Member

Choose a reason for hiding this comment

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

Hmmm...even so, if $theme_dir is not at the beginning of the string, then the substr() function wouldn't remove what we intend to, which could be more confusing. You already have this inside an if statement that assures that $template_file_path starts with $theme_dir, so it really seems like both approaches are the same, with no possibility for edge cases to occur.

Copy link
Member Author

Choose a reason for hiding this comment

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

@joemcgill Let me clarify what I mean:

  • str_starts_with( '/dirname/dirname', '/dirname' ) results in true
  • substr( '/dirname/dirname', strlen( '/dirname' ) ) results in the expected /dirname
  • str_replace( '/dirname', '', '/dirname/dirname' ) results in '', which would be incorrect

Obviously this is not realistic to happen here since a file wouldn't be within a directory within the theme that has the same folder path as the overall theme directory. But if anything, it's safer to use substr() here over str_replace() to prevent it. IMO str_replace() is less precise since the intention here is to replace the instance of $theme that the string starts with, not any instance of it within the string.

Anyway, this is a nit-pick discussion 😂

src/wp-includes/block-template-utils.php Outdated Show resolved Hide resolved
@felixarntz
Copy link
Member Author

@joemcgill

Ideally, I think we should add some intentional expiry to these transients to ensure they get cleaned up during transient garbage collection. We should also try to set them to not autoload, since we don't need all of this data on every pageload, only the template data that is in play for that pageload.

Good catch! Rather than using an expiry though, which isn't needed given the name won't change (the version isn't part of the transient name), I think we should follow the same approach that is used for the recently introduced theme pattern cache (see https://core.trac.wordpress.org/changeset/56765): When switching the theme, delete the caches for the relevant themes. This will ensure a maximum of 2 of those transients are present (and autoloaded). I have added this in ad18c94.

I'm also wondering if there is a way we can incorporate these enhancements with _get_block_templates_files, which is a previously noted performance botte neck (see https://core.trac.wordpress.org/ticket/58196#comment:26)

This PR is actually already benefitting block templates: _get_block_templates_files() itself doesn't retrieve the file contents, but the only place it is used is get_block_templates(), and that function calls _build_block_template_result_from_file() for every block template, which is the function enhanced here. So the caching benefits retrieving both block templates and block template parts.

@felixarntz felixarntz marked this pull request as ready for review October 11, 2023 22:44
Copy link
Contributor

@costdev costdev 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 the ping @felixarntz! A few thoughts for consideration, but no blockers that I see. Nice job with the test coverage! 👍

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.

I don't like this PR. I think is really problematic.

This would be a breaking change. This would REQUIRE users to enable development mode. This would not allow to upload new files to the server and for those to read.

I agree, we need to cache the file_get_contents, but this change would break a lot of people's sites that use FTP.

I am going to put together a PR with my idea of how we can fix this issue.

@spacedmonkey
Copy link
Member

Just to reflect a conversion on slack. I am strongly against this change being merged into core in the WP 6.4 cycle. Caching the file_get_content has clear performance benefits, but this is not a bug fix, it is an enhancement. This should have it's own ticket and big discussed there. If this is every committed, it needs to be committed early in a release cycle, so it get tested. But at the moment, there is no cache invalidation, for files being uploaded via FTP. Uploading files via FTP is how many developers work on client sites. I didn't always update the theme version number when making changes. We can't break that workflow.

@felixarntz
Copy link
Member Author

@spacedmonkey

This would be a breaking change. This would REQUIRE users to enable development mode. This would not allow to upload new files to the server and for those to read.

The behavior here is exactly the same as the pattern caching change that you committed last week after us agreeing on it. I acknowledge that this led to a bug, but the problem of loading non-existing files isn't possible here, since this cache literally caches the file contents. Related to #5462 (comment), we should however ensure that any existing file caches are invalidated as soon as someone sets WP_DEVELOPMENT_MODE to "theme", to prevent stale caches being used once WP_DEVELOPMENT_MODE is turned off again.

Uploading files via FTP is how many developers work on client sites. I didn't always update the theme version number when making changes. We can't break that workflow.

Users that are making changes to a theme (except updating to a new version) are developing the theme and therefore should set WP_DEVELOPMENT_MODE to "theme". This is why that constant was introduced. If we now change our opinion on it, the constant has literally no value. Theme development should happen on development or staging instances. We shouldn't encourage developing on production as that is clearly a problematic pattern. If a developer wants to do that though, they can still set WP_DEVELOPMENT_MODE accordingly.

Caching the file_get_content has clear performance benefits, but this is not a bug fix, it is an enhancement.

Again, this is similar to the pattern caching change that you committed last week. Can you please clarify why you think it is okay to commit that change this late in the cycle, but not the change here, even though they do something similar? I acknowledge that now it is even later in the beta cycle, and maybe it's too late. But the argument of this being an enhancement when the pattern caching change https://core.trac.wordpress.org/ticket/59490 was a bug makes no sense to me.

@spacedmonkey
Copy link
Member

This change is similar to but not exactly same as.
If we are going cache all the contents of files, this would make the transient very big. Those files could have mbs work of markup in them. We are trying to avoid large cached, autoloaded options. Caching large blobs of markup in transient is something I am not sure about.

Are filter run, block parsed and strings translated if we cache like this. Didn't we just have an issue where hooks were not applied? @ockham @gziolo can you give context here.

Again, I will say, adding caching here is a good idea. But we could see a performance benefit from an in memory cache, like in my PR. If there is no breakage from the in memory cache, we could work at making that in memory cache persistent. Going straight to persistent, will likely bring up errors. Doing this so late in the process, is far to risky. I do think it something we can explore in WP 6.5.

@felixarntz
Copy link
Member Author

@spacedmonkey

If we are going cache all the contents of files, this would make the transient very big. Those files could have mbs work of markup in them. We are trying to avoid large cached, autoloaded options. Caching large blobs of markup in transient is something I am not sure about.

That is a fair point. We could add an expiration to the transient and thus not autoload it. That would lead to 1-2 more DB queries in the frontend, but those are way faster than all the file_get_contents() calls, so that would be fine. Using only object cache will leave a lot of sites out of getting the performance benefits.

Are filter run, block parsed and strings translated if we cache like this. Didn't we just have an issue where hooks were not applied?

This change caches only file_get_contents() results, so there is zero chance of any filters being skipped by this caching.

@felixarntz
Copy link
Member Author

@spacedmonkey I pushed two import changes related to your feedback:

  • 4414d8b ensures that existing caches are wiped when theme development mode is enabled, to prevent stale caches from being served if theme development mode is disabled again later.
  • d0f971f adds an expiration to the transient. This ensures periodic refresh, natural expiration over time, as well as prevents the transient from being autoloaded. I benchmarked it, and the additional DB query indeed doesn't matter. DB queries are generally much faster than file system lookups. I see the same improvements as before.

@spacedmonkey
Copy link
Member

@felixarntz Other than a performance improvement, what bug is this fixing again? Why does this need to go in RC?

$template_data['template_content'][ $relative_path ] = file_get_contents( $template_file_path );

// Update the cache.
set_transient( 'wp_theme_template_contents_' . $theme->get_stylesheet(), $template_data, WEEK_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.

This means this will not be autoloaded, is this by design.

Copy link
Member Author

Choose a reason for hiding this comment

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

@spacedmonkey I explained this here: #5463 (comment)

One of your concerns was that these transients may become large and therefore shouldn't be autoloaded. I made this change in response to your feedback.

@@ -520,7 +587,7 @@ function _remove_theme_attribute_from_template_part_block( &$block ) {
*/
function _build_block_template_result_from_file( $template_file, $template_type ) {
$default_template_types = get_default_block_template_types();
$template_content = file_get_contents( $template_file['path'] );
$template_content = _get_block_template_file_content( $template_file['path'] );
Copy link
Member

Choose a reason for hiding this comment

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

Instead of adding caching to to the file get, why don't we just save this data to the post table. This is how block themes work. You have files stored and once you edit them and save changes, it copies the changes to a post. Why not just copy the data to post on activation or first use.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure that that is possible.

Only modified templates and template parts are saved in the posts table. If we do that for every template file in the theme, it will break existing functionality, as it will look like every template has been modified by the user. So that would not be feasible.

@spacedmonkey
Copy link
Member

In my testing here are the size of autoload options after this cache was added.

Theme Size of transient
TT2 5.261 kb
TT3 5.429 kb
TT4 14.876 kb
Frost 5.518 kb
Ollie 22.651 kb

I am worried about these cache growing large, for complex these. Autoloading a large option may worse then file operations.

@spacedmonkey
Copy link
Member

Was it considered, to prime this transient cache on theme activation. Saying by doing a glob on the /templates directory and saving the cache. This way caches would be loaded by a request in admin or wp cli and front end users could have to prime the caches.

@spacedmonkey
Copy link
Member

Added some people that worked on this function before. Hope we can get some more context.

@felixarntz
Copy link
Member Author

@spacedmonkey

In my testing here are the size of autoload options after this cache was added.

The options are no longer autoloaded based on your previous feedback. Please make sure to read my explanations in #5463 (comment).

Was it considered, to prime this transient cache on theme activation. Saying by doing a glob on the /templates directory and saving the cache.

On first thought, I like this idea. However, it wouldn't actually work because of theme updates. We will need to be able to refresh the caches on demand, to cover the scenario of the theme version being different. We can't hook into the update process for that since new theme versions are often deployed in other ways, e.g. Composer, git, FTP.

@joemcgill joemcgill dismissed their stale review October 13, 2023 17:38

No longer applicable

@joemcgill
Copy link
Member

Sorry for the delay in responding here yesterday.

For the record, I'm still a very big fan of the idea of trying to avoid these types of redundant file parsing lookups during template rendering. That said, I think there are a few different approaches to consider here, like saving these files to a post type, different considerations of how transients get cached and loaded, and exploring methods of ensuring that cache busting could be in play for site owners who are directly editing their theme files on their live site 🙈🤠.

To give us time to properly consider and test these scenarios, I think it's best that we consider this along with other template loading enhancements that have been discussed for 6.5 rather than trying to rush this into this release during the final week of the beta period before RC. Ideally, this (and things like it) should benefit from being in place at the start of the beta period.

In the meantime, I'm retracting my original review as no longer applicable, since my original blocking concerns have been addressed.

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.

4 participants