Problem/Motivation

See #2368797: Optimize ajaxPageState to keep Drupal 8 sites fast on high-latency networks, prevent CSS/JS aggregation from taking down sites and use HTTP GET for AJAX requests, #49.5 and #50.5.

Also, see #11:

I have multiple #lazy_builder-powered elements on the page which use #attached property. It results in the numerous libraries dependencies re-calculation.

Steps to reproduce:

  1. Install fresh 9x-dev Drupal;
  2. Enable forum module;
  3. Created some test node and add 50+ comments;
  4. Open a page as an admin who has both edit and delete links (so they are lazy-built);
  5. Run the profiler before and after applying the patch;

To summarize: dependency calculation is expensive in some cases. Let's make it better.

Proposed resolution

  • Replace in_array with cheaper isset;
  • Add static cache to the getLibrariesWithDependencies method;

Here we can see the results after applying the proposed changes:

Before (Blackfire profile) After (Blackfire profile)
callstack (before) callstack (after)

The win is at least4% of the page generation time, but it can be more for complex websites setups with more libraries 😋

Remaining tasks

  • Investigate
  • Review
  • Commit

User interface changes

None.

API changes

None.

Issue fork drupal-2407187

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Wim Leers’s picture

Status: Postponed » Active

The parent issue landed, we can work on this now.

Wim Leers’s picture

Version: 8.0.x-dev » 8.1.x-dev

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.0-beta1 was released on March 2, 2016, which means new developments and disruptive changes should now be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

artem_sylchuk’s picture

Hi guys, during the investigation of the possible performance optimizations in my current project I found that the most called and time-consuming function is the "in_array" call coming from LibraryDependencyResolver. I have a multiple #lazy_builder-powered elements on the page which use #attached property. It results in the numerous libraries dependencies re-calculation. I've replaced in_array with cheaper isset calls, plus added static cache to the getLibrariesWithDependencies method.

In my case it allowed to cut down the page generation time from 5.3s to 3.66s according to the blackfire.

abramm’s picture

Hi @james_kerrigan,

I've looked through the patch quickly and noticed you're using a static variable inside getLibrariesWithDependencies().
It'd be better to replace it with an object property.

artem_sylchuk’s picture

artem_sylchuk’s picture

Status: Needs review » Needs work

Seems it breaks the aggregation.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

artem_sylchuk’s picture

I investigated the reasons why my changes were breaking the aggregation and found that it was because of the changed order of libraries in getLibrariesWithDependencies. Fixed in a new patch and works perfectly for me.

Status: Needs review » Needs work

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

artem_sylchuk’s picture

As a part of #ContributionWeekend2021 I decided to finish with this issue.
I checked failing tests and they expected numeric keys in arrays.
But in order to get rid of heavy array_search calls I made keys identical to the values: they both hold the library name now.
It allowed to use much cheaper isset call. So I updated tests to check for the library name as a key.

For the test I installed fresh 9x-dev drupal core, enabled the forum module, created some test node and added 50 comments.
I opened page as an admin who sees the edit and delete links (that are lazy-built).
Here are results before the patch: https://blackfire.io/profiles/bba7cc60-1c7d-4cad-9941-81247a410a2f/graph
And after: https://blackfire.io/profiles/f72f7d6d-fcee-4074-b02f-9c06787f3cea/graph
Win is around 10% of the page generation time.

Before:
before

After:
after

Matroskeen made their first commit to this issue’s fork.

Matroskeen’s picture

Title: Investigate whether the algorithm in LibraryDependencyResolver::getMinimalRepresentativeSubset() can be optimized » Optimize LibraryDependencyResolver::getMinimalRepresentativeSubset() and win ~10%
Issue summary: View changes

The reports look good to me and the numbers are really impressive!

Although I'm not familiar enough with the asset library system to review the code, I'd like to make it easier for other reviewers.
Therefore, I opened a merge request based on the latest patch and compiled issues comments into an updated issue summary.

artem_sylchuk’s picture

Title: Optimize LibraryDependencyResolver::getMinimalRepresentativeSubset() and win ~10% » Optimize LibraryDependencyResolver::getMinimalRepresentativeSubset() and win ~23%
Issue summary: View changes
Wim Leers’s picture

YAY! Somebody started working on this optimization half a decade after I proposed it! 🥳😄

Thank you for your awesome work and especially your very convincing profiling screenshots 🤩

--- a/core/tests/Drupal/Tests/Core/Asset/LibraryDependencyResolverTest.php
+++ b/core/tests/Drupal/Tests/Core/Asset/LibraryDependencyResolverTest.php

AFAICT the changes here are merely reformatting the code.

Is that right?

Because right now it seems as if you're modifying the test coverage.

Making this change without modifying the test coverage is much less risky.

Can you please revert these changes? 😊🙏

Wim Leers’s picture

Priority: Normal » Major

Bumping to Major based on the 23% and >1 second difference you are observing. I suspect it's a pretty extreme case, but in that case it definitely is hugely important. And it won't hurt any other Drupal site either obviously 😄

artem_sylchuk’s picture

Hi, that's not only code re-format, the tests were updated too, unfortunately :(
In order to get rid of the in_array call I had to replace numeric keys with the library name.
As far as I remember I couldn't do just some array_flip and leave keys untouched.
I'll try again to check if it is possible to do the same without touching the tests.

longwave made their first commit to this issue’s fork.

longwave’s picture

I opened an alternate merge request that keeps the return values from getLibrariesWithDependencies() the same numeric array as before, and that means the tests don't have to change.

artem_sylchuk’s picture

Hey @longwave, looks like you've done what I was going to do, thanks! Looks much better now

Matroskeen’s picture

Awesome!

It means we can close merge request !327 in favor of merge request !342

And now we're looking for another review 😇

Berdir’s picture

I've definitely noticed library/css processing showing up in profiling as well. Just a note, be careful to manage expectations here. Many calls to small, fast functions tend to be *massively* overreported (easily 5x or more, especially if xdebug is also enabled as the warning icon in the screenshots in the issue summary indicate) in blackfire and similar tools. To have realistic numbers on how this improves things, try to have xdebug and blackfire extensions disabled and test with a simple benchmarking tool like ab.

Will try to do a review.

andypost’s picture

Additionally to #33 I suggest to check https://github.com/tideways/php-xhprof-extension which adopted new Observer API for php8 so it should not bring such viable overhead

jchristi’s picture

Perhaps also have a look at eBPF-based profiling: https://github.com/iovisor/bcc

andypost’s picture

Wim Leers’s picture

Will try to do a review.

🥳

Berdir’s picture

Tested this quickly

* Made sure xdebug and blackfire extensions are disabled.
* Created an article
* Generated 50 commits for it
* Running 50 ab requests with my uid 1 session cookie: ab -H 'Cookie: SESS....=....' -n 50 http://d8/node/1

JS/CSS aggregation enabled, render/page caches disabled:
Without the patch: 3 tries, 125-128ms
With the patch: 3 tries, 122-123ms

JS/CSS aggregation enabled, render/page caches enabled, warm cache:

Without the patch: 3 tries, 55ms
With the patch: 3 tries, 51-53ms

So, clearly an improvement, but also less than what the issue title currently claims. That is with the latest patch/MR. It is possible that the other implementation was slightly more efficient, I don't know, didn't look at either MR.

I also compared with blackfire, there it is 150ms to 134ms (so xhprof is ~3 slower). That's 11%. If xdebug was enabled as I suspect from the initial screenshots then that might just blow it up to 20%+.

It is also possible that the gain would improve on more complex sites, with more libraries.

Berdir’s picture

FileSize
54.35 KB

Forgot the blackfire diff of the two runs.

artem_sylchuk’s picture

Thank you for your review @Berdir
Seems I was too excited to see results in the balckfire that I didn't notice that I was running it with xdebug enabled.
So, looks like the real improvement is much smaller than I claimed initially.
Probably that should be reflected in the issue title.

In my case I had 200 comments on the page, each of them had additional element using #lazy_builder, so it resulted in 400+ bigpipe requests. Patch helped a lot for me, but anyway I ended up completely rewriting the comments links and used singe #lazy_builder that updates drupalSettings with links data used by custom script that places links on the page. Not sure if it is ok that it is so easy to get so many lazy-built elements on the page.

Wim Leers’s picture

Title: Optimize LibraryDependencyResolver::getMinimalRepresentativeSubset() and win ~23% » Optimize LibraryDependencyResolver::getMinimalRepresentativeSubset() and win ~4% (simple page) to ~23% (complex page)

Thanks, @Berdir!

In my case I had 200 comments on the page, each of them had additional element using #lazy_builder, so it resulted in 400+ bigpipe requests

Aha, that is indeed a pretty exceptional scenario, and probably explains the difference. Perhaps it's also a site with relatively many asset libraries? I think that @Berdir tested with bartik (can you confirm, @Berdir?).

Probably that should be reflected in the issue title.

What do you think about the change I made in this comment, @artem_sylchuk & @Berdir?

artem_sylchuk’s picture

Sorry for the confusion,
The 23% from the screenshots difference was caused by running blackfire with xdebug enabled.
Those screenshots were taken on clean drupal installation with forum module + 50 comments on the page.

In my case I had 200 comments on the page

Here I've meant the project where I faced the issue for the first time and it forced me to look for any possible optimisation.
I don't have any profiler runs remained from that time, so hard to say how big the performance improvement was.

Wim Leers’s picture

Title: Optimize LibraryDependencyResolver::getMinimalRepresentativeSubset() and win ~4% (simple page) to ~23% (complex page) » Optimize LibraryDependencyResolver::getMinimalRepresentativeSubset() and win >=4%

No worries!

How about this simpler claim then? 😊

artem_sylchuk’s picture

Sounds good for me, thanks! :)

artem_sylchuk’s picture

Even the performance win isn't so big probably this still can be marked as RTBC?
The patch should give better results on the web-sites with a lot of libraries and wide use of the lazy-built elements.

Matroskeen’s picture

Issue summary: View changes
Status: Needs review » Reviewed & tested by the community

So we have proof of performance win and a green merge request.
I can't see any remaining items, so I assume this is ready to go!

  • catch committed 3e64338 on 9.2.x
    Issue #2407187 by artem_sylchuk, longwave, Matroskeen, Berdir, Wim Leers...

  • catch committed 97f33ac on 9.1.x
    Issue #2407187 by artem_sylchuk, longwave, Matroskeen, Berdir, Wim Leers...
catch’s picture

Version: 9.2.x-dev » 9.1.x-dev
Status: Reviewed & tested by the community » Fixed

Code looks solid and the numbers are encouraging.

Committed/pushed to 9.2.x and cherry-picked to 9.1.x, thanks!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.