From 5cd3545981a8458e4b2198df8bd70515d7190cba Mon Sep 17 00:00:00 2001
From: catch <6915-catch@users.noreply.drupalcode.org>
Date: Thu, 30 Jan 2025 16:23:07 +0000
Subject: [PATCH] Issue #3500739 by berdir, kristiaanvandeneynde: Aggregate
 cache operations per bin in performance tests to allow for more specific
 asserts

---
 .../FunctionalJavascript/PerformanceTest.php  |  15 +++
 ...nTelemetryAuthenticatedPerformanceTest.php |   9 ++
 .../OpenTelemetryFrontPagePerformanceTest.php |   1 +
 .../StandardPerformanceTest.php               | 116 ++++++++++++++++++
 core/tests/Drupal/Tests/PerformanceData.php   |  98 ++++++++++++++-
 .../Drupal/Tests/PerformanceTestTrait.php     |  44 ++++++-
 6 files changed, 276 insertions(+), 7 deletions(-)

diff --git a/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php b/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php
index 6598576660c1..ddd9bc7d684e 100644
--- a/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php
+++ b/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php
@@ -74,7 +74,21 @@ public function testLogin(): void {
     $expected = [
       'QueryCount' => 4,
       'CacheGetCount' => 61,
+      'CacheGetCountByBin' => [
+        'config' => 10,
+        'data' => 6,
+        'access_policy' => 1,
+        'bootstrap' => 6,
+        'dynamic_page_cache' => 2,
+        'discovery' => 11,
+        'render' => 23,
+        'menu' => 1,
+        'default' => 1,
+      ],
       'CacheSetCount' => 2,
+      'CacheSetCountByBin' => [
+        'dynamic_page_cache' => 2,
+      ],
       'CacheDeleteCount' => 0,
       'CacheTagChecksumCount' => 3,
       'CacheTagIsValidCount' => 31,
@@ -85,6 +99,7 @@ public function testLogin(): void {
       'StylesheetBytes' => 92000,
     ];
     $this->assertMetrics($expected, $performance_data);
+    $this->assertSame(['core.extension.list.module'], $performance_data->getCacheOperations()['get']['default']);
 
     // Check that the navigation toolbar is cached without any high-cardinality
     // cache contexts (user, route, query parameters etc.).
diff --git a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryAuthenticatedPerformanceTest.php b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryAuthenticatedPerformanceTest.php
index df52db42bfaf..f0a6312984af 100644
--- a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryAuthenticatedPerformanceTest.php
+++ b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryAuthenticatedPerformanceTest.php
@@ -49,11 +49,20 @@ public function testFrontPageAuthenticatedWarmCache(): void {
     $expected = [
       'QueryCount' => 4,
       'CacheGetCount' => 40,
+      'CacheGetCountByBin' => [
+        'config' => 20,
+        'discovery' => 5,
+        'access_policy' => 2,
+        'data' => 7,
+        'bootstrap' => 4,
+        'dynamic_page_cache' => 2,
+      ],
       'CacheSetCount' => 0,
       'CacheDeleteCount' => 0,
       'CacheTagChecksumCount' => 0,
       'CacheTagIsValidCount' => 11,
       'CacheTagInvalidationCount' => 0,
+      'CacheTagLookupQueryCount' => 6,
       'ScriptCount' => 1,
       'ScriptBytes' => 123850,
       'StylesheetCount' => 2,
diff --git a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryFrontPagePerformanceTest.php b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryFrontPagePerformanceTest.php
index ee3d61d033a4..ff5d5c79ea5a 100644
--- a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryFrontPagePerformanceTest.php
+++ b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryFrontPagePerformanceTest.php
@@ -76,6 +76,7 @@ protected function testFrontPageHotCache(): void {
       'CacheTagChecksumCount' => 0,
       'CacheTagIsValidCount' => 1,
       'CacheTagInvalidationCount' => 0,
+      'CacheTagLookupQueryCount' => 1,
       'ScriptCount' => 1,
       'ScriptBytes' => 11850,
       'StylesheetCount' => 2,
diff --git a/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php b/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php
index c425f516695e..9026bca418ae 100644
--- a/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php
+++ b/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php
@@ -128,15 +128,68 @@ protected function testAnonymous(): void {
     $expected = [
       'QueryCount' => 36,
       'CacheGetCount' => 122,
+      'CacheGetCountByBin' => [
+        'page' => 1,
+        'config' => 20,
+        'data' => 8,
+        'access_policy' => 1,
+        'bootstrap' => 8,
+        'dynamic_page_cache' => 2,
+        'discovery' => 38,
+        'render' => 35,
+        'default' => 5,
+        'entity' => 2,
+        'menu' => 2,
+      ],
       'CacheSetCount' => 45,
       'CacheDeleteCount' => 0,
       'CacheTagChecksumCount' => 37,
       'CacheTagIsValidCount' => 43,
       'CacheTagInvalidationCount' => 0,
+      'CacheTagLookupQueryCount' => 29,
+      'CacheTagGroupedLookups' => [
+        ['route_match'],
+        ['access_policies', 'config:user.role.anonymous'],
+        ['routes'],
+        ['entity_types'],
+        ['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'],
+        ['CACHE_MISS_IF_UNCACHEABLE_HTTP_METHOD:form', 'config:block.block.stark_search_form_narrow', 'config:search.settings'],
+        ['config:block.block.stark_main_menu', 'config:system.menu.main'],
+        ['config:block.block.stark_search_form_wide'],
+        ['config:block.block.stark_account_menu', 'config:system.menu.account'],
+        ['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'],
+      ],
       'StylesheetCount' => 1,
       'StylesheetBytes' => 3450,
     ];
     $this->assertMetrics($expected, $performance_data);
+    $expected_default_cache_cids = [
+      'views_data:node_field_data:en',
+      'views_data:en',
+      'views_data:views:en',
+      'views_data:node:en',
+      'theme_registry:stark',
+    ];
+    $this->assertSame($expected_default_cache_cids, $performance_data->getCacheOperations()['get']['default']);
 
     // Test node page.
     $performance_data = $this->collectPerformanceData(function () {
@@ -166,6 +219,34 @@ protected function testAnonymous(): void {
       'CacheSetCount' => 16,
       'CacheDeleteCount' => 0,
       'CacheTagInvalidationCount' => 0,
+      'CacheTagLookupQueryCount' => 25,
+      'CacheTagGroupedLookups' => [
+        ['route_match'],
+        ['entity_types'],
+        ['entity_field_info', 'node_values'],
+        ['access_policies', 'config:user.role.anonymous'],
+        ['routes'],
+        ['entity_bundles'],
+        ['user_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'],
+        ['CACHE_MISS_IF_UNCACHEABLE_HTTP_METHOD:form', 'config:block.block.stark_search_form_narrow', 'config:search.settings'],
+        ['config:block.block.stark_main_menu', 'config:system.menu.main'],
+        ['config:block.block.stark_search_form_wide'],
+        ['config:block.block.stark_account_menu', 'config:system.menu.account'],
+        ['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'],
+      ],
       'StylesheetCount' => 1,
       'StylesheetBytes' => 3150,
     ];
@@ -204,6 +285,7 @@ protected function testAnonymous(): void {
       'CacheTagChecksumCount' => 23,
       'CacheTagIsValidCount' => 32,
       'CacheTagInvalidationCount' => 0,
+      'CacheTagLookupQueryCount' => 24,
       'StylesheetCount' => 1,
       'StylesheetBytes' => 3150,
     ];
@@ -260,6 +342,39 @@ protected function testLogin(): void {
       'CacheTagChecksumCount' => 1,
       'CacheTagIsValidCount' => 37,
       'CacheTagInvalidationCount' => 0,
+      'CacheTagLookupQueryCount' => 28,
+      'CacheTagGroupedLookups' => [
+        // Form submission and login.
+        ['route_match'],
+        ['routes'],
+        ['entity_types'],
+        ['access_policies', 'config:user.role.anonymous'],
+        ['entity_field_info', 'user_values'],
+        // The user page after the redirect.
+        ['route_match'],
+        ['entity_types'],
+        ['entity_field_info'],
+        ['entity_bundles'],
+        ['user_values'],
+        ['access_policies', 'config:user.role.authenticated'],
+        ['routes'],
+        ['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'],
+        ['config:system.menu.account', 'config:system.menu.main'],
+        ['config:block.block.stark_main_menu'],
+        ['config:block.block.stark_search_form_wide'],
+        ['config:block.block.stark_account_menu'],
+        ['config:block.block.stark_breadcrumbs'],
+        ['config:block.block.stark_primary_admin_actions'],
+        ['config:block.block.stark_messages'],
+        ['config:block.block.stark_primary_local_tasks', 'local_task'],
+        ['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);
     $this->drupalLogout();
@@ -317,6 +432,7 @@ protected function testLoginBlock(): void {
       'CacheTagChecksumCount' => 1,
       'CacheTagIsValidCount' => 43,
       'CacheTagInvalidationCount' => 0,
+      'CacheTagLookupQueryCount' => 29,
     ];
     $this->assertMetrics($expected, $performance_data);
   }
diff --git a/core/tests/Drupal/Tests/PerformanceData.php b/core/tests/Drupal/Tests/PerformanceData.php
index 8e75654c9b25..e26c31b939ca 100644
--- a/core/tests/Drupal/Tests/PerformanceData.php
+++ b/core/tests/Drupal/Tests/PerformanceData.php
@@ -7,7 +7,7 @@
 /**
  * Value object to store performance information collected from requests.
  *
- * @see Drupal\Tests\PerformanceTestTrait::collectPerformanceData().
+ * @see \Drupal\Tests\PerformanceTestTrait::collectPerformanceData().
  */
 class PerformanceData {
 
@@ -56,6 +56,13 @@ class PerformanceData {
    */
   protected int $cacheDeleteCount = 0;
 
+  /**
+   * List of cids keyed by operation and bin.
+   *
+   * @var string[][]
+   */
+  protected array $cacheOperations = [];
+
   /**
    * The number of cache tag checksum checks.
    */
@@ -71,6 +78,13 @@ class PerformanceData {
    */
   protected int $cacheTagInvalidationCount = 0;
 
+  /**
+   * The grouped cache tag lookups.
+   *
+   * @var string[]
+   */
+  protected array $cacheTagGroupedLookups = [];
+
   /**
    * The original return value.
    */
@@ -207,6 +221,58 @@ public function getCacheGetCount(): int {
     return $this->cacheGetCount;
   }
 
+  /**
+   * Sets the cache operations.
+   *
+   * @param string[][] $cacheOperations
+   *   List of cids keyed by operation and bin.
+   *
+   * @return void
+   */
+  public function setCacheOperations(array $cacheOperations): void {
+    $this->cacheOperations = $cacheOperations;
+  }
+
+  /**
+   * Gets the cache operations.
+   *
+   * @return string[][]
+   *   List of cids keyed by operation and bin.
+   */
+  public function getCacheOperations(): array {
+    return $this->cacheOperations;
+  }
+
+  /**
+   * Returns the cache get operation count grouped by bin.
+   *
+   * @return int[]
+   *   Count of cache get operations keyed by bin.
+   */
+  public function getCacheGetCountByBin(): array {
+    return array_map(fn (array $cids) => count($cids), $this->cacheOperations['get'] ?? []);
+  }
+
+  /**
+   * Returns the cache set operation count grouped by bin.
+   *
+   * @return int[]
+   *   Count of cache set operations keyed by bin.
+   */
+  public function getCacheSetCountByBin(): array {
+    return array_map(fn (array $cids) => count($cids), $this->cacheOperations['set'] ?? []);
+  }
+
+  /**
+   * Returns the cache delete operation count grouped by bin.
+   *
+   * @return int[]
+   *   Count of cache delete operations keyed by bin.
+   */
+  public function getCacheDeleteCountByBin(): array {
+    return array_map(fn (array $cids) => count($cids), $this->cacheOperations['delete'] ?? []);
+  }
+
   /**
    * Sets the cache set count.
    *
@@ -307,6 +373,36 @@ public function getCacheTagInvalidationCount(): int {
     return $this->cacheTagInvalidationCount;
   }
 
+  /**
+   * Sets the grouped cache tag lookups.
+   *
+   * @param string[] $groupedLookups
+   *   Grouped cache tag lookups by query.
+   */
+  public function setCacheTagGroupedLookups(array $groupedLookups): void {
+    $this->cacheTagGroupedLookups = $groupedLookups;
+  }
+
+  /**
+   * Gets the grouped cache tag lookups.
+   *
+   * @@return string[]
+   *   Grouped cache tag lookups by query.
+   */
+  public function getCacheTagGroupedLookups(): array {
+    return $this->cacheTagGroupedLookups;
+  }
+
+  /**
+   * Gets the cache tag lookup query count.
+   *
+   * @return int
+   *   The number of cache tag lookup queries recorded.
+   */
+  public function getCacheTagLookupQueryCount(): int {
+    return count($this->cacheTagGroupedLookups);
+  }
+
   /**
    * Sets the original return value.
    *
diff --git a/core/tests/Drupal/Tests/PerformanceTestTrait.php b/core/tests/Drupal/Tests/PerformanceTestTrait.php
index d7b780f3aa50..39b08b68b75c 100644
--- a/core/tests/Drupal/Tests/PerformanceTestTrait.php
+++ b/core/tests/Drupal/Tests/PerformanceTestTrait.php
@@ -132,26 +132,41 @@ public function collectPerformanceData(callable $callable, ?string $service_name
       $cache_tag_is_valid_count = 0;
       $cache_tag_invalidation_count = 0;
       $cache_tag_checksum_count = 0;
+      $cache_tag_lookup_query_args = [];
       foreach ($performance_test_data['database_events'] as $event) {
+        $normalized_query = static::normalizeQuery($event->queryString, $this->databasePrefix);
+
         // Don't log queries from the database cache backend because they're
         // logged separately as cache operations.
         if (!static::isDatabaseCache($event)) {
-          // Make the query easier to read and log it.
-          static::logQuery(
-            $performance_data,
-            str_replace([$this->databasePrefix, "\r\n", "\r", "\n"], ['', ' ', ' ', ' '], $event->queryString),
-            $event->args
-          );
+          static::logQuery($performance_data, $normalized_query, $event->args);
+        }
+        // Keep track of cache tag lookup queries.
+        elseif (str_starts_with($normalized_query, 'SELECT "tag", "invalidations" FROM "cachetags"')) {
+          $cache_tag_lookup_query_args[] = array_values($event->args);
         }
       }
+      $cache_operations = [];
       foreach ($performance_test_data['cache_operations'] as $operation) {
         if (in_array($operation['operation'], ['get', 'getMultiple'], TRUE)) {
+          if (!isset($cache_operations['get'][$operation['bin']])) {
+            $cache_operations['get'][$operation['bin']] = [];
+          }
+          $cache_operations['get'][$operation['bin']][] = $operation['cids'];
           $cache_get_count++;
         }
         elseif (in_array($operation['operation'], ['set', 'setMultiple'], TRUE)) {
+          if (!isset($cache_operations['get'][$operation['bin']])) {
+            $cache_operations['set'][$operation['bin']] = [];
+          }
+          $cache_operations['set'][$operation['bin']][] = $operation['cids'];
           $cache_set_count++;
         }
         elseif (in_array($operation['operation'], ['delete', 'deleteMultiple'], TRUE)) {
+          if (!isset($cache_operations['delete'][$operation['bin']])) {
+            $cache_operations['delete'][$operation['bin']] = [];
+          }
+          $cache_operations['delete'][$operation['bin']][] = $operation['cids'];
           $cache_delete_count++;
         }
       }
@@ -168,6 +183,8 @@ public function collectPerformanceData(callable $callable, ?string $service_name
       $performance_data->setCacheTagChecksumCount($cache_tag_checksum_count);
       $performance_data->setCacheTagIsValidCount($cache_tag_is_valid_count);
       $performance_data->setCacheTagInvalidationCount($cache_tag_invalidation_count);
+      $performance_data->setCacheOperations($cache_operations);
+      $performance_data->setCacheTagGroupedLookups($cache_tag_lookup_query_args);
     }
 
     return $performance_data;
@@ -681,4 +698,19 @@ protected function getMetrics(PerformanceData $performance_data): array {
     ];
   }
 
+  /**
+   * Normalizes a query by removing the database prefix and newlines.
+   *
+   * @param string $query_string
+   *   The query string to normalize.
+   * @param string $database_prefix
+   *   The database prefix to remove from the query.
+   *
+   * @return string
+   *   The normalized query string.
+   */
+  protected static function normalizeQuery(string $query_string, string $database_prefix): string {
+    return str_replace([$database_prefix, "\r\n", "\r", "\n"], ['', ' ', ' ', ' '], $query_string);
+  }
+
 }
-- 
GitLab