Add the option --experimental_max_directories_to_eagerly_visit_in_globbing. The first legacy glob that a package requires will, if this option is enabled, cause up to that many directories to be eagerly visited by a glob(['**']). The results are thrown away for memory reasons.

--
MOS_MIGRATED_REVID=135148361
diff --git a/src/main/java/com/google/devtools/build/lib/packages/GlobCache.java b/src/main/java/com/google/devtools/build/lib/packages/GlobCache.java
index 694601d..5259385 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/GlobCache.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/GlobCache.java
@@ -26,7 +26,6 @@
 import com.google.devtools.build.lib.util.Preconditions;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.UnixGlob;
-
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -39,6 +38,7 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
 import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
 
 /**
@@ -76,29 +76,37 @@
    * System call caching layer.
    */
   private AtomicReference<? extends UnixGlob.FilesystemCalls> syscalls;
+  private final int maxDirectoriesToEagerlyVisit;
 
   /**
    * The thread pool for glob evaluation.
    */
   private final ThreadPoolExecutor globExecutor;
+  private final AtomicBoolean globalStarted = new AtomicBoolean(false);
 
   /**
    * Create a glob expansion cache.
-   * @param packageDirectory globs will be expanded relatively to this
-   *                         directory.
+   *
+   * @param packageDirectory globs will be expanded relatively to this directory.
    * @param packageId the name of the package this cache belongs to.
    * @param locator the package locator.
    * @param globExecutor thread pool for glob evaluation.
+   * @param maxDirectoriesToEagerlyVisit the number of directories to eagerly traverse on the first
+   *     glob for a given package, in order to warm the filesystem. -1 means do no eager traversal.
+   *     See {@code PackageCacheOptions#maxDirectoriesToEagerlyVisitInGlobbing}.
    */
