[8.0.0] Prevent excessive thread creation in disk cache garbage collection (#24113)

Fixes https://github.com/bazelbuild/bazel/issues/24098

With this change the disk cache garbage collection works correctly:
```
241027 09:06:34.732:I 681 [com.google.devtools.build.lib.remote.disk.DiskCacheGarbageCollectorIdleTask.run] Disk cache garbage collection started
241027 09:07:06.123:I 681 [com.google.devtools.build.lib.remote.disk.DiskCacheGarbageCollectorIdleTask.run] Deleted 190243 of 446229 files, reclaimed 5.4 GiB of 15.4 GiB
```

Closes #24099.

PiperOrigin-RevId: 690652512
Change-Id: Ie8d1fa6b2afb0bd5bd85fdb6835871023a64ad24

Commit
https://github.com/bazelbuild/bazel/commit/374658377f662701f1e92f757090949fdd0da1ab

Co-authored-by: Roman Salvador <rsalvador@salesforce.com>
diff --git a/src/main/java/com/google/devtools/build/lib/remote/disk/DiskCacheGarbageCollector.java b/src/main/java/com/google/devtools/build/lib/remote/disk/DiskCacheGarbageCollector.java
index a57c2bf..5e19689 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/disk/DiskCacheGarbageCollector.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/disk/DiskCacheGarbageCollector.java
@@ -121,16 +121,24 @@
       long totalBytes,
       long deletedEntries,
       long deletedBytes,
-      boolean concurrentUpdate) {
+      boolean concurrentUpdate,
+      Duration elapsedTime) {
 
     /** Returns a human-readable summary. */
     public String displayString() {
-      return "Deleted %d of %d files, reclaimed %s of %s%s"
+      double elapsedSeconds = elapsedTime.toSecondsPart() + elapsedTime.toMillisPart() / 1000.0;
+      int filesPerSecond = (int) Math.round((double) deletedEntries / elapsedSeconds);
+      int mbPerSecond = (int) Math.round((deletedBytes / (1024.0 * 1024.0)) / elapsedSeconds);
+
+      return "Deleted %d of %d files, reclaimed %s of %s in %.2f seconds (%d files/s, %d MB/s)%s"
           .formatted(
               deletedEntries(),
               totalEntries(),
               bytesCountToDisplayString(deletedBytes()),
               bytesCountToDisplayString(totalBytes()),
+              elapsedSeconds,
+              filesPerSecond,
+              mbPerSecond,
               concurrentUpdate() ? " (concurrent update detected)" : "");
     }
   }
@@ -180,6 +188,7 @@
   }
 
   private CollectionStats runUnderLock() throws IOException, InterruptedException {
+    Instant startTime = Instant.now();
     EntryScanner scanner = new EntryScanner();
     EntryDeleter deleter = new EntryDeleter();
 
@@ -191,13 +200,15 @@
     }
 
     DeletionStats deletionStats = deleter.await();
+    Duration elapsedTime = Duration.between(startTime, Instant.now());
 
     return new CollectionStats(
         allEntries.size(),
         allEntries.stream().mapToLong(Entry::size).sum(),
         deletionStats.deletedEntries(),
         deletionStats.deletedBytes(),
-        deletionStats.concurrentUpdate());
+        deletionStats.concurrentUpdate(),
+        elapsedTime);
   }
 
   /** Lists all disk cache entries, performing I/O in parallel. */
diff --git a/src/main/java/com/google/devtools/build/lib/remote/disk/DiskCacheGarbageCollectorIdleTask.java b/src/main/java/com/google/devtools/build/lib/remote/disk/DiskCacheGarbageCollectorIdleTask.java
index 7c03e16..b5ebaae 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/disk/DiskCacheGarbageCollectorIdleTask.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/disk/DiskCacheGarbageCollectorIdleTask.java
@@ -36,7 +36,8 @@
   private final DiskCacheGarbageCollector gc;
 
   private static final ExecutorService executorService =
-      Executors.newCachedThreadPool(
+      Executors.newFixedThreadPool(
+          Math.max(4, Runtime.getRuntime().availableProcessors()),
           new ThreadFactoryBuilder().setNameFormat("disk-cache-gc-%d").build());
 
   private DiskCacheGarbageCollectorIdleTask(Duration delay, DiskCacheGarbageCollector gc) {
diff --git a/src/test/java/com/google/devtools/build/lib/remote/disk/DiskCacheGarbageCollectorTest.java b/src/test/java/com/google/devtools/build/lib/remote/disk/DiskCacheGarbageCollectorTest.java
index 41b778a..9e164c6 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/disk/DiskCacheGarbageCollectorTest.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/disk/DiskCacheGarbageCollectorTest.java
@@ -61,7 +61,7 @@
 
     CollectionStats stats = runGarbageCollector(Optional.of(kbytes(2)), Optional.empty());
 
-    assertThat(stats).isEqualTo(new CollectionStats(2, kbytes(2), 0, 0, false));
+    assertThat(stats).isEqualTo(new CollectionStats(2, kbytes(2), 0, 0, false, Duration.ZERO));
     assertFilesExist("ac/123", "cas/456");
   }
 
@@ -75,7 +75,8 @@
 
     CollectionStats stats = runGarbageCollector(Optional.of(kbytes(2)), Optional.empty());
 
-    assertThat(stats).isEqualTo(new CollectionStats(4, kbytes(4), 2, kbytes(2), false));
+    assertThat(stats)
+        .isEqualTo(new CollectionStats(4, kbytes(4), 2, kbytes(2), false, Duration.ZERO));
     assertFilesExist("ac/123", "cas/456");
     assertFilesDoNotExist("ac/abc", "cas/def");
   }
@@ -90,7 +91,8 @@
 
     CollectionStats stats = runGarbageCollector(Optional.of(kbytes(2)), Optional.empty());
 
-    assertThat(stats).isEqualTo(new CollectionStats(4, kbytes(4), 2, kbytes(2), false));
+    assertThat(stats)
+        .isEqualTo(new CollectionStats(4, kbytes(4), 2, kbytes(2), false, Duration.ZERO));
     assertFilesExist("cas/456", "cas/def");
     assertFilesDoNotExist("ac/123", "ac/abc");
   }
@@ -103,7 +105,7 @@
 
     CollectionStats stats = runGarbageCollector(Optional.empty(), Optional.of(days(3)));
 
-    assertThat(stats).isEqualTo(new CollectionStats(2, kbytes(2), 0, 0, false));
+    assertThat(stats).isEqualTo(new CollectionStats(2, kbytes(2), 0, 0, false, Duration.ZERO));
     assertFilesExist("ac/123", "cas/456");
   }
 
@@ -117,7 +119,8 @@
 
     CollectionStats stats = runGarbageCollector(Optional.empty(), Optional.of(Duration.ofDays(3)));
 
-    assertThat(stats).isEqualTo(new CollectionStats(4, kbytes(4), 2, kbytes(2), false));
+    assertThat(stats)
+        .isEqualTo(new CollectionStats(4, kbytes(4), 2, kbytes(2), false, Duration.ZERO));
     assertFilesExist("ac/123", "cas/456");
     assertFilesDoNotExist("ac/abc", "cas/def");
   }
@@ -130,7 +133,7 @@
 
     CollectionStats stats = runGarbageCollector(Optional.of(kbytes(2)), Optional.of(days(1)));
 
-    assertThat(stats).isEqualTo(new CollectionStats(2, kbytes(2), 0, 0, false));
+    assertThat(stats).isEqualTo(new CollectionStats(2, kbytes(2), 0, 0, false, Duration.ZERO));
     assertFilesExist("ac/123", "cas/456");
   }
 
@@ -144,7 +147,8 @@
 
     CollectionStats stats = runGarbageCollector(Optional.of(kbytes(2)), Optional.of(days(4)));
 
-    assertThat(stats).isEqualTo(new CollectionStats(4, kbytes(4), 2, kbytes(2), false));
+    assertThat(stats)
+        .isEqualTo(new CollectionStats(4, kbytes(4), 2, kbytes(2), false, Duration.ZERO));
     assertFilesExist("ac/123", "cas/456");
     assertFilesDoNotExist("ac/abc", "cas/def");
   }
@@ -159,7 +163,8 @@
 
     CollectionStats stats = runGarbageCollector(Optional.of(kbytes(3)), Optional.of(days(3)));
 
-    assertThat(stats).isEqualTo(new CollectionStats(4, kbytes(4), 2, kbytes(2), false));
+    assertThat(stats)
+        .isEqualTo(new CollectionStats(4, kbytes(4), 2, kbytes(2), false, Duration.ZERO));
     assertFilesExist("ac/123", "cas/456");
     assertFilesDoNotExist("ac/abc", "cas/def");
   }
@@ -171,7 +176,7 @@
 
     CollectionStats stats = runGarbageCollector(Optional.of(1L), Optional.of(days(1)));
 
-    assertThat(stats).isEqualTo(new CollectionStats(0, 0, 0, 0, false));
+    assertThat(stats).isEqualTo(new CollectionStats(0, 0, 0, 0, false, Duration.ZERO));
     assertFilesExist("gc/foo", "tmp/foo");
   }
 
@@ -212,7 +217,14 @@
             rootDir,
             executorService,
             new DiskCacheGarbageCollector.CollectionPolicy(maxSizeBytes, maxAge));
-    return gc.run();
+    CollectionStats resultStats = gc.run();
+    return new CollectionStats(
+        resultStats.totalEntries(),
+        resultStats.totalBytes(),
+        resultStats.deletedEntries(),
+        resultStats.deletedBytes(),
+        resultStats.concurrentUpdate(),
+        Duration.ZERO);
   }
 
   private void writeFiles(Entry... entries) throws IOException {