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

WP_Theme_JSON: Invalidate blocks metadata cache when needed #3359

Conversation

ockham
Copy link
Contributor

@ockham ockham commented Sep 28, 2022

Tentative fix for 56644. Alternative to #3352.

Props @oandregal @andrewserong @talldan for tracking this down in WordPress/gutenberg#44434.

The essential conclusion from WordPress/gutenberg#44434 is that blocks metadata is cached upon reading. However, since it's read during registration of the Template Part block, metadata is never updated after that block is registered, and thus absent for a whole slew of blocks. OTOH, Global Styles uses a sanitization mechanism that, among other things, verifies that the block that a style is applied to is actually registered. This means that styles are stripped from all blocks that happen to be registered after Template Part.

The solution I'm proposing is to make WP_Theme_JSON::get_blocks_metadata()'s caching a bit smarter, i.e. by checking if there are any blocks that have been registered for which no blocks metadata exists in cache, and to add that missing metadata.

Testing Instructions

Step-by-step reproduction instructions

(based on WordPress/gutenberg#44434, originally from https://core.trac.wordpress.org/ticket/56644).

  1. Go to the site editor.
  2. Insert a button.
  3. Open the global styles sidebar.
  4. Open the blocks section.
  5. Navigate to the button block.
  6. Apply a background color.
  7. Save the global styles.
  8. Verify that the background color persist. On trunk, it would be discarded on Save.

Note that this PR doesn't fix WordPress/gutenberg#44619, which, while closely related, isn't the same issue. (Some of the comments on this PR refer to that issue; I've opted to tackle it separately, see WordPress/gutenberg#44434 (comment).)

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


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.

@ockham ockham self-assigned this Sep 28, 2022
Copy link
Contributor

@andrewserong andrewserong 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 working on this one @ockham, this feels much closer to an ideal fix! It largely resolves being able to save global styles in the site editor, but I'm still encountering the issue of the Button block styling not working in TwentyTwentyTwo — so I'm wondering if there's another level of cache clearing that we need to implement to get that working, too. With this PR applied, note that the custom background colour on the Site Title block is working, however the Button blocks only have their default element styling, not the green / zero border radius styling for the button in the theme's theme.json file:

image

In #3352, I found I needed to clear the resolver cache, too, so I wonder if we need to update the resolver's caching logic in a similar way as you've done here in the Theme JSON class?

@ockham
Copy link
Contributor Author

ockham commented Sep 29, 2022

Ah, good spot, @andrewserong! Turns out I only tested for user modifications (setting the background color of the button block in the Global Styles menu) but forgot to test for theme-supplied styling 😅

Anyway, this part might be harder to fix 😕 I think that the problem actually also goes back to WP_Theme_JSON (via WP_Theme_JSON_Resolver::get_theme_data). In WP_Theme_JSON's constructor, we set $this->theme_json after sanitizing the theme.json contents based on the list of known -- i.e. currently registered -- blocks:

$valid_block_names = array_keys( static::get_blocks_metadata() );
$valid_element_names = array_keys( static::ELEMENTS );
$theme_json = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names );
$this->theme_json = static::maybe_opt_in_into_settings( $theme_json );

$this->theme_json is then read frequently across that file; and it's never updated to take into account that the list of registered blocks might've changed.

@ockham
Copy link
Contributor Author

ockham commented Sep 29, 2022

Inside WP_Theme_JSON_Resolver::get_theme_data, we could probably do something around here:

Akin to the changes I made to WP_Theme_JSON, we'd need to check if more blocks have been registered. Unfortunately, that's probably harder here, since AFAICS, we don't have access to the list of blocks that were registered at the time that the WP_Theme_JSON object was created (ironically, this is partly a consequence of the fix to WP_Theme_JSON::get_blocks_metadata, which would otherwise give us that 😅 ).

@ockham
Copy link
Contributor Author

ockham commented Sep 29, 2022

Yeah, so I haven't found a good way to smartly update the resolver's caching logic.

So I've tentatively pushed b746d24 which I believe fixes the issue -- by removing the caching altogether for the theme-supplied styling and settings 😬 I'm not sure how big the performance impact would be.

I'll be AFK on Friday and Monday. @andrewserong would you mind taking the lead on this one (so we'll have a fix ready for WP 6.1 Beta 3 on Tuesday)? I guess we basically need to decide between this solution and #3352 (which would require some more action on the Gutenberg side, see #3352 (comment)).

Maybe folks familiar with Global Styles can weigh in on both solutions (and this PR's performance impact). cc/ @oandregal @talldan @jorgefilipecosta 😊

@andrewserong
Copy link
Contributor

Thanks for all the digging here, @ockham!

I'll be AFK on Friday and Monday. @andrewserong would you mind taking the lead on this one (so we'll have a fix ready for WP 6.1 Beta 3 on Tuesday)?

Yes, I'm happy to dig into this one today and on Monday and try to figure out what would be the best trade-off. It'd be great to get a fix in for Beta 3, whichever direction we do go in. I'll give this PR a test and do some more exploration 👍

@andrewserong
Copy link
Contributor

Update: I've looked into this a bit further, and I think we might need to add in a variable to keep track of the number of blocks that have been registered to fix the resolver — or to put it differently, I think we'll achieve more reliable results for this particular caching if we generate a cache key and store it in a static variable.

