WindowCache: add bulk purge(), call from bulk sites

Purging a Pack from the WindowCache requires a linear search over all
the entries in the cache, and thus is rather expensive. Since git gc
often deletes more than one packfile at a time, avoid multiplying this
expensive operation when possible by purging a Set of Packs when closing
deleted pack files during a directory scan. Purge the Set of Packs with
a single linear search of the cache.  Closing the PackDirectory also
cccbngljkihltghcnbiftcubdvgugdcvujkejehbjr

Change-Id: Ic9b45cab57e1ef610c2e20ad392d8b45f8091f41
Signed-off-by: Martin Fick <mfick@nvidia.com>
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/Pack.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/Pack.java
index 3518342..9073c16 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/Pack.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/Pack.java
@@ -296,15 +296,28 @@ void resolve(Set<ObjectId> matches, AbbreviatedObjectId id, int matchLimit)
 	}
 
 	/**
-	 * Close the resources utilized by this repository
+	 * Close the resources utilized by these pack files
+	 *
+	 * @param packs
+	 *            packs to close
+	 */
+	public static void close(Set<Pack> packs) {
+		WindowCache.purge(packs);
+		packs.forEach(p -> p.closeIndices());
+	}
+
+	/**
+	 * Close the resources utilized by this pack file
 	 */
 	public void close() {
 		WindowCache.purge(this);
-		synchronized (this) {
-			loadedIdx.clear();
-			reverseIdx.clear();
-			bitmapIdx.clear();
-		}
+		closeIndices();
+	}
+
+	private synchronized void closeIndices() {
+		loadedIdx.clear();
+		reverseIdx.clear();
+		bitmapIdx.clear();
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackDirectory.java
index 51a8148..e31126f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackDirectory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackDirectory.java
@@ -24,6 +24,7 @@
 import java.util.Collections;
 import java.util.EnumMap;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -111,9 +112,7 @@ void create() throws IOException {
 	void close() {
 		PackList packs = packList.get();
 		if (packs != NO_PACKS && packList.compareAndSet(packs, NO_PACKS)) {
-			for (Pack p : packs.packs) {
-				p.close();
-			}
+			Pack.close(Set.of(packs.packs));
 		}
 	}
 
@@ -484,9 +483,7 @@ private PackList scanPacksImpl(PackList old) {
 			return old;
 		}
 
-		for (Pack p : forReuse.values()) {
-			p.close();
-		}
+		Pack.close(new HashSet<>(forReuse.values()));
 
 		if (list.isEmpty()) {
 			return new PackList(snapshot, NO_PACKS.packs);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCache.java
index 30f8240..3ec7e6e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCache.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCache.java
@@ -17,6 +17,7 @@
 import java.util.Collections;
 import java.util.Map;
 import java.util.Random;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -397,7 +398,11 @@ static final ByteWindow get(Pack pack, long offset)
 	}
 
 	static final void purge(Pack pack) {
-		cache.removeAll(pack);
+		purge(Collections.singleton(pack));
+	}
+
+	static final void purge(Set<Pack> packs) {
+		cache.removeAll(packs);
 	}
 
 	/** cleanup released and/or garbage collected windows. */
@@ -710,20 +715,21 @@ private void removeAll() {
 	/**
 	 * Clear all entries related to a single file.
 	 * <p>
-	 * Typically this method is invoked during {@link Pack#close()}, when we
-	 * know the pack is never going to be useful to us again (for example, it no
-	 * longer exists on disk). A concurrent reader loading an entry from this
-	 * same pack may cause the pack to become stuck in the cache anyway.
+	 * Typically this method is invoked during {@link Pack#close()}, or
+	 * {@link Pack#close(Set)}, when we know the packs are never going to be
+	 * useful to us again (for example, they no longer exist on disk after gc).
+	 * A concurrent reader loading an entry from these same packs may cause a
+	 * pack to become stuck in the cache anyway.
 	 *
-	 * @param pack
-	 *            the file to purge all entries of.
+	 * @param packs
+	 *            the files to purge all entries of.
 	 */
-	private void removeAll(Pack pack) {
+	private void removeAll(Set<Pack> packs) {
 		for (int s = 0; s < tableSize; s++) {
 			final Entry e1 = table.get(s);
 			boolean hasDead = false;
 			for (Entry e = e1; e != null; e = e.next) {
-				if (e.ref.getPack() == pack) {
+				if (packs.contains(e.ref.getPack())) {
 					e.kill();
 					hasDead = true;
 				} else if (e.dead)