Add cache keys index metrics

The following metrics were added foe each cache:
* cache/chroniclemap/keys_index_size_<cache-name> for showing the
  current size of the index (that one is one to one with disk entries)
* cache/chroniclemap/keys_index_add_latency_<cache-name> to indicate how
  much time it takes to add key to the index over time
* cache/chroniclemap/keys_index_remove_and_consume_older_than_latency_<cache-name>
  to indicate how much time it takes to remove and consume all keys
  older than expiration time
* cache/chroniclemap/keys_index_remove_lru_key_latency_<cache-name> to
  indicate how much time it takes to remove and consumer LRU key

Bug: Issue 15121
Change-Id: I8141c0f00dfe6e7dc3b06517c3383d19fe883492
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/CacheKeysIndex.java b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/CacheKeysIndex.java
index 34e3eac..a6f608d 100644
--- a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/CacheKeysIndex.java
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/CacheKeysIndex.java
@@ -18,6 +18,10 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.errorprone.annotations.CompatibleWith;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Description.Units;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer0;
 import java.util.Collections;
 import java.util.LinkedHashSet;
 import java.util.Objects;
@@ -63,10 +67,62 @@
     }
   }
 
-  private final Set<TimedKey> keys;
+  private class Metrics {
+    private final Timer0 addLatency;
+    private final Timer0 removeAndConsumeOlderThanLatency;
+    private final Timer0 removeAndConsumeLruKeyLatency;
 
-  CacheKeysIndex() {
+    private Metrics(MetricMaker metricMaker, String name) {
+      String sanitizedName = metricMaker.sanitizeMetricName(name);
+
+      metricMaker.newCallbackMetric(
+          "cache/chroniclemap/keys_index_size_" + sanitizedName,
+          Integer.class,
+          new Description(
+              String.format(
+                  "The number of cache index keys for %s cache that are currently in memory",
+                  name)),
+          keys::size);
+
+      addLatency =
+          metricMaker.newTimer(
+              "cache/chroniclemap/keys_index_add_latency_" + sanitizedName,
+              new Description(
+                      String.format("The latency of adding key to the index for %s cache", name))
+                  .setCumulative()
+                  .setUnit(Units.NANOSECONDS));
+
+      removeAndConsumeOlderThanLatency =
+          metricMaker.newTimer(
+              "cache/chroniclemap/keys_index_remove_and_consume_older_than_latency_"
+                  + sanitizedName,
+              new Description(
+                      String.format(
+                          "The latency of removing and consuming all keys older than expiration"
+                              + " time for the index for %s cache",
+                          name))
+                  .setCumulative()
+                  .setUnit(Units.NANOSECONDS));
+
+      removeAndConsumeLruKeyLatency =
+          metricMaker.newTimer(
+              "cache/chroniclemap/keys_index_remove_lru_key_latency_" + sanitizedName,
+              new Description(
+                      String.format(
+                          "The latency of removing and consuming LRU key from the index for %s"
+                              + " cache",
+                          name))
+                  .setCumulative()
+                  .setUnit(Units.NANOSECONDS));
+    }
+  }
+
+  private final Set<TimedKey> keys;
+  private final Metrics metrics;
+
+  CacheKeysIndex(MetricMaker metricMaker, String name) {
     this.keys = Collections.synchronizedSet(new LinkedHashSet<>());
+    this.metrics = new Metrics(metricMaker, name);
   }
 
   @SuppressWarnings("unchecked")
@@ -75,8 +131,10 @@
     TimedKey timedKey = new TimedKey((T) key, created);
 
     // bubble up MRU key by re-adding it to a set
-    keys.remove(timedKey);
-    keys.add(timedKey);
+    try (Timer0.Context timer = metrics.addLatency.start()) {
+      keys.remove(timedKey);
+      keys.add(timedKey);
+    }
   }
 
   void refresh(T key) {
@@ -84,32 +142,36 @@
   }
 
   void removeAndConsumeKeysOlderThan(long time, Consumer<T> consumer) {
-    Set<TimedKey> toRemoveAndConsume;
-    synchronized (keys) {
-      toRemoveAndConsume =
-          keys.stream().filter(key -> key.created < time).collect(toUnmodifiableSet());
+    try (Timer0.Context timer = metrics.removeAndConsumeOlderThanLatency.start()) {
+      Set<TimedKey> toRemoveAndConsume;
+      synchronized (keys) {
+        toRemoveAndConsume =
+            keys.stream().filter(key -> key.created < time).collect(toUnmodifiableSet());
+      }
+      toRemoveAndConsume.forEach(
+          key -> {
+            keys.remove(key);
+            consumer.accept(key.getKey());
+          });
     }
-    toRemoveAndConsume.forEach(
-        key -> {
-          keys.remove(key);
-          consumer.accept(key.getKey());
-        });
   }
 
   boolean removeAndConsumeLruKey(Consumer<T> consumer) {
-    Optional<TimedKey> lruKey;
-    synchronized (keys) {
-      lruKey = keys.stream().findFirst();
-    }
+    try (Timer0.Context timer = metrics.removeAndConsumeLruKeyLatency.start()) {
+      Optional<TimedKey> lruKey;
+      synchronized (keys) {
+        lruKey = keys.stream().findFirst();
+      }
 
-    return lruKey
-        .map(
-            key -> {
-              keys.remove(key);
-              consumer.accept(key.getKey());
-              return true;
-            })
-        .orElse(false);
+      return lruKey
+          .map(
+              key -> {
+                keys.remove(key);
+                consumer.accept(key.getKey());
+                return true;
+              })
+          .orElse(false);
+    }
   }
 
   void invalidate(@CompatibleWith("T") Object key) {
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheFactory.java b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheFactory.java
index 98d7f42..1161699 100644
--- a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheFactory.java
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheFactory.java
@@ -117,7 +117,11 @@
 
       cache =
           new ChronicleMapCacheImpl<>(
-              in, config, memLoader, new InMemoryCacheLoadingFromStoreImpl<>(mem, false));
+              in,
+              config,
+              metricMaker,
+              memLoader,
+              new InMemoryCacheLoadingFromStoreImpl<>(mem, false));
 
     } catch (IOException e) {
       throw new UncheckedIOException(e);
@@ -165,7 +169,11 @@
 
       cache =
           new ChronicleMapCacheImpl<>(
-              in, config, memLoader, new InMemoryCacheLoadingFromStoreImpl<>(mem, true));
+              in,
+              config,
+              metricMaker,
+              memLoader,
+              new InMemoryCacheLoadingFromStoreImpl<>(mem, true));
     } catch (IOException e) {
       throw new UncheckedIOException(e);
     }
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheImpl.java b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheImpl.java
index 12a463b..3740959 100644
--- a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheImpl.java
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheImpl.java
@@ -13,7 +13,6 @@
 // limitations under the License.
 package com.googlesource.gerrit.modules.cache.chroniclemap;
 
