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

Fix wp_head performance regression for classic themes #3536

Conversation

felixarntz
Copy link
Member

@felixarntz felixarntz commented Oct 27, 2022

cc @scruffian @oandregal @draganescu

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


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.

@spacedmonkey spacedmonkey self-requested a review November 1, 2022 16:32
@spacedmonkey
Copy link
Member

cc @oandregal

@oandregal
Copy link
Member

oandregal commented Nov 2, 2022

👋 I've seen the changes and this is what I can share:

  • get_core_data: this data needs to be available both for classic and block themes. It loads the default editor configuration (for example, the default color palettes)
  • get_block_data: this data is necessary for both classic and block themes. It loads style information from blocks.
  • get_theme_data: this data is required for both classic and block themes. For classic, it takes it from the theme supports and returns it as if it was a theme.json. There may be some opportunities to micro-optimize here, such as do not load the i18n schema if the theme and its parent do not have a theme.json (we'd need to measure how impactful that single change is, that´d be a nice single PR).
  • get_user_data: this is safe to skip for classic themes. There are edge cases, such as a theme that has theme.json and then it removes it, though I'm inclined to think user styles are safe to ignore in that case. Coincidentally, I've done the same thing in a prototype I'm working on. It can be extracted as a small, focused PR.

Coincidentally, for the area of "theme.json APIs", I'm compiling a list of tasks I find most pressing at WordPress/gutenberg#45171 I'm very optimistic about how those will impact the performance and code quality. For example, I've just published a prototype that reduces the total time of a request from 514.44ms to 343.71ms, a 33% performance increase.

Another thing that it'd be useful is to prepare these PRs directly in Gutenberg. That way, they can be tested widely and bugs will be raised earlier than the WordPress RC cycles, which is always a lot less stressful and give people time to figure out long-term solutions rather than patches for the RC.

@oandregal
Copy link
Member

This PR improves median wp_head execution time from 16.45ms to 11.92ms (~28% faster) for classic themes (concretely tested with Twenty Twenty-One

Felix, would you share the steps you did to test this, so it can be replicated in a variety of environments?

As a side note, it'd be great to track some statistics across WordPress versions and watch its evolution, like we do for the editor (see live at https://codehealth.vercel.app/).

@felixarntz
Copy link
Member Author

@oandregal Thank you for providing additional context on why (parts of) these methods are needed for classic themes as well. Based on what you're saying, I still think there are a number of performance optimizations to make here to avoid running FSE-only logic for classic themes. Here are some of the high-level ideas I'm thinking:

  • In get_core_data(), parsing WP's own theme.json is a rather wasteful implementation in itself, more so with the additional translation parsing. Since this file is shipped by core (and thus never changes between core updates), we should find a more efficient solution. For example, how about we generate a fully PHP-ready parsed version including i18n strings of WP's theme.json at build time? I see how theme.json is certainly more portable than the same data in PHP array format, but we could create an autogenerated version of that file, like a theme-json.php file that can then simply be required by PHP, no JSON parsing and i18n schema parsing necessary at runtime.
  • In get_block_data(), everything looks fine as is to me.
  • In get_theme_data(), we should simply not parse the i18n schema and try to parse the theme's theme.json if it doesn't exist, like you're saying and like this PR already does.
  • get_user_data() can bail early if in a classic theme, also like you're saying and like this PR already does.

I'll make some updates to the proposed code here accordingly, the main thing though that requires some more work would be to generate some version of a theme-json.php file as part of the build process. If we build something like that, we could even share it with e.g. theme developers so they can run it on their theme.json files too.

Felix, would you share the steps you did to test this, so it can be replicated in a variety of environments?

It was a fairly basic process, here's what I did:

  • Modify WP core's wp_head() function directly, to measure microtime( true ) before the do_action( 'wp_head' ) call and after it, then get the difference to know how long the action took.
  • I did this with trunk and with this PR branch, using a site with Twenty Twenty-One on just the regular basic "Hello world" singular post URL.
  • I ran both tests 9 times each (to account for variance) and calculated the median.

If we wanted to dive in more deeply, it would also be interesting to see what the difference would be on a more complex piece of content, e.g. the Gutenberg demo page.

@felixarntz
Copy link
Member Author

@oandregal I've pushed some updates here as mentioned above, please have a look. I have included what the theme-json.php file could look like in principle, although of course this would need to be auto-generated by a build process eventually.

One additional concern/question I have: Why is the condition around whether to reuse the previously "cached" theme data only present around parsing the (child) theme's theme.json (see https://github.com/WordPress/wordpress-develop/pull/3536/files#diff-d6b86476eed058e7cf8b6e57fa52c4fd75b1f0907e1a9ccb0149528a24f7578bR254)? Are there any problems if we change it to apply to the entire method, similar to how it is for the other get_*_data() methods? Otherwise some of that logic is still run on every single method call over and over again. cc @hellofromtonya

@spacedmonkey
Copy link
Member

This PR is heavily related - #3556.

@spacedmonkey
Copy link
Member

This is hard to code review the code to generate theme-php.php, fix the unit test and coding standards.

@felixarntz
Copy link
Member Author

@spacedmonkey

This is hard to code review the code to generate theme-php.php, fix the unit test and coding standards.

No point to review the theme-json.php file, as eventually it will be auto-generated. There's also no point in fixing the WPCS etc violations in that file - see the TODO comment and the issue comments above outlining that. :)

I'll work on further polishing here today.

@draganescu @hellofromtonya Any chance I could get your thoughts on the changes and proposed approach from this draft PR? Please make sure to review my above issue comments for context.

@spacedmonkey
Copy link
Member

No point to review the theme-json.php file, as eventually it will be auto-generated. There's also no point in fixing the WPCS etc violations in that file - see the TODO comment and the issue comments above outlining that. :)

I don't think we should merge this without the automatic generation code, in JS grunt.

I think we should consider making this out to another PR, where we have more time to review / test.

@felixarntz
Copy link
Member Author

@spacedmonkey

I don't think we should merge this without the automatic generation code, in JS grunt.

That's exactly what I'm saying.

I think we should consider making this out to another PR, where we have more time to review / test.

Sounds good, I've opened WordPress/gutenberg#45616 for that.

Will remove the theme-json.php part from here and get this PR ready for 6.1.1.

@felixarntz felixarntz marked this pull request as ready for review November 9, 2022 01:04
@felixarntz
Copy link
Member Author

@spacedmonkey @draganescu @hellofromtonya This PR is now ready for a full review, including test coverage. Would be great to get your feedback so that we can aim to get this into 6.1.1.

@@ -582,8 +593,8 @@ public static function get_user_global_styles_post_id() {
public static function theme_has_support() {
if ( ! isset( static::$theme_has_support ) ) {
static::$theme_has_support = (
is_readable( static::get_file_path_from_theme( 'theme.json' ) ) ||
is_readable( static::get_file_path_from_theme( 'theme.json', true ) )
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.

This is nice, get_file_path_from_theme already calls is_readable.

Copy link
Contributor

@peterwilsoncc peterwilsoncc left a comment

Choose a reason for hiding this comment

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

Added a fwe notes inline.

tests/phpunit/tests/theme/wpThemeJsonResolver.php Outdated Show resolved Hide resolved
tests/phpunit/tests/theme/wpThemeJsonResolver.php Outdated Show resolved Hide resolved
tests/phpunit/tests/theme/wpThemeJsonResolver.php Outdated Show resolved Hide resolved
Comment on lines +596 to +597
static::get_file_path_from_theme( 'theme.json' ) !== '' ||
static::get_file_path_from_theme( 'theme.json', true ) !== ''
Copy link
Contributor

Choose a reason for hiding this comment

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

Yoda

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 don't think this is needed here. Yoda conditions are only required around variable to value comparisons, not with functions (since setting a function to another value would result in an error anyway).

src/wp-includes/class-wp-theme-json-resolver.php Outdated Show resolved Hide resolved
src/wp-includes/class-wp-theme-json-resolver.php Outdated Show resolved Hide resolved
@@ -400,6 +408,9 @@ private static function remove_json_comments( $array ) {
* @return array Custom Post Type for the user's origin config.
*/
public static function get_user_data_from_wp_global_styles( $theme, $create_post = false, $post_status_filter = array( 'publish' ) ) {
if ( ! static::theme_has_support() ) {
Copy link
Member

Choose a reason for hiding this comment

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

The only edge case I can think of is a theme that uses theme.json and removes it in a later version. In that case, this changes the behavior: the code will now ignore user data while it was used before.

Copy link
Member

Choose a reason for hiding this comment

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

YES! Love this change.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's a fair point. However, I would consider this indeed an edge-case which I don't think is worth addressing here. If a theme removed its theme.json it would come with a whole lot of other behavioral changes anyway - I don't think realistically any theme author is going to change a block theme to become a classic theme.

@oandregal
Copy link
Member

👋 I've run some local profiling tests using TwentyTwentyOne. My test was loading the "hello world" post as a logged-out user. These are the numbers (median of 9 tries): it takes 393.98ms with this request and 411.04ms without these changes. This improves 17.06ms, a 4.15% improvement for that theme.

@oandregal
Copy link
Member

oandregal commented Nov 9, 2022

This improves 17.06ms, a 4.15% improvement for that theme.

For reference, block themes also improve in 12.47ms.

This PR does a few changes:

1. remove is_readable for theme.json check (both themes with and without theme.json)
2. avoid looking up the parent if the theme data is already set (both themes with and withouth theme.json)
3. remove one query for themes without theme.json
4. do not load the translation for themes with no theme.json

I've done my testing with TwentyTwentyOne and TwentyTwentyThree (none is a child theme). Given the changes and the numbers are so close (-12ms vs -17ms for themes with and without theme.json), it sounds like the most impactful change was actually 1 (the only code that both themes have in common in my testing). Removing the query for classic themes is a 5ms improvement at best. I'm time-bounded at the moment and could not dig deeper. My impression is that the results are not lined up with the initial expectations (we are doing more work for classic themes). Would that be a fair statement?

I'm a bit torn about whether this is worth doing. It'd be great if we could see if only applying the changes at 1 and 2 brings the same amount of improvement. If that's the case I'd remove all the other changes, as they make the code harder to read and would not bring a gain worth pursuing.

As I said, I won't have much time to look into this in the following days, though it looks like a safe change from a code point of view. Hope this is helpful.

@oandregal
Copy link
Member

oandregal commented Nov 9, 2022

I also want to share the two things that I consider more impactful at the moment to focus on:

  1. Review and cache the public API methods. An initial investigation I did resulted in this PR, which provides a 200ms reduction, a 33% improvement. Every ms counts, so squeezing all of them at the maximum is always helpful. Though there are so many things to work on that I think it'd be helpful to look at performance strategically: intervene for the bigger wins first, then look at the smaller bits. I'll continue to look into this more when I'm back at full speed.

  2. Having an automated way to look at numbers and define what's important for us. At the moment, everyone is tracking things manually, and it takes a lot of time from the PR author and from the reviewers. It's also a bit inconsistent: should we measure the full time-to-request as logged-out users or is it best tracking particular actions such as wp_head? Having a common understanding of what to improve and tracking automatically instead of manually is a huge win that would help us to move forward faster. Unfortunately, I don't think I'll be able to help in this area much. It'd be lovely if people with more experience and background in setting these things up would take the lead.

@spacedmonkey
Copy link
Member

In trunk
Screenshot 2022-11-09 at 10 50 57
In this branch.
Screenshot 2022-11-09 at 10 50 13

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.

PR works. It doesn't solve all the issues in wp_head, but it a great start

@felixarntz
Copy link
Member Author

@oandregal Thank you for running additional performance benchmarks on the latest code here! This is super helpful.

I've also run the latest iteration of the PR through the same methodology that I originally used, and posted results in an updated PR description. Of course the benefit is much less than originally, now that I've broken out fixing the core theme.json parsing piece into WordPress/gutenberg#45616. Nevertheless, at a ~10% wp_head performance improvement I think this is a worthwhile change especially as they look straightforward from a code perspective - baby steps so to speak, but together they'll add up eventually, and doing the work here is particularly relevant to allow for the above Gutenberg issue to actually have the intended impact.

@felixarntz felixarntz requested review from peterwilsoncc and removed request for eugene-manuilov November 9, 2022 23:40
Copy link
Contributor

@peterwilsoncc peterwilsoncc left a comment

Choose a reason for hiding this comment

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

LGTM with a nit pick inline

src/wp-includes/class-wp-theme-json-resolver.php Outdated Show resolved Hide resolved
Co-authored-by: Peter Wilson <519727+peterwilsoncc@users.noreply.github.com>
@felixarntz
Copy link
Member Author

@felixarntz felixarntz closed this Nov 10, 2022
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.

5 participants