Problem/Motivation
JavaScript and CSS asset processing takes around 20ms on my machine with the standard profile.
With Drupal 7, we mostly dealt with individual files - this led to lots of different potential combinations of files, but very little metadata for each file.
With Drupal 8, everything is libraries. Which means the list of libraries and their ordering is predictable, but more metadata is required on every request.
On my machine I see ~10ms for CSS assets, ~8ms for Js assets and ~4.5ms for Js settings assets.
With the patch applied, it saves 4,500 function calls
Proposed resolution
Add caching to AssetResolver::getCssAssets() and AssetResolver::getJsAssets().
Given the same list of assets, we can assume that the sorting etc. is going to be the same. If the list of assets changes, so will the cache ID.
As well as saving loading metadata and sorting, this also saves file_exists() calls for aggregates with warm caches. Given page cache or reverse proxy request don't check for file existence, this ought to be OK, although #1014086: Stampedes and cold cache performance issues with css/js aggregation would improve things on cache misses and make things more robust overall.
Remaining tasks
Attaching a patch that adds dumb caching to this. Needs cache service injected. Needs cache tags.
Also system_js_settings_alter() will be broken because that adds per-page/per-user settings changes. We might need to take the alter out of the caching there.
Would be good to verify just how much this saves for performance before going to far with the patch, but removing 4,500 function calls seems promising.
User interface changes
API changes
Comment | File | Size | Author |
---|---|---|---|
#33 | interdiff.txt | 4.04 KB | dawehner |
#33 | 2506369-33.patch | 25.09 KB | dawehner |
#31 | interdiff.txt | 1.1 KB | dawehner |
#31 | 2506369-31.patch | 25.09 KB | dawehner |
#28 | interdiff.txt | 5.09 KB | dawehner |
Comments
Comment #1
catchComment #3
Wim Leers+1 on concept, this is exactly what I've been wanting to get to ever since working on #2381473: Decrease the size of the asset library cache item at DDD Montpellier.
Will review ASAP.
Comment #4
catchSo problems with this:
1. locale_js_alter() - this has to run per-language. We could probably add the current language to the cache key though. Then document that the results of the hook are cached.
2. hook_js_settings_alter() - this has to be per-request unless we change the API quite a lot. system_js_settings_alter() adds per-path things, user_js_settings_alter() adds per-user things. Per-user could be shifted client-side by using AJAX/local storage and updating settings based on that. system_js_settings_alter() is a complete mess but I don't see a way to avoid running this and fill things in client-side - or at least it can't be AJAX/client side caching so it'd have to be markup added elsewhere then moved into drupalSettings by js, which is worse than just putting it in drupalSettings.
On the plus side, hook_js_settings_alter() is not the big saving here.
Also note that #1014086: Stampedes and cold cache performance issues with css/js aggregation helps with the optimizer side of this, but even if we do that issue, there are still significant separate savings to be made here.
Since this is 20ms and is likely to involve at least some API change, bumping to critical for now.
Comment #5
dawehnerI'm curious whether we could have a build time hook as well as a runtime hook, so that we can save what we need but still have the flexility, for example for
system_js_settings_alter()
/I've seen damian complaining recently about too much time needed there as well.
Comment #6
Wim LeersCan't we just not cache
getJsSettingsAssets()
? Precisely because it is dynamic, it doesn't make a whole lot of sense to cache it.RE:
locale_js_alter()
: once again, what we're missing is… cacheability metadata :) Alternate suggestion: make\Drupal\Core\Asset\AttachedAssetsInterface
implement\Drupal\Core\Cache\CacheableDependencyInterface
, and give it setters to indicate that it should vary by language.Of course, that opens the door for more such dependencies. But it also opens the door towards configurable overrides (i.e. some module has Config to override a certain asset, then we need the corresponding cache tag to be associated).
Note that I'm fine with hardcoding per-language — I'm just pointing out that there is a more general solution.
Comment #7
Wim LeersComment #8
catchSo it would be nice to be able to do this, but given locale_js_alter() adds the language dependency in the alter hook, we'd need to return the cacheability metadata there. And that then would mean we'd need to support not only cache contexts but cache redirection. I think that would be a useful capability to add outside the render API, but not sure we need it here.
Also, I found this comment from you on another issue #2421323-2: Parse js files from library definitions during rebuild to minimize variable_set() calls which firmed up my belief that hard-coding language is OK.
Attaching a new patch - adds 'library_info' cache tags, took caching out of getJsSettings (although I think we may need to cache part of that to get a full benefit here - will tr to add once the number of fails has gone down a bit).
Comment #10
Wim LeersSounds good!
Comment #11
catchSome chance this is a green patch.
Everything gets cached except the js settings alter hooks more or less, and we save the separate settings cache entry by re-organising the code slightly.
Haven't repeated profiling yet - will do that if tests pass though.
Comment #13
catchTrying without the debug.
Comment #15
catchComment #16
catchsystem_js_settings_alter() now runs uncached again. The most expensive thing it does is LibraryDependencyResolver::getMinimalRepresentativeSubset() (~3ms) - this could be moved into the cached case somehow.
With the current patch (empty front page, user/1) here's an xhprof diff:
Comment #17
catchAdding hook_js_settings_build() and moving half of system_js_settings_alter() into there.
If this passes, this gets us green and -4,400 function calls. Original proof of concept that didn't actually work was ~4,500 so I think this is as much saving as we can get.
Comment #18
dawehneris there a reason why we not just call out to sha512 from the beginning?
I guess this needs to be dynamic, given that this even depends on the session ...
Comment #19
catchDo you mean hashing more parts of the cid? It can be easier for debugging to have the theme/language outside the hash.
Fixing #2. I'm 99.9% certain the MAINTENANCE_MODE check is no longer necessary, but yes good catch that depends on session. Moved back to the alter.
Comment #20
Wim LeersThe patch looks splendid :)
We should document why the theme needs to be part of the cache key.
We should document why the theme and current language need to be part of the cache key.
You've changed the logic a bit here, and I think you have a small mistake:
$settings_needed
implies$settings_have_changed
, so you only need to include$settings_needed
in the if-test.Weird newline.
Comment #22
catchFixes #1, #2, #3 and #4 from #20. #4 was cruft from a previous approach to caching the asset settings, but I do think we should slightly rename the variables there so kept that bit.
And apparently we do need to check maintenance mode after all.
Comment #23
dawehnerI think we should explicitly update also hook_js_alter() / hook_css_alter() and document that this is no longer request aware.
If I understand correctly this is about ensuring that the right theme is used for ajax requests ...
Comment #25
Wim LeersThe failure in
CKEditorLoadingTest
points to a likely bug indrupalSettings
, this is the failing line:The other failures seem similar.
Comment #26
dawehnerThe failures are interesting, looking at them atm.
Comment #27
dawehnerThe problem is with that patch we add the theme_token to normal requests, unless before. This itself should not be a problem, so let's fix our code against unsane checks ...
the second interdiff just fixes the problem mentioned there ...
Comment #28
dawehnerAlright, so this patch fixes first all the failures with the approach of interdiff-1.txt and then apply interdiff-2.txt on top of it
Comment #29
Wim LeersGiven that
theme_token
is only added for thedrupal/ajax
library, this change seems wrong?The majority of the work is now done in
system_js_settings_builder()
, but the session-dependent values are still handled here. Perhaps we should describe it that way ( )?I think the deleted comment contains important information that we now lost?
Comment #30
Wim LeersLeft it at NR for more reviews, but I think this looks at the very least close to RTBC otherwise.
NW so we can get those details fixed, and then hopefully RTBC.
Comment #31
dawehnerThank you for your review!
Tried to improve the documentation to be clear, what depends on the current request and what not.
Well, those bits are now in
system_js_settings_build()
, aren't they?Comment #32
Wim LeersNit: s/request dependent/request-dependent/
and core/drupal.ajax' 'theme_token' ajaxPageState.
Another review round.
80 cols.
80 cols.
80 cols.
"Add"? Shouldn't it be "Modify"?
After this one, it will really be RTBC.
Comment #33
dawehnerThank you for review again.
Agreed.
Comment #34
Wim LeersThe conclusion of #2381473, at the end of #2381473-37: Decrease the size of the asset library cache item, is precisely what this patch does: cache at a higher level. We could choose to remove the asset library cache entirely and only keep this cache. But doing that can be a follow-up, and can be what we repurpose #2381473 for, after this lands.
Great patch. Thanks!
Comment #35
alexpottThis issue addresses a critical bug and is allowed per https://www.drupal.org/core/beta-changes. Committed 5d3a761 and pushed to 8.0.x. Thanks!
Comment #37
Wim LeersYay! This also allowed me to fix two majors: #2381473: Decrease the size of the asset library cache item and #2378737: Consider not executing hook_library_(info)_alter() *per* library, but once for all :)
Comment #39
Wim LeersNote that
$assets
includes JS settings. So it'll actually vary by user, even though CSS assets don't care about the JS settings asset.We should also fix the JS assets caching, but that one will be trickier.
Comment #40
Fabianx CreditAttribution: Fabianx at Tag1 Consulting commentedA good way to fix the JS assets caching is to split this up in two cache entries:
a) Generic libraries + settings --- shared across all users // just would need to change getJsSettingsAssets() to not merge the assets settings
b) Attached settings + hook_js_settings_build result
Unfortunately it would be a behavior change to have hook_js_settings_build() operate on the pure libraries definition without any dynamic parts - though that would be the correct fix.
Then the code would - except for changing the cid to include only $attachments->getLibrariesToLoad() instead of the whole attachments - not need changing at all.
Maybe however that is how hook_js_settings_build() was thought of and we _should_ do this even before release?
Comment #41
Fabianx CreditAttribution: Fabianx at Tag1 Consulting commentedPer #40 I opened #2603138: CSS/JS asset caching can easily be trashed as critical follow-up (justification in the issue).
Comment #42
Wim LeersThanks for filing that issue!