diff --git a/core/core.services.yml b/core/core.services.yml index e82dee9234d92516a63fa5347cf488812d4cc310..c2fc23bce52ecb4275de4e784b7e31fcc6bf1926 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 0000000000000000000000000000000000000000..9325856beb1ab3f64de1c4836ec1872b915fd452 --- /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 00bf806c3dee1df743646160e9f5110fdb3034b4..0a6724fb1cde0db268e8366d9b5e34d514489c68 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 aa41952128c240b3d1a4bd00a664dd70834f00fc..cb88c69495a7721314036aacdabb3f4ad59e2bd8 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 0000000000000000000000000000000000000000..af3dedfab5656e8ab939dfc4876dc9bb3a7b31ed --- /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 ba01cd809c44ab647dd8f15cc0d2816433876197..dd4675f4b62ef892a6b6903d57f972c0505006fd 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 24c7193b0ee0cd61359ecc26adefbcf09440d33f..c96d9fe2e2e70e223d88bfa1049fe09e6fe28840 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 3c045918b6af246df2a314906183db590643c4a4..b754ec94f444ecb3b5a0c415bb6b1099e52d61c4 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); }