Problem/Motivation

#2554235: Make the content entity storage and entity query use the last installed definitions instead of the ones living in code has resulted in a performance regression in 8.7.x and above. And it gets worse with the path alias move to entities.

For example if you have the history module enabled the post to history/NODE_ID/read is now doing 4 additional queries to the key_value table because we're retrieving the entity definitions for user and node. On 8.6.x we didn't retrieve these definitions at all. These same 4 queries are seen on the post to quickedit/metadata when viewing a node whilst logged in. And there are an additional 2 queries caused by the entity definition change during a user login.

Another thing that's seems problematic about #2554235: Make the content entity storage and entity query use the last installed definitions instead of the ones living in code is that we pass the EntityType object from code into \Drupal\Core\Entity\Sql\SqlContentEntityStorage::__construct() and then do...
parent::__construct($entity_type, $entity_field_manager, $cache, $memory_cache, $entity_type_bundle_info); which uses the passed in entity type to set some defaults. We then later do:

    $this->entityType = $this->entityTypeManager->getActiveDefinition($entity_type->id());
    $this->fieldStorageDefinitions = $this->entityFieldManager->getActiveFieldStorageDefinitions($entity_type->id());

It's these two calls that a responsible for an additional 2 queries to the key_value table (and this happens per entity type). For me there are two problems here... the additional queries but also the use of the potentially different info in the call to the parent constructor.

Proposed resolution

TBD. I created https://github.com/alexpott/prefetch_key_value to see if we could prefetch things. It makes some improvement but there we still be 1 additional query on all the above mentioned requests. And potentially if you have non-optimised entity definitions then it could be more.

Remaining tasks

User interface changes

API changes

Data model changes

Release notes snippet

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

alexpott created an issue. See original summary.

alexpott’s picture

Issue summary: View changes

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.

Berdir’s picture

Berdir’s picture

Status: Active » Needs review
FileSize
10.03 KB

What about something like this, which is consistent with how entity type/field manager cache those things. A single cache entry for all entity types and a cache entry for the field storage definitions per entity type, as they can be quite big:

> select name, length(value) as length from key_value where collection = 'entity.definitions.installed' order by length desc;
+----------------------------------------------------+--------+
| name                                               | length |
+----------------------------------------------------+--------+
| node.field_storage_definitions                     |  60272 |
| media.field_storage_definitions                    |  53352 |
| paragraph.field_storage_definitions                |  40090 |
| menu_link_content.field_storage_definitions        |  29116 |
| taxonomy_term.field_storage_definitions            |  26073 |
| block_content.field_storage_definitions            |  22656 |
| webform_submission.field_storage_definitions       |  22110 |
| crop.field_storage_definitions                     |  21423 |
| user.field_storage_definitions                     |  21009 |
| paragraphs_library_item.field_storage_definitions  |  20169 |
| content_moderation_state.field_storage_definitions |  16169 |
| file.field_storage_definitions                     |  12357 |
| redirect.field_storage_definitions                 |  11918 |
| shortcut.field_storage_definitions                 |  10084 |
| path_alias.field_storage_definitions               |   9528 |
| monitoring_sensor_result.field_storage_definitions |   8888 |
| webform.entity_type                                |   5921 |
| paragraphs_library_item.entity_type                |   4979 |
| webform_submission.entity_type                     |   4956 |
| search_api_index.entity_type                       |   4948 |
...

Also using the discovery bin, just like those services.

For some reason that I don't know, we don't inject the service into entity field manager but query the key value storage directly, so I have to fix that.

Also, in case someone does writes directly to key value, it will skip cache invalidation, but there's not much to be done about that.

Lets see how well this works. I did manual testing on a standard install and it saved 10 key value queries for me. And cache lookups should be minimal due to fast chained.

Status: Needs review » Needs work

The last submitted patch, 5: entity-last-installed-cache-3131585-5.patch, failed testing. View results

Berdir’s picture

Fixing the test fails. Workspace test needs an updated container, fixed one bug to unset the statically cached entity type definitions when one is deleted and updated the unit test.

