Problem/Motivation
As discovered in #2473983: [meta] Evaluate Entity Field API Scalability, rebuilding the field map gets extremely expensive when you have many bundle and/or many fields.
The reason for that is that to build that field map, we need to iterate on every fieldable entity type and every bundle of them, to fetch all their field definitions for each bundle. That means that field_entity_bundle_field_info() needs to be run for every bundle, and with a lot of field configs, that can get very slow, as the query logic is implemented in PHP.
Tests have shown that rebuilding the field map with 300 bundles * 20 fields takes 60s. That is way above an acceptable value and critical, since that happens on normal page requests with a cold cache when for example comment.module is enabled. Or when fields are deleted, see #2482231: Deleting configuration entities is super slow once you have a few..
Not explicitly tested, but loading all those configs will also take considerable amounts of memory.
Proposed resolution
While #2247379: Optimize config entity query conditions on ID will help quite a bit with the performance of field_entity_bundle_field_info() for a single run, as it means we can run the filter solely on the ID's and don't have to load all those configs into memory, at least the memory consumption will not go down considerably, in the end, we still need to load *all the field configs* into memory.
The proposed solution is to introduced a persistent field map for per-bundle fields in the key value storage, any module that adds those needs to inform the entity manager about them. There are existing methods for similar purposes already but they only exist on the storage. This extracts them into a separate interface and moves them to the entity manager, which also notifies the storage now.
This is an API change, but code actually gets simpler, and it also allows us to add an event if we want to, similar to field storage/entity type changes.
For base fields, the approach of looping over them is kept. The overhead of that is by far not as bad, we do not expect hundreds of fieldable entity types and it would be more complex to either have a separate field map for them or even manage them by bundle when we know that they will exist on all of them.
Uploading the existing patch from #2473983: [meta] Evaluate Entity Field API Scalability
Remaining tasks
* Extend OnFieldDefinitionCreate/Delete to update $this->fieldMap directly. That should result in another performance boost for deleting fields, as we can use the already updated field map and don't have to recalculate it. We will not persist it to avoid conflicting cache writes, just to speed up the running request.
* Find other places that add by-bundle fields, AFAIK currently only tests and ensure that they call the relevant methods on the entity manager.
* Create a change record or update existing ones.
* This will also need an upgrade path in head2head, but it shouldn't be complicated. We just need to run the old implementation once more to initialize the persistent by-bundle field map.
User interface changes
API changes
| Comment | File | Size | Author |
|---|---|---|---|
| #35 | field-map-state-2482295-35-interdiff.txt | 3.44 KB | berdir |
| #35 | field-map-state-2482295-35.patch | 40.28 KB | berdir |
| #24 | field_create_output.txt | 21.45 KB | berdir |
Comments
Comment #1
dawehnerThis is a bit confusing, don't we want to always override existing entries?
Should we have a dedicated reset method for the field map?
nitpick: 2 empty lines
Comment #2
berdirThanks.
1. We never want to override existing data? Adding the same field to another bundle should not result in replacing the information that it exists on the first bundle?
2. Right now it is the same code, but see the first point in remaining tasks. The code there will soon be slightly different because I want to update $this->fieldMap if it is already loaded instead of just dropping it and building it up again completely on the next call.
Comment #3
larowlanAt one stage we were striving to reduce the size/complexity of EntityManagerInterface, but this seems to going against that trend? Is there somewhere else this could live?
If we keyed these by bundle, we could save the array_search right?
nit > 80
Should this be a FQ reference?
Comment #4
berdirThat trend never got further than being created as an issue I think :)
We have field storage definition listeners there as well, so for now, this is the correct place IMHO.
1. Yeah. Not quite sure if we want to expose them in the public method, but we can still just array_values() the bundle list there.
Comment #5
berdirAddressing those reviews and my todos.
Comment #6
amateescu commentedIt would be useful to add an empty line here to show that the call to the storage handler is not related to the field map stuff below.
Also, in both methods, it looks like we can save a few stack calls if we initialize $target_entity_type_id with $field_definition->getTargetEntityTypeId() and $field_name with $field_definition->getName() from the start. For an install profile that adds 100 fields, it means we can save 800 stack calls.
The comment doesn't sound right :)
Comment #7
berdir1. Makes sense. I doubt that the additional stack calls make a difference, but we can make many of those long lines *a lot* shorter by using local variables.
2. Fixed the comment.
Comment #8
yched commented+1 on the concept, that's probably our best way out.
I would tend to keep the inline var, but matter of taste, feel free to ignore
For clarity, this could be
since this is initializing the entries ?
$field_bundles_type is not a great var name ;-) It is indeed an array with the 'bundles' and 'type' entries for the field, but well, that's a bit litteral (and coupled to the current content)
What about $map_entry ?
Missing a comment for the code block ?
'// Update the field map collection" ?
Related : the collection name (entity.definitions.field_map) and the var names that refer to its content ($field_bundle_map, $entity_field_bundle_map) are a bit confusing with respect to $this->fieldMap.
The former is the subset of the latter for non-base fields, right ? Not sure how we could make that relationship clearer, but calling them both field_map is slippery :-)
<3
Comment #9
berdir1. I'm fine with removing the local variable (that's what you meant, right?)
2. Sure
3. $map_entry works for me. Nested loops are always tricky with variable names :)
4. Added comment, changed to entity.definitions.field_bundle_map, which is the same as the local variable, and you didn't have any complaints about that one :)
Comment #11
yched commentedre : entity.definitions.field_bundle_map / $field_bundle_map
right, so consistency between the two is cool, but the name is not too telling either :-)
We have "the field map", which is internally built on top of a smaller map stored in a collection and manually updated for non-base fields (= configurable fields ? bundle fields ?)
--> maybe bundle_field_map ? config_field_map ?
Comment #13
alexpottDiscussed with @catch, @effulgentsia, @webchick and @xjm. This meets the performance criteria for a critical. The performance improvement is measured in seconds once there are more than a few hundred fields.
Comment #14
berdirRenamed to bundle_field_map, also one case that I missed before. Made the getAll() $bundle_field_map*s. I don't think config_field_map is correct, there could be bundle fields that aren't config, like the test implementation that I updated to make sure it's present in the field map.
Comment #15
yched commentedThanks for bearing with me :-)
This looks ready to me.
Comment #21
berdirForgot to update the unit test.
Comment #22
amateescu commentedLet's do eet :)
Comment #23
catchStill struggling with this being in key/value and not cache, especially given the gets/sets are right next to cache gets/sets. A cache being expensive to build isn't sufficient reason to put something into key/value for me - if the issues is invalidations we can and have been trying to reduce the frequency of those.
If it really, really does have to be in key/value, then it needs a comment explaining why and I couldn't see one.
Nit: initialized.
Same typo on initialized.
Overall this seems good, but also it'd be good to know numbers here now that the config issue is in.
Comment #24
berdir1. Yes, I think it really does. With 300 bundles, it took 60 *seconds* to build the field map. That's just way, way beyound what we can let a cache clear/rebuild take. The config entity query doesn't help with this, since we're doing a partial match on the ID. #2247379: Optimize config entity query conditions on ID would help, but we would still have to loop over 300 bundles and doing partial string matches on possibly many hundred array keys over and over again. And we would have to load all those config entities into memory.
I'm not sure how and where to explain this exactly, what do you think about this?
If you'd like to know how it will perform with the ID issue then we have to postpone on that and push that, which is something that we should do anyway. Because even if we commit this, we can still benefit from that for actual field definition (re-)builds.
2. PHPStorm highlighted it, but I didn't see what was wrong ;)
3. At least I'm consistent ;)
Re-run my benchmark script. As mentioned before, this issue will actually not make that faster, it is likely even a bit slower because we need to update the collection. The real difference is this:
HEAD:
Patch:
Trying to delete field storages directly with 500 bundles and *20 fields still took a very long time since that has to delete hundreds of fields first and doesn't seem like a very realistic scenario, I tried alexpott's smaller script for that:
As you can see, deleting is still way slower than creating, but it is also 5x times faster than he reported in #2482231: Deleting configuration entities is super slow once you have a few..
I did have a quick look at xhprof for create and delete...
create:
It looks we're executing a huge amount of merge queries. A lot of them are due to 3 calls to invalidateTags() (from EntityManager::clearCachedDefinitions(), ViewsData::clear() and EntityViewBuilder::resetCache()). The last one might no longer be necessary, and a big part of the slowness there is fast chained, that's invalidating the fast backend on every cache tag invalidation for every bin. So #2431259: Optimize FastChainedCacheBackend by introducing heat based shut off would help with that.
delete:
Looks like a big part of the remaining time now *is* config dependencies. I'm seeing 40% spent in ::getConfigDependencyManager(). And a large part of *that* is fetching data from the cache, fetching it from slow and putting it into the fast backend. So more stuff that the heat issue could improve. And the second big part is unfortunately still in EntityManager::getFieldMap(). The problem is that my optimization of updating $this->fieldMap doesn't actually work, because delete (and save) calls clearCachedFieldDefinitions() first. And that empties $this->fieldMap(). Since we actually no longer need that there, as we can update it, let's try to move it to clearCachedDefinitions(), where it still might be needed, for cases where the entity types list changes...
With that change, the delete output is:
So saving ~2s per deleted field, nice. And ConfigManager::getConfigDependencyManager() is now at ~60%, but we need to test the impact of the heat issue first I think.
Comment #26
berdirI apparently forgot how to interdiff. Shouldn't upload patches at 1am.
Comment #27
alexpottNice work @Berdir.
Yep I profiled the patch a couple of days ago wrt to config entity delete - it's now checking dependencies that makes this expensive.
Comment #30
berdirWell, that doesn't look pretty.
Wondering if we want to move that optimization to #2482231: Deleting configuration entities is super slow once you have a few.? I don't think that's critical. On the other side, my optimization isn't very useful without it..
An alternative fix would be to switch getBundles() to a config entity query, we could even use the new lookup key feature on the field name or so.
Comment #31
berdirAs discussed, tried to improve the docs some more and removed the fieldMap part again, let's explore that in #2482231: Deleting configuration entities is super slow once you have a few..
See also #2247379-22: Optimize config entity query conditions on ID, for some really interesting numbers.
Comment #32
amateescu commentedThe new documentation is really helpful. Let's see if @catch feels the same way :)
Comment #33
berdirIf @catch is still (very) unhappy about the key value stuff here, there is a possible alternative.
We could introduce hook_entity_field_map(), and then field.module could implement that and build that information based the list of field config ID's, more or less. We still need the type, so we still need to load the field storage configs. This is where the current benchmark scripts fall a bit short, I'd expect a lot more field storage configs than my script creates if you really have a few hundreds bundles, so a real site would likely use more memory there.
I think I prefer to use a collection/the current patch, it means we can try to make the fieldMap rebulding work and further optimize deletions, and it keeps the complexity inside the entity manager and not hooks. But if necessary, I can look into that.
Comment #34
yched commented@Berdir : I didn't really get what were the "this->fieldMap" modifications mentioned in your recent comments
("let's try to move it to clearCachedDefinitions()" in #24, "removed the fieldMap part again" in #31), I wasn't able to see them in the interdiffs ?
Other than that, nitpicks :
Nitpick, code grouping looks a bit off : all the code up to
$this->cacheBackend->delete('entity_field_map');(excluded) falls under the "// Update the bundle field map, used by EntityManager::getFieldMap()." comment ?Then, the line about $this->cacheBackend is a bit on its own, could deserve its own comment ?
(side note, it's a bit misleading that this cache delete() looks disconnected from the corresponding cache read / writes in getFieldMap(), since those use a shortcut API from DefaultPluginManager. Could getFieldMap() use regular $this->cacheBackend->get() / ->set() too ?)
Likewise re: code grouping and comments
Comment #35
berdir@yched: Sorry, not sure why it didn't end up in the interdiff at least in the patch that added. I moved $this->fieldMap = [] from clearCachedFieldDefinitions() to clearCachedDefinitions(), so that FieldConfig calling it wouldn't invalidate it and we could keep using the static cache.
1. Changed the structure and comments a bit. There is no wrapper for delete, but using them for get()/set() is correct IMHO.
2. Same as above.
Also removed some unused use statements.
Comment #36
yched commentedThanks @Berdir, code looks good.
Regarding where we should reset $this->fieldMap, in the end the current patch leaves it untouched in clearCachedFieldDefinitions().
So a FieldConfig CRUD :
- updates the collection's partial map and the static full map through onFieldDefinitionXxx(),
- and then its postSave()/postDelete() wipes the static map though clearCachedFieldDefinitions() anyway, so it will be built again from the collection if we need it again.
?
Not that I have major complaints (not ideal, but doesn't necessarily have to block this patch here), just checking I got things right.
Although, this made me notice : the existing EM::onFieldStorageDefinitionXxx() methods currently all internally take care of calling clearCachedFieldDefinitions(). For consistency, shouldn't it be the same for the similar onFieldDefinitionXxx() methods added here ?
Comment #37
berdirYes, my idea was to prevent that clearCachedFieldDefinitions() empties $this->fieldMap by moving it to clearCachedDefinitions() (we still need it when entity types are added/changed/removed)
And yes, I was wondering about moving clearing caches into the new methods as well, but I think I'd rather explore all that in a new issue (or #2482231: Deleting configuration entities is super slow once you have a few.). The test fails in #24 look nasty and I don't want to hold this up :)
Comment #38
berdirOpened #2487287: Optimize/clean up cache clears when saving/deleting FieldConfig entities.
Comment #39
catchSo I have two concerns with using key/value as opposed to just having the cache item:
1. If the key/value store gets out of sync (a failed write or similar), then neither drush cr nor rebuild.php will reset it - could be very difficult to track down/debug in those cases - you'd have to manually compare the data in each to figure out the problem probably.
2. We're using key/value here specifically because we clear caches too often. In theory it ought to be possible to have a write-through cache that's never cleared during the normal lifetime of a site - unless someone runs drush cr.
#1 isn't really resolvable here - we just have to decide whether to accept that risk vs. the risk of a site going down because of a lengthy rebuild - that is probably an OK trade-off.
#2 is a good goal, but not sure if we'll get there for 8.0.0.
Issue like #2487287: Optimize/clean up cache clears when saving/deleting FieldConfig entities should get us closer to that though, and then we could remove the k/v store in a later minor release if core itself doesn't flush the cache entry any longer.
Given all that, I've gone ahead and committed/pushed the patch to 8.0.x, but would really like to see us get to the point where it's not necessary again so we can rip it out.