I've opened up #3373 as an alternative — basically, taking the same general idea as this PR, but attempting to largely preserve the caching if the number of blocks hasn't changed. This appears to be necessary because when testing this PR, it seems that by removing the resolver caching, read_json_file winds up being called 136 times on a single load of a test site running TwentyTwentyTwo theme.

Because we need to keep track of the number of blocks registered, I thought it might make for a good parallel between the two classes for them to share a similar approach to cache busting. It also gives us a function where we can keep the cache key generation logic explicit, to hopefully aid future debugging if need be. There's definitely more we could do to improve the caching behaviour, but so far #3373 seems to resolve the issue while largely preserving the caching as far as I can tell.

Happy for any feedback if folks have time to take a look at it!

$parent_theme->merge( static::$theme );
static::$theme = $parent_theme;
}
$theme_json_data = static::read_json_file( static::get_file_path_from_theme( 'theme.json' ) );
Copy link
Member

Choose a reason for hiding this comment

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

What if we only cached the result of reading the data from the filesystem/database for every source. That's perhaps the most impactful thing we do here in terms of performance.

Copy link
Member

Choose a reason for hiding this comment

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

The other sources may suffer the same issue (be loaded before blocks are registered).

Comment on lines 734 to 744

if ( null === static::$blocks_metadata ) {
static::$blocks_metadata = array();
} else {
// Do we have metadata for all currently registered blocks?
$blocks = array_diff_key( $blocks, static::$blocks_metadata );
if ( count( $blocks ) === 0 ) {
return static::$blocks_metadata;
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adding an explanation I posted over at WordPress/gutenberg#44658 (comment) to clarify how this works:

This mostly leverages PHP's array_diff_key: It looks at the list of currently registered $blocks and compares its keys (which are block names, e.g. core/heading) to those of the list of blocks metadata -- which are also keyed by block name. It then returns the subset of registered $blocks whose keys aren't in static::$blocks_metadata. If that subset is empty, we return the cached blocks metadata, as we can assume that we have metadata for all registered blocks already.

Otherwise, we add metadata -- for that subset of registered blocks only that doesn't have them yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Importantly, this means that cache invalidation is not based on the number of blocks, but indeed on which blocks have been registered.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In #3373, @ajlende noted that

Unfortunately we can't rely on only the registered block names because there is at least one instance where we unregister an old version of a block and re-register a new one with the same name within the same function.

#3373 (comment)

tl;dr: I think that in practice, the list of registered block names is a good enough indicator for caching.

The example that's present in Core, register_legacy_post_comments_block, is basically used to replace occurrences of the legacy Post Comments block with its successor (the Comments block), in "legacy" (compatibility) mode.

However, while this logic was relevant to have in the Gutenberg plugin while Core still included the legacy Post Comments block, the latter is no longer the case -- it has been scrapped from Core per #3154. So the unregistering logic will never kick in.

And even if it did, the metadata we're using to (re-)register it are the same that the Post Comments block was using.

This brings us to the bigger question: Can this situation -- a block is registered, then unregistered, and then re-registered under the same name but with different metadata -- happen in principle? I think the chance is fairly low. While we've seen one precedent of block re-registration in Core (where the functionality of one block was absorbed into another, but the original block hat to be retained for back-compat), this is something that isn't going to happen very often; and if it is, we're probably going to use the same metadata to register it.

I think the same would apply to plugins. I don't think there are a lot of plugins that will register a block only to un-register and then re-register it; what's more conceivable is that a different plugin does the un-registration and re-registration bit. (Generally, I'd expect that other plugin to be from the same author -- e.g. something like WooCommerce Blocks which can be used "on top of" WooCommerce. Otherwise, it's debatable if a plugin should really touch another plugin's block namespace that way at all.)

Re-registering blocks from a different author or even Core seems so risky that I don't think we need to provision for that case: That existing block's attributes (and other metadata) basically define the block's "interface", so any replacement would have to keep that intact. This doesn't seem to leave a lot of conceivable use cases.

@ockham
Copy link
Contributor Author

ockham commented Oct 4, 2022

Based on WordPress/gutenberg#44434 (comment) (which is in turn based on the concerns expressed in #3359 (comment)), I'll revert the commit that disables caching for WP_Theme_JSON_Resolver::get_theme_data.

Copy link
Contributor

@cbravobernal cbravobernal left a comment

Choose a reason for hiding this comment

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

It works as expected! LGTM

ockham and others added 2 commits October 4, 2022 14:59
Co-authored-by: Tonya Mork <tonya.mork@automattic.com>
Co-authored-by: Tonya Mork <tonya.mork@automattic.com>
@ockham
Copy link
Contributor Author

ockham commented Oct 4, 2022

Thank you @hellofromtonya! I've applied your suggestions 😊

Copy link
Contributor

@dream-encode dream-encode left a comment

Choose a reason for hiding this comment

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

This looks OK. One suggestion.

Co-authored-by: David B <dream-encode@users.noreply.github.com>
@dream-encode
Copy link
Contributor

Merged into core in https://core.trac.wordpress.org/changeset/54385.

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

Successfully merging this pull request may close these issues.

[WP 6.1] Block Styles in theme.json do not appear in the Editor or Frontend
6 participants