Cache Head/Tail in events FsStore

Since the head and tail are sequences, and sequences can only
increase, it is possible to speed up certain comparisons
by caching the last known value of the sequence.

Change-Id: I16e035152cc23442c9c559ab684359501934ceaf
diff --git a/src/main/java/com/googlesource/gerrit/plugins/events/fsstore/FsStore.java b/src/main/java/com/googlesource/gerrit/plugins/events/fsstore/FsStore.java
index 8109836..57f3869 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/events/fsstore/FsStore.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/events/fsstore/FsStore.java
@@ -84,6 +84,9 @@
   protected final Stores stores;
   protected final UUID uuid;
 
+  protected final SequenceCache cachedHead;
+  protected final SequenceCache cachedTail;
+
   @Inject
   public FsStore(SitePaths site) throws IOException {
     this(site.data_dir.toPath().resolve("plugin").resolve("events").resolve("fstore-v2"));
@@ -94,6 +97,8 @@
     stores = new Stores(paths);
     stores.initFs();
     uuid = UUID.fromString(stores.uuid.get());
+    cachedHead = new SequenceCache(stores.head);
+    cachedTail = new SequenceCache(stores.tail);
   }
 
   @Override
@@ -108,12 +113,13 @@
 
   @Override
   public long getHead() throws IOException {
-    return stores.head.spinGet(MAX_GET_SPINS);
+    return cachedHead.spinGet(MAX_GET_SPINS);
   }
 
   @Override
   public String get(long num) throws IOException {
-    if (getTail() <= num && num <= getHead()) {
+    if (cachedTail.isLessThanOrEqualTo(num, MAX_GET_SPINS)
+        && cachedHead.isGreaterThanOrEqualTo(num, MAX_GET_SPINS)) {
       try {
         return Fs.readFile(paths.events.path(num));
       } catch (NoSuchFileException e) {
@@ -124,18 +130,20 @@
 
   @Override
   public long getTail() throws IOException {
-    if (getHead() == 0) {
+    if (cachedHead.isZero(MAX_GET_SPINS)) {
       return 0;
     }
-    long tail = stores.tail.spinGet(MAX_GET_SPINS);
+    long tail = cachedTail.spinGet(MAX_GET_SPINS);
     return tail < 1 ? 1 : tail;
   }
 
   @Override
   public void trim(long trim) throws IOException {
-    long head = getHead();
-    if (trim >= head) {
-      trim = head - 1;
+    if (cachedHead.isLessThanOrEqualTo(trim, MAX_GET_SPINS)) {
+      long head = getHead();
+      if (trim >= head) {
+        trim = head - 1;
+      }
     }
     if (trim > 0) {
       for (long i = getTail(); i <= trim; i++) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/events/fsstore/SequenceCache.java b/src/main/java/com/googlesource/gerrit/plugins/events/fsstore/SequenceCache.java
new file mode 100644
index 0000000..876bbf9
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/events/fsstore/SequenceCache.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2017 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.events.fsstore;
+
+import java.io.IOException;
+
+/**
+ * Cache the last known value of the sequence and use it to speed up certain comparisons by taking
+ * advantage of the fact that sequences can only ever increase.
+ */
+public class SequenceCache {
+  protected final UpdatableFileValue<Long> updatable;
+
+  protected long cachedValue = 0;
+
+  /** UpdatableFileValue<Long> must be an increasing sequence */
+  public SequenceCache(UpdatableFileValue<Long> updatable) {
+    this.updatable = updatable;
+  }
+
+  public Long spinGet(long maxTries) throws IOException {
+    long value = updatable.spinGet(maxTries);
+    synchronized (this) {
+      if (value > cachedValue) {
+        cachedValue = value;
+      } else {
+        value = cachedValue;
+      }
+    }
+    return value;
+  }
+
+  public boolean isZero(long maxTries) throws IOException {
+    return isEqualTo(0L, maxTries);
+  }
+
+  public boolean isEqualTo(long num, long maxTries) throws IOException {
+    return !cachedGreaterThan(num) && spinGet(maxTries) == num;
+  }
+
+  public boolean isGreaterThan(long num, long maxTries) throws IOException {
+    return cachedGreaterThan(num) || spinGet(maxTries) > num;
+  }
+
+  public boolean isGreaterThanOrEqualTo(long num, long maxTries) throws IOException {
+    return cachedGreaterThanOrEqualTo(num) || spinGet(maxTries) >= num;
+  }
+
+  public boolean isLessThan(long num, long maxTries) throws IOException {
+    return !cachedGreaterThanOrEqualTo(num) && spinGet(maxTries) < num;
+  }
+
+  public boolean isLessThanOrEqualTo(long num, long maxTries) throws IOException {
+    return !cachedGreaterThan(num) && spinGet(maxTries) <= num;
+  }
+
+  protected synchronized boolean cachedGreaterThan(long num) {
+    return cachedValue > num;
+  }
+
+  protected synchronized boolean cachedGreaterThanOrEqualTo(long num) {
+    return cachedValue >= num;
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/events/fsstore/FsStoreTest.java b/src/test/java/com/googlesource/gerrit/plugins/events/fsstore/FsStoreTest.java
index e852459..7e78875 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/events/fsstore/FsStoreTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/events/fsstore/FsStoreTest.java
@@ -222,7 +222,7 @@
    *
    * <p>Local(spinning) 1 workers 1M 14.34s ~14us/event find events|wc -l 1.3s rm -rf 42s
    *
-   * <p>Mixed workers: NFS(WAN) 1 worker (+NFS LAN continuous) count=10, 10m6s
+   * <p>Mixed workers: NFS(WAN) 1 worker (+NFS LAN continuous) count=10, 3m28s
    */
   public static void main(String[] argv) throws Exception {
     if (argv.length > 0) {