Introduce a graph `LookupHint` to determine whether it is worthwhile to construct a list of keys to make a batch request.

For graphs that store nodes in memory, it is wasteful to construct a list just to make a single call to `getBatch()` because the keys can just be looked up individually.

PiperOrigin-RevId: 478068994
Change-Id: I6f9200f63eee63e5a9d6a7ec8a22723ed39d445a
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutorWrappingWalkableGraph.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutorWrappingWalkableGraph.java
index a333a2c..9546129 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutorWrappingWalkableGraph.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutorWrappingWalkableGraph.java
@@ -38,6 +38,11 @@
           }
 
           @Override
+          public LookupHint getLookupHint(SkyKey key) {
+            return LookupHint.INDIVIDUAL;
+          }
+
+          @Override
           public Map<SkyKey, ? extends NodeEntry> getBatchMap(
               @Nullable SkyKey requestor, Reason reason, Iterable<? extends SkyKey> keys)
               throws InterruptedException {
diff --git a/src/main/java/com/google/devtools/build/skyframe/InMemoryGraph.java b/src/main/java/com/google/devtools/build/skyframe/InMemoryGraph.java
index f9d87a3..e698e10 100644
--- a/src/main/java/com/google/devtools/build/skyframe/InMemoryGraph.java
+++ b/src/main/java/com/google/devtools/build/skyframe/InMemoryGraph.java
@@ -54,6 +54,11 @@
   }
 
   @Override
+  default LookupHint getLookupHint(SkyKey key) {
+    return LookupHint.INDIVIDUAL;
+  }
+
+  @Override
   Map<SkyKey, ? extends NodeEntry> getBatchMap(
       @Nullable SkyKey requestor, Reason reason, Iterable<? extends SkyKey> keys);
 
diff --git a/src/main/java/com/google/devtools/build/skyframe/QueryableGraph.java b/src/main/java/com/google/devtools/build/skyframe/QueryableGraph.java
index 1a2824e..a58c8a2ca 100644
--- a/src/main/java/com/google/devtools/build/skyframe/QueryableGraph.java
+++ b/src/main/java/com/google/devtools/build/skyframe/QueryableGraph.java
@@ -61,6 +61,26 @@
     return getBatchMap(requestor, reason, keys)::get;
   }
 
