Problem/Motivation

When working on #3617141: Do not load installed field storage definitions in SqlContentEntityStorage::__construct(), I was a bit surprised that loading the active entity type definition is more expensive than the field storage definitions.

The reason is that we load them all from from a single cache entry. It' been optimized for a cold cache bootstrap, but it's not optimized for a regular request that only needs *some* of the data.

Steps to reproduce

Proposed resolution

Like field storage definitions, cache entity types separately, only load the ones we need.

Remaining tasks

User interface changes

Introduced terminology

API changes

Data model changes

Release notes snippet

Issue fork drupal-3617152

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

berdir created an issue. See original summary.

berdir’s picture

Status: Active » Needs review

This complicates having a global cache, because we don't know if we only partially loaded them or not. There are almost no direct calls to that, so I'm tempted to just drop the global cache to avoid storing these things twice (three times with the regular definition) in apcu. The awkward part is that we'd write all the caches every time we do, deleting one-off caches is also a bit weird.

I'm also tempted to drop the build-all approach to this, it would add a bunch of queries to cold caches, but we might save some memory?

the only direct non-updates call I found in contrib is https://www.drupal.org/project/config_track, which is weird, no idea why that uses installed definitions, it's just for config entities.

catch’s picture

StatusFileSize
new290.67 KB

One thing I've noticed with this is that the cache set on a completely cold cache is one of the slowest we have.

On the Umami gander dashboard, we have timings for each cache operation, and it shows up as taking 82ms out of 918ms for the whole request:

https://gander.tag1.io/explore?schemaVersion=1&panes=%7B%22y16%22%3A%7B%...

Attaching a screenshot in case the link doesn't work and for when it goes stale.

My guess would be that the global cache item takes a decent chunk of that time.

Another thing I'm wondering is if we remove the build-all and get on demand per-entity, would it mean we only get the items for entity loading first, but then if views needs all entity schema later in the request, we'd get the rest later. If it's deferred that late, it might happen in placeholder rendering, which means that it would be out of the critical path to send an initial big pipe response. Anything we can push off to later in the request is usually a good thing.

Also if we don't actually need all those cache entries on cold caches, we might save some time overall maybe or at least not lose any time.

catch’s picture

Previous issue I opened about this, but I think if this works out then we should just close that one as duplicate, writing at the end of the request we'd need to handle race conditions: #3593091: Defer EntityLastInstalledSchemaRepository cache warming to the end of the request

berdir’s picture

I can try to explore dropping the build all, shouldn't be complicated. I first thought hat there wouldn't be any conflicting race conditions when doing it one-by-one, but maybe during updates or so, when the change the data and then other request writes the old info, so might be better to go with immediate write for now.

The very slow setMultiple() cache writes are a bit surprising, I also see some similar one for the default bin and even config.

berdir’s picture

Status: Needs review » Needs work
catch’s picture

The very slow setMultiple() cache writes are a bit surprising, I also see some similar one for the default bin and even config.

If we're looking at the same ones, then the slow config one is when the cache for every view config entity gets cached at once, and the 'default' one is views data.

Opened an issue for the views config cache write, maybe we can avoid some of that: #3617537: Views block derivatives load every view.

Views data already has issues like #3454277: Allow fields to be opted out of views data although no great progress.

berdir’s picture

Status: Needs work » Needs review

Dropped the build mechanism, debugging it a bit, one thing I noticed is that on a multilingual site, we first get a bunch of field storage definitions through \Drupal\content_translation\Hook\ContentTranslationHooks::entityBaseFieldInfo(), so #3616962: Do not load installed field storage definitions in ContentTranslationHandler::__construct() wouldn't help, as we really need the data, which didn't do the build all, so we loaded those and then we loaded them again when the first entity type active definition is loaded, which is in this order on umami with a drush cr

* \Drupal\media_library\MediaLibraryEditorOpener::__construct through the \Drupal\Core\Entity\EntityResolverManager route altering, which #3617141: Do not load installed field storage definitions in SqlContentEntityStorage::__construct() will actually help
* \Drupal\menu_link_content\Plugin\Deriver\MenuLinkContentDeriver::getDerivativeDefinitions

(no views build, not on a drush cr).

On the web request, the first thing then is in fact views data, through a fiber. Which builds all field definitions due to the getBundles() call.

catch’s picture

Added a draft MR that does what I think we might want:

- gets all definitions from key/value if we get a miss and static caches them, no global persistent cache still
- writes individual cache items only when they're explicitly requested

The idea of this is:

- the key/value getMultiple() itself seems to be pretty cheap compared to the cache ::setMultiple()
- I think we'll still see some memory savings even with the static cache because serializing tends to be quite memory intensive
- much less increase in queries and cache gets except in one case where they go up a bit.
- still only one cache get when caches are warm and that's all we need.

Out of time for at least 2-3 days probably.

berdir’s picture

Hm. One downside is that this will take quite a while to warm up the caches, no? every time one entry is not yet cached, it will load all, then store just that one in the cache and not the others used in that request. So if you you need 5 of them in a typical request, it will take 5 requests of load-everything, including those that already are cached until the cache is warm?

I already thought about using the BackendChain with memory cache pattern here to simplify the lookups, we could introduce that to make a difference between bulk-loading and the cache, but then we'd also add cache misses for already bulk-loaded items, we would need to make it explicit to avoid that, but we might also write back already persistently-cached items then.

