Add TaskTree Caching stats to query output

Change-Id: I91d48e5f1199ba61a4edf63e4444285ed820a40c
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/HitHashMap.java b/src/main/java/com/googlesource/gerrit/plugins/task/HitHashMap.java
new file mode 100644
index 0000000..ff9ff3b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/HitHashMap.java
@@ -0,0 +1,87 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.task;
+
+import java.util.HashMap;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+public class HitHashMap<K, V> extends HashMap<K, V> implements StatisticsMap<K, V> {
+  public static class Statistics {
+    public long hits;
+    public int size;
+  }
+
+  public static final long serialVersionUID = 1;
+
+  protected Statistics statistics;
+
+  @Override
+  public V get(Object key) {
+    V v = super.get(key);
+    if (statistics != null && v != null) {
+      statistics.hits++;
+    }
+    return v;
+  }
+
+  @Override
+  public V getOrDefault(Object key, V dv) {
+    V v = get(key);
+    if (v == null) {
+      return dv;
+    }
+    return v;
+  }
+
+  @Override
+  public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
+    V v = get(key);
+    if (v == null) {
+      v = mappingFunction.apply(key);
+      if (v != null) {
+        put(key, v);
+      }
+    }
+    return v;
+  }
+
+  @Override
+  public V computeIfPresent(
+      K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
+    throw new UnsupportedOperationException(); // Todo if needed
+  }
+
+  @Override
+  public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
+    throw new UnsupportedOperationException(); // Todo if needed
+  }
+
+  @Override
+  public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
+    throw new UnsupportedOperationException(); // Todo if needed
+  }
+
+  @Override
+  public void initStatistics() {
+    statistics = new Statistics();
+  }
+
+  @Override
+  public Object getStatistics() {
+    statistics.size = size();
+    return statistics;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/HitHashMapOfCollection.java b/src/main/java/com/googlesource/gerrit/plugins/task/HitHashMapOfCollection.java
new file mode 100644
index 0000000..7429dba
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/HitHashMapOfCollection.java
@@ -0,0 +1,63 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.task;
+
+import static java.util.stream.Collectors.toList;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+
+public class HitHashMapOfCollection<K, V extends Collection<?>> extends HitHashMap<K, V> {
+  public static class Statistics extends HitHashMap.Statistics {
+    public List<Integer> top5CollectionSizes;
+    public List<Integer> bottom5CollectionSizes;
+  }
+
+  public static final long serialVersionUID = 1;
+
+  protected Statistics statistics;
+
+  @Override
+  public void initStatistics() {
+    super.initStatistics();
+    statistics = new Statistics();
+  }
+
+  @Override
+  public Object getStatistics() {
+    super.getStatistics();
+    statistics.hits = super.statistics.hits;
+    statistics.size = super.statistics.size;
+
+    List<Integer> collectionSizes =
+        values().stream().map(l -> l.size()).sorted(Comparator.reverseOrder()).collect(toList());
+    statistics.top5CollectionSizes = new ArrayList<>(5);
+    statistics.bottom5CollectionSizes = new ArrayList<>(5);
+    for (int i = 0; i < 5 && i < collectionSizes.size(); i++) {
+      statistics.top5CollectionSizes.add(collectionSizes.get(i));
+      int bottom = collectionSizes.size() - 6 + i;
+      if (bottom > 4 && bottom < collectionSizes.size()) {
+        // The > 4 ensures that there are no entries also in the top list
+        statistics.bottom5CollectionSizes.add(collectionSizes.get(bottom));
+      }
+    }
+    if (statistics.bottom5CollectionSizes.isEmpty()) {
+      statistics.bottom5CollectionSizes = null;
+    }
+    return statistics;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/StatisticsMap.java b/src/main/java/com/googlesource/gerrit/plugins/task/StatisticsMap.java
new file mode 100644
index 0000000..aeeb434
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/StatisticsMap.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.task;
+
+import java.util.Map;
+
+public interface StatisticsMap<K, V> extends Map<K, V> {
+  void initStatistics();
+
+  Object getStatistics();
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
index 00b7dcf..4dfeb99 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
@@ -56,6 +56,7 @@
     public long numberOfTaskPluginAttributes;
     public PredicateCache.Statistics predicateCache;
     public Preloader.Statistics preloader;
+    public TaskTree.Statistics treeCaches;
   }
 
   public static class TaskAttribute {
@@ -334,6 +335,7 @@
       statistics = new Statistics();
       predicateCache.initStatistics();
       definitions.preloader.initStatistics();
+      definitions.initStatistics();
     }
   }
 
@@ -344,6 +346,7 @@
           pluginInfosByChange.values().stream().filter(tpa -> tpa != null).count();
       statistics.predicateCache = predicateCache.getStatistics();
       statistics.preloader = definitions.preloader.getStatistics();
+      statistics.treeCaches = definitions.getStatistics();
     }
     return statistics;
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
index 47b9e75..c7964ad 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
@@ -67,6 +67,10 @@
     Node create(NodeList parent, Task definition) throws Exception;
   }
 
+  public static class Statistics {
+    public Object definitionsPerSubSectionCache;
+  }
+
   protected static final String TASK_DIR = "task";
 
   protected final AccountResolver accountResolver;
@@ -76,9 +80,11 @@
   protected final NodeList root = new NodeList();
   protected final Provider<ChangeQueryBuilder> changeQueryBuilderProvider;
   protected final Provider<ChangeQueryProcessor> changeQueryProcessorProvider;
-  protected final Map<SubSectionKey, List<Task>> definitionsBySubSection = new HashMap<>();
+  protected final StatisticsMap<SubSectionKey, List<Task>> definitionsBySubSection =
+      new HitHashMapOfCollection<>();
 
   protected ChangeData changeData;
+  protected Statistics statistics;
 
   @Inject
   public TaskTree(
@@ -547,6 +553,18 @@
     return BranchNameKey.create(allUsers.get(), RefNames.refsUsers(acct));
   }
 
+  public void initStatistics() {
+    statistics = new Statistics();
+    definitionsBySubSection.initStatistics();
+  }
+
+  public Statistics getStatistics() {
+    if (statistics != null) {
+      statistics.definitionsPerSubSectionCache = definitionsBySubSection.getStatistics();
+    }
+    return statistics;
+  }
+
   protected static List<Node> refresh(List<Node> nodes)
       throws ConfigInvalidException, StorageException {
     for (Node node : nodes) {