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

Comments

dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/Entity/EntityManager.php
    @@ -680,6 +698,50 @@ public function getFieldMapByFieldType($field_type) {
    +    if (!isset($field_bundle_map[$field_definition->getName()])) {
    +      $field_bundle_map[$field_definition->getName()] = [
    +        'type' => $field_definition->getType(),
    +        'bundles' => [],
    +      ];
    

    This is a bit confusing, don't we want to always override existing entries?

  2. +++ b/core/lib/Drupal/Core/Entity/EntityManager.php
    @@ -680,6 +698,50 @@ public function getFieldMapByFieldType($field_type) {
    +    $this->cacheBackend->delete('entity_field_map');
    +    $this->fieldMap = [];
    ...
    +    $this->cacheBackend->delete('entity_field_map');
    +    $this->fieldMap = [];
    

    Should we have a dedicated reset method for the field map?

  3. +++ b/core/lib/Drupal/Core/Field/FieldDefinitionListenerInterface.php
    @@ -0,0 +1,47 @@
    +
    +
    

    nitpick: 2 empty lines

berdir’s picture

Thanks.

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.

larowlan’s picture

At 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?

  1. +++ b/core/lib/Drupal/Core/Entity/EntityManager.php
    @@ -680,6 +698,50 @@ public function getFieldMapByFieldType($field_type) {
    +        'bundles' => [],
    ...
    +    $field_bundle_map[$field_definition->getName()]['bundles'][] = $field_definition->getTargetBundle();
    ...
    +    $key = array_search($field_definition->getTargetBundle(), $field_bundle_map[$field_definition->getName()]['bundles']);
    ...
    +      $field_bundle_map[$field_definition->getName()]['bundles'] = array_values($field_bundle_map[$field_definition->getName()]['bundles']);
    

    If we keyed these by bundle, we could save the array_search right?

  2. +++ b/core/lib/Drupal/Core/Field/FieldDefinitionListenerInterface.php
    @@ -0,0 +1,47 @@
    + * Defines an interface for reacting to field definition creation, deletion, and updates.
    

    nit > 80

  3. +++ b/core/lib/Drupal/Core/Field/FieldDefinitionListenerInterface.php
    @@ -0,0 +1,47 @@
    +   * @see purgeFieldData()
    

    Should this be a FQ reference?

berdir’s picture

That 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.

berdir’s picture

Addressing those reviews and my todos.

amateescu’s picture

+++ b/core/lib/Drupal/Core/Entity/EntityManager.php
@@ -680,6 +698,70 @@ public function getFieldMapByFieldType($field_type) {
+    $this->getStorage($field_definition->getTargetEntityTypeId())->onFieldDefinitionCreate($field_definition);
+    $field_bundle_map = $this->keyValueFactory->get('entity.definitions.field_map')->get($field_definition->getTargetEntityTypeId());
...
+    $this->getStorage($field_definition->getTargetEntityTypeId())->onFieldDefinitionDelete($field_definition);
+    $field_bundle_map = $this->keyValueFactory->get('entity.definitions.field_map')->get($field_definition->getTargetEntityTypeId());

It 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.

+++ b/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.module
@@ -68,6 +68,21 @@ function entity_schema_test_entity_bundle_field_info(EntityTypeInterface $entity
+    // Notify the entity storage that our field is gone.
+    \Drupal::entityManager()->onFieldDefinitionCreate($field_definitions['custom_bundle_field']);

The comment doesn't sound right :)

berdir’s picture

1. 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.

yched’s picture

+1 on the concept, that's probably our best way out.

  1. +++ b/core/lib/Drupal/Core/Entity/EntityManager.php
    @@ -642,19 +645,34 @@ public function getFieldMap() {
    +            $base_fields = $this->getBaseFieldDefinitions($entity_type_id);
    +            foreach ($base_fields as $field_name => $base_field_definition) {
    

    I would tend to keep the inline var, but matter of taste, feel free to ignore

  2. +++ b/core/lib/Drupal/Core/Entity/EntityManager.php
    @@ -642,19 +645,34 @@ public function getFieldMap() {
    +              $this->fieldMap[$entity_type_id][$field_name]['type'] = $base_field_definition->getType();
    +              $this->fieldMap[$entity_type_id][$field_name]['bundles'] = array_combine($bundles, $bundles);
    

    For clarity, this could be

    $this->fieldMap[$entity_type_id][$field_name] = [
      'type' => ...,
      'bundles' => ...,
    ];
    

    since this is initializing the entries ?

  3. +++ b/core/lib/Drupal/Core/Entity/EntityManager.php
    @@ -642,19 +645,34 @@ public function getFieldMap() {
    +          foreach ($field_bundle_map as $field_name => $field_bundles_type) {
    

    $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 ?

  4. +++ b/core/lib/Drupal/Core/Entity/EntityManager.php
    @@ -680,6 +698,83 @@ public function getFieldMapByFieldType($field_type) {
    +    $field_bundle_map = $this->keyValueFactory->get('entity.definitions.field_map')->get($entity_type_id);
    

    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 :-)

  5. +++ b/core/modules/field/src/Entity/FieldConfig.php
    @@ -160,7 +160,7 @@ public function preSave(EntityStorageInterface $storage) {
    -      $entity_manager->getStorage($this->entity_type)->onFieldDefinitionCreate($this);
    +      $entity_manager->onFieldDefinitionCreate($this);
    
    @@ -174,7 +174,7 @@ public function preSave(EntityStorageInterface $storage) {
    -      $entity_manager->getStorage($this->entity_type)->onFieldDefinitionUpdate($this, $this->original);
    +      $entity_manager->onFieldDefinitionUpdate($this, $this->original);
    
    @@ -221,7 +221,7 @@ public static function postDelete(EntityStorageInterface $storage, array $fields
    -        \Drupal::entityManager()->getStorage($field->entity_type)->onFieldDefinitionDelete($field);
    +        \Drupal::entityManager()->onFieldDefinitionDelete($field);
    

    <3

berdir’s picture

StatusFileSize
new39.52 KB
new4.42 KB

1. 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 :)

The last submitted patch, 7: field-map-state-2482295-7.patch, failed testing.

yched’s picture

re : 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 ?

Status: Needs review » Needs work

The last submitted patch, 9: field-map-state-2482295-9.patch, failed testing.

alexpott’s picture

Issue tags: +Triaged D8 critical

Discussed 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.

berdir’s picture

Status: Needs work » Needs review
StatusFileSize
new39.51 KB
new3.69 KB

Renamed 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.

yched’s picture

Status: Needs review » Reviewed & tested by the community

Thanks for bearing with me :-)
This looks ready to me.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 14: field-map-state-2482295-14.patch, failed testing.

The last submitted patch, 9: field-map-state-2482295-9.patch, failed testing.

The last submitted patch, 14: field-map-state-2482295-14.patch, failed testing.

berdir’s picture

Status: Needs work » Needs review
StatusFileSize
new39.56 KB
new2.94 KB

Forgot to update the unit test.

amateescu’s picture

Status: Needs review » Reviewed & tested by the community

Let's do eet :)

catch’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/lib/Drupal/Core/Entity/EntityManager.php
    @@ -680,6 +699,85 @@ public function getFieldMapByFieldType($field_type) {
    +    $this->keyValueFactory->get('entity.definitions.bundle_field_map')->set($entity_type_id, $bundle_field_map);
    

    Still 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.

  2. +++ b/core/lib/Drupal/Core/Entity/EntityManager.php
    @@ -680,6 +699,85 @@ public function getFieldMapByFieldType($field_type) {
    +    // If the field map is initalized, update it as well, so that calls to it
    

    Nit: initialized.

  3. +++ b/core/lib/Drupal/Core/Entity/EntityManager.php
    @@ -680,6 +699,85 @@ public function getFieldMapByFieldType($field_type) {
    +    // If the field map is initalized, update it as well, so that calls to it
    

    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.

berdir’s picture

1. 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:

$ time drush cr
Cache rebuild complete. 

real	3m3.615s
user	3m1.280s
sys	0m0.436s

Patch:

$ time drush cr
Cache rebuild complete.

real	0m7.387s
user	0m6.212s
sys	0m0.180s

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:

$ drush scr alex_create.php 
0: Created bundle with 10 fields in 0.15s
1: Created bundle with 10 fields in 0.14s
2: Created bundle with 10 fields in 0.19s
3: Created bundle with 10 fields in 0.16s
4: Created bundle with 10 fields in 0.13s
5: Created bundle with 10 fields in 0.13s
6: Created bundle with 10 fields in 0.16s
7: Created bundle with 10 fields in 0.16s
8: Created bundle with 10 fields in 0.16s
9: Created bundle with 10 fields in 0.15s
10: Created bundle with 10 fields in 0.17s
11: Created bundle with 10 fields in 0.18s
12: Created bundle with 10 fields in 0.2s
13: Created bundle with 10 fields in 0.13s
14: Created bundle with 10 fields in 0.16s
15: Created bundle with 10 fields in 0.15s
16: Created bundle with 10 fields in 0.14s
17: Created bundle with 10 fields in 0.12s
18: Created bundle with 10 fields in 0.12s
19: Created bundle with 10 fields in 0.16s
20: Created bundle with 10 fields in 0.12s
21: Created bundle with 10 fields in 0.14s
22: Created bundle with 10 fields in 0.13s
23: Created bundle with 10 fields in 0.09s
24: Created bundle with 10 fields in 0.09s
25: Created bundle with 10 fields in 0.11s
26: Created bundle with 10 fields in 0.16s
27: Created bundle with 10 fields in 0.14s
28: Created bundle with 10 fields in 0.12s
29: Created bundle with 10 fields in 0.12s
30: Created bundle with 10 fields in 0.16s
31: Created bundle with 10 fields in 0.16s
32: Created bundle with 10 fields in 0.15s
33: Created bundle with 10 fields in 0.16s
34: Created bundle with 10 fields in 0.16s
35: Created bundle with 10 fields in 0.18s
36: Created bundle with 10 fields in 0.17s
37: Created bundle with 10 fields in 0.13s
38: Created bundle with 10 fields in 0.14s
39: Created bundle with 10 fields in 0.14s
40: Created bundle with 10 fields in 0.11s
41: Created bundle with 10 fields in 0.17s
42: Created bundle with 10 fields in 0.17s
43: Created bundle with 10 fields in 0.15s
44: Created bundle with 10 fields in 0.13s
45: Created bundle with 10 fields in 0.12s
46: Created bundle with 10 fields in 0.09s
47: Created bundle with 10 fields in 0.1s
48: Created bundle with 10 fields in 0.1s
49: Created bundle with 10 fields in 0.12s
50: Created 50 bundle with 10 fields each in 7.08s


$ drush scr alex_delete.php 
Deleted field storage contact_message.field_map_storage_0 7.79s
Deleted field storage contact_message.field_map_storage_1 6.88s
Deleted field storage contact_message.field_map_storage_2 6.85s
Deleted field storage contact_message.field_map_storage_3 5.93s
Deleted field storage contact_message.field_map_storage_4 6.88s
Deleted field storage contact_message.field_map_storage_5 6.14s
Deleted field storage contact_message.field_map_storage_6 5.87s
Deleted field storage contact_message.field_map_storage_7 5.17s
Deleted field storage contact_message.field_map_storage_8 5.26s

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:

$ drush scr alex_delete.php 
Deleted field storage contact_message.field_map_storage_0 5.86s
Deleted field storage contact_message.field_map_storage_1 5.75s
Deleted field storage contact_message.field_map_storage_2 5.55s
Deleted field storage contact_message.field_map_storage_3 5.28s
Deleted field storage contact_message.field_map_storage_4 4.82s
Deleted field storage contact_message.field_map_storage_5 4.25s
Deleted field storage contact_message.field_map_storage_6 4.04s
Deleted field storage contact_message.field_map_storage_7 3.82s
Deleted field storage contact_message.field_map_storage_8 3.53s
Deleted field storage contact_message.field_map_storage_9 3.42s

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.

Status: Needs review » Needs work

The last submitted patch, 24: field-map-state-2482295-24-interdiff.patch, failed testing.

berdir’s picture

Status: Needs work » Needs review

I apparently forgot how to interdiff. Shouldn't upload patches at 1am.

alexpott’s picture

Nice 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.

The last submitted patch, 24: field-map-state-2482295-24.patch, failed testing.

The last submitted patch, 24: field-map-state-2482295-24.patch, failed testing.

berdir’s picture

Status: Needs review » Needs work

Well, 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.

berdir’s picture

Status: Needs work » Needs review
StatusFileSize
new40.38 KB
new1.42 KB

As 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.

amateescu’s picture

Assigned: Unassigned » catch
Status: Needs review » Reviewed & tested by the community

The new documentation is really helpful. Let's see if @catch feels the same way :)

berdir’s picture

If @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.

yched’s picture

@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 :

  1. +++ b/core/lib/Drupal/Core/Entity/EntityManager.php
    @@ -680,6 +707,90 @@ public function getFieldMapByFieldType($field_type) {
    +    if (!isset($bundle_field_map[$field_name])) {
    +      // This field did not exist yet, initialize it with the type and empty
    +      // bundle list.
    +      $bundle_field_map[$field_name] = [
    +        'type' => $field_definition->getType(),
    +        'bundles' => [],
    +      ];
    +    }
    +    $bundle_field_map[$field_name]['bundles'][$bundle] = $bundle;
    +
    +    // Update the field map collection.
    +    $this->keyValueFactory->get('entity.definitions.bundle_field_map')->set($entity_type_id, $bundle_field_map);
    +    $this->cacheBackend->delete('entity_field_map');
    

    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 ?)

  2. +++ b/core/lib/Drupal/Core/Entity/EntityManager.php
    @@ -680,6 +707,90 @@ public function getFieldMapByFieldType($field_type) {
    +    // Update the bundle field map, used by EntityManager::getFieldMap().
    +    $bundle_field_map = $this->keyValueFactory->get('entity.definitions.bundle_field_map')->get($entity_type_id);
    +
    +    // Unset the bundle. If there are no bundles left, remove the field from
    +    // the map.
    +    unset($bundle_field_map[$field_name]['bundles'][$bundle]);
    +    if (empty($bundle_field_map[$field_name]['bundles'])) {
    +      unset($bundle_field_map[$field_name]);
    +    }
    +    $this->keyValueFactory->get('entity.definitions.bundle_field_map')->set($entity_type_id, $bundle_field_map);
    +    $this->cacheBackend->delete('entity_field_map');
    

    Likewise re: code grouping and comments

berdir’s picture

StatusFileSize
new40.28 KB
new3.44 KB

@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.

yched’s picture

Thanks @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 ?

berdir’s picture

Yes, 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 :)

berdir’s picture

catch’s picture

Status: Reviewed & tested by the community » Fixed

So 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.

  • catch committed 0f88161 on 8.0.x
    Issue #2482295 by Berdir: Rebuilding field map with many bundles/fields...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.