+  /** A hint about the most efficient way to look up a key in the graph. */
+  enum LookupHint {
+    INDIVIDUAL,
+    BATCH
+  }
+
+  /**
+   * Hints to the caller about the most efficient way to look up a key in this graph.
+   *
+   * <p>A return of {@link LookupHint#INDIVIDUAL} indicates that the given key can efficiently be
+   * looked up by calling {@link #get}. In such a case, it is not worth the effort to aggregate the
+   * key into a collection with other keys for a {@link #getBatch} call.
+   *
+   * <p>A return of {@link LookupHint#BATCH} indicates that the given key should ideally be
+   * requested with other keys as part of a call to {@link #getBatch}. This may be the case if, for
+   * example, the corresponding node is stored remotely, and requesting keys in a single batch
+   * reduces trips to remote storage.
+   */
+  LookupHint getLookupHint(SkyKey key);
+
   /**
    * A version of {@link #getBatch} that returns an {@link InterruptibleSupplier} to possibly
    * retrieve the results later.
diff --git a/src/main/java/com/google/devtools/build/skyframe/SkyFunctionEnvironment.java b/src/main/java/com/google/devtools/build/skyframe/SkyFunctionEnvironment.java
index ef306099..938ab2b 100644
--- a/src/main/java/com/google/devtools/build/skyframe/SkyFunctionEnvironment.java
+++ b/src/main/java/com/google/devtools/build/skyframe/SkyFunctionEnvironment.java
@@ -35,6 +35,7 @@
 import com.google.devtools.build.lib.util.GroupedList;
 import com.google.devtools.build.skyframe.EvaluationProgressReceiver.EvaluationState;
 import com.google.devtools.build.skyframe.NodeEntry.DependencyState;
+import com.google.devtools.build.skyframe.QueryableGraph.LookupHint;
 import com.google.devtools.build.skyframe.QueryableGraph.Reason;
 import com.google.devtools.build.skyframe.proto.GraphInconsistency.Inconsistency;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
@@ -490,16 +491,13 @@
     checkActive();
     int sizeBeforeRequest = newlyRequestedDepsValues.size();
     SkyValue depValue = lookupRequestedDep(depKey);
-    if (depValue == null) {
+    if (depValue != null) {
+      processDepValue(depKey, depValue);
+    } else {
       NodeEntry depEntry = evaluatorContext.getGraph().get(skyKey, Reason.DEP_REQUESTED, depKey);
-      depValue = getValueOrNullMarker(depEntry);
-      newlyRequestedDepsValues.put(depKey, depValue);
-      if (depValue != NULL_MARKER) {
-        maybeUpdateMaxTransitiveSourceVersion(depEntry);
-      }
+      depValue = processDepEntry(depKey, depEntry);
     }
     endDepGroup(sizeBeforeRequest);
-    processDepValue(depKey, depValue);
 
     return unwrapOrThrow(
         skyKey, depValue, exceptionClass1, exceptionClass2, exceptionClass3, exceptionClass4);
@@ -510,30 +508,37 @@
   public SkyframeLookupResult getValuesAndExceptions(Iterable<? extends SkyKey> depKeys)
       throws InterruptedException {
     checkActive();
-    List<SkyKey> missingKeys = new ArrayList<>();
+
+    // Lazily initialized when we encounter a missing key and the graph's lookup hint indicates that
+    // the key should be requested in a batch. If the graph supports efficient lookups of individual
+    // keys, we avoid constructing a list.
+    List<SkyKey> missingKeys = null;
 
     int sizeBeforeRequest = newlyRequestedDepsValues.size();
     for (SkyKey key : depKeys) {
       SkyValue value = lookupRequestedDep(key);
-      if (value == null) {
-        missingKeys.add(key);
-      } else if (value != PENDING_MARKER) {
+      if (value == PENDING_MARKER) {
+        continue; // Duplicate key in this request.
+      }
+      if (value != null) {
         processDepValue(key, value);
+      } else if (evaluatorContext.getGraph().getLookupHint(key) == LookupHint.BATCH) {
+        if (missingKeys == null) {
+          missingKeys = new ArrayList<>();
+        }
+        missingKeys.add(key);
+      } else {
+        NodeEntry depEntry = evaluatorContext.getGraph().get(skyKey, Reason.DEP_REQUESTED, key);
+        processDepEntry(key, depEntry);
       }
     }
     endDepGroup(sizeBeforeRequest);
 
-    if (!missingKeys.isEmpty()) {
+    if (missingKeys != null) {
       NodeBatch missingEntries =
           evaluatorContext.getGraph().getBatch(skyKey, Reason.DEP_REQUESTED, missingKeys);
       for (SkyKey key : missingKeys) {
-        NodeEntry depEntry = missingEntries.get(key);
-        SkyValue valueOrNullMarker = getValueOrNullMarker(depEntry);
-        processDepValue(key, valueOrNullMarker);
-        newlyRequestedDepsValues.put(key, valueOrNullMarker);
-        if (valueOrNullMarker != NULL_MARKER) {
-          maybeUpdateMaxTransitiveSourceVersion(depEntry);
-        }
+        processDepEntry(key, missingEntries.get(key));
       }
     }
 
@@ -577,7 +582,19 @@
     };
   }
 
-  private void processDepValue(SkyKey depKey, SkyValue depValue) throws InterruptedException {
+  @CanIgnoreReturnValue
+  private SkyValue processDepEntry(SkyKey depKey, @Nullable NodeEntry depEntry)
+      throws InterruptedException {
+    SkyValue valueOrNullMarker = getValueOrNullMarker(depEntry);
+    processDepValue(depKey, valueOrNullMarker);
+    newlyRequestedDepsValues.put(depKey, valueOrNullMarker);
+    if (valueOrNullMarker != NULL_MARKER) {
+      maybeUpdateMaxTransitiveSourceVersion(depEntry);
+    }
+    return valueOrNullMarker;
+  }
+
+  private void processDepValue(SkyKey depKey, SkyValue depValue) {
     if (depValue == NULL_MARKER) {
       valuesMissing = true;
       return;
diff --git a/src/test/java/com/google/devtools/build/skyframe/NotifyingHelper.java b/src/test/java/com/google/devtools/build/skyframe/NotifyingHelper.java
index e7ddc37..f25d614 100644
--- a/src/test/java/com/google/devtools/build/skyframe/NotifyingHelper.java
+++ b/src/test/java/com/google/devtools/build/skyframe/NotifyingHelper.java
@@ -84,6 +84,11 @@
     }
 
     @Override
+    public LookupHint getLookupHint(SkyKey key) {
+      return delegate.getLookupHint(key);
+    }
+
+    @Override
     public void remove(SkyKey key) {
       delegate.remove(key);
     }