Problem/Motivation

The EntityFieldManager stores the relationship between fields, types, and bundles in both a regular cache item and the key/value store. The key/value store is updated on field/bundle crud. This was introduced ten years ago in #2482295: Rebuilding field map with many bundles/fields is very slow. It has a tendency to get out of sync with the reality of the site, e.g. defining a new bundle field in hook_entity_bundle_field_info() does not cause the new field to make its way into the cache. It's also not easy to figure out where a manual update is required vs. not.

The key/value map was introduced to avoid loading field definitions on cold caches - partly due to memory, but also due to the very large number of database queries that were executed by the field module. These database queries have been consolidated in #3537863: Optimize field module's hook_entity_bundle_info() implementation and #3538006: Optimize EntityFieldManager::buildBundleFieldDefinitions(). Core performance tests show that by removing the key/value optimisation, we either add a negligible amount of database queries/cache operations, or in some scenarios remove one or two. This is because the full bundle info is often requested on the same requests that get the key/value map - e.g. where views data is built.

Proposed resolution

Instead of getting the field/bundle map from key/value, get it from the (now optimized) regular bundle/field info instead.

Remaining tasks

Decide if we want to take the full step of removing the key/value supporting code in this issue or a follow-up.

User interface changes

None.

API changes

There will eventually be some deprecations. Also bundle fields that used to now show up where they should (like layout builder blocks) now will.

Data model changes

Probably none.

Issue fork drupal-3045509

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

andrewbelcher created an issue. See original summary.

joachim’s picture

Title: Fields added hook_entity_bundle_field_info don't get entered into the bundle_field_map on bundle creation » EntityFieldManager::getFieldMap() doesn't show bundle fields

Confirming this.

> Fields added hook_entity_bundle_field_info don't get entered into the bundle_field_map on bundle creation

But bundle creation isn't the only problem. If you add a new bundle field in code, it doesn't get added to the map either.

Basically, EntityFieldManager::getFieldMap() doesn't show bundle fields that are defined in code, only those that are config fields.

However, the reason that a state key is used for bundle fields is explained in the comment:

        // In the second step, the per-bundle fields are added, based on the
        // persistent bundle field map stored in a key value collection. This
        // data is managed in the EntityManager::onFieldDefinitionCreate()
        // and EntityManager::onFieldDefinitionDelete() methods. Rebuilding this
        // information in the same way as base fields would not scale, as the
        // time to query would grow exponentially with more fields and bundles.
        // A cache would be deleted during cache clears, which is the only time
        // it is needed, so a key value collection is used.

So if I am reading that correctly, it's stored in the key-value system because querying for all the config field entities would not scale, because they are stored in the database.

But fields added with hook_entity_bundle_field_info() are defined in code, so they don't have this problem.
EntityFieldManager::getFieldMap() could just invoke hook_entity_bundle_field_info() and return the result.

BUT field module implements that hook to return config fields to the field system (https://api.drupal.org/api/drupal/core%21modules%21field%21field.module/...), so we can't invoke that hook as it would cause the problem that the use of the key-value system is working around. I guess we could get a list of each implementation from the module manager, and skip field module's implementation? That feels pretty ugly though.

I'm changing the issue title to the effective problem, as I am not sure what the best fix is here.

berdir’s picture

> EntityFieldManager::getFieldMap() could just invoke hook_entity_bundle_field_info() and return the result.

No it can't because that's exactly how field.module exposes its own bundle fields too.

And the hook needs to be called for all entity types and all bundles.. exactly that is the performance problem about the whole thing.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

mupsi’s picture

Hi there! I'm adding a related issue open for Layout Builder: https://www.drupal.org/project/drupal/issues/3034979

There is a working patch there, but the general agreement seems to be that it's not a viable approach on the long term. Making sure that getFieldMap() returns fields declared in hook_entity_bundle_field_info is a much cleaner way to solve the issue.

As of now, without this patch, computed fields and layout builder can't coexist. It even breaks sites if layout builder is enabled on view modes with active computed fields, as explained on this issue: https://www.drupal.org/project/computed_field_plugin/issues/3093367 (just to be clear, the issue is open on 'Computed Field Plugin' but it actually happens for all computed fields; it's not linked to the module at all).

