Commit 6951f4d7 authored by catch's avatar catch
Browse files

Issue #3225328 by mxr576, Berdir, longwave: Improve page performance by...

Issue #3225328 by mxr576, Berdir, longwave: Improve page performance by sorting cache contexts/tags on-demand
parent e4ce9936
......@@ -28,8 +28,7 @@ class Cache {
*/
public static function mergeContexts(array ...$cache_contexts) {
$cache_contexts = array_unique(array_merge(...$cache_contexts));
assert(\Drupal::service('cache_contexts_manager')->assertValidTokens($cache_contexts));
sort($cache_contexts);
assert(\Drupal::service('cache_contexts_manager')->assertValidTokens($cache_contexts), sprintf('Failed to assert that "%s" are valid cache contexts.', implode(', ', $cache_contexts)));
return $cache_contexts;
}
......@@ -53,7 +52,6 @@ public static function mergeContexts(array ...$cache_contexts) {
public static function mergeTags(array ...$cache_tags) {
$cache_tags = array_unique(array_merge(...$cache_tags));
assert(Inspector::assertAllStrings($cache_tags), 'Cache tags must be valid strings');
sort($cache_tags);
return $cache_tags;
}
......
......@@ -23,6 +23,11 @@ class ContextCacheKeys extends CacheableMetadata {
* The cache context keys.
*/
public function __construct(array $keys) {
// Domain invariant: cache keys must be always sorted.
// Sorting keys warrants that different combination of the same keys
// generates the same cache cid.
// @see \Drupal\Core\Render\RenderCache::createCacheID()
sort($keys);
$this->keys = $keys;
}
......
......@@ -159,8 +159,12 @@ public function onRespond(ResponseEvent $event) {
// Expose the cache contexts and cache tags associated with this page in a
// X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags header respectively.
$response_cacheability = $response->getCacheableMetadata();
$response->headers->set('X-Drupal-Cache-Tags', implode(' ', $response_cacheability->getCacheTags()));
$response->headers->set('X-Drupal-Cache-Contexts', implode(' ', $this->cacheContextsManager->optimizeTokens($response_cacheability->getCacheContexts())));
$cache_tags = $response_cacheability->getCacheTags();
sort($cache_tags);
$response->headers->set('X-Drupal-Cache-Tags', implode(' ', $cache_tags));
$cache_contexts = $this->cacheContextsManager->optimizeTokens($response_cacheability->getCacheContexts());
sort($cache_contexts);
$response->headers->set('X-Drupal-Cache-Contexts', implode(' ', $cache_contexts));
$max_age_message = $response_cacheability->getCacheMaxAge();
if ($max_age_message === 0) {
$max_age_message = '0 (Uncacheable)';
......
......@@ -296,8 +296,8 @@ protected function assertBlockRenderedWithExpectedCacheability(array $expected_k
// - the built render array;
$build = $this->getBlockRenderArray();
$this->assertSame($expected_keys, $build['#cache']['keys']);
$this->assertSame($expected_contexts, $build['#cache']['contexts']);
$this->assertSame($expected_tags, $build['#cache']['tags']);
$this->assertEqualsCanonicalizing($expected_contexts, $build['#cache']['contexts']);
$this->assertEqualsCanonicalizing($expected_tags, $build['#cache']['tags']);
$this->assertSame($expected_max_age, $build['#cache']['max-age']);
$this->assertFalse(isset($build['#create_placeholder']));
// - the rendered render array;
......@@ -307,9 +307,9 @@ protected function assertBlockRenderedWithExpectedCacheability(array $expected_k
$cid = implode(':', $expected_keys) . ':' . implode(':', \Drupal::service('cache_contexts_manager')->convertTokensToKeys($final_cache_contexts)->getKeys());
$cache_item = $this->container->get('cache.render')->get($cid);
$this->assertNotEmpty($cache_item, 'The block render element has been cached with the expected cache ID.');
$this->assertSame(Cache::mergeTags($expected_tags, ['rendered']), $cache_item->tags);
$this->assertSame($final_cache_contexts, $cache_item->data['#cache']['contexts']);
$this->assertSame($expected_tags, $cache_item->data['#cache']['tags']);
$this->assertEqualsCanonicalizing(Cache::mergeTags($expected_tags, ['rendered']), $cache_item->tags);
$this->assertEqualsCanonicalizing($final_cache_contexts, $cache_item->data['#cache']['contexts']);
$this->assertEqualsCanonicalizing($expected_tags, $cache_item->data['#cache']['tags']);
$this->assertSame($expected_max_age, $cache_item->data['#cache']['max-age']);
$this->container->get('cache.render')->delete($cid);
......
......@@ -92,8 +92,7 @@ public function testCacheTags() {
'config:field.storage.comment.comment_body',
'config:user.settings',
];
sort($expected_cache_tags);
$this->assertEquals($expected_cache_tags, $build['#cache']['tags']);
$this->assertEqualsCanonicalizing($expected_cache_tags, $build['#cache']['tags']);
// Create a comment on that entity. Comment loading requires that the uid
// also exists in the {users} table.
......@@ -140,8 +139,7 @@ public function testCacheTags() {
'config:field.storage.comment.comment_body',
'config:user.settings',
];
sort($expected_cache_tags);
$this->assertEquals($expected_cache_tags, $build['#cache']['tags']);
$this->assertEqualsCanonicalizing($expected_cache_tags, $build['#cache']['tags']);
// Build a render array with the entity in a sub-element so that lazy
// builder elements bubble up outside of the entity and we can check that
......
......@@ -79,8 +79,8 @@ public function testAccessCacheability() {
$result = $node->access('update', $account, TRUE);
$this->assertFalse($result->isAllowed());
$this->assertEquals(['user.permissions'], $result->getCacheContexts());
$this->assertEquals(['config:workflows.workflow.editorial', 'node:' . $node->id()], $result->getCacheTags());
$this->assertEqualsCanonicalizing(['user.permissions'], $result->getCacheContexts());
$this->assertEqualsCanonicalizing(['config:workflows.workflow.editorial', 'node:' . $node->id()], $result->getCacheTags());
$this->assertEquals(CacheBackendInterface::CACHE_PERMANENT, $result->getCacheMaxAge());
$authenticated->grantPermission('use editorial transition create_new_draft');
......@@ -89,8 +89,8 @@ public function testAccessCacheability() {
\Drupal::entityTypeManager()->getAccessControlHandler('node')->resetCache();
$result = $node->access('update', $account, TRUE);
$this->assertTrue($result->isAllowed());
$this->assertEquals(['user.permissions'], $result->getCacheContexts());
$this->assertEquals(['config:workflows.workflow.editorial', 'node:' . $node->id()], $result->getCacheTags());
$this->assertEqualsCanonicalizing(['user.permissions'], $result->getCacheContexts());
$this->assertEqualsCanonicalizing(['config:workflows.workflow.editorial', 'node:' . $node->id()], $result->getCacheTags());
$this->assertEquals(CacheBackendInterface::CACHE_PERMANENT, $result->getCacheMaxAge());
}
......
......@@ -298,7 +298,7 @@ public function testProcessedTextElement() {
// The cache tags set by the filter_test_cache_merge filter.
'merge:tag',
];
$this->assertEquals($expected_cache_tags, $build['#cache']['tags'], 'Expected cache tags present.');
$this->assertEqualsCanonicalizing($expected_cache_tags, $build['#cache']['tags'], 'Expected cache tags present.');
$expected_cache_contexts = [
// The cache context set by the filter_test_cache_contexts filter.
'languages:' . LanguageInterface::TYPE_CONTENT,
......@@ -308,7 +308,7 @@ public function testProcessedTextElement() {
// The cache tags set by the filter_test_cache_merge filter.
'user.permissions',
];
$this->assertEquals($expected_cache_contexts, $build['#cache']['contexts'], 'Expected cache contexts present.');
$this->assertEqualsCanonicalizing($expected_cache_contexts, $build['#cache']['contexts'], 'Expected cache contexts present.');
$expected_markup = '<p>Hello, world!</p><p>This is a dynamic llama.</p>';
$this->assertEquals($expected_markup, $build['#markup'], 'Expected #lazy_builder callback has been applied.');
}
......
......@@ -214,9 +214,9 @@ public function testBuild() {
];
$breadcrumb = $breadcrumb_builder->build($route_match);
$this->assertEquals($expected1, $breadcrumb->getLinks());
$this->assertEquals(['route'], $breadcrumb->getCacheContexts());
$this->assertEquals(['taxonomy_term:1', 'taxonomy_term:23', 'taxonomy_vocabulary:5'], $breadcrumb->getCacheTags());
$this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
$this->assertEqualsCanonicalizing(['route'], $breadcrumb->getCacheContexts());
$this->assertEqualsCanonicalizing(['taxonomy_term:1', 'taxonomy_term:23', 'taxonomy_vocabulary:5'], $breadcrumb->getCacheTags());
$this->assertEqualsCanonicalizing(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
// Second test.
$expected2 = [
......@@ -227,8 +227,8 @@ public function testBuild() {
];
$breadcrumb = $breadcrumb_builder->build($route_match);
$this->assertEquals($expected2, $breadcrumb->getLinks());
$this->assertEquals(['route'], $breadcrumb->getCacheContexts());
$this->assertEquals(['taxonomy_term:1', 'taxonomy_term:2', 'taxonomy_term:23', 'taxonomy_vocabulary:5'], $breadcrumb->getCacheTags());
$this->assertEqualsCanonicalizing(['route'], $breadcrumb->getCacheContexts());
$this->assertEqualsCanonicalizing(['taxonomy_term:1', 'taxonomy_term:2', 'taxonomy_term:23', 'taxonomy_vocabulary:5'], $breadcrumb->getCacheTags());
$this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
}
......
......@@ -221,8 +221,8 @@ public function testBuild() {
];
$breadcrumb = $breadcrumb_builder->build($route_match);
$this->assertEquals($expected1, $breadcrumb->getLinks());
$this->assertEquals(['route'], $breadcrumb->getCacheContexts());
$this->assertEquals(['taxonomy_term:1', 'taxonomy_vocabulary:5'], $breadcrumb->getCacheTags());
$this->assertEqualsCanonicalizing(['route'], $breadcrumb->getCacheContexts());
$this->assertEqualsCanonicalizing(['taxonomy_term:1', 'taxonomy_vocabulary:5'], $breadcrumb->getCacheTags());
$this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
// Second test.
......@@ -234,8 +234,8 @@ public function testBuild() {
];
$breadcrumb = $breadcrumb_builder->build($route_match);
$this->assertEquals($expected2, $breadcrumb->getLinks());
$this->assertEquals(['route'], $breadcrumb->getCacheContexts());
$this->assertEquals(['taxonomy_term:1', 'taxonomy_term:2', 'taxonomy_vocabulary:5'], $breadcrumb->getCacheTags());
$this->assertEqualsCanonicalizing(['route'], $breadcrumb->getCacheContexts());
$this->assertEqualsCanonicalizing(['taxonomy_term:1', 'taxonomy_term:2', 'taxonomy_vocabulary:5'], $breadcrumb->getCacheTags());
$this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
}
......
......@@ -722,14 +722,14 @@ protected function assertResourceResponse($expected_status_code, $expected_docum
// Expected cache tags: X-Drupal-Cache-Tags header.
$this->assertSame($expected_cache_tags !== FALSE, $response->hasHeader('X-Drupal-Cache-Tags'));
if (is_array($expected_cache_tags)) {
$this->assertSame($expected_cache_tags, explode(' ', $response->getHeader('X-Drupal-Cache-Tags')[0]));
$this->assertEqualsCanonicalizing($expected_cache_tags, explode(' ', $response->getHeader('X-Drupal-Cache-Tags')[0]));
}
// Expected cache contexts: X-Drupal-Cache-Contexts header.
$this->assertSame($expected_cache_contexts !== FALSE, $response->hasHeader('X-Drupal-Cache-Contexts'));
if (is_array($expected_cache_contexts)) {
$optimized_expected_cache_contexts = \Drupal::service('cache_contexts_manager')->optimizeTokens($expected_cache_contexts);
$this->assertSame($optimized_expected_cache_contexts, explode(' ', $response->getHeader('X-Drupal-Cache-Contexts')[0]));
$this->assertEqualsCanonicalizing($optimized_expected_cache_contexts, explode(' ', $response->getHeader('X-Drupal-Cache-Contexts')[0]));
}
// Expected Page Cache header value: X-Drupal-Cache header.
......@@ -788,7 +788,7 @@ protected function assertSameDocument(array $expected_document, array $actual_do
}
foreach ($expected_document as $member_name => $expected_member) {
$actual_member = $actual_document[$member_name];
$this->assertEquals($expected_member, $actual_member, "The '$member_name' member was not as expected.");
$this->assertEqualsCanonicalizing($expected_member, $actual_member, "The '$member_name' member was not as expected.");
}
}
......
......@@ -207,7 +207,7 @@ public function testGetPagedCollection() {
$data = $response->getResponseData()->getData();
$this->assertCount(1, $data);
$this->assertEquals($this->node2->uuid(), $data->toArray()[0]->getId());
$this->assertEquals(['node:2', 'node_list'], $response->getCacheableMetadata()->getCacheTags());
$this->assertEqualsCanonicalizing(['node:2', 'node_list'], $response->getCacheableMetadata()->getCacheTags());
}
/**
......
......@@ -310,7 +310,7 @@ public function testNormalize() {
$this->assertTrue(!isset($normalized['included'][1]['attributes']['created']));
// Make sure that the cache tags for the includes and the requested entities
// are bubbling as expected.
$this->assertSame(
$this->assertEqualsCanonicalizing(
['file:1', 'node:1', 'taxonomy_term:1', 'taxonomy_term:2', 'user:1'],
$jsonapi_doc_object->getCacheTags()
);
......@@ -400,7 +400,7 @@ public function testNormalizeUuid() {
$this->assertCount(12, $normalized['included'][1]['attributes']);
// Make sure that the cache tags for the includes and the requested entities
// are bubbling as expected.
$this->assertSame(
$this->assertEqualsCanonicalizing(
['node:1', 'taxonomy_term:1', 'taxonomy_term:2', 'user:1'],
$jsonapi_doc_object->getCacheTags()
);
......
......@@ -122,7 +122,7 @@ public function testOnBuildRender($refinable_dependent_access) {
'content' => $block_content,
];
$expected_cache = $expected_build + [
$expected_build_with_expected_cache = $expected_build + [
'#cache' => [
'contexts' => [],
'tags' => [
......@@ -137,7 +137,7 @@ public function testOnBuildRender($refinable_dependent_access) {
$result = $event->getBuild();
$this->assertEquals($expected_build, $result);
$event->getCacheableMetadata()->applyTo($result);
$this->assertEquals($expected_cache, $result);
$this->assertEqualsCanonicalizing($expected_build_with_expected_cache['#cache'], $result['#cache']);
}
/**
......@@ -443,7 +443,7 @@ public function testOnBuildRenderEmptyBuild() {
$result = $event->getBuild();
$this->assertEquals($expected_build, $result);
$event->getCacheableMetadata()->applyTo($result);
$this->assertEquals($expected_cache, $result);
$this->assertEqualsCanonicalizing($expected_cache, $result);
}
/**
......@@ -488,7 +488,7 @@ public function testOnBuildRenderEmptyBuildWithCacheTags() {
$result = $event->getBuild();
$this->assertEquals($expected_build, $result);
$event->getCacheableMetadata()->applyTo($result);
$this->assertEquals($expected_cache, $result);
$this->assertEqualsCanonicalizing($expected_cache, $result);
}
/**
......
......@@ -34,8 +34,8 @@ public function testBasics(array $embed_attributes, $expected_view_mode, array $
$this->assertCount(1, $this->cssSelect('div[data-media-embed-test-view-mode="' . $expected_view_mode . '"]'));
$this->assertHasAttributes($this->cssSelect('div[data-media-embed-test-view-mode="' . $expected_view_mode . '"]')[0], $expected_attributes);
$this->assertSame($expected_cacheability->getCacheTags(), $result->getCacheTags());
$this->assertSame($expected_cacheability->getCacheContexts(), $result->getCacheContexts());
$this->assertEqualsCanonicalizing($expected_cacheability->getCacheTags(), $result->getCacheTags());
$this->assertEqualsCanonicalizing($expected_cacheability->getCacheContexts(), $result->getCacheContexts());
$this->assertSame($expected_cacheability->getCacheMaxAge(), $result->getCacheMaxAge());
$this->assertSame(['library'], array_keys($result->getAttachments()));
$this->assertSame(['media/filter.caption'], $result->getAttachments()['library']);
......@@ -152,8 +152,8 @@ public function testAccessUnpublished($allowed_to_view_unpublished, $expected_re
$this->assertCount(1, $this->cssSelect('div[data-media-embed-test-view-mode="default"]'));
}
$this->assertSame($expected_cacheability->getCacheTags(), $result->getCacheTags());
$this->assertSame($expected_cacheability->getCacheContexts(), $result->getCacheContexts());
$this->assertEqualsCanonicalizing($expected_cacheability->getCacheTags(), $result->getCacheTags());
$this->assertEqualsCanonicalizing($expected_cacheability->getCacheContexts(), $result->getCacheContexts());
$this->assertSame($expected_cacheability->getCacheMaxAge(), $result->getCacheMaxAge());
$this->assertSame($expected_attachments, $result->getAttachments());
}
......@@ -408,7 +408,7 @@ public function testFilterIntegration(array $filter_ids, array $additional_attri
$this->setRawContent($result->getProcessedText());
$this->assertCount($expected_verification_success ? 1 : 0, $this->cssSelect($verification_selector));
$this->assertCount(1, $this->cssSelect('div[data-media-embed-test-view-mode="default"]'));
$this->assertSame([
$this->assertEqualsCanonicalizing([
'_media_test_embed_filter_access:media:1',
'_media_test_embed_filter_access:user:2',
'config:image.style.thumbnail',
......@@ -417,7 +417,7 @@ public function testFilterIntegration(array $filter_ids, array $additional_attri
'media_view',
'user:2',
], $result->getCacheTags());
$this->assertSame(['timezone', 'user.permissions'], $result->getCacheContexts());
$this->assertEqualsCanonicalizing(['timezone', 'user.permissions'], $result->getCacheContexts());
$this->assertSame(Cache::PERMANENT, $result->getCacheMaxAge());
$this->assertSame(['library'], array_keys($result->getAttachments()));
$this->assertSame($expected_asset_libraries, $result->getAttachments()['library']);
......
......@@ -62,7 +62,7 @@ public function testTranslationSelection($text_langcode, $expected_title_langcod
// based on the host entity's language, which should require a cache context
// to be associated. (The host entity's language may itself be selected
// based on the request context, but that is of no concern to this filter.)
$this->assertSame($result->getCacheContexts(), ['timezone', 'user.permissions']);
$this->assertEqualsCanonicalizing($result->getCacheContexts(), ['timezone', 'user.permissions']);
}
/**
......
......@@ -417,8 +417,8 @@ private function assertAccess(AccessResult $access_result, $is_allowed, $expecte
if ($access_result instanceof AccessResultReasonInterface && isset($expected_reason)) {
$this->assertSame($expected_reason, $access_result->getReason());
}
$this->assertSame($expected_cache_tags, $access_result->getCacheTags());
$this->assertSame($expected_cache_contexts, $access_result->getCacheContexts());
$this->assertEqualsCanonicalizing($expected_cache_tags, $access_result->getCacheTags());
$this->assertEqualsCanonicalizing($expected_cache_contexts, $access_result->getCacheContexts());
}
}
......@@ -124,7 +124,7 @@ public function testOutboundPathAndRouteProcessing() {
$renderer->renderRoot($build);
$expected_cacheability = $default_menu_cacheability->merge($expectation['cacheability']);
$this->assertEquals($expected_cacheability, BubbleableMetadata::createFromRenderArray($build));
$this->assertEqualsCanonicalizing($expected_cacheability, BubbleableMetadata::createFromRenderArray($build));
$menu_link_content->delete();
}
......@@ -146,7 +146,7 @@ public function testOutboundPathAndRouteProcessing() {
$build = $menu_tree->build($tree);
$renderer->renderRoot($build);
$expected_cacheability = $expected_cacheability->merge($default_menu_cacheability);
$this->assertEquals($expected_cacheability, BubbleableMetadata::createFromRenderArray($build));
$this->assertEqualsCanonicalizing($expected_cacheability, BubbleableMetadata::createFromRenderArray($build));
}
}
......@@ -33,7 +33,7 @@ public function testCacheContexts() {
$build = $list_builder->render();
$this->container->get('renderer')->renderRoot($build);
$this->assertEquals(['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'url.query_args.pagers:0', 'user.node_grants:view', 'user.permissions'], $build['#cache']['contexts']);
$this->assertEqualsCanonicalizing(['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'url.query_args.pagers:0', 'user.node_grants:view', 'user.permissions'], $build['#cache']['contexts']);
}
}
......@@ -565,7 +565,7 @@ public function testGet() {
static::recursiveKSort($expected);
$actual = $this->serializer->decode((string) $response->getBody(), static::$format);
static::recursiveKSort($actual);
$this->assertSame($expected, $actual);
$this->assertEqualsCanonicalizing($expected, $actual);
// Not only assert the normalization, also assert deserialization of the
// response results in the expected object.
......
......@@ -398,13 +398,13 @@ protected function assertResourceResponse($expected_status_code, $expected_body,
// Expected cache tags: X-Drupal-Cache-Tags header.
$this->assertSame($expected_cache_tags !== FALSE, $response->hasHeader('X-Drupal-Cache-Tags'));
if (is_array($expected_cache_tags)) {
$this->assertSame($expected_cache_tags, explode(' ', $response->getHeader('X-Drupal-Cache-Tags')[0]));
$this->assertEqualsCanonicalizing($expected_cache_tags, explode(' ', $response->getHeader('X-Drupal-Cache-Tags')[0]));
}
// Expected cache contexts: X-Drupal-Cache-Contexts header.
$this->assertSame($expected_cache_contexts !== FALSE, $response->hasHeader('X-Drupal-Cache-Contexts'));
if (is_array($expected_cache_contexts)) {
$this->assertSame($expected_cache_contexts, explode(' ', $response->getHeader('X-Drupal-Cache-Contexts')[0]));
$this->assertEqualsCanonicalizing($expected_cache_contexts, explode(' ', $response->getHeader('X-Drupal-Cache-Contexts')[0]));
}
// Expected Page Cache header value: X-Drupal-Cache header.
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment