Problem/Motivation
Each time the permissions cache context is calculated, this is called, leading to five cache gets (to the database cache backend) on anonymous requests.
Proposed resolution
Add a class property to store hashes calculated during the request.
Remaining tasks
User interface changes
API changes
API addition - new service which provides the memory backend as a 'static cache' with cache tag support.
Beta phase evaluation
| Issue category | Task because nothing is broken. |
|---|---|
| Issue priority | Major because of Performance. |
| Prioritized changes | The main goal of this issue is performance. |
| Disruption | Not disruptive for core/contributed and custom modules/themes, because it is a pure internal re-factoring. |
| Comment | File | Size | Author |
|---|---|---|---|
| #31 | interdiff.txt | 2.53 KB | olli |
| #31 | 2501117-31.patch | 8.29 KB | olli |
| #25 | interdiff.txt | 1.32 KB | olli |
| #25 | 2501117-25.patch | 9.9 KB | olli |
| #23 | 2501117-23.patch | 10.33 KB | wim leers |
Comments
Comment #1
catchComment #3
catchSo this 'fixes' the test. Another way would be to add a reset method to the permissions hash generator, both pretty ugly.
But I think what we really want here is a cache tag enabled memory backend (which we already have the code for), then the cache tag invalidation would just work.
Bumping to major since this is quite a lot of database hits for no good reason at the moment on every request.
Comment #4
catchPatch for the record.
Comment #5
catchHere's a different approach.
I added a new @cache.static service, which uses the memory cache backend, and hence respects cache tag invalidations.
The simpletest has to clear the cache tags due to parent vs. child site, but this would be seamless on a real request.
Comment #7
wim leersIMO the better solution would be to statically cache the results of each cache context. Then all cache contexts benefit in the same way.
(We could key the static cache on the hash of the request it is given, to not cause a lot of pain for tests that are running multiple requests.)
Comment #8
catch@Wim so the problem with that is we need to be able to invalidate the static caches if they change. When I added a basic static cache above, our tests failed because adding a new permission to a role didn't invalidate. #4 shows a way to make the tests pass but that won't help real sites.
So I think #5 is the right approach (assuming the 3 fails are silly oversights by me).
Comment #9
cilefen commentedWould something like this help? #2260187: [PP-1] Deprecate drupal_static() and drupal_static_reset()
Comment #10
catchFixed the unit test and added coverage for the static cache to it.
@cilefen I think that will be useful for some things, but it doesn't have the ability by itself to handle cache tag invalidations, which this new memory cache backend does.
Comment #11
wim leersWell, but that's only really a problem in tests, right? Is it not okay for those few tests to call a
\Drupal\Core\Cache\Context\CacheContextsManager::resetCache()method?That'd improve performance for every single cache context, and would prevent a lot of repetitive work. I can imagine this saving hundreds of function calls easily?
Comment #12
catchWell if you programmatically update a permission for a role, then calculate the permissions hash for a cache context afterwards, should the hash get updated?
Comment #13
wim leersI was gonna say … but then we don't know the necessary cache tags :P
In any case, this is a step forward, but I just feel like it'd be much better, architecturally, perf-wise and reasoning/DX-wise, to enforce that for a given request, the values for a cache context remain constant, which implies they don't need to be calculated every single time.
But, what you're saying makes sense too. It basically means that static caching should be done by every individual cache context, because we cannot do it at a higher level, because we won't know how to invalidate it.
So, let's do this.
If we're adding this back to core, then we must remove it again from
sites/default/development.services.yml.80 cols.
New parameter, but docblock not updated.
Just
@covers ::generatewill do.s/Hasn/Hash/
:)
Comment #14
catchFixes #13.
We can still look at caching at a higher level but I wonder what that looks like for pre-generation etc.
Comment #15
catchCouple of stray whitespace changes, just fixing those.
Comment #18
wim leersberdir filed exactly the same issue, for the exact same reasons: #2509478: PermissionsHashGenerator should have a static cache. Marked it as a duplicate.
Copying his IS verbatim:
Comment #19
catchUnit test was fine, but found a bug in the actual code.
Comment #20
wim leersComment #21
fabianx commentedLove it! RTBC + 1
Comment #23
wim leers#2417895: AccountPermissionsCacheContext/PermissionsHashGenerator must special case user 1, since permissions don't apply to it conflicted, rebased.
Comment #25
olli commentedRemoved arguments:
I think other bins are named as cache.[bin name] so replaced [memory] with [static].
To make the test pass I changed account1 to account2 because account1 (super user) doesn't use the static cache.
Comment #26
dawehnerMaybe a naive question, I see that
Drupal\Core\Cache\Context\CacheContextsManager::convertTokensToKeysis kinda the root of the evilness here.So as you will see there, we ask MANY things multiple times, of which the PermissionHashGenerator is just one of them by accident.
Would it not be much more effective to static cache on that heigher level?
(microsec)
MemUse
(bytes)
PeakMemUse
(bytes)
Comment #27
berdirYes, but as often, the trick is invalidation. The permission hash generator knows that it's based on the current user. But the context manager doesn't know that the the generated user.permission varies by the current user. We could cache it by current user, but I'm not sure that will always work. We have the cacheablity metadata now, but I'm not sure that will help.
And I think this will still be called directly as well.
Comment #28
dawehnerQuick tip: This can be just $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
Did some quick profiling, this now safes 1% in total for the default frontpage as anonymous user without page cache:
Comment #29
olli commentedThanks for the tip and profiling @dawehner!
It would be nice to get rid of these. How about adding MemoryBackend::reset() method that is called by WebTestBase::refreshVariables()?
Comment #30
wim leers#29:
So calling
MemoryBackend::reset()shouldn't be necessary. OTOH, indeed, invalidating the cache tags manually also shouldn't be necessary. Why is that?Comment #31
olli commented#30: It's necessary to see any changes made in the other thread.
Here's a patch with MemoryBackend::reset().
Comment #32
fabianx commentedBack to RTBC, that looks great!
Comment #33
wim leersI don't understand:
MemoryBackend::reset()is being called?Roleentities to be saved, and hence the cache tags for those roles to be invalidated) sufficient?Comment #34
fabianx commented#33: Cache tags are cached statically, hence they are only loaded / invalidated once per request.
Comment #35
berdirYes, this is correct and better.
As always, we have two runtime environments in simpletest, the test and actual requests. It's impossible to for the code in the requests to reset the static caches in the test environment, that's why we have refreshVariables() and reset config, cache tags on the checksum service and so on already.
Comment #36
fabianx commentedRTBC + 1 - x-post
Comment #37
wim leersAhhhhhh! I was very confused because
CacheBackendInterface::reset()does not exist. So I was wondering how this could possibly ever be called automatically. But, of course, usingis_callable()directly like https://api.drupal.org/api/drupal/core%21modules%21simpletest%21src%21We... does… well, that works.Let's create a
ResettableCacheBackendInterfacein a follow-up?Comment #38
wim leersOh, and RTBC+1 then :)
Sorry for the distraction & confusion!
Comment #39
wim leersThanks to @Berdir for pointing me to #2311945: Add ResettableCacheBackendInterface, which is the issue to do #37.
Comment #40
wim leersOops, olli already linked it in #30. I had no idea based on the issue title that that was what it did. Now retitled that other issue.
Comment #41
olli commentedSorry, one more question... How about injecting a BackendChain (here or in a follow-up)? Isn't this an use case for it?
Comment #42
dawehnerWhile reading the memory backend code I got confused that we serialize/unserialize the data there. I cannot imagine that there is a proper usecase for it.
Comment #43
berdirReferences. If we don't, then we might end up with changing cached data by reference (objects), that's not something that you'd expect as a user of the API.
Comment #44
dawehner#2538956: Document that MemoryBackend::prepareItem()/::set() uses unserialize()/serialize() to break references
Comment #45
fabianx commentedI added a beta evaluation.
Comment #46
alexpottConceptually I think I'd prefer this to be using something like #2260187: [PP-1] Deprecate drupal_static() and drupal_static_reset() because to me it looks like we're using the memory cache as a substitute for a static property that we want to be able to clear at test time.
Comment #47
wim leers#46: I don't understand what you mean; this issue uses the memory cache backend specifically to allow cache tag invalidations to still work. #2260187: [PP-1] Deprecate drupal_static() and drupal_static_reset() does not fix that AFAICT.
But I suspect I'm overlooking/misreading something :)
Comment #48
catch@alexpott so in core the memory cache is only useful for tests.
However, some kind of queue processing which both messes with permissions and say does pre-generation of the render cache for something would need the same behaviour. We might never have that queue processor, but having the cache tags 'just work' with the property was nice I thought.
Comment #49
alexpottCommitted 9aaddd2 and pushed to 8.0.x. Thanks!
Thanks for adding the beta evaluation to the issue summary.
Comment #52
xtineroque commentedAdded #3096986: Use CacheFastBackend for PermissionsHashGenerator.
Comment #53
ndobromirov commentedThis is somewhat related.