Optimize field module's hook_entity_bundle_info() implementation

Created on 24 July 2025, 5 days ago

Problem/Motivation

I've recently been doing a performance audit of a site with a lot of webforms.

The webform module uses config entities to define the webforms (which are bundles), and these are then bundles of the webform submission entities, which are content entities.

However, webform submission entities generally do not have any configurable fields at all - the submission data goes into one field.

This brings up the question of whether they should implement DynamicallyFieldableEntityStorageInterface at all, but that would be an API change for webform module and I don't use the module enough to know what the ramifications would be. Might open an issue over there to find out.

However, I think we can heavily optimize field module's hook_entity_bundle_field_info() implementation, which currently can run hundreds of queries that return nothing, even though we can be pretty sure that it'll be called for every bundle on a site if it's called for one, given it's called by EntityFieldManager::getFieldDefinitions(). This would also be an improvement for bundles that do have fields (e.g. something like paragraphs), because we could get those config IDs in a single query instead of potentially dozens.

Steps to reproduce

Proposed resolution

Remaining tasks

User interface changes

Introduced terminology

API changes

Data model changes

Release notes snippet

📌 Task
Status

Active

Version

11.0 🔥

Component

field system

Created by

🇬🇧United Kingdom catch

Live updates comments and jobs are added and updated live.
  • Performance

    It affects performance. It is often combined with the Needs profiling tag.

Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @catch
  • Pipeline finished with Failed
    5 days ago
    Total: 439s
    #556189
  • Pipeline finished with Failed
    5 days ago
    #556263
  • Pipeline finished with Failed
    5 days ago
    Total: 637s
    #556266
  • 🇬🇧United Kingdom catch

    So this reduces the number of entity queries from 1 per bundle to one per entity type. With webform it's not hard to get to hundreds of bundles (that don't actually have any configurable fields on them), so it can reduce the number of database queries by several hundred on a cold cache. On more regular sites with say 10 entity types and 5 bundles each it would still reduce 50 queries to 10.

    Also because the first time it loads the field config entities it's for the entity type, not each bundle, it results in less database queries/cache sets / cache gets from the config entity storage too.

    Performance tests are showing the improvement (by failing - haven't updated them yet), however seeing a _lot_ of kernel test failures which is likely to be the memory cache entry not being invalidated properly yet.

  • Pipeline finished with Failed
    5 days ago
    Total: 151s
    #556286
  • Pipeline finished with Failed
    5 days ago
    Total: 549s
    #556293
  • Pipeline finished with Success
    5 days ago
    Total: 893s
    #556403
  • 🇬🇧United Kingdom catch

    A related issue is EntityFieldManager::getFieldDefinitions() - again in the webform case you can have a large number of bundles with no bundle fields at all, this means hundreds of cache sets of empty arrays on cold caches to chained fast.

    We could potentially change this to a cache by entity type? Will open a separate issue to look at that.

  • 🇬🇧United Kingdom catch
  • 🇬🇧United Kingdom catch

    Adding before/after xhprof screenshots from the 10.4 site I found this on, and also the (hacky, don't use it) backport patch I used to get the numbers.

  • 🇬🇧United Kingdom catch
  • 🇬🇧United Kingdom catch
  • 🇬🇧United Kingdom catch

    Would it make sense to enable memory cache for the field config entity type?

    I kind of thought we already do... Would need to look properly at what's going on in config entity loading.

    Found another similar issue in EntityFieldManager: 📌 Optimize EntityFieldManager::buildBundleFielDefinitions() Active .

  • I kind of thought we already do... Would need to look properly at what's going on in config entity loading.

    Right now, Drupal\Core\Entity\Attribute\ConfigEntityType has $static_cache default to FALSE,
    and the only config entity types that have static_cache: TRUE are role and language_content_settings.

    I'm not sure about the history about that or any complexities associated with enabling the static cache on more config entity types. Maybe @berdir would know?

  • 🇬🇧United Kingdom catch

    I can see it's coming \Drupal\Core\Entity\EntityFieldManager::getFieldLabels(). But that is only called if there is a field storage for a given entity type. so I assume you actually do have a configurable field there?

    That code loops over all bundles just to figure out which bundles have a given field. We already have this information, in the field map. Maybe we can prevent this higher up in the chain?

    This isn't how I'm reproducing it but it might be a similar pattern causing the problem.

    Uploading an xhprof screenshot that shows the callers (to Drupal\Core\Entity\EntityFieldManager::getFieldDefinitions) , there are 965 bundles on the site (not all webforms, also paragraphs etc. but webforms are about 2/3rds), so each caller with 965 calls to it is looping over every bundle (unless it's an amazing coincidence).

    The main culprits are:

    Drupal\jsonapi\ResourceType\ResourceTypeRepository::getAllFieldNames() Drupal\jsonapi\ResourceType\ResourceTypeRepository::getFields()
    Drupal\jsonapi\ResourceType\ResourceTypeRepository::calculateRelatableResourceTypes()
    Drupal\search_api\Plugin\search_api\processor\ReverseEntityReferences::getEntityReferences()

    It might be that we need to follow a similar approach to your suggestion above in all four places and add some docs discouraging people from looping over all bundles.

    Confirmed that none of the webform bundles have a configurable field fwiw.

  • 🇨🇭Switzerland berdir Switzerland

    Ah, jsonapi, yeah, I didn't test with that. The call chain for me was views data.

    Maybe 📌 Change JSON:API routes from every bundle/field to per entity with arguments Active would help then? I'm assuming those calls are coming from route building. If we have those generic routes, then the need to fetch all fields and bundles would just go away... not a quickfix like this obviously.

  • 🇬🇧United Kingdom catch

    On the front page of Umami after a cache clear, I still get 15 calls to FieldHooks::entityBundleFieldInfo() - didn't check how many total bundles there are, I think this is from views data as you say.

    On Umami this is about 330ms for me, which is not tens of seconds bad, but it's still 330ms that will block rendering of a lot of pages on a site (e.g. anything with a view on it).

    For me webform + jsonapi is an extreme case and we should definitely do 📌 Change JSON:API routes from every bundle/field to per entity with arguments Active for that case, but we have a lot of code paths calling EntityFieldInfo::getFieldDefinitions() bundle by bundle and should either try to optimize that or convert cases to using the field map.

  • 🇬🇧United Kingdom catch

    I took a look at converting EntityFieldManager::getFieldLabels() to use the field map and it doesn't help unfortunately.

    The field map doesn't contain the field data itself, only which bundles have a field. Because this includes base fields/properties, every bundle is included in the field map, so you end up calling ::getFieldDefinitions() on all bundles anyway when building views data because it wants the field labels for base fields too.

    If we knew which fields had overrides or not and which ones were base vs. configured in advance, we could pre-filter, but that information is from ::getFieldDefinitions(), so short of storing that in key/value too, it's just circular.

    Opened an issue anyway since it might help in cases where you really only want the field labels for one field 📌 Use the field map in ::getFieldLabels() Active .

Production build 0.71.5 2024