Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
Problem/Motivation
+++ 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
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'] |
Comment | File | Size | Author |
---|---|---|---|
#46 | diff-alter.png | 21.43 KB | dawehner |
#46 | head-fixed.png | 9 KB | dawehner |
#46 | head-not-fixed.png | 8.63 KB | dawehner |
#45 | interdiff.txt | 175 bytes | dawehner |
#45 | 2378737-45.patch | 53.21 KB | dawehner |
Comments
Comment #1
Wim LeersComment #2
dawehnerIn case we do that, we should do it for both library hooks.
Comment #3
Wim LeersOh, 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.Comment #4
BerdirAfaik, 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?
Comment #5
Wim LeersI think dawehner meant "once for all libraries of a certain extension", but we should indeed clarify that.
Comment #6
Berdir@WimLeers: We already do that
function hook_library_info_alter(&$libraries, $module) {
, and $libraries is documented asThe JavaScript/CSS libraries provided by $module
.Comment #7
Wim LeersThat'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.
Comment #8
dawehnerIf 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
Comment #10
BerdirNo idea where the $theme is supposed to be coming from here...?
$name doesn't make sense anymore, then?
Comment #11
dawehnerquickedit_library_alter() calls itself ...
Valid point.
Comment #12
Wim Leershook_library_info_alter()
does things differently:$libraries
is keyed bylibraryname
, notextension/libraryname
. And it also receives$module
. I think we wanthook_library_alter()
to behave exactly the same, as per #6.And:
Awesome!
Comment #13
dawehnerAdd a related issue
Comment #14
dawehnerAs you maybe have seen or maybe haven't seen is that even with that patch,
LibraryDiscovery::getLibrariesByExtension()
andLibraryDiscoveryParser
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
againThis patch changes the library parser to just fire one alter hook for all modules.
Comment #15
Wim LeersYep, 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 theCacheCollector
pattern.Comment #16
dawehnerNote: the current patch is in the middle of realizing the point 2)
Comment #17
dawehnermhh this is really tricky, storing both by extension and not by extension due to Parser::build() feels just wrong.
Comment #18
catch2ms on stock head makes this major.
Fine with an API change here.
Comment #19
dawehnerSo 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.
Comment #21
dawehnerFixed it.
Comment #23
dawehnerWho could have guessed that we have tests :)
Comment #25
dawehnerThis could be it ...
Comment #26
andypostwould be great to use overrides for libraries in theme, but separate issue. Themers like to override libraries in yml
I'd use findDefinitions() as DefaultPluginManager use
good question! needs @todo and issue
removed @name still documented
great to see that fixed
Comment #27
dawehnerThank you for you review!
Alright fixed your reviews ...
just changed it.
Comment #28
andypostAwesome!
Comment #29
damiankloip CreditAttribution: damiankloip commentedI think you're doing that with the below code?
I think we want isset() there? As empty(FALSE) will always return TRUE. We don't need to keep setting it.
Comment #30
dawehner@damiankloip is right, as always.
Comment #32
damiankloip CreditAttribution: damiankloip commented!isset() :)
Comment #33
damiankloip CreditAttribution: damiankloip commentedHere you go.
Comment #34
dawehnerHa, it failed, good to know!
Comment #35
dawehnerComment #36
dawehnerComment #37
damiankloip CreditAttribution: damiankloip commentedAs 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.
Comment #38
BerdirThis 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?
Comment #39
Wim LeersA bunch of nitpicks and one actual question.
The library discovery parser.
s/rebuild it/rebuilds them/
s/be alter/be able/
:)
The "the module's" part can be removed here; we allow modules to alter *all* libraries.
Nice! :)
This bit is going to conflict with #2382533: Attach assets only via the asset library system.
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 :)
s/it is/they are/
Why is this necessary?
Comment #40
dawehnerLet 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?
Well sure, this is life and this is why we use git.
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.
Comment #42
dawehnercore is moving, that is great!!
@berdir
Note: I also fixed your review, see the interdiff.
Comment #44
Wim LeersHaha :) I know, I just wanted to give you an advance warning. That issue now landed, hence #42 also doesn't apply.
I overlooked the fact that we now always parse core libraries. Now it makes sense. Thanks.
Comment #45
dawehnerMaybe this is all you need.
Comment #46
dawehnerWhile 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)
Comment #47
Wim LeersRE: 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.
Comment #48
dawehner#2354705: Mark a couple of asset services as non public introduced that regression :(
Mh, k, maybe a reason this is critical.
Comment #49
Wim LeersNo 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.
Comment #50
catchSo 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.
Comment #51
Wim LeersI 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.
Comment #52
webchickWhy 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.
Comment #53
dawehner@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.
Comment #54
webchickYeah, 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.
Comment #55
catchWhy..
all..
..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.
Comment #56
Wim LeersWe realized we could remove
hook_library_alter()
entirely: #2390707: Remove hook_library_alter() implementations.Should we postpone this one?
Comment #57
catchYes let's postpone.
Comment #58
Wim Leers#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?
Comment #59
Wim Leers#2381473: Decrease the size of the asset library cache item has been fixed, so this can definitely be closed as a duplicate now.