Any ideas on which direction to go to fix this issue with getFieldMap()? I'd like to help but I really don't know where to start.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

gease’s picture

For the moment, the only solution seems to be implementing
\Drupal::service('field_definition.listener')->onFieldDefinitionCreate($field_definition); in module's hook_install() (of module that defines any field through hook_entity_bundle_field_info()
On the larger scale, there can hardly be any solution than to introduce another hook or in general to make fields from configuration and fields from code be discovered with different mechanisms.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

m.stenta’s picture

We ran into this in farmOS, where @symbioquine discovered that bundle fields defined via hook_entity_bundle_field_info() are not writable via JSON:API.

This is probably worth mentioning in the issue description. I would be curious what other systems depend on the field map for proper functioning. This is the first we've run into.

I guess we could get a list of each implementation from the module manager, and skip field module's implementation? That feels pretty ugly though.

@joachim seems to outline all the considerations very well in comment #2. And although it feels ugly, this approach may be the "best" way to solve this.

To summarize/restate the problem:

EntityFieldModule::getFieldMap() currently does not load bundle fields defined by hook_entity_bundle_field_info() (reference). It ONLY loads bundle fields defined in config, and it does this in its own special way. It could conceivably get ALL bundle field definitions by invoking hook_entity_bundle_field_info(), because the field module implements that to define config fields, but it sounds like there is a concern about the scalability/performance of that, per the code comment that @joachim references:

the time to query would grow exponentially with more fields and bundles

For reference, here is where that performance logic and comment were added originally: #2482295: Rebuilding field map with many bundles/fields is very slow / commit: https://git.drupalcode.org/project/drupal/-/commit/0f881613ef53745ad4bab...

Ultimately we want all the bundle fields defined by hook_entity_bundle_field_info() to be included in the field map, including the ones that are defined by field module, but because of that performance concern, it sounds like the field module's definitions need to be handled separately. It feels ugly to have to make that exception for the field module, but perhaps that is the "least bad" option.

One question this brings to mind, though: do we need to worry about the same performance concern with other implementations of hook_entity_bundle_field_info()? Or is the concern mostly with config fields managed by the field module, I wonder?

I am exploring a workaround for this in the farmOS codebase... if I have any success I'll share it here to keep the conversation going...

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

wim leers’s picture

CRZDEV made their first commit to this issue’s fork.

crzdev’s picture

Adding basis version & MR to 10.3.x & 11.x trying at least to keep functionallity for layout builder of https://www.drupal.org/project/drupal/issues/3034979

m.stenta’s picture

The work being done in #3129179: Provide some way to rebuild the persistent bundle field map may provided a way forward for us in farmOS.

That issue adds a new method for rebuilding the bundle field map, like so:

\Drupal::service('entity_field.manager')->rebuildBundleFieldMap();

A quick workaround for this issue is to implement hook_modules_installed() and hook_modules_uninstalled() to call that method whenever modules are installed/uninstalled. That covers fields that are added/removed with a module. However, if the module adds a bundle field in a new version, it will also need to include an update hook that runs the method.

If it's useful for anyone, this is the pull request we are considering for fixing this in the farmOS ecosystem (until a proper core fix emerges): https://github.com/farmOS/farmOS/pull/854

m.stenta’s picture

catch’s picture

Just ran into #3034979: Layout builder doesn't support bundle computed field

I think we should take this out of key/value.

We could have field module implement similar logic in it's bundle field info hook instead maybe.

joachim’s picture

dmitry.korhov’s picture

StatusFileSize
new1.43 KB
catch’s picture

Status: Active » Needs work

I think the field map key value collection is rendered redundant by #3537863: Optimize field module's hook_entity_bundle_info() implementation which optimized field_entity_bundle_field_info() itself, not to mention that was getting called despite the field map on cold cache requests.

So I'm looking at whether we can remove it entirely.

