From dcb0e065319427e395e8b42d488c31b264ae08dc Mon Sep 17 00:00:00 2001 From: catch <6915-catch@users.noreply.drupalcode.org> Date: Sun, 9 Feb 2025 09:09:39 +0000 Subject: [PATCH] Issue #3436146 by berdir, kristiaanvandeneynde, catch: Introduce a list of "frequent cache tags" to reduce lookup query amount --- core/core.services.yml | 3 ++ .../CacheTagsChecksumPreloadInterface.php | 24 +++++++++ .../Core/Cache/CacheTagsChecksumTrait.php | 27 +++++++++- .../Core/Cache/DatabaseCacheTagsChecksum.php | 2 +- .../CacheTagPreloadSubscriber.php | 52 +++++++++++++++++++ .../src/Cache/CacheTagsChecksumDecorator.php | 10 +++- ...nTelemetryAuthenticatedPerformanceTest.php | 2 +- .../StandardPerformanceTest.php | 44 ++++------------ 8 files changed, 126 insertions(+), 38 deletions(-) create mode 100644 core/lib/Drupal/Core/Cache/CacheTagsChecksumPreloadInterface.php create mode 100644 core/lib/Drupal/Core/Cache/EventSubscriber/CacheTagPreloadSubscriber.php diff --git a/core/core.services.yml b/core/core.services.yml index e82dee9234d9..c2fc23bce52e 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -232,6 +232,9 @@ services: - { name: cache_tags_invalidator} - { name: backend_overridable } Drupal\Core\Cache\CacheTagsChecksumInterface: '@cache_tags.invalidator.checksum' + cache_tags.preloader: + class: Drupal\Core\Cache\EventSubscriber\CacheTagPreloadSubscriber + autowire: true cache.backend.chainedfast: class: Drupal\Core\Cache\ChainedFastBackendFactory arguments: ['@settings'] diff --git a/core/lib/Drupal/Core/Cache/CacheTagsChecksumPreloadInterface.php b/core/lib/Drupal/Core/Cache/CacheTagsChecksumPreloadInterface.php new file mode 100644 index 000000000000..9325856beb1a --- /dev/null +++ b/core/lib/Drupal/Core/Cache/CacheTagsChecksumPreloadInterface.php @@ -0,0 +1,24 @@ +<?php + +namespace Drupal\Core\Cache; + +/** + * Registers cache tags for preloading. + * + * Implementations of \Drupal\Core\Cache\CacheTagsChecksumInterface that + * support this interface will fetch registered cache tags on the next + * lookup. + * + * @see \Drupal\Core\Cache\EventSubscriber\CacheTagPreloadSubscriber + */ +interface CacheTagsChecksumPreloadInterface { + + /** + * Register cache tags for preloading. + * + * @param array $cache_tags + * List of cache tags to load. + */ + public function registerCacheTagsForPreload(array $cache_tags): void; + +} diff --git a/core/lib/Drupal/Core/Cache/CacheTagsChecksumTrait.php b/core/lib/Drupal/Core/Cache/CacheTagsChecksumTrait.php index 00bf806c3dee..0a6724fb1cde 100644 --- a/core/lib/Drupal/Core/Cache/CacheTagsChecksumTrait.php +++ b/core/lib/Drupal/Core/Cache/CacheTagsChecksumTrait.php @@ -32,6 +32,11 @@ trait CacheTagsChecksumTrait { */ protected $tagCache = []; + /** + * Registered cache tags to preload. + */ + protected array $preloadTags = []; + /** * Callback to be invoked just after a database transaction gets committed. * @@ -131,13 +136,24 @@ public function isValid($checksum, array $tags) { */ protected function calculateChecksum(array $tags) { $checksum = 0; + // If there are no cache tags, then there is no cache tag to checksum, - // so return early.. + // so return early. if (empty($tags)) { return $checksum; } - $query_tags = array_diff($tags, array_keys($this->tagCache)); + // If there are registered preload tags, add them to the tags list then + // reset the list. This needs to make sure that it only returns the + // requested cache tags, so store the combination of requested and + // preload cache tags in a separate variable. + $tags_with_preload = $tags; + if ($this->preloadTags) { + $tags_with_preload = array_unique(array_merge($tags, $this->preloadTags)); + $this->preloadTags = []; + } + + $query_tags = array_diff($tags_with_preload, array_keys($this->tagCache)); if ($query_tags) { $tag_invalidations = $this->getTagInvalidationCounts($query_tags); $this->tagCache += $tag_invalidations; @@ -160,6 +176,13 @@ public function reset() { $this->invalidatedTags = []; } + /** + * Implements \Drupal\Core\Cache\CacheTagsChecksumPreloadInterface::registerCacheTagsForPreload() + */ + public function registerCacheTagsForPreload(array $cache_tags): void { + $this->preloadTags = array_merge($this->preloadTags, $cache_tags); + } + /** * Fetches invalidation counts for cache tags. * diff --git a/core/lib/Drupal/Core/Cache/DatabaseCacheTagsChecksum.php b/core/lib/Drupal/Core/Cache/DatabaseCacheTagsChecksum.php index aa41952128c2..cb88c69495a7 100644 --- a/core/lib/Drupal/Core/Cache/DatabaseCacheTagsChecksum.php +++ b/core/lib/Drupal/Core/Cache/DatabaseCacheTagsChecksum.php @@ -8,7 +8,7 @@ /** * Cache tags invalidations checksum implementation that uses the database. */ -class DatabaseCacheTagsChecksum implements CacheTagsChecksumInterface, CacheTagsInvalidatorInterface { +class DatabaseCacheTagsChecksum implements CacheTagsChecksumInterface, CacheTagsInvalidatorInterface, CacheTagsChecksumPreloadInterface { use CacheTagsChecksumTrait; diff --git a/core/lib/Drupal/Core/Cache/EventSubscriber/CacheTagPreloadSubscriber.php b/core/lib/Drupal/Core/Cache/EventSubscriber/CacheTagPreloadSubscriber.php new file mode 100644 index 000000000000..af3dedfab565 --- /dev/null +++ b/core/lib/Drupal/Core/Cache/EventSubscriber/CacheTagPreloadSubscriber.php @@ -0,0 +1,52 @@ +<?php + +namespace Drupal\Core\Cache\EventSubscriber; + +use Drupal\Core\Cache\CacheTagsChecksumInterface; +use Drupal\Core\Cache\CacheTagsChecksumPreloadInterface; +use Drupal\Core\Site\Settings; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\HttpKernel\KernelEvents; + +/** + * Preloads frequently used cache tags. + */ +class CacheTagPreloadSubscriber implements EventSubscriberInterface { + + public function __construct(protected CacheTagsChecksumInterface $cacheTagsChecksum) { + } + + /** + * Preloads frequently used cache tags. + * + * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event + * The request event. + */ + public function onRequest(RequestEvent $event): void { + if ($event->isMainRequest() && $this->cacheTagsChecksum instanceof CacheTagsChecksumPreloadInterface) { + $default_preload_cache_tags = array_merge([ + 'route_match', + 'access_policies', + 'routes', + 'router', + 'entity_types', + 'entity_field_info', + 'entity_bundles', + 'local_task', + 'library_info', + 'user_values', + ], Settings::get('cache_preload_tags', [])); + $this->cacheTagsChecksum->registerCacheTagsForPreload($default_preload_cache_tags); + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents(): array { + $events[KernelEvents::REQUEST][] = ['onRequest', 500]; + return $events; + } + +} diff --git a/core/modules/system/tests/modules/performance_test/src/Cache/CacheTagsChecksumDecorator.php b/core/modules/system/tests/modules/performance_test/src/Cache/CacheTagsChecksumDecorator.php index ba01cd809c44..dd4675f4b62e 100644 --- a/core/modules/system/tests/modules/performance_test/src/Cache/CacheTagsChecksumDecorator.php +++ b/core/modules/system/tests/modules/performance_test/src/Cache/CacheTagsChecksumDecorator.php @@ -5,13 +5,14 @@ namespace Drupal\performance_test\Cache; use Drupal\Core\Cache\CacheTagsChecksumInterface; +use Drupal\Core\Cache\CacheTagsChecksumPreloadInterface; use Drupal\Core\Cache\CacheTagsInvalidatorInterface; use Drupal\performance_test\PerformanceDataCollector; /** * Wraps an existing cache tags checksum invalidator to track calls separately. */ -class CacheTagsChecksumDecorator implements CacheTagsChecksumInterface, CacheTagsInvalidatorInterface { +class CacheTagsChecksumDecorator implements CacheTagsChecksumInterface, CacheTagsInvalidatorInterface, CacheTagsChecksumPreloadInterface { public function __construct(protected readonly CacheTagsChecksumInterface $checksumInvalidator, protected readonly PerformanceDataCollector $performanceDataCollector) {} @@ -71,6 +72,13 @@ public function reset() { $this->checksumInvalidator->reset(); } + /** + * {@inheritdoc} + */ + public function registerCacheTagsForPreload(array $cache_tags): void { + $this->checksumInvalidator->registerCacheTagsForPreload($cache_tags); + } + /** * Logs a cache tag operation. * diff --git a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryAuthenticatedPerformanceTest.php b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryAuthenticatedPerformanceTest.php index 24c7193b0ee0..c96d9fe2e2e7 100644 --- a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryAuthenticatedPerformanceTest.php +++ b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryAuthenticatedPerformanceTest.php @@ -64,7 +64,7 @@ public function testFrontPageAuthenticatedWarmCache(): void { 'CacheTagChecksumCount' => 0, 'CacheTagIsValidCount' => 10, 'CacheTagInvalidationCount' => 0, - 'CacheTagLookupQueryCount' => 5, + 'CacheTagLookupQueryCount' => 2, 'ScriptCount' => 1, 'ScriptBytes' => 123850, 'StylesheetCount' => 2, diff --git a/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php b/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php index 3c045918b6af..b754ec94f444 100644 --- a/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php +++ b/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php @@ -145,18 +145,13 @@ protected function testAnonymous(): void { 'CacheTagChecksumCount' => 37, 'CacheTagIsValidCount' => 42, 'CacheTagInvalidationCount' => 0, - 'CacheTagLookupQueryCount' => 29, + 'CacheTagLookupQueryCount' => 22, 'CacheTagGroupedLookups' => [ - ['route_match'], - ['entity_types'], - ['routes'], + ['route_match', 'access_policies', 'routes', 'router', 'entity_types', 'entity_field_info', 'entity_bundles', 'local_task', 'library_info', 'user_values'], ['config:views.view.frontpage'], ['config:core.extension', 'views_data'], - ['entity_field_info'], - ['entity_bundles'], ['node_values'], ['node:1', 'node_list'], - ['user_values'], ['rendered', 'user:0', 'user_view'], ['config:filter.format.restricted_html', 'node_view'], ['block_view', 'config:block.block.stark_site_branding', 'config:system.site'], @@ -167,14 +162,12 @@ protected function testAnonymous(): void { ['config:block.block.stark_breadcrumbs'], ['config:block.block.stark_primary_admin_actions'], ['config:block.block.stark_messages'], - ['local_task'], ['config:block.block.stark_primary_local_tasks'], ['config:block.block.stark_secondary_local_tasks'], ['config:block.block.stark_help'], ['config:block.block.stark_powered'], ['config:block.block.stark_syndicate'], ['config:block.block.stark_content', 'config:block.block.stark_page_title', 'config:block_list', 'http_response'], - ['library_info'], ['config:user.role.anonymous'], ], 'StylesheetCount' => 1, @@ -218,14 +211,10 @@ protected function testAnonymous(): void { 'CacheSetCount' => 16, 'CacheDeleteCount' => 0, 'CacheTagInvalidationCount' => 0, - 'CacheTagLookupQueryCount' => 25, + 'CacheTagLookupQueryCount' => 19, 'CacheTagGroupedLookups' => [ - ['route_match'], - ['entity_types'], - ['entity_field_info', 'node_values'], - ['routes'], - ['entity_bundles'], - ['user_values'], + ['route_match', 'access_policies', 'routes', 'router', 'entity_types', 'entity_field_info', 'entity_bundles', 'local_task', 'library_info', 'user_values'], + ['node_values'], ['rendered', 'user:0', 'user_view'], ['config:filter.format.restricted_html', 'node:1', 'node_view'], ['block_view', 'config:block.block.stark_site_branding', 'config:system.site'], @@ -236,14 +225,12 @@ protected function testAnonymous(): void { ['config:block.block.stark_breadcrumbs'], ['config:block.block.stark_primary_admin_actions'], ['config:block.block.stark_messages'], - ['local_task'], ['config:block.block.stark_primary_local_tasks'], ['config:block.block.stark_secondary_local_tasks'], ['config:block.block.stark_help'], ['config:block.block.stark_powered'], ['config:block.block.stark_syndicate'], ['config:block.block.stark_content', 'config:block.block.stark_page_title', 'config:block_list', 'http_response'], - ['library_info'], ['config:user.role.anonymous'], ], 'StylesheetCount' => 1, @@ -284,7 +271,7 @@ protected function testAnonymous(): void { 'CacheTagChecksumCount' => 23, 'CacheTagIsValidCount' => 33, 'CacheTagInvalidationCount' => 0, - 'CacheTagLookupQueryCount' => 23, + 'CacheTagLookupQueryCount' => 16, 'StylesheetCount' => 1, 'StylesheetBytes' => 3150, ]; @@ -341,20 +328,12 @@ protected function testLogin(): void { 'CacheTagChecksumCount' => 1, 'CacheTagIsValidCount' => 37, 'CacheTagInvalidationCount' => 0, - 'CacheTagLookupQueryCount' => 26, + 'CacheTagLookupQueryCount' => 17, 'CacheTagGroupedLookups' => [ // Form submission and login. - ['route_match'], - ['routes'], - ['entity_types'], - ['entity_field_info', 'user_values'], + ['route_match', 'access_policies', 'routes', 'router', 'entity_types', 'entity_field_info', 'entity_bundles', 'local_task', 'library_info', 'user_values'], // The user page after the redirect. - ['route_match'], - ['entity_types'], - ['entity_field_info'], - ['entity_bundles'], - ['user_values'], - ['routes'], + ['route_match', 'access_policies', 'routes', 'router', 'entity_types', 'entity_field_info', 'entity_bundles', 'local_task', 'library_info', 'user_values'], ['rendered', 'user:2', 'user_view'], ['block_view', 'config:block.block.stark_site_branding', 'config:system.site'], ['CACHE_MISS_IF_UNCACHEABLE_HTTP_METHOD:form', 'config:block.block.stark_search_form_narrow', 'config:search.settings'], @@ -365,12 +344,11 @@ protected function testLogin(): void { ['config:block.block.stark_breadcrumbs'], ['config:block.block.stark_primary_admin_actions'], ['config:block.block.stark_messages'], - ['access_policies', 'config:block.block.stark_primary_local_tasks', 'config:user.role.authenticated', 'local_task'], + ['config:block.block.stark_primary_local_tasks', 'config:user.role.authenticated'], ['config:block.block.stark_secondary_local_tasks'], ['config:block.block.stark_help'], ['config:block.block.stark_powered'], ['config:block.block.stark_syndicate'], - ['library_info'], ], ]; $this->assertMetrics($expected, $performance_data); @@ -429,7 +407,7 @@ protected function testLoginBlock(): void { 'CacheTagChecksumCount' => 1, 'CacheTagIsValidCount' => 41, 'CacheTagInvalidationCount' => 0, - 'CacheTagLookupQueryCount' => 27, + 'CacheTagLookupQueryCount' => 20, ]; $this->assertMetrics($expected, $performance_data); } -- GitLab