Problem/Motivation
Right now local tasks and actions are not render cacheable at all even though they should because generating them might be expensive. After #507488: Convert page elements (local tasks, actions) into blocks landing in it might be possible why we should investigate this.
See #507488-319: Convert page elements (local tasks, actions) into blocks, this makes /nn/1
10% faster for the anonymous user.
Proposed resolution
Add necessary cacheability metadata to the places that can provide dynamic local tasks so that caching can be enabled for the blocks. At least following places can make local tasks dynamic and needs to be able to add cacheability metadata:
- hook_local_tasks_alter (adding another parameter for the hook)
- Access results of all links (making access result cacheable dependency)
- Plugin definition (LocalTaskDefault implements CacheableDependencyInterface. LocalTaskDefault picks up cache_tags, cache_contexts and cache_max_age items from PluginDefinitions)
- Derivers (Deriver can add cacheability metadata into cache_tags, cache_contexts and cache_max_age items in the derivative definitions)
Remaining tasks
TBD
User interface changes
None.
API changes
Their hooks are modified.
Data model changes
None.
Comment | File | Size | Author |
---|---|---|---|
#89 | interdiff.txt | 896 bytes | lauriii |
#89 | make_local_tasks_and-2511516-89.patch | 32.8 KB | lauriii |
#88 | interdiff.txt | 2.89 KB | lauriii |
#88 | make_local_tasks_and-2511516-88.patch | 32.8 KB | lauriii |
#81 | interdiff.txt | 6.18 KB | lauriii |
Comments
Comment #1
Wim LeersSee #507488-319: Convert page elements (local tasks, actions) into blocks, this makes
/user/1
10% faster for the anonymous user.Comment #2
Wim LeersComment #3
Wim LeersComment #4
Wim LeersHoly shit, what a clusterfuck.
…
We have:
which is fine.
But we also have:
… which is … actually fine, we can cache it per route, but still. It's not actually fine, because e.g.
contact_menu_local_tasks_alter()
not only looks at the route, but depends on yet more state: it loads the user being contacted on the contact page, and checks if they have an e-mail address. Still cacheable, but yeah……
It gets better!
…
The above is why we DON'T use
LocalTaskManager::getLocalTasksForRoute()
to determine the local tasks (as one would expect)… no, we usemenu_local_tasks()
, which callsLocalTaskManager::getLocalTasksForRoute()
, but THEN still alter it FURTHER using bothmenu_local_tasks()
andmenu_local_tasks_alter()
.So… in trying to decouple local tasks from the menu system, we've introduced a new alter hook, but we've still kept the two old hooks, and we still have to use the old API. It's pretty clear that this API is far, far from completed. I think we should mark all of it as
@internal
NOW, so that if we don't fix it before release, we still can do so later. The current API is all sorts of confusing and broken.I'm gonna work on something else, because this makes me wanna punch myself and — worse — kill kittens.
Comment #5
Wim LeersHere's a partial patch of what I did before getting too frustrated.
Comment #6
Wim LeersI'm also willing to bet that most of the slowness of local tasks/actions reported in #507488: Convert page elements (local tasks, actions) into blocks is directly caused by the confusing amalgamation explained in #4.
Comment #7
dawehnerHere is a description in way less words.
Done
Comment #8
pwolanin CreditAttribution: pwolanin as a volunteer and at Acquia commentedIt seems like local tasks, local actions, and contextual links should implement CacheableDependencyInterface ?
Comment #9
Wim Leers#8: +1
And then the ones that just depend on
links.*.yml
files, likeContextualLinkDefault
, would simply return no tags, no contexts, and a permanent max-age.Comment #10
pwolanin CreditAttribution: pwolanin as a volunteer and at Acquia commentedShould we still wait to start on this until the block conversion goes in?
Comment #11
Wim LeersIMHO this can start already.
Comment #12
moshe weitzman CreditAttribution: moshe weitzman at Acquia commentedShowing up as a performance suck in current flamegraphs. Tagging.
Comment #13
Wim Leers#507488: Convert page elements (local tasks, actions) into blocks just landed. Let's do this!
Comment #14
moshe weitzman CreditAttribution: moshe weitzman at Acquia commentedComment #15
Fabianx CreditAttribution: Fabianx for Acquia commentedThe IS is clear, but I think we need a plan how to resolve the cacheability concerns around this.
Comment #16
XanoAll Plugins Are Created Equal™
Comment #18
XanoIf we collect plugin definition cacheability metadata through a separate object, we will not only have to add an argument to the alter hook, but add it to the original plugin discovery, and derivers as well. This would be a BC break for discovery methods and derivers. This would also limit this feature to local tasks only, as we'd only convert that alter hook.
Instead, this patch proposes to include cacheability metadata inside plugin definitions. This way, discovery methods, derivers, *and* alter hooks can add/alter cacheability, and we can use the metadata outside plugin managers as well (useful for Plugin Selectors, for instance, which build render arrays based on plugins). It also automatically makes this available for any plugin manager that extends
DefaultPluginManager
, so it's not just limited to local tasks.Comment #20
XanoThis should fix the fatals.
This approach does not break BC, but it does break the tests for (almost) all plugin managers.
Comment #21
XanoComment #23
XanoNever mind those patches. I misunderstood the problem. It's really just about
hook_menu_local_task_alter()
implementations not setting#cache
properly.Comment #24
XanoSee #2564011: Use local task/action cacheability metadata when displaying them for an improved, more in-scope version of what I was trying to do earlier.
Comment #25
lauriiiIt seems like all the local task plugin definitions have been cached per route.name anyway so in order to get this done, those parts don't have to be able to define additional cache contexts because we can trust that they can only have route.name cache context. This is from LocalTaskManager:
In my patch I only make hook_menu_local_tasks_alter provide cacheability metadata and enable the caching for local task blocks by default.
Comment #26
lauriiiIt seems like local actions can really only depend on route name and route parameters so adding route cache context to local actions block should do everything that is needed.
Comment #28
XanoLooking good!
Why
route.name
instead ofroute
?Unused
use
statement.Comment #29
Wim LeersShould be
route.name
, because the local tasks determined to be shown only depends on the current route' name:Also, it should not add that here, to an individual local task, but to
$build
. If$build
does not get any local tasks, then that is because no local tasks exist for that route name, and that emptiness varies by theroute.name
cache context.This is not necessary.
LocalTasksBlock::build()
callsLocalTaskManager::getLocalTasks()
, which callsLocalTaskManager::getTasksBuild()
, which is the method I commented on above, and that always adds that cache context.Oops :)
Unnecessary, can be reverted.
s/route/route.name/
Comment #30
lauriiiI removed #29.1 completely because it is not needed any more which also makes #29.2 irrelevant.
Comment #31
XanoTrailing whitespace.
Comment #32
lauriiiWill be working on this. It seems like the local task plugin definitions have to be able to provide cacheable metadata for the title.
Comment #33
BerdirThe local tasks might be based on the route name, but e.g. different nodes obviously have different route parameters and resulting, different links/HTML even though the route name is the same?
Comment #35
XanoComment #37
lauriiiThis should fix some of the test failures
Comment #39
Wim LeersD'oh, yes.
That is why:
receives the route match. Because
\Drupal\Core\Menu\LocalTaskDefault::getRouteParameters()
extracts the route parameters to use for the task from the route match.Comment #40
lauriiiI added the access result as a cacheable dependency which AFAIK should fix this. How ever for some reason some times the access result doesn't have the cacheable metadata inside itself why this probably will still fail.
Comment #41
dawehnerIf you change the LocalTaskManager class you should also expand the unit tests of this class.
Is there a reason why this method needs to be public? I don't see any use of it in the public ...
->merge just returns a new instance, so you need to update $cacheable_metadata itself
I don't see the point in initializing it twice.
Urgs, I think its wrong to add all the unpublished comments(given there could be 10k or more), can't we use the comment_list tag instead for the invalidation?
Comment #43
lauriiiThanks a lot for the review! I fixed everything on #41 except the first one. This one should still have test failures because of the problem I'm having with access results and their cacheable metadata.
Comment #45
lauriiiAdded some test coverage to web tests.
Comment #47
lauriiiNot actively working on since I'm blocked on the remaining failures that I don't know how to solve by myself.
Comment #48
XanoWhy this instead of making
LocalTaskInterface
extendCacheableDependencyInterface
like #2564011: Use local task/action cacheability metadata when displaying them does? #8 also suggested this.Comment #49
Wim Leers@lauriii asked me to take a look at this. I spent about 3 hours debugging this probably. The two remaining failures here are the combination of two bugs:
comment_list
cache tag because the singular local task generated byUnapprovedComments
depends on all the comments in the system ……
FieldUiLocalTask
depends in a similar way onconfig:entity_view_display_list
: it returns a set of local tasks that depends on the entity view displays enabled for a certain entity bundle.Fixing this fixes the problem :)
Same goes for
config:entity_form_display_list
.\Drupal\Core\Entity\Entity\EntityViewDisplay::postSave()
but failed to call the parent method, which means the default cache tag invalidation logic for config entities (or even entities in general) no longer is invoked! Henceconfig:entity_view_mode_display_list
is never invalidated. (Which you can easily prove by saving any entity view display, then looking at thecachetags
table and note that that cache tag still does not appear!)Comment #50
Wim LeersSo this still needs test coverage. That's why I added that tag in #49.
IMHO: s/cacheable_metadata/cacheability/ and
getCacheability()
.Because not the metadata is cacheable, it's metadata *about* cacheability.
Also: see #2561773: CacheableMetadata is misnamed.
Similarly, IMHO we shouldn't typehint to
CacheableMetadata
, but toRefinableCacheableDependencyInterface.
Nit: it's better to use the
getListCacheTags()
method like I did in #49.Comment #51
dawehnerNeeds also expansion of the unit test.
Comment #52
Wim Leers#51++
Comment #55
Wim LeersThe two previous fails fixed, two new ones, because Dynamic Page Cache landed in the mean time:
So… turns out this patch is causing the local task block on the front page to have the
user
context, which means the entire page gets theuser
cache context, which means Dynamic Page Cache doesn't cache it anymore.Why is the
user
cache context being added here?!Comment #56
pwolanin CreditAttribution: pwolanin as a volunteer and at Acquia commentedI agree with xjm, I thought we were going to use CacheableDependencyInterface here.
Comment #57
lauriiiThere was user cache context added accidentally everywhere in the
contact_menu_local_tasks_alter
even though it should be only added to the places whereentity.user.contact_form
local task exists. Triggering tests to see failing tests.Comment #60
Xano@pwolanin, @xjm, and me already asked this before, but why do we add an new method to
LocalTaskInterface
instead of just reusingCacheableDependencyInterface
? Also see #2564011: Use local task/action cacheability metadata when displaying them.Comment #61
Wim Leers+1 We should absolutely do that. I should've remarked the same in #50.
(It is possible because we have one
LocalTaskInterface
object per local task — i.e. a singleLocalTaskInterface
implementation does NOT correspond to multiple local tasks. We only use agetCacheableMetadata()
method in cases where a single object returns multiple cacheable things. So, a simple rule of thumb is that if thegetCacheableMetadata()
method does NOT need a parameter, thenCacheableDependencyInterface
should be used instead.)Comment #62
lauriiiThis should fix the last test fail. This might cause other tests to fail. I removed the user cache context from
contact_menu_local_tasks_alter
because I misread the code last time and it should be instead cached per route.Comment #63
Wim LeersI've been chatting with @lauriii while he was looking for the root cause of the remaining failure in #57. That remaining failure is simply an expectation for the Standard install profile, that the Dynamic Page Cache is able to cache node pages.
So this patch makes it so that the Dynamic Page Cache is no longer able to cache node pages.
Why? Because in HEAD, the local tasks block sets max-age=0, which means it gets placeholdered automatically. This patch removes that, because the whole point of this issue/patch is to start caching the local tasks block. But in doing so, the
user
cache context bubbles on node pages.Why? Because authenticated (non-admin) users don't have the
permission, only the permission. This means that for every article node, we need to check if the owner of the article matches the current user. Hence theuser
cache context is necessary. Hence that bubbles. And this is entirely correct.Why is that then not being placeholdered? Because #2543334: Auto-placeholdering for #lazy_builder with bubbling of contexts and tags hasn't been committed yet. Once that is committed, a bubbled
user
cache context will cause the local tasks block to be placeholdered, so that theuser
cache context doesn't bubble to the response level. Then the Dynamic Page Cache will once again be able to cache the response (and render the placeholder after retrieving the vast majority of the response from the cache).Conclusion: this should just comment out the failing assertion in
StandardTest
and add a@todo
to enable it again after #2543334 lands :)So, again, in summary: Dynamic Page Cache expectations in Standard install profile fail, because correct cacheability metadata bubbles, which then correctly causes DPC to not cache the response. HEAD only has simple auto-placeholdering, and it is that auto-placeholdering that was allowing DPC to cache node pages in HEAD. Since we now are bubbling the *actual* cacheability metadata, and we don't have auto-placeholdering for bubbled cacheability metadata yet (but it's RTBC!), DPC is no longer caching it. Everything works as designed :)
Remaining things:
@todo
.CacheableDependencyInterface
.Comment #66
Wim LeersOh, and to be clear: that makes #62 a fine way of proving that indeed the
edit own
permission causes theuser
cache context to bubble, which causesStandardTest
to fail in #57. But it's absolutely a wrong change.Comment #67
dawehnerHere are some unit tests in the meantime. Do them first, otherwise you miss the point of them.
Yeah let's ensure that we don't forgot that line though in there.
Let's swap those two lines, its easier to read that way.
Is that some magic vim trick?
In an ideal world we would have value objects just like for breadcrumbs, well, there is still stuff to do for Drupal 9
This is not needed
Comment #68
lauriiiThanks for the review & unit tests.
Things I did:
Comment #71
lauriiiFailing test only for #49. Fixed other tests also on the main patch.
Comment #74
Wim LeersThis is starting to look close :)
This doesn't seem wise, because
LocalTaskDefault
is also what*.links.menu.yml
files are mapped to, and so this is actually going to be scalar data, not objects, 99% of the time.This is only passing because the only plugin definitions that are setting
cacheability_metadata
do so from code that generates plugin definitions. Try using it from a*.links.menu.yml
file, and it will fail.So the unit test coverage is wrong. Ideally, we'd have integration tests for this, but I think fixing the unit tests should be sufficient.
See #50.2.
Docblock needs to be updated also.
Instead of this, you could specify
comment_list
incomment.links.task.yml
. Then literally nothing would have to change in this class :)Should also be
RefinableCacheableDependencyInterface
.Yay! :)
Should also be
RefinableCacheableDependencyInterface
.I don't think this is used :)
Comment #75
lauriiiThanks a lot for great review! :) Fixed all points from #74
Comment #77
dawehnerI agree that local task default should just use with raw values, having an object in there just doesn't make sense.
Ha indeed it isn't, but we could in this helper method to make it a bit more clear. Currently no method is actually provided.
Comment #79
lauriiiFixed one misuse of the API
Comment #80
Wim LeersNit: can be chained.
Unused.
See #74.5.
If there was a
use
statement at the top, we wouldn't need to have the FQCN here.Unused.
Unused.
Nit: s/Setup/Set up/
s/cacheablity/cacheability/
s/its/it is/
s/cacheable/cacheability/
Comment #81
lauriiiThanks again for the review.
Comment #84
lauriiiComment #87
Wim LeersI only found nits. :)
Unused.
s/Ensures/Ensure/
s/mode/display/
s/Cacheble/Cacheability/
Also: you're adding this to
UnitTestCase
, to avoid future repetition. I understand that.But:
So, let's remove this, and instead move this logic into
LocalTaskManagerTest::setUp()
and file an separate issue for this instead.One that's done, I think this is RTBC!
Comment #88
lauriiiFixed :)
Comment #89
lauriiiTypofix
Comment #90
Wim LeersPatch looks great.
This will probably be The Last Big Win :)
If you wonder how that's possible in combination with Dynamic Page Cache that went in last week: it's because the Local Tasks block is currently NOT being cached by Dynamic Page Cache, precisely because it is uncacheable (Dynamic Page Cache is smart like that). So we're still calculating and rendering the Local Tasks on every page load. With this patch in, that will no longer be the case.
Comment #91
Wim Leers:)
Comment #92
catchThis looks great. Committed/pushed to 8.0.x, thanks!
Comment #94
vijaycs85Created #2572125: Content translation local tasks are not getting displayed due to caching as follow up.