If we go with this approach, we should also do the same for field storage definitions, that should remove several queries on umami cold cache.

Other than that, the one reason I'm not quite sure about this approach is that we also "install" config entity types, which we never request, because that concept actually doesn't mean anything for them. select name from key_value where collection = 'entity.definitions.installed' and name like '%entity_type' on umami gives me 33 results, only 9 of those are content entity types and are requested. that we no longer cache them is good but we still load them into memory on misses. But we could possibly look into no longer "installing" them in a separate issue. That would break modules relying on them being there, that config_track module above would go from being slower to being completely broken then.

catch’s picture

So if you you need 5 of them in a typical request, it will take 5 requests of load-everything, including those that already are cached until the cache is warm?

Yes this true but it's probably a similar warm up pattern to most cache collectors, and it should mean each of those requests is individually slightly faster. So the coldest case does (hopefully a lot) less work but the slightly warmer cases do fractionally more work. Also makes the whole thing much less blocking - right now the first request has to do all the work, and if there are other requests there's a choice between same work or some kind of lock wait. If different pages request different entity types first they can be writing the individual cache items in parallel.

Assuming this is the case we can probably add test coverage to the Umami performance test like testNodePageEvenWarmer...

only 9 of those are content entity types and are requested. that we no longer cache them is good but we still load them into memory on misses. But we could possibly look into no longer "installing" them in a separate issue. That would break modules relying on them being there, that config_track module above would go from being slower to being completely broken then.

I think we should definitely try this. We could probably trigger a deprecation if a config entity is requested or something in 11.x?

berdir’s picture

since the build part of the cache is so fast, I think the multiple requests would need to be *very* close together with, I'd expect that a few (as in, 2-3?) milliseconds should be plenty to start hitting warm persistent caches already? It generally also happens within larger build processes, such as views data and routing on colld caches. routing already has a lock, we could add one to views data too, then it's pretty unlikely that we get stampedes on these requests.

Yes, BC like that might work, means we have to hardcode that logic directly into the service, but for BC that seems fine. Unsure what the logic would be. not-config-entity or only-content-entity? nothing else exists in core, but it might in contrib/custom code.

catch’s picture

since the build part of the cache is so fast, I think the multiple requests would need to be *very* close together with, I'd expect that a few (as in, 2-3?) milliseconds should be plenty to start hitting warm persistent caches already?

With individual cache writes even if there's a lot of individual gets and sets you're right that other requests should start getting hits very quickly but if there's e.g. 20 gets and sets taking 1ms each that's still about 40ms of blocking I/O before other things can happen. I really meant vs HEAD though where it's more like 80ms.

For bc probably safest to only exclude/deprecate config entities since we can't really know what a third entity type would need.

berdir’s picture

Status: Needs review » Needs work

> I really meant vs HEAD though where it's more like 80ms.

True. Both options are significantly better than HEAD I think, should be minor differences.

I think my long term plan is to find ways to stop building the field map and all bundle field definitions completely, so I'd take a few extra queries now with the possibility to not load that data at all later. But at the same time, it might not be "a few" especially with field storage definitions and then if we expect to load most/all of them anyway, we gain various extra queries with little to no memory wins. But then there's the slow cache build-up, I don't really know.

Looking at my site, I have 147 items in that collection, 110 of which are entity_type definitions. In regards to size, it's almost 1MB for field storage definitions in string length and .36MB for entity type definitions, so field storage definitions make up the bulk anyway.

I think if we add the bulk load to field storage definition load as well then we have a net positive on query count as these happen first on multilingual sites, and then I think we could explore if we want to take the hit on extra cache get misses on cold cache so we know which ones aren't cached yet and write them back on the cache from the bulk-loaded list. I think that would also increase the chances of fewer requests needing to bulk load as they start to hit the if they're slightly behind the leading one, plus there's the lock thing anyway. Then we can revisit the bulk load if/when we no longer have a field map

I'll also file a follow-up to consider a lock on views data and one to get rid of the config entities.

berdir’s picture

Status: Needs work » Needs review

The separate all list is actually needed, what happened before is that there were some cache hits, then miss, which fell back to loadAll, but that had already partially loaded data and returned early.

This has higher cache get/set counts on cold caches, but total queries don't go up (I kind of expected them to, but I guess I tested cache clear on drush and on web requests, first active entity type definition is still the first thing to be called) and it should reach a stable cache quicker.

Didn't update all performance test yet, can't really be bothered anymore to do manual query list updates after working with #3618432: Performance tests: Move expected metrics and queries to files, for better diffs and automated updates I think I'll just focus on pushing that through.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new91 bytes

The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

berdir’s picture

Status: Needs work » Needs review

Rebased and updated all performance tests now.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new91 bytes

The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

berdir changed the visibility of the branch 3617152-entitylastinstalledschemarepositorygetlastinstalleddefinition-loads-all to hidden.

berdir’s picture

Status: Needs work » Needs review

bot was confused about the multiple open branches.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new91 bytes

The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

berdir’s picture

Status: Needs work » Needs review

Rebased again.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new91 bytes

The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

berdir’s picture

Status: Needs work » Needs review

Rebaased.