-import com.google.common.annotations.VisibleForTesting;
 import com.google.common.cache.AbstractLoadingCache;
 import com.google.common.cache.CacheStats;
 import com.google.common.flogger.FluentLogger;
@@ -27,7 +26,6 @@
 import java.sql.Timestamp;
 import java.time.Duration;
 import java.time.Instant;
-import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.atomic.LongAdder;
@@ -57,7 +55,7 @@
 
     this.cacheDefinition = def;
     this.config = config;
-    this.keysIndex = new CacheKeysIndex<>();
+    this.keysIndex = new CacheKeysIndex<>(metricMaker, def.name());
     this.store = createOrRecoverStore(def, config, metricMaker);
     this.memLoader =
         new ChronicleMapCacheLoader<>(
@@ -68,12 +66,13 @@
   ChronicleMapCacheImpl(
       PersistentCacheDef<K, V> def,
       ChronicleMapCacheConfig config,
+      MetricMaker metricMaker,
       ChronicleMapCacheLoader<K, V> memLoader,
       InMemoryCache<K, V> mem) {
 
     this.cacheDefinition = def;
     this.config = config;
-    this.keysIndex = new CacheKeysIndex<>();
+    this.keysIndex = new CacheKeysIndex<>(metricMaker, def.name());
     this.memLoader = memLoader;
     this.mem = mem;
     this.store = memLoader.getStore();
@@ -342,9 +341,4 @@
   public String name() {
     return store.name();
   }
-
-  @VisibleForTesting
-  Set<CacheKeysIndex<K>.TimedKey> keys() {
-    return keysIndex.keys();
-  }
 }
diff --git a/src/main/resources/Documentation/metrics.md b/src/main/resources/Documentation/metrics.md
index 565fe62..b0bc902 100644
--- a/src/main/resources/Documentation/metrics.md
+++ b/src/main/resources/Documentation/metrics.md
@@ -20,5 +20,17 @@
 * cache/chroniclemap/max_autoresizes_<cache-name>
   : The maximum number of times the cache can automatically expand its capacity.
 
+* cache/chroniclemap/keys_index_size_<cache-name>
+  : The number of index keys for the cache that are currently in memory.
+
+* cache/chroniclemap/keys_index_add_latency_<cache-name>
+  : The latency of adding cache key to an index.
+
+* cache/chroniclemap/keys_index_remove_and_consume_older_than_latency_<cache-name>
+  : The latency of removing and consuming all keys older than expiration time for an index.
+
+* cache/chroniclemap/keys_index_remove_lru_key_latency_<cache-name>
+  : The latency of removing and consuming LRU key from an index.
+
 * "cache/chroniclemap/store_put_failures_<cache-name>
   : The number of errors caught when inserting entries in chronicle-map store
\ No newline at end of file
diff --git a/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/CacheKeysIndexTest.java b/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/CacheKeysIndexTest.java
index 405e5a5..27c9e76 100644
--- a/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/CacheKeysIndexTest.java
+++ b/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/CacheKeysIndexTest.java
@@ -20,6 +20,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
 
+import com.google.gerrit.metrics.DisabledMetricMaker;
 import java.util.List;
 import java.util.function.Consumer;
 import org.junit.Before;
@@ -30,7 +31,7 @@
 
   @Before
   public void setup() {
-    index = new CacheKeysIndex<>();
+    index = new CacheKeysIndex<>(new DisabledMetricMaker(), "test-cache");
   }
 
   @Test
diff --git a/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheTest.java b/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheTest.java
index ecc457a..9c839f0 100644
--- a/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheTest.java
+++ b/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheTest.java
@@ -20,6 +20,7 @@
 import com.codahale.metrics.Counter;
 import com.codahale.metrics.Gauge;
 import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.Timer;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.acceptance.WaitUtil;
@@ -455,22 +456,69 @@
   }
 
   @Test