alexpott’s picture

To test this patch I installed Standard and disabled render cache.

I've looked at the patch in #8 and it is indeed saving 8 queries to state on viewing a node with an image. See https://blackfire.io/profiles/compare/3b99f947-872b-4cdc-ac9e-afd53146fa... - there is a cost though - memory usage jumps from 12.5 MB to 13 MB.

Similarly on node edit of the article node - we save 14 queries to state - see https://blackfire.io/profiles/compare/f11eee09-c284-4cb4-ae0f-c39bed0acb...() and we use around 500 kB of memory more.

So this is about whether the tradeoff is worth it.

  1. +++ b/core/lib/Drupal/Core/Entity/EntityFieldManager.php
    @@ -161,6 +170,11 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, Ent
    +      @trigger_error('The entity.last_installed_schema.repository service must be passed to EntityFieldManager::__construct(), it is required before drupal:10.0.0.', E_USER_DEPRECATED);
    

    There is a format for these things... we need a CR and then to link to it.

  2. +++ b/core/modules/workspaces/tests/src/Functional/WorkspacesUninstallTest.php
    @@ -45,6 +45,7 @@ public function testUninstallingWorkspace() {
         // Verify that the revision metadata key has been removed.
    +    $this->rebuildContainer();
         $entity_type = \Drupal::entityDefinitionUpdateManager()->getEntityType('node');
         $revision_metadata_keys = $entity_type->get('revision_metadata_keys');
         $this->assertArrayNotHasKey('workspace', $revision_metadata_keys);
    

    Got to love tests that do an uninstall via the UI but then don't rebuild the container in the test runner.

  3. I wonder if we could also try to not load the in-code / cached definitions. Loading both the last installed definitions and "current" definitions seems OTT.
alexpott’s picture

We can claw a tiny amount back by not caching the active definitions on both the entity type manager and entity last installed schema repository. 61 kB - see https://blackfire.io/profiles/compare/f7487f44-f79c-4de8-b4f3-3ec7826cb1...

andypost’s picture

#10 profile shows +8 calls to getLastInstalledDefinition

OIwait +713% 0.8 µs → 6.5 µs

Berdir’s picture

Thanks for testing. IMHO queries/s is more likely to be the bottleneck of a site.

For context, this patch + #2575105: Use cache collector for state meant that one of the large NP8 sites went from 8k key_value queries per minute (that's 130/s) to 400 per minute. I don't have data on memory usage, apparently new relic doesn't collect that.

@andypost: anything that says "µs" is well within random variations, you can also see that database queries are 500µs slower but it's the same queries, that's perfectly fine and normal. The first profile is also 1.5% slower, but again, that's mostly just random variations.

Note: the node edit profile link doesn't seem to work.

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.

daffie’s picture

All code changes look good to me.
The patch adds the use of a backend cache to the entity.last_installed_schema.repository service.
There is already a lot of testing for this service and to me that would fail if the added backend cache was not implemented correctly.
@alexpott adds performance profiling results that show that the patch is saving in the number queries. It also shows that the memory usage is a little bit higher, from 12.5MB to 13MB. To me almost any performance increase outweighs a memory usage increase.
In comment #12 @Berdir confirms that the patch from this issue in combination with the patch from #2575105: Use cache collector for state makes the number of queries to the key_value table decrease from 8000 per minute to 400 per minute.
I have added a CR for the new construction parameters to the classes EntityFieldManager and EntityLastInstalledSchemaRepository.
This is for me enough to give this issue a RTBC.

  • catch committed 685419f on 9.2.x
    Issue #3131585 by Berdir, alexpott, daffie: Performance regression...
catch’s picture

Status: Reviewed & tested by the community » Fixed

This looks great and the numbers look good.

Committed 685419f and pushed to 9.2.x. Thanks!

I'm leaving this fixed on 9.2.x rather than backporting due to the various constructor changes.

Wim Leers’s picture

Status: Fixed » Closed (fixed)

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