catch’s picture

Yep it's redundant - performance tests are more or less neutral, arguably a small improvement.

Following tests broke in a notable way:

1. A couple of unit tests that are testing the key/value behaviour specifically. Tending towards deleting those methods because they are very much testing the implementation and having to mock too much.

2. One unit test that creates a bundle on the entity_test_update entity type, which never actually gets created because entity_test doesn't manage the entity type - this is the key/value implementation hiding some bugs - I think the field gets created for a bundle that doesn't exist.

Not quite ready for review yet but getting closer.

catch’s picture

Title: EntityFieldManager::getFieldMap() doesn't show bundle fields » EntityFieldManager key/value field map gets out of sync, doesn't recognise bundle fields
Priority: Normal » Major
Status: Needs work » Needs review

Started to look at removing the key/value implementation itself, but there's a fair amount to do there.

I think the current MR is sufficient to fix the bug here, so moving to needs review.

We'll also need to remove the key/value implementation, and the supporting code + test coverage, but we could consider backporting the bug fix to 11.3.x, which would be more or less the current MR. That leaves either having two MRs here, one with the full removal/deprecation, or possibly doing that part in a follow-up to this issue.

catch changed the visibility of the branch 3045509-entityfieldmanagergetfieldmap-doesnt-show to hidden.

catch’s picture

Issue summary: View changes
catch’s picture

Profiled with xhprof on the front page of Umami, cold cache:

HEAD:

Overall Summary	
Total Incl. Wall Time (microsec):	6,486,853 microsecs
Total Incl. MemUse (bytes):	44,286,080 bytes
Total Incl. PeakMemUse (bytes):	48,065,536 bytes
Number of Function Calls:	1,698,642

MR:

Overall Summary	
Total Incl. Wall Time (microsec):	6,492,833 microsecs
Total Incl. MemUse (bytes):	44,348,560 bytes
Total Incl. PeakMemUse (bytes):	46,360,624 bytes
Number of Function Calls:	1,697,197

At a high level, it's not making a lot of difference either way. Some of the memory usage changes are probably from switching branches etc., I don't think this MR has any chance of actually reducing memory usage, but it does show it's not hugely inflating it either.

I also checked EntityFieldManager::getFieldDefinitions() specifically, since that's the method that is most likely to be called more often. It's called 370 times with the MR vs. 350 times without.

This difference translates into an extra four calls to ::buildBundleFieldDefinitions - 16 in HEAD, 20 with the MR.

So the field map is preventing some processing, but the percentage is quite small compared to what has to happen anyway.

On completely cold caches, which is the only place that we access the key/value map because it's behind a cache item, we also have #3537962: EntityFieldManager::getFieldDefinitions() per-bundle caching can be expensive still to do, which would further reduce the number of cache operations from building this data. That might be enough to cancel any performance regression out entirely.

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.

catch’s picture

#3537962: EntityFieldManager::getFieldDefinitions() per-bundle caching can be expensive now has an MR, so combined with the changes here:

11.x latest:

Overall Summary	
Total Incl. Wall Time (microsec):	2,593,851 microsecs
Total Incl. MemUse (bytes):	44,158,248 bytes
Total Incl. PeakMemUse (bytes):	46,170,232 bytes
Number of Function Calls:	1,650,670

This issue + #3537962: EntityFieldManager::getFieldDefinitions() per-bundle caching can be expensive rebased on 11.x:

Total Incl. Wall Time (microsec):	2,209,596 microsecs
Total Incl. MemUse (bytes):	44,374,416 bytes
Total Incl. PeakMemUse (bytes):	46,390,112 bytes
Number of Function Calls:	1,650,909

There is still the additional four calls to ::buildBundleFieldDefinitions() as expected, but the overall difference in function calls and memory usage is pretty imperceptible. Pretty confident that this is the reduction in database queries, cache sets and gets vs. the additional hook invocations. Given it also allows us to remove a lot of code and fix several bugs not really seeing a downside.

catch’s picture

Status: Needs work » Needs review