-  public void shouldResetKeysIndexWhenInvalidateAll() throws Exception {
+  public void shouldTriggerKeysIndexSizeCacheMetric() throws Exception {
     String cachedValue = UUID.randomUUID().toString();
     int maxEntries = 10;
-    int maxHotKeyCapacity = 3;
+    int expectedKeysSize = 3;
+    final Duration METRIC_TRIGGER_TIMEOUT = Duration.ofSeconds(2);
+    String keysIndexSizeMetricName = "cache/chroniclemap/keys_index_size_" + testCacheName;
     gerritConfig.setInt("cache", testCacheName, "maxEntries", maxEntries);
     gerritConfig.save();
 
     ChronicleMapCacheImpl<String, String> cache = newCacheWithMetrics(testCacheName, cachedValue);
 
-    for (int i = 0; i < maxHotKeyCapacity; i++) {
+    assertThat(getMetric(keysIndexSizeMetricName).getValue()).isEqualTo(0);
+
+    for (int i = 0; i < expectedKeysSize; i++) {
       cache.put(cachedValue + i, cachedValue);
     }
 
-    assertThat(cache.keys()).isNotEmpty();
+    WaitUtil.waitUntil(
+        () -> (int) getMetric(keysIndexSizeMetricName).getValue() == expectedKeysSize,
+        METRIC_TRIGGER_TIMEOUT);
+  }
+
+  @Test
+  public void shouldTriggerKeysIndexAddLatencyCacheMetric() throws Exception {
+    int maxEntries = 10;
+    final Duration METRIC_TRIGGER_TIMEOUT = Duration.ofSeconds(2);
+    String keysIndexAddLatencyMetricName =
+        "cache/chroniclemap/keys_index_add_latency_" + testCacheName;
+    gerritConfig.setInt("cache", testCacheName, "maxEntries", maxEntries);
+    gerritConfig.save();
+
+    ChronicleMapCacheImpl<String, String> cache = newCacheWithMetrics(testCacheName, null);
+    assertThat(getTimer(keysIndexAddLatencyMetricName).getCount()).isEqualTo(0L);
+
+    String cachedValue = UUID.randomUUID().toString();
+    cache.put(cachedValue, cachedValue);
+
+    WaitUtil.waitUntil(
+        () -> getTimer(keysIndexAddLatencyMetricName).getCount() == 1L, METRIC_TRIGGER_TIMEOUT);
+  }
+
+  @Test
+  public void shouldResetKeysIndexWhenInvalidateAll() throws Exception {
+    String cachedValue = UUID.randomUUID().toString();
+    int maxEntries = 10;
+    int expectedKeysSize = 3;
+    final Duration METRIC_TRIGGER_TIMEOUT = Duration.ofSeconds(2);
+    String keysIndexSizeMetricName = "cache/chroniclemap/keys_index_size_" + testCacheName;
+    gerritConfig.setInt("cache", testCacheName, "maxEntries", maxEntries);
+    gerritConfig.save();
+
+    ChronicleMapCacheImpl<String, String> cache = newCacheWithMetrics(testCacheName, cachedValue);
+
+    for (int i = 0; i < expectedKeysSize; i++) {
+      cache.put(cachedValue + i, cachedValue);
+    }
+
+    WaitUtil.waitUntil(
+        () -> (int) getMetric(keysIndexSizeMetricName).getValue() == expectedKeysSize,
+        METRIC_TRIGGER_TIMEOUT);
     cache.invalidateAll();
-    assertThat(cache.keys()).isEmpty();
+    WaitUtil.waitUntil(
+        () -> (int) getMetric(keysIndexSizeMetricName).getValue() == 0, METRIC_TRIGGER_TIMEOUT);
   }
 
   @Test
@@ -480,12 +528,22 @@
     String percentageFreeMetricName = "cache/chroniclemap/percentage_free_space_" + sanitized;
     String autoResizeMetricName = "cache/chroniclemap/remaining_autoresizes_" + sanitized;
     String maxAutoResizeMetricName = "cache/chroniclemap/max_autoresizes_" + sanitized;
+    String keysIndexSizeMetricName = "cache/chroniclemap/keys_index_size_" + sanitized;
+    String keysIndexAddLatencyMetricName = "cache/chroniclemap/keys_index_add_latency_" + sanitized;
+    String keysIndexRemoveOlderThanLatencyMetricName =
+        "cache/chroniclemap/keys_index_remove_and_consume_older_than_latency_" + sanitized;
+    String keysIndexRemoveLruLatencyMetricName =
+        "cache/chroniclemap/keys_index_remove_lru_key_latency_" + sanitized;
 
     newCacheWithMetrics(cacheName, null);
 
     getMetric(percentageFreeMetricName);
     getMetric(autoResizeMetricName);
     getMetric(maxAutoResizeMetricName);
+    getMetric(keysIndexSizeMetricName);
+    getTimer(keysIndexAddLatencyMetricName);
+    getTimer(keysIndexRemoveOlderThanLatencyMetricName);
+    getTimer(keysIndexRemoveLruLatencyMetricName);
   }
 
   private int valueSize(String value) {
@@ -588,4 +646,10 @@
     assertWithMessage(name).that(counter).isNotNull();
     return counter;
   }
+
+  private Timer getTimer(String name) {
+    Timer timer = (Timer) metricRegistry.getMetrics().get(name);
+    assertWithMessage(name).that(timer).isNotNull();
+    return timer;
+  }
 }