Problem/Motivation

From #2378263-4: hook_library_alter() must be manually invoked by users of LibraryDiscovery, and has no test coverage:

+++ b/core/lib/Drupal/Core/Asset/LibraryDiscovery.php
@@ -22,22 +23,51 @@ class LibraryDiscovery implements LibraryDiscoveryInterface {
-    return $this->collector->get($extension);
+    if (!isset($this->libraryDefinitions[$extension])) {
+      $libraries = $this->collector->get($extension);
+      $this->libraryDefinitions[$extension] = [];
+      foreach ($libraries as $name => $definition) {
+        // Allow modules and themes to dynamically attach request and context
+        // specific data for this library; e.g., localization.
+        $this->moduleHandler->alter('library', $definition, $name);
+        $this->libraryDefinitions[$extension][$name] = $definition;
+      }
+    }
+
+    return $this->libraryDefinitions[$extension];
   }

So we do execute an alter hook for EVERY used library, both on runtime but also when the libraries are recieved.
Given that we will require to use libraries everywhere, we have a problem here.

Proposed resolution

Let's run just one alter hook for ALL used libraries. Therefore the signature of both hook_library_alter() and hook_library_info_alter() has to change.

Remaining tasks

User interface changes

None.

API changes

  • The signature of hook_library_alter(&$libraries, $module) changed to hook_library_alter(&$libraries)
  • The signature of hook_library_info_alter(&$libraries, $module) changed to hook_library_info_alter(&$libraries)
  • LibraryDiscoveryInterface::getLibrariesByExtension() now has the provider as part of the key:
    Before
    getLibrariesByExtension('node') === 
     ['form' => [...]];
    
    After
    getLibrariesByExtension('node') === 
     ['node/form' => [...]];
    

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Task because performance is not necessary a bug
Issue priority Major because its a measureable improvement
Prioritized changes The main goal of this issue is performance.
Disruption Modules implementing hook_library_(info_)_alter() needs to be adapted, but NOT modules using $module.libraries.yml-files and ['#attached']['library']
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Wim Leers’s picture

Issue summary: View changes
dawehner’s picture

Title: Consider not executing hook_library_alter() *per* library, but once for all » Consider not executing hook_library_(info)_alter() *per* library, but once for all

In case we do that, we should do it for both library hooks.

Wim Leers’s picture

Oh, good point. That's more consistent. But note that hook_library_info_alter() is only called upon a cache clear; its results are cached across page loads, so there is no performance cost there.

Berdir’s picture

Afaik, libraries are discovered when requested, by module and not all together? That's makes it impossible to implement a single alter hook for hook_library_info_alter() unless we change that pattern?

Wim Leers’s picture

That's makes it impossible to implement a single alter hook for hook_library_info_alter() unless we change that pattern?

I think dawehner meant "once for all libraries of a certain extension", but we should indeed clarify that.

Berdir’s picture

