Cache repository locations in LocalDiskRepositoryManager

Obtaining the actual location of a repository using base-path
and project name can be slow as it involves some guessing to
locate the repository. Cache the locations once they are
obtained to avoid repeated work, thereby improving performance
when opening repositories.

For example, on a site with 20k repositories on NFS, ls-projects
with this change takes ~60s and ~90s without. Also, a query which
wraps a large (~2k) list of manifest[1] operators will take ~300ms
with this change and ~2s without it.

[1] https://gerrit.googlesource.com/plugins/manifest

Release-Notes: skip
Change-Id: I8eab3c813c4ac9433e93c7ace96d38efe332be27
diff --git a/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java b/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
index 1fdf194..2c0fdca 100644
--- a/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
+++ b/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
@@ -33,8 +33,10 @@
 import java.nio.file.attribute.BasicFileAttributes;
 import java.util.Collections;
 import java.util.EnumSet;
+import java.util.Map;
 import java.util.SortedSet;
 import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ConfigConstants;
@@ -108,6 +110,7 @@
   }
 
   private final Path basePath;
+  private final Map<Project.NameKey, FileKey> fileKeyByProject = new ConcurrentHashMap<>();
 
   @Inject
   LocalDiskRepositoryManager(SitePaths site, @GerritServerConfig Config cfg) {
@@ -129,17 +132,23 @@
 
   @Override
   public Repository openRepository(Project.NameKey name) throws RepositoryNotFoundException {
-    return openRepository(getBasePath(name), name);
-  }
+    FileKey cachedLocation = fileKeyByProject.get(name);
+    if (cachedLocation != null) {
+      try {
+        return RepositoryCache.open(cachedLocation);
+      } catch (IOException e) {
+        fileKeyByProject.remove(name, cachedLocation);
+      }
+    }
 
-  private Repository openRepository(Path path, Project.NameKey name)
-      throws RepositoryNotFoundException {
     if (isUnreasonableName(name)) {
       throw new RepositoryNotFoundException("Invalid name: " + name);
     }
-    FileKey loc = FileKey.lenient(path.resolve(name.get()).toFile(), FS.DETECTED);
+    FileKey location = FileKey.lenient(getBasePath(name).resolve(name.get()).toFile(), FS.DETECTED);
     try {
-      return RepositoryCache.open(loc);
+      Repository repo = RepositoryCache.open(location);
+      fileKeyByProject.put(name, location);
+      return repo;
     } catch (IOException e) {
       throw new RepositoryNotFoundException("Cannot open repository " + name, e);
     }