-  public GlobCache(final Path packageDirectory,
-                   final PackageIdentifier packageId,
-                   final CachingPackageLocator locator,
-                   AtomicReference<? extends UnixGlob.FilesystemCalls> syscalls,
-                   ThreadPoolExecutor globExecutor) {
+  public GlobCache(
+      final Path packageDirectory,
+      final PackageIdentifier packageId,
+      final CachingPackageLocator locator,
+      AtomicReference<? extends UnixGlob.FilesystemCalls> syscalls,
+      ThreadPoolExecutor globExecutor,
+      int maxDirectoriesToEagerlyVisit) {
     this.packageDirectory = Preconditions.checkNotNull(packageDirectory);
     this.packageId = Preconditions.checkNotNull(packageId);
     this.globExecutor = Preconditions.checkNotNull(globExecutor);
     this.syscalls = syscalls == null ? new AtomicReference<>(UnixGlob.DEFAULT_SYSCALLS) : syscalls;
+    this.maxDirectoriesToEagerlyVisit = maxDirectoriesToEagerlyVisit;
 
     Preconditions.checkNotNull(locator);
     childDirectoryPredicate = new Predicate<Path>() {
@@ -129,6 +137,18 @@
       throws BadGlobException {
     Future<List<Path>> cached = globCache.get(Pair.of(pattern, excludeDirs));
     if (cached == null) {
+      if (maxDirectoriesToEagerlyVisit > -1
+          && !globalStarted.getAndSet(true)
+          && !pattern.startsWith("**")) {
+        UnixGlob.forPath(packageDirectory)
+            .setMaxDirectoriesToEagerlyVisit(maxDirectoriesToEagerlyVisit)
+            .addPattern("**")
+            .setExcludeDirectories(true)
+            .setDirectoryFilter(childDirectoryPredicate)
+            .setThreadPool(globExecutor)
+            .setFilesystemCalls(syscalls)
+            .globAsync(true);
+      }
       cached = safeGlobUnsorted(pattern, excludeDirs);
       setGlobPaths(pattern, excludeDirs, cached);
     }
diff --git a/src/main/java/com/google/devtools/build/lib/packages/PackageFactory.java b/src/main/java/com/google/devtools/build/lib/packages/PackageFactory.java
index 86406f4..52cb946 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/PackageFactory.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/PackageFactory.java
@@ -334,6 +334,8 @@
   private final ThreadPoolExecutor threadPool;
   private Map<String, String> platformSetRegexps;
 
+  private int maxDirectoriesToEagerlyVisitInGlobbing;
+
   private final ImmutableList<EnvironmentExtension> environmentExtensions;
   private final ImmutableMap<String, PackageArgument<?>> packageArguments;
 
@@ -429,6 +431,10 @@
     threadPool.setMaximumPoolSize(globbingThreads);
   }
 
+  public void setMaxDirectoriesToEagerlyVisitInGlobbing(
+      int maxDirectoriesToEagerlyVisitInGlobbing) {
+    this.maxDirectoriesToEagerlyVisitInGlobbing = maxDirectoriesToEagerlyVisitInGlobbing;
+  }
 
   /**
    * Returns the immutable, unordered set of names of all the known rule
@@ -1422,8 +1428,14 @@
       Path packageDirectory,
       PackageIdentifier packageId,
       CachingPackageLocator locator) {
-    return createLegacyGlobber(new GlobCache(packageDirectory, packageId, locator, syscalls,
-        threadPool));
+    return createLegacyGlobber(
+        new GlobCache(
+            packageDirectory,
+            packageId,
+            locator,
+            syscalls,
+            threadPool,
+            maxDirectoriesToEagerlyVisitInGlobbing));
   }
 
   /** Returns a new {@link LegacyGlobber}. */
@@ -1440,8 +1452,15 @@
       Path packageDirectory,
       PackageIdentifier packageId,
       CachingPackageLocator locator) {
-    return new LegacyGlobber(new GlobCache(packageDirectory, packageId, locator, syscalls,
-        threadPool), /*sort=*/ false);
+    return new LegacyGlobber(
+        new GlobCache(
+            packageDirectory,
+            packageId,
+            locator,
+            syscalls,
+            threadPool,
+            maxDirectoriesToEagerlyVisitInGlobbing),
+        /*sort=*/ false);
   }
 
   @Nullable
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/PackageCacheOptions.java b/src/main/java/com/google/devtools/build/lib/pkgcache/PackageCacheOptions.java
index 0664229..f22fceb 100644
--- a/src/main/java/com/google/devtools/build/lib/pkgcache/PackageCacheOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/PackageCacheOptions.java
@@ -110,6 +110,17 @@
       help = "Number of threads to use for glob evaluation.")
   public int globbingThreads;
 
+  @Option(
+    name = "experimental_max_directories_to_eagerly_visit_in_globbing",
+    defaultValue = "-1",
+    category = "undocumented",
+    help =
+        "If non-negative, the first time a glob is evaluated in a package, the subdirectories of "
+            + "the package will be traversed in order to warm filesystem caches and compensate for "
+            + "lack of parallelism in globbing. At most this many directories will be visited."
+  )
+  public int maxDirectoriesToEagerlyVisitInGlobbing;
+
   @Option(name = "fetch",
       defaultValue = "true",
       category = "undocumented",
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
index c87db1e..9d4d72d 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
@@ -920,6 +920,8 @@
 
     syscalls.set(newPerBuildSyscallCache(packageCacheOptions.globbingThreads));
     this.pkgFactory.setGlobbingThreads(packageCacheOptions.globbingThreads);
+    this.pkgFactory.setMaxDirectoriesToEagerlyVisitInGlobbing(
+        packageCacheOptions.maxDirectoriesToEagerlyVisitInGlobbing);
     checkPreprocessorFactory();
     emittedEventState.clear();
 
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/UnixGlob.java b/src/main/java/com/google/devtools/build/lib/vfs/UnixGlob.java
index 1ea067d..82b5e55 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/UnixGlob.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/UnixGlob.java
@@ -35,7 +35,6 @@
 import com.google.devtools.build.lib.profiler.Profiler;
 import com.google.devtools.build.lib.profiler.ProfilerTask;
 import com.google.devtools.build.lib.util.Preconditions;
-
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
@@ -45,6 +44,7 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
 import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.regex.Pattern;
@@ -68,9 +68,10 @@
                                          FilesystemCalls syscalls,
                                          ThreadPoolExecutor threadPool)
       throws IOException, InterruptedException {
-    GlobVisitor visitor = (threadPool == null)
-        ? new GlobVisitor(checkForInterruption)
-        : new GlobVisitor(threadPool, checkForInterruption);
+    GlobVisitor visitor =
+        (threadPool == null)
+            ? new GlobVisitor(checkForInterruption)
+            : new GlobVisitor(threadPool, checkForInterruption, -1);
     return visitor.glob(base, patterns, excludeDirectories, dirPred, syscalls);
   }
 
@@ -81,22 +82,26 @@
       boolean checkForInterruption,
       FilesystemCalls syscalls,
       ThreadPoolExecutor threadPool) throws IOException, InterruptedException {
-    GlobVisitor visitor = (threadPool == null)
-        ? new GlobVisitor(checkForInterruption)
-        : new GlobVisitor(threadPool, checkForInterruption);
+    GlobVisitor visitor =
+        (threadPool == null)
+            ? new GlobVisitor(checkForInterruption)
+            : new GlobVisitor(threadPool, checkForInterruption, -1);
     visitor.glob(base, patterns, excludeDirectories, dirPred, syscalls);
     return visitor.getNumGlobTasksForTesting();
   }
 
-  private static Future<List<Path>> globAsyncInternal(Path base, Collection<String> patterns,
-                                                      boolean excludeDirectories,
-                                                      Predicate<Path> dirPred,
-                                                      FilesystemCalls syscalls,
-                                                      boolean checkForInterruption,
-                                                      ThreadPoolExecutor threadPool) {
+  private static Future<List<Path>> globAsyncInternal(
+      Path base,
+      Collection<String> patterns,
+      boolean excludeDirectories,
+      Predicate<Path> dirPred,
+      FilesystemCalls syscalls,
+      boolean checkForInterruption,
+      ThreadPoolExecutor threadPool,
+      int maxDirectoriesToEagerlyVisit) {
     Preconditions.checkNotNull(threadPool, "%s %s", base, patterns);
-    return new GlobVisitor(threadPool, checkForInterruption).globAsync(
-        base, patterns, excludeDirectories, dirPred, syscalls);
+    return new GlobVisitor(threadPool, checkForInterruption, maxDirectoriesToEagerlyVisit)
+        .globAsync(base, patterns, excludeDirectories, dirPred, syscalls);
   }
 
   /**
@@ -304,6 +309,7 @@
     private ThreadPoolExecutor threadPool;
     private AtomicReference<? extends FilesystemCalls> syscalls =
         new AtomicReference<>(DEFAULT_SYSCALLS);
+    private int maxDirectoriesToEagerlyVisit = -1;
 
     /**
      * Creates a glob builder with the given base path.
@@ -384,6 +390,11 @@
       return this;
     }
 
+    public Builder setMaxDirectoriesToEagerlyVisit(int maxDirectoriesToEagerlyVisit) {
+      this.maxDirectoriesToEagerlyVisit = maxDirectoriesToEagerlyVisit;
+      return this;
+    }
+
     /**
      * Executes the glob.
      */
@@ -421,8 +432,15 @@
      * @param checkForInterrupt if the returned future may throw InterruptedException.
      */
     public Future<List<Path>> globAsync(boolean checkForInterrupt) {
-      return globAsyncInternal(base, patterns, excludeDirectories, pathFilter, syscalls.get(),
-          checkForInterrupt, threadPool);
+      return globAsyncInternal(
+          base,
+          patterns,
+          excludeDirectories,
+          pathFilter,
+          syscalls.get(),
+          checkForInterrupt,
+          threadPool,
+          maxDirectoriesToEagerlyVisit);
     }
   }
 
@@ -489,15 +507,21 @@
     private final AtomicLong totalOps = new AtomicLong(0);
     private final AtomicLong pendingOps = new AtomicLong(0);
     private final AtomicReference<IOException> failure = new AtomicReference<>();
+    private final int maxDirectoriesToEagerlyVisit;
+    private final AtomicInteger visitedDirectories = new AtomicInteger(0);
     private volatile boolean canceled = false;
 
-    public GlobVisitor(ThreadPoolExecutor executor, boolean failFastOnInterrupt) {
+    GlobVisitor(
+        ThreadPoolExecutor executor,
+        boolean failFastOnInterrupt,
+        int maxDirectoriesToEagerlyVisit) {
       this.executor = executor;
       this.result = new GlobFuture(this, failFastOnInterrupt);
+      this.maxDirectoriesToEagerlyVisit = maxDirectoriesToEagerlyVisit;
     }
 
-    public GlobVisitor(boolean failFastOnInterrupt) {
-      this(null, failFastOnInterrupt);
+    GlobVisitor(boolean failFastOnInterrupt) {
+      this(null, failFastOnInterrupt, -1);
     }
 
     /**
@@ -534,11 +558,23 @@
     }
 
     /**
+     * Whether or not to store the results of this glob. If this glob is being done purely to warm
+     * the filesystem, we do not store the results, since it would take unnecessary memory.
+     */
+    private boolean storeGlobResults() {
+      return maxDirectoriesToEagerlyVisit == -1;
+    }
+
+    /**
      * Same as {@link #glob}, except does so asynchronously and returns a {@link Future} for the
      * result.
      */
-    public Future<List<Path>> globAsync(Path base, Collection<String> patterns,
-        boolean excludeDirectories, Predicate<Path> dirPred, FilesystemCalls syscalls) {
+    public Future<List<Path>> globAsync(
+        Path base,
+        Collection<String> patterns,
+        boolean excludeDirectories,
+        Predicate<Path> dirPred,
+        FilesystemCalls syscalls) {
 
       FileStatus baseStat = syscalls.statNullable(base, Symlinks.FOLLOW);
       if (baseStat == null || patterns.isEmpty()) {
@@ -744,7 +780,7 @@
       }
 
       if (idx == context.patternParts.length) { // Base case.
-        if (!(context.excludeDirectories && baseIsDir)) {
+        if (storeGlobResults() && !(context.excludeDirectories && baseIsDir)) {
           results.add(base);
         }
 
@@ -756,6 +792,10 @@
         return;
       }
 
+      if (maxDirectoriesToEagerlyVisit > -1
+          && visitedDirectories.incrementAndGet() > maxDirectoriesToEagerlyVisit) {
+        return;
+      }
       final String pattern = context.patternParts[idx];
 
       // ** is special: it can match nothing at all.
@@ -803,7 +843,7 @@
             context.queueGlob(child, childIsDir, idx + 1);
           } else {
             // Instead of using an async call, just repeat the base case above.
-            if (idx + 1 == context.patternParts.length) {
+            if (storeGlobResults() && idx + 1 == context.patternParts.length) {
               results.add(child);
             }
           }
diff --git a/src/test/java/com/google/devtools/build/lib/packages/GlobCacheTest.java b/src/test/java/com/google/devtools/build/lib/packages/GlobCacheTest.java
index 7285ede..4d120c1 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/GlobCacheTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/GlobCacheTest.java
@@ -24,18 +24,16 @@
 import com.google.devtools.build.lib.util.Pair;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /**
  * Tests for {@link GlobCache}
@@ -89,20 +87,26 @@
     scratch.file("isolated/sub/sub.js",
         "# this is sub/sub.js");
 
-    cache = new GlobCache(packageDirectory, PackageIdentifier.createInMainRepo("isolated"),
-        new CachingPackageLocator() {
-      @Override
-      public Path getBuildFileForPackage(PackageIdentifier packageId) {
-        String packageName = packageId.getPackageFragment().getPathString();
-        if (packageName.equals("isolated")) {
-          return scratch.resolve("isolated/BUILD");
-        } else if (packageName.equals("isolated/sub")) {
-          return scratch.resolve("isolated/sub/BUILD");
-        } else {
-          return null;
-        }
-      }
-    }, null, TestUtils.getPool());
+    cache =
+        new GlobCache(
+            packageDirectory,
+            PackageIdentifier.createInMainRepo("isolated"),
+            new CachingPackageLocator() {
+              @Override
+              public Path getBuildFileForPackage(PackageIdentifier packageId) {
+                String packageName = packageId.getPackageFragment().getPathString();
+                if (packageName.equals("isolated")) {
+                  return scratch.resolve("isolated/BUILD");
+                } else if (packageName.equals("isolated/sub")) {
+                  return scratch.resolve("isolated/sub/BUILD");
+                } else {
+                  return null;
+                }
+              }
+            },
+            null,
+            TestUtils.getPool(),
+            -1);
   }
 
   @After
diff --git a/src/test/java/com/google/devtools/build/lib/packages/util/PackageFactoryApparatus.java b/src/test/java/com/google/devtools/build/lib/packages/util/PackageFactoryApparatus.java
index e53690e..505a845 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/util/PackageFactoryApparatus.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/util/PackageFactoryApparatus.java
@@ -120,7 +120,8 @@
             packageId,
             getPackageLocator(),
             null,
-            TestUtils.getPool());
+            TestUtils.getPool(),
+            -1);
     LegacyGlobber globber = PackageFactory.createLegacyGlobber(globCache);
     Package externalPkg =
         factory.newExternalPackageBuilder(
diff --git a/src/test/java/com/google/devtools/build/lib/packages/util/PackageFactoryTestBase.java b/src/test/java/com/google/devtools/build/lib/packages/util/PackageFactoryTestBase.java
index 5d8c1da..6db8486 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/util/PackageFactoryTestBase.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/util/PackageFactoryTestBase.java
@@ -42,9 +42,6 @@
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
-
-import org.junit.Before;
-
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.util.Collection;
@@ -53,6 +50,7 @@
 import java.util.concurrent.Semaphore;
 import java.util.logging.Handler;
 import java.util.logging.LogRecord;
+import org.junit.Before;
 
 /**
  * Base class for PackageFactory tests.
@@ -113,7 +111,8 @@
             pkg.getPackageIdentifier(),
             PackageFactoryApparatus.createEmptyLocator(),
             null,
-            TestUtils.getPool());
+            TestUtils.getPool(),
+            -1);
     assertThat(globCache.globUnsorted(include, exclude, false)).containsExactlyElementsIn(expected);
   }
 
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/PackageFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/PackageFunctionTest.java
index b5d8c16..9175145 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/PackageFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/PackageFunctionTest.java
@@ -24,7 +24,6 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
 import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.cmdline.PackageIdentifier;
@@ -36,6 +35,7 @@
 import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
 import com.google.devtools.build.lib.skyframe.util.SkyframeExecutorTestUtils;
 import com.google.devtools.build.lib.testutil.ManualClock;
+import com.google.devtools.build.lib.testutil.TestUtils;
 import com.google.devtools.build.lib.util.BlazeClock;
 import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
 import com.google.devtools.build.lib.vfs.Dirent;
@@ -56,8 +56,10 @@
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Map;
-import java.util.Set;
 import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 import javax.annotation.Nullable;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -761,74 +763,255 @@
     }
   }
 
+  @Test
+  public void testGlobsHappenInParallel() throws Exception {
+    scratch.file(
+        "foo/BUILD",
+        "load('//foo:my_library.bzl', 'my_library')",
+        "[sh_library(name = x + '-matched') for x in glob(['bar/*'], exclude_directories = 0)]",
+        "cc_library(name = 'cc', srcs = glob(['cc/*']))",
+        "my_library(name = 'my', srcs = glob(['sh/*']))");
+    scratch.file(
+        "foo/my_library.bzl",
+        "def my_library(name = None, srcs = []):",
+        "  native.sh_library(name = name, srcs = srcs, deps = native.glob(['inner/*']))");
+    scratch.file("foo/bar/1");
+    Path barPath = scratch.file("foo/bar/2").getParentDirectory();
+    Path ccPath = scratch.file("foo/cc/src.file").getParentDirectory();
+    Path shPath = scratch.dir("foo/sh");
+    Path innerPath = scratch.dir("foo/inner");
+    PackageCacheOptions packageCacheOptions = Options.getDefaults(PackageCacheOptions.class);
+    packageCacheOptions.defaultVisibility = ConstantRuleVisibility.PUBLIC;
+    packageCacheOptions.showLoadingProgress = true;
+    packageCacheOptions.globbingThreads = 7;
+    packageCacheOptions.maxDirectoriesToEagerlyVisitInGlobbing = 10;
+    getSkyframeExecutor()
+        .preparePackageLoading(
+            new PathPackageLocator(outputBase, ImmutableList.of(rootDirectory)),
+            packageCacheOptions,
+            "",
+            UUID.randomUUID(),
+            ImmutableMap.<String, String>of(),
+            new TimestampGranularityMonitor(BlazeClock.instance()));
+
+    SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("@//foo"));
+    final CountDownLatch allDirsRequested = new CountDownLatch(4);
+    Listener synchronizeListener =
+        new Listener() {
+          @Override
+          public Object accept(Path path, FileOp op, Order order) throws IOException {
+            if (op == FileOp.READDIR && order == Order.BEFORE) {
+              allDirsRequested.countDown();
+              try {
+                assertThat(
+                        allDirsRequested.await(
+                            TestUtils.WAIT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS))
+                    .isTrue();
+              } catch (InterruptedException e) {
+                throw new IllegalStateException(e);
+              }
+            }
+            return NO_RESULT_MARKER;
+          }
+        };
+    fs.setCustomOverride(barPath, synchronizeListener);
+    fs.setCustomOverride(ccPath, synchronizeListener);
+    fs.setCustomOverride(shPath, synchronizeListener);
+    fs.setCustomOverride(innerPath, synchronizeListener);
+    PackageValue value = validPackage(skyKey);
+    assertFalse(value.getPackage().containsErrors());
+    assertThat(value.getPackage().getTarget("bar/1-matched").getName()).isEqualTo("bar/1-matched");
+    assertThat(value.getPackage().getTarget("cc/src.file")).isNotNull();
+    assertThat(
+            (Iterable<?>)
+                value
+                    .getPackage()
+                    .getTarget("my")
+                    .getAssociatedRule()
+                    .getAttributeContainer()
+                    .getAttr("srcs"))
+        .isEmpty();
+  }
+
+  @Test
+  public void testGlobsDontHappenInParallel() throws Exception {
+    scratch.file(
+        "foo/BUILD",
+        "load('//foo:my_library.bzl', 'my_library')",
+        "[sh_library(name = x + '-matched') for x in glob(['bar/*'], exclude_directories = 0)]",
+        "cc_library(name = 'cc', srcs = glob(['cc/*']))",
+        "my_library(name = 'my', srcs = glob(['sh/*']))");
+    scratch.file(
+        "foo/my_library.bzl",
+        "def my_library(name = None, srcs = []):",
+        "  native.sh_library(name = name, srcs = srcs, deps = native.glob(['inner/*']))");
+    scratch.file("foo/bar/1");
+    Path barPath = scratch.file("foo/bar/2").getParentDirectory();
+    Path ccPath = scratch.file("foo/cc/src.file").getParentDirectory();
+    Path shPath = scratch.dir("foo/sh");
+    Path innerPath = scratch.dir("foo/inner");
+    PackageCacheOptions packageCacheOptions = Options.getDefaults(PackageCacheOptions.class);
+    packageCacheOptions.defaultVisibility = ConstantRuleVisibility.PUBLIC;
+    packageCacheOptions.showLoadingProgress = true;
+    packageCacheOptions.globbingThreads = 7;
+    packageCacheOptions.maxDirectoriesToEagerlyVisitInGlobbing = -1;
+    getSkyframeExecutor()
+        .preparePackageLoading(
+            new PathPackageLocator(outputBase, ImmutableList.of(rootDirectory)),
+            packageCacheOptions,
+            "",
+            UUID.randomUUID(),
+            ImmutableMap.<String, String>of(),
+            new TimestampGranularityMonitor(BlazeClock.instance()));
+
+    SkyKey skyKey = PackageValue.key(PackageIdentifier.parse("@//foo"));
+    final AtomicBoolean atLeastOneUnfinishedRequest = new AtomicBoolean(false);
+    final CountDownLatch allDirsRequested = new CountDownLatch(4);
+    Listener synchronizeListener =
+        new Listener() {
+          @Override
+          public Object accept(Path path, FileOp op, Order order) throws IOException {
+            if (op == FileOp.READDIR && order == Order.BEFORE) {
+              allDirsRequested.countDown();
+              try {
+                if (!allDirsRequested.await(1, TimeUnit.SECONDS)) {
+                  atLeastOneUnfinishedRequest.set(true);
+                }
+              } catch (InterruptedException e) {
+                throw new IllegalStateException(e);
+              }
+            }
+            return NO_RESULT_MARKER;
+          }
+        };
+    fs.setCustomOverride(barPath, synchronizeListener);
+    fs.setCustomOverride(ccPath, synchronizeListener);
+    fs.setCustomOverride(shPath, synchronizeListener);
+    fs.setCustomOverride(innerPath, synchronizeListener);
+    PackageValue value = validPackage(skyKey);
+    assertFalse(value.getPackage().containsErrors());
+    assertThat(value.getPackage().getTarget("bar/1-matched").getName()).isEqualTo("bar/1-matched");
+    assertThat(value.getPackage().getTarget("cc/src.file")).isNotNull();
+    assertThat(
+            (Iterable<?>)
+                value
+                    .getPackage()
+                    .getTarget("my")
+                    .getAssociatedRule()
+                    .getAttributeContainer()
+                    .getAttr("srcs"))
+        .isEmpty();
+    assertThat(atLeastOneUnfinishedRequest.get()).isTrue();
+  }
+
   private static class CustomInMemoryFs extends InMemoryFileSystem {
-    private abstract static class FileStatusOrException {
-      abstract FileStatus get() throws IOException;
-
-      private static class ExceptionImpl extends FileStatusOrException {
-        private final IOException exn;
-
-        private ExceptionImpl(IOException exn) {
-          this.exn = exn;
-        }
-
-        @Override
-        FileStatus get() throws IOException {
-          throw exn;
-        }
-      }
-
-      private static class FileStatusImpl extends FileStatusOrException {
-
-        @Nullable
-        private final FileStatus fileStatus;
-
-        private  FileStatusImpl(@Nullable FileStatus fileStatus) {
-          this.fileStatus = fileStatus;
-        }
-
-        @Override
-        @Nullable
-        FileStatus get() {
-          return fileStatus;
-        }
-      }
-    }
-
-    private Map<Path, FileStatusOrException> stubbedStats = Maps.newHashMap();
-    private Set<Path> makeUnreadableAfterReaddir = Sets.newHashSet();
+    private final Map<Path, Listener> customOverrides = Maps.newHashMap();
 
     public CustomInMemoryFs(ManualClock manualClock) {
       super(manualClock);
     }
 
-    public void stubStat(Path path, @Nullable FileStatus stubbedResult) {
-      stubbedStats.put(path, new FileStatusOrException.FileStatusImpl(stubbedResult));
+    public void stubStat(final Path targetPath, @Nullable final FileStatus stubbedResult) {
+      setCustomOverride(
+          targetPath,
+          new Listener() {
+            @Override
+            public Object accept(Path path, FileOp op, Order order) {
+              if (targetPath.equals(path) && op == FileOp.STAT && order == Order.BEFORE) {
+                return stubbedResult;
+              } else {
+                return NO_RESULT_MARKER;
+              }
+            }
+          });
     }
 
-    public void stubStatError(Path path, IOException stubbedResult) {
-      stubbedStats.put(path, new FileStatusOrException.ExceptionImpl(stubbedResult));
+    public void stubStatError(final Path targetPath, final IOException stubbedResult) {
+      setCustomOverride(
+          targetPath,
+          new Listener() {
+            @Override
+            public Object accept(Path path, FileOp op, Order order) throws IOException {
+              if (targetPath.equals(path) && op == FileOp.STAT && order == Order.BEFORE) {
+                throw stubbedResult;
+              } else {
+                return NO_RESULT_MARKER;
+              }
+            }
+          });
+    }
+
+    void setCustomOverride(Path path, Listener listener) {
+      customOverrides.put(path, listener);
     }
 
     @Override
     public FileStatus stat(Path path, boolean followSymlinks) throws IOException {
-      if (stubbedStats.containsKey(path)) {
-        return stubbedStats.get(path).get();
+      Listener listener = customOverrides.get(path);
+      if (listener != null) {
+        Object status = listener.accept(path, FileOp.STAT, Order.BEFORE);
+        if (status != NO_RESULT_MARKER) {
+          return (FileStatus) status;
+        }
       }
-      return super.stat(path, followSymlinks);
+      FileStatus fileStatus = super.stat(path, followSymlinks);
+      if (listener != null) {
+        Object status = listener.accept(path, FileOp.STAT, Order.AFTER);
+        if (status != NO_RESULT_MARKER) {
+          return (FileStatus) status;
+        }
+      }
+      return fileStatus;
     }
 
-    public void scheduleMakeUnreadableAfterReaddir(Path path) {
-      makeUnreadableAfterReaddir.add(path);
+    public void scheduleMakeUnreadableAfterReaddir(final Path targetPath) {
+      setCustomOverride(
+          targetPath,
+          new Listener() {
+            @Override
+            public Object accept(Path path, FileOp op, Order order) throws IOException {
+              if (targetPath.equals(path) && op == FileOp.READDIR && order == Order.AFTER) {
+                targetPath.setReadable(false);
+              }
+              return NO_RESULT_MARKER;
+            }
+          });
     }
 
+    @SuppressWarnings("unchecked")
     @Override
     public Collection<Dirent> readdir(Path path, boolean followSymlinks) throws IOException {
+      Listener listener = customOverrides.get(path);
+      if (listener != null) {
+        Object status = listener.accept(path, FileOp.READDIR, Order.BEFORE);
+        if (status != NO_RESULT_MARKER) {
+          return (Collection<Dirent>) status;
+        }
+      }
       Collection<Dirent> result = super.readdir(path, followSymlinks);
-      if (makeUnreadableAfterReaddir.contains(path)) {
-        path.setReadable(false);
+      if (listener != null) {
+        Object status = listener.accept(path, FileOp.READDIR, Order.AFTER);
+        if (status != NO_RESULT_MARKER) {
+          return (Collection<Dirent>) status;
+        }
       }
       return result;
     }
   }
+
+  private static final Object NO_RESULT_MARKER = new Object();
+
+  private enum Order {
+    BEFORE,
+    AFTER
+  }
+
+  private enum FileOp {
+    STAT,
+    READDIR
+  }
+
+  private interface Listener {
+    Object accept(Path path, FileOp op, Order order) throws IOException;
+  }
 }