@WimLeers: We already do that function hook_library_info_alter(&$libraries, $module) {, and $libraries is documented as The JavaScript/CSS libraries provided by $module.

Wim Leers’s picture

That's an excellent point. So this would just be making things consistent.

I'm convinced.

I'll get a patch going after #2378263: hook_library_alter() must be manually invoked by users of LibraryDiscovery, and has no test coverage lands.

dawehner’s picture

Status: Active » Needs review
Issue tags: +Performance
FileSize
6.25 KB

If you look at head standard install you will see in total 87 calls to \Drupal::moduleHandler()->alter() caused by \Drupal\Core\Asset\LibraryDiscovery::getLibrariesByExtension
which for example results in 87 calls to \Drupal::theme() which calls in 87 $container->get() calls, so in total we talk about about 1% of a cached request.

Doing this, saves me 2.2 ms

Status: Needs review » Needs work

The last submitted patch, 8: 2378737-8.patch, failed testing.

Berdir’s picture

  1. +++ b/core/modules/quickedit/quickedit.module
    @@ -85,21 +85,21 @@ function quickedit_page_attachments(array &$page) {
    +function quickedit_library_alter(array &$libraries, $theme = NULL) {
    +  // Retrieve the admin theme.
    +  if (!isset($theme)) {
    +    $theme = Drupal::config('system.theme')->get('admin');
    +  }
    +  if (isset($libraries['quickedit/quickedit'])) {
    

    No idea where the $theme is supposed to be coming from here...?

  2. +++ b/core/modules/system/theme.api.php
    @@ -765,20 +765,21 @@ function hook_library_info_alter(&$libraries, $module) {
    -function hook_library_alter(array &$library, $name) {
    -  if ($name == 'core/jquery.ui.datepicker') {
    +function hook_library_alter(array &$libraries, $name) {
    +  if (isset($libraries['core/jquery.ui.datepicker'])) {
    +    $library = &$libraries['core/jquery.ui.datepicker'];
    

    $name doesn't make sense anymore, then?

dawehner’s picture

Status: Needs work » Needs review
FileSize
2.24 KB
7.29 KB

No idea where the $theme is supposed to be coming from here...?

quickedit_library_alter() calls itself ...

$name doesn't make sense anymore, then?

Valid point.

Wim Leers’s picture

Status: Needs review » Needs work
+++ b/core/lib/Drupal/Core/Asset/LibraryDiscovery.php
@@ -59,12 +59,19 @@ public function getLibrariesByExtension($extension) {
+      $this->moduleHandler->alter('library', $libraries);

hook_library_info_alter() does things differently:
$libraries is keyed by libraryname, not extension/libraryname. And it also receives $module. I think we want hook_library_alter() to behave exactly the same, as per #6.

And:

Doing this, saves me 2.2 ms

Awesome!

dawehner’s picture

dawehner’s picture

FileSize
13.67 KB
6.57 KB

As you maybe have seen or maybe haven't seen is that even with that patch, LibraryDiscovery::getLibrariesByExtension() and LibraryDiscoveryParser
still act on a per extension base.

Even this is kinda an API change, but we should somehow execute just a single alter hook for all library definitions used on a page.
Can we do that somehow? This alter hook than probably has to be removed from the LibraryDiscovery again

This patch changes the library parser to just fire one alter hook for all modules.

Wim Leers’s picture

As you maybe have seen or maybe haven't seen is that […] act on a per extension base.

Yep, I saw.

Doesn't #14 load the libraries of all modules in the system, rather than only the ones that are actually needed? Do we want that?
If we do want that, I don't think we need the CacheCollector pattern that we currently use, because we're no longer lazily extending the cached list of discovered library definitions, and that's AFAICT the main reason to use the CacheCollector pattern.

dawehner’s picture

FileSize
36.42 KB
26.78 KB
  • Worked on rewriting the existing tests for the library parser
  • While rewriting the LibraryDiscovery I realized that the cache collector has the advantage of providing locking, so we could put the runtime hook into that.

Note: the current patch is in the middle of realizing the point 2)

dawehner’s picture

FileSize
29.11 KB
38.77 KB

mhh this is really tricky, storing both by extension and not by extension due to Parser::build() feels just wrong.

catch’s picture

Priority: Normal » Major

2ms on stock head makes this major.

Fine with an API change here.

dawehner’s picture

Status: Needs work » Needs review
FileSize
45.89 KB
11.49 KB

So far, here is a simple version, which should work already.

Note: We optimize here for the common case: Get a library by name, not by extension. On an actual request
you reference to specific libraries, not to all extensions of a specific library.

Status: Needs review » Needs work

The last submitted patch, 19: 2378737-19.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
FileSize
45.85 KB
1.54 KB

Fixed it.

Status: Needs review » Needs work

The last submitted patch, 21: 2378737-21.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
FileSize
49.43 KB

Who could have guessed that we have tests :)

Status: Needs review » Needs work

The last submitted patch, 23: 2378737-23.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
FileSize
51.26 KB
3.18 KB

This could be it ...

andypost’s picture

would be great to use overrides for libraries in theme, but separate issue. Themers like to override libraries in yml

  1. +++ b/core/lib/Drupal/Core/Asset/LibraryDiscovery.php
    @@ -56,27 +74,51 @@ public function __construct(CacheCollectorInterface $library_discovery_collector
    +  protected function loadLibraryDefinitions() {
    

    I'd use findDefinitions() as DefaultPluginManager use

  2. +++ b/core/lib/Drupal/Core/Asset/LibraryDiscovery.php
    @@ -56,27 +74,51 @@ public function __construct(CacheCollectorInterface $library_discovery_collector
    +    // @todo Should themes be able to take part in this process as well?
    +    $this->moduleHandler->alter('library', $libraries);
    

    good question! needs @todo and issue

  3. +++ b/core/modules/system/theme.api.php
    @@ -786,20 +785,21 @@ function hook_library_info_alter(&$libraries, $module) {
    - * @param array $library
    - *   The JavaScript/CSS library that is being added.
    + * @param array $libraries
    + *   The Javascript/CSS libraries which are added, keyed by library name.
      * @param string $name
      *   The name of the library.
    ...
    -function hook_library_alter(array &$library, $name) {
    ...
    +function hook_library_alter(array &$libraries) {
    

    removed @name still documented

  4. +++ b/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryParserTest.php
    @@ -392,20 +411,20 @@ public function testLibraryThirdPartyWithMissingLicense() {
    -    $library = $libraries['no-license-info'];
    +    $library = $libraries['licenses/no-license-info'];
    

    great to see that fixed

dawehner’s picture

FileSize
51.24 KB
3.94 KB

Thank you for you review!

Alright fixed your reviews ...

I'd use findDefinitions() as DefaultPluginManager use

just changed it.

andypost’s picture

Status: Needs review » Reviewed & tested by the community
Parent issue: » #2050269: hook_library_info_alter() is not called for themes

Awesome!

damiankloip’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/lib/Drupal/Core/Asset/LibraryDiscovery.php
    @@ -56,27 +74,52 @@ public function __construct(CacheCollectorInterface $library_discovery_collector
    +    // @todo store that a library does not exist.
    

    I think you're doing that with the below code?

  2. +++ b/core/lib/Drupal/Core/Asset/LibraryDiscovery.php
    @@ -56,27 +74,52 @@ public function __construct(CacheCollectorInterface $library_discovery_collector
    +    if (empty($this->libraryDefinitions[$library_name])) {
    

    I think we want isset() there? As empty(FALSE) will always return TRUE. We don't need to keep setting it.

dawehner’s picture

Status: Needs work » Needs review
FileSize
51.19 KB
755 bytes

@damiankloip is right, as always.

Status: Needs review » Needs work

The last submitted patch, 30: 2378737-30.patch, failed testing.

damiankloip’s picture

!isset() :)

damiankloip’s picture

Status: Needs work » Needs review
FileSize
51.19 KB
640 bytes

Here you go.

dawehner’s picture

Ha, it failed, good to know!

dawehner’s picture

Issue summary: View changes
dawehner’s picture

Issue summary: View changes
damiankloip’s picture

Status: Needs review » Reviewed & tested by the community

As the person that did the initial conversion to reduce the cache calls, this is looking great!

I am ok to rtbc I think, I only changed one character in the last patch.

Berdir’s picture

+++ b/core/modules/system/src/Form/ThemeAdminForm.php
@@ -52,6 +52,7 @@ public function buildForm(array $form, FormStateInterface $form_state, array $th
+    \Drupal::cache('discovery')->delete('library_info');

This hardcodes how and where caches are stored in a form submit function. Should we add a clearCachedLibraries() method or so to a service, similar to how we clear plugin caches?

Wim Leers’s picture

Status: Reviewed & tested by the community » Needs work

A bunch of nitpicks and one actual question.

  1. +++ b/core/lib/Drupal/Core/Asset/LibraryDiscovery.php
    @@ -37,18 +31,42 @@ class LibraryDiscovery implements LibraryDiscoveryInterface {
    +   *   The library discovery.
    

    The library discovery parser.

  2. +++ b/core/lib/Drupal/Core/Asset/LibraryDiscovery.php
    @@ -56,27 +74,51 @@ public function __construct(CacheCollectorInterface $library_discovery_collector
    +   * Loads all library definitions, either from cache or rebuild it.
    

    s/rebuild it/rebuilds them/

  3. +++ b/core/lib/Drupal/Core/Asset/LibraryDiscovery.php
    @@ -56,27 +74,51 @@ public function __construct(CacheCollectorInterface $library_discovery_collector
    +    // Themes should also be alter to alter them, see
    

    s/be alter/be able/

    :)

  4. +++ b/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php
    @@ -41,9 +50,50 @@ class LibraryDiscoveryParser {
    +    // Allow modules to alter the module's registered libraries.
    

    The "the module's" part can be removed here; we allow modules to alter *all* libraries.

  5. +++ b/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php
    @@ -60,27 +110,17 @@ public function __construct($root, ModuleHandlerInterface $module_handler) {
    -    if ($extension === 'core') {
    -      $path = 'core';
    -      $extension_type = 'core';
    -    }
    -    else {
    -      if ($this->moduleHandler->moduleExists($extension)) {
    -        $extension_type = 'module';
    -      }
    -      else {
    -        $extension_type = 'theme';
    -      }
    -      $path = $this->drupalGetPath($extension_type, $extension);
    -    }
    +    $path = $extension->getPath();
    +    $extension_type = $extension->getType();
    +    $extension_name = $extension->getName();
    

    Nice! :)

  6. +++ b/core/modules/quickedit/quickedit.module
    @@ -85,21 +85,21 @@ function quickedit_page_attachments(array &$page) {
    -          $library['css'][$theme_path . '/' . $path] = array(
    +          $libraries['quickedit/quickedit']['css'][$theme_path . '/' . $path] = array(
    

    This bit is going to conflict with #2382533: Attach assets only via the asset library system.

  7. +++ b/core/modules/system/theme.api.php
    @@ -747,18 +747,15 @@ function hook_js_settings_alter(array &$settings) {
    + *   All JavaScript/CSS libraries. Keyed by library name.
    
    @@ -786,20 +783,19 @@ function hook_library_info_alter(&$libraries, $module) {
    + *   The Javascript/CSS libraries which are added, keyed by library name.
    

    We say "asset libraries", not "JavaScript/CSS libraries". I know that's what was already there, but we might as well fix it if we're changing it :)

  8. +++ b/core/modules/system/theme.api.php
    @@ -786,20 +783,19 @@ function hook_library_info_alter(&$libraries, $module) {
    + * Allows modules and themes to dynamically attach further assets to libraries
      * when it is added to the page; e.g., to add JavaScript settings.
    

    s/it is/they are/

  9. +++ b/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryParserTest.php
    @@ -125,16 +134,23 @@ public function testBuildByExtensionWithTheme() {
    +    foreach ($result as $library_name => $library) {
    +      if (strpos($library_name, 'core/') === 0) {
    +        unset($result[$library_name]);
    +      }
    +    }
    

    Why is this necessary?

dawehner’s picture

Status: Needs work » Needs review
FileSize
51.28 KB
3.78 KB

This hardcodes how and where caches are stored in a form submit function. Should we add a clearCachedLibraries() method or so to a service, similar to how we clear plugin caches?

Let me quick explain why this change is needed.
Previously we retrieved library information per extension, so that enabling/disabling a theme did not caused issues, because the entry
in the cache collector simply did not existed yet. Once you just use a simple cache, this is no longer true and we need to invalidate it somehow.
Anyone has a better idea?

This bit is going to conflict with #2382533: Attach assets only via the asset library system.

Well sure, this is life and this is why we use git.

Why is this necessary?

Oh well, the reason is the following: We now parse core libraries always, so comparing with an empty result doesn't work that simple anymore.

Status: Needs review » Needs work

The last submitted patch, 40: 2378737-40.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
FileSize
732 bytes
53.38 KB

core is moving, that is great!!

@berdir
Note: I also fixed your review, see the interdiff.

Status: Needs review » Needs work

The last submitted patch, 42: 2378737-42.patch, failed testing.

Wim Leers’s picture

Well sure, this is life and this is why we use git.

Haha :) I know, I just wanted to give you an advance warning. That issue now landed, hence #42 also doesn't apply.

Oh well, the reason is the following: We now parse core libraries always, so comparing with an empty result doesn't work that simple anymore.

I overlooked the fact that we now always parse core libraries. Now it makes sense. Thanks.

dawehner’s picture

Status: Needs work » Needs review
FileSize
53.21 KB
175 bytes

Maybe this is all you need.

dawehner’s picture

FileSize
8.63 KB
9 KB
21.43 KB

While measuring the performance I realized that HEAD is broken in the sense that the cache entry for libraries is not saved.
Given that we save 150ms as anonymous user.

In case you have HEAD fixed, the difference is 6.37ms (2.5% of the request)

Wim Leers’s picture

RE: HEAD being broken: Symfony 2.6 must have made a change then, because this definitely used to work, otherwise we wouldn't have found #2381473: Decrease the size of the asset library cache item. It'd also have shown up on flame graphs.

EDIT: #2354705: Mark a couple of asset services as non public is the culprit.

dawehner’s picture

Priority: Major » Critical

#2354705: Mark a couple of asset services as non public introduced that regression :(

Mh, k, maybe a reason this is critical.

Wim Leers’s picture

Status: Needs review » Reviewed & tested by the community

No more complaints for this patch.

Critical +1, because this is why testbot got significantly slower. (No more 18–19 minute test runs…)

Would be great to land this today, before the Ghent sprint.

catch’s picture

So while the CacheCollector being made private caused the 150ms (!) regression, I'm not sure about removing it here.

We might want to further optimize the CacheCollector (add role to the cid so that anonymous users don't get things like contextual links or quickedit loaded? Make it per-library instead of per module?) but given #2381473: Decrease the size of the asset library cache item going back to one monolothic cache entry doesn't seem great.

Wim Leers’s picture

I think it might be better to defer optimizing the size of the library cache entry to that other issue. Here's my perspective/reasoning.

This issue gains speed by not executing hook_library_alter() many times, and guaranteeing it's only going to be executed once. To do that, it must know all the libraries that are going to be requested for the current page. The simplest possible way to guarantee that, is by handling *all* assets in one go.
The fact that this is faster than HEAD in profiling proves that this simple (perhaps even "naïve") approach is faster, should be sufficiently convincing, I think?

We're not "going back to one monolithic cache entry", we already had that in HEAD.

I agree that ideally, we'd know how we're going to solve #2381473: Decrease the size of the asset library cache item, to prevent removing the cache collector pattern here and readding it there. But this is proven to be faster, and we don't yet know whether we can come up with an even faster alternative that creates smaller cache entries.

webchick’s picture

Why are we marking this critical instead of just rolling back #2354705: Mark a couple of asset services as non public, if that's what introduced the testbot slowdowns? That was a normal "clean-up" style task that does not make D8 closer to release in any way.

dawehner’s picture

@webchick
That itself is fair, but I also think that executing alter hooks for all of them will more and more be a problem on real sites,
with a lot of contrib modules, for core itself its maybe just the tip of the iceberg.

https://github.com/symfony/symfony/issues/12924 is the symfony issue which is also kinda broken, to be honest.

webchick’s picture

Priority: Critical » Major

Yeah, don't get me wrong, I think this issue is probably also good to do, but I worry about us trying to "rush" it in in order to fix a regression in time for a big sprint that was caused by some other (normal) issue. Alex Pott is doing a revert of that one though, so we have some time to evaluate this one on its own merits.

Moving back to Major since the revert will take care of the critical.

catch’s picture

Status: Reviewed & tested by the community » Needs review
  1. +++ b/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php
    @@ -41,9 +50,50 @@ class LibraryDiscoveryParser {
    +    $libraries_core = (array) $this->buildByExtension($core_extension);
    

    Why..

  2. +++ b/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php
    @@ -41,9 +50,50 @@ class LibraryDiscoveryParser {
    +    foreach ((array) $this->moduleHandler->getModuleList() as $extension) {
    

    all..

  3. +++ b/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php
    @@ -41,9 +50,50 @@ class LibraryDiscoveryParser {
    +    foreach ((array) $this->themeHandler->listInfo() as $extension) {
    

    ..the casts to array? Don't these return empty arrays if nothing is there anyway? If not they should.

I'm still not sure on dropping the cache collector - all core implementations check if the library exists before attempting to alter - so running it on cached libraries, then individually on uncached libraries as they're retrieved seems quite possible.

Wim Leers’s picture

We realized we could remove hook_library_alter() entirely: #2390707: Remove hook_library_alter() implementations.

Should we postpone this one?

catch’s picture

Status: Needs review » Postponed

Yes let's postpone.

Wim Leers’s picture

#2390707: Remove hook_library_alter() implementations landed. Which means this is unblocked. But actually, that other issue removed the problem that this issue was trying to turn into a smaller problem. So I think we either want to rescope this to handle #2381473: Decrease the size of the asset library cache item, or close this in favor of #2381473: Decrease the size of the asset library cache item?

Wim Leers’s picture

Status: Postponed » Closed (duplicate)

#2381473: Decrease the size of the asset library cache item has been fixed, so this can definitely be closed as a duplicate now.