We can go even further as well - opened #3565128: Avoid calls to EntityFieldManager::getFieldMap() where unnecessary which theoretically could remove all the cold cache calls to the field map in core, which ought to remove those four additional buildBundleField() calls 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.

catch’s picture

Status: Needs work » Needs review

Rebased.

godotislate’s picture

A couple questions on the MR. One has to do with deprecation and clean up, which is a remaining task in the IS. I think we can do the clean up here? There's also code in Drupal\Core\Field\FieldDefinitionListener::onFieldDefinitionCreate() that gets and sets the map from keyvalue, and maybe that (and the associated test code) can be removed as well. Presumably an entry in the keyvalue store is not API, so not sure if that needs to be deprecated. But we can add deprecation warnings to the class constructors for the key value argument removal.

catch’s picture

One has to do with deprecation and clean up, which is a remaining task in the IS. I think we can do the clean up here?

I think it depends if we think the current MR is backportable to 11.3.x, if it is, I'd rather open a follow-up for the cleanup + deprecations because there's going to be a lot of it. Otherwise we'll need separate 11.x and 11.3.x MRs here anyway. I don't mind very much, but that's what made me stop (shortly after I started ripping things out).

godotislate’s picture

I think it depends if we think the current MR is backportable to 11.3.x, if it is, I'd rather open a follow-up for the cleanup + deprecations because there's going to be a lot of it.

Makes sense. Though maybe getting/setting keyvalue in Drupal\Core\Field\FieldDefinitionListener::onFieldDefinitionCreate() and ::onFieldDefinitionDelete()can be removed here? It will save the select and insert queries.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

smustgrave’s picture

Status: Needs review » Needs work

Think the bot took some time off but this one has merge conflicts it appears.

amateescu’s picture

Issue tags: -Needs tests

I was about to suggest using the cache pre-warming mechanism for the field map, but looked at the code and found out that's already done.

Still NW for #42 and the merge conflicts, otherwise I've read this a few times in past weeks and I think it looks great.

catch’s picture

Status: Needs work » Needs review

Rebased and addressed #42.

catch’s picture

Reverted the commit that addresses #42, it causes unit test failures, we'll then have to adjust those tests to expect the key value store to do nothing, when we'll be able to remove it in the follow-up which will require adjusting them again. I still think being able to backport this to a patch release is more important than the deprecations.

Will open that follow-up now so what needs to get done doesn't get lost.

catch’s picture

godotislate’s picture

Status: Needs review » Reviewed & tested by the community

This lgtm now.

godotislate’s picture

@catch is that latest commit (ea6bd872) something pushed to the wrong branch?

catch’s picture

@godotislate - it is, but also I removed it again and force-pushed so it's no longer there (except as a ghost on this issue).

  • amateescu committed 502de2e8 on main
    fix: #3045509 EntityFieldManager key/value field map gets out of sync,...
amateescu’s picture

Version: main » 11.x-dev
Status: Reviewed & tested by the community » Patch (to be ported)

Committed 502de2e and pushed to main. Thanks!

We'll need a backport MR for 11.x and 11.3.x :)

catch’s picture

Status: Patch (to be ported) » Reviewed & tested by the community

Made an 11.x backport MR. The only conflicts were in performance test assertions so moving directly back to RTBC.

  • amateescu committed bef52312 on 11.x
    fix: #3045509 EntityFieldManager key/value field map gets out of sync,...
amateescu’s picture

Version: 11.x-dev » 11.3.x-dev
Status: Reviewed & tested by the community » Patch (to be ported)

Committed bef5231 and pushed to 11.x. Thanks!

Needs another MR for 11.3.x...

catch’s picture

Status: Patch (to be ported) » Reviewed & tested by the community

11.3 MR is up - just performance tests again.

  • amateescu committed 69b779f7 on 11.3.x
    fix: #3045509 EntityFieldManager key/value field map gets out of sync,...
amateescu’s picture

Status: Reviewed & tested by the community » Fixed

Committed 69b779f and pushed to 11.3.x. Thanks!

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

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