Create a "MultiBuilder" for aggregating multiple TreeArtifactValues at once.

The basic (single-thread use case) version offers the optimization that it builds an ImmutableSortedMap so that a copy is avoided in the TreeArtifactValue constructor.

PiperOrigin-RevId: 320028967
diff --git a/src/main/java/com/google/devtools/build/lib/actions/cache/MetadataInjector.java b/src/main/java/com/google/devtools/build/lib/actions/cache/MetadataInjector.java
index 1ec3765..890d38a 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/cache/MetadataInjector.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/cache/MetadataInjector.java
@@ -27,7 +27,7 @@
    *
    * <p>This can be used to save filesystem operations when the metadata is already known.
    *
-   * <p>{@linkplain Artifact#isTreeArtifacts Tree artifacts} and their {@linkplain
+   * <p>{@linkplain Artifact#isTreeArtifact Tree artifacts} and their {@linkplain
    * Artifact#isChildOfDeclaredDirectory children} must not be passed here. Instead, they should be
    * passed to {@link #injectDirectory}.
    *
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TreeArtifactValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/TreeArtifactValue.java
index 9587c22..03df389 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/TreeArtifactValue.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TreeArtifactValue.java
@@ -21,10 +21,12 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.Maps;
+import com.google.devtools.build.lib.actions.Artifact.SpecialArtifact;
 import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact;
 import com.google.devtools.build.lib.actions.FileArtifactValue;
 import com.google.devtools.build.lib.actions.HasDigest;
 import com.google.devtools.build.lib.actions.cache.DigestUtils;
+import com.google.devtools.build.lib.actions.cache.MetadataInjector;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.SerializationConstant;
 import com.google.devtools.build.lib.vfs.Dirent;
@@ -36,9 +38,13 @@
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
 import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
 
 /**
  * Value for TreeArtifacts, which contains a digest and the {@link FileArtifactValue}s of its child
@@ -46,6 +52,31 @@
  */
 public class TreeArtifactValue implements HasDigest, SkyValue {
 
+  /** Builder for constructing multiple instances of {@link TreeArtifactValue} at once. */
+  public interface MultiBuilder {
+    /**
+     * Puts a child tree file into this builder under its {@linkplain TreeFileArtifact#getParent
+     * parent}.
+     */
+    void putChild(TreeFileArtifact child, FileArtifactValue metadata);
+
+    /**
+     * For each unique parent seen by this builder, passes the aggregated metadata to {@link
+     * MetadataInjector#injectDirectory}.
+     */
+    void injectTo(MetadataInjector metadataInjector);
+  }
+
+  /** Returns a new {@link MultiBuilder}. */
+  public static MultiBuilder newMultiBuilder() {
+    return new BasicMultiBuilder();
+  }
+
+  /** Returns a new thread-safe {@link MultiBuilder}. */
+  public static MultiBuilder newConcurrentMultiBuilder() {
+    return new ConcurrentMultiBuilder();
+  }
+
   @SerializationConstant @AutoCodec.VisibleForSerialization
   static final TreeArtifactValue EMPTY =
       new TreeArtifactValue(
@@ -71,8 +102,7 @@
    * Returns a TreeArtifactValue out of the given Artifact-relative path fragments and their
    * corresponding FileArtifactValues.
    */
-  static TreeArtifactValue create(
-      Map<TreeFileArtifact, ? extends FileArtifactValue> childFileValues) {
+  static TreeArtifactValue create(Map<TreeFileArtifact, FileArtifactValue> childFileValues) {
     if (childFileValues.isEmpty()) {
       return EMPTY;
     }
@@ -273,4 +303,38 @@
     explodeDirectory(treeArtifactPath, PathFragment.EMPTY_FRAGMENT, explodedDirectory);
     return explodedDirectory.build();
   }
+
+  private static final class BasicMultiBuilder implements MultiBuilder {
+    private final Map<
+            SpecialArtifact, ImmutableSortedMap.Builder<TreeFileArtifact, FileArtifactValue>>
+        map = new HashMap<>();
+
+    @Override
+    public void putChild(TreeFileArtifact child, FileArtifactValue metadata) {
+      map.computeIfAbsent(child.getParent(), parent -> ImmutableSortedMap.naturalOrder())
+          .put(child, metadata);
+    }
+
+    @Override
+    public void injectTo(MetadataInjector metadataInjector) {
+      map.forEach((parent, builder) -> metadataInjector.injectDirectory(parent, builder.build()));
+    }
+  }
+
+  @ThreadSafe
+  private static final class ConcurrentMultiBuilder implements MultiBuilder {
+    private final ConcurrentMap<SpecialArtifact, ConcurrentMap<TreeFileArtifact, FileArtifactValue>>
+        map = new ConcurrentHashMap<>();
+
+    @Override
+    public void putChild(TreeFileArtifact child, FileArtifactValue metadata) {
+      map.computeIfAbsent(child.getParent(), parent -> new ConcurrentHashMap<>())
+          .put(child, metadata);
+    }
+
+    @Override
+    public void injectTo(MetadataInjector metadataInjector) {
+      map.forEach(metadataInjector::injectDirectory);
+    }
+  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/FilesystemValueCheckerTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/FilesystemValueCheckerTest.java
index ad9354b..6179a52 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/FilesystemValueCheckerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/FilesystemValueCheckerTest.java
@@ -33,6 +33,7 @@
 import com.google.devtools.build.lib.actions.FileArtifactValue.RemoteFileArtifactValue;
 import com.google.devtools.build.lib.actions.FileStateValue;
 import com.google.devtools.build.lib.actions.FileValue;
+import com.google.devtools.build.lib.actions.cache.MetadataInjector;
 import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.actions.util.TestAction;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
@@ -888,17 +889,11 @@
 
   private static ActionExecutionValue actionValueWithTreeArtifacts(
       List<TreeFileArtifact> contents) {
-    Map<Artifact, Map<TreeFileArtifact, FileArtifactValue>> directoryData = new HashMap<>();
+    TreeArtifactValue.MultiBuilder treeArtifacts = TreeArtifactValue.newMultiBuilder();
 
     for (TreeFileArtifact output : contents) {
+      Path path = output.getPath();
       try {
-        Map<TreeFileArtifact, FileArtifactValue> dirDatum =
-            directoryData.get(output.getParent());
-        if (dirDatum == null) {
-          dirDatum = new HashMap<>();
-          directoryData.put(output.getParent(), dirDatum);
-        }
-        Path path = output.getPath();
         FileArtifactValue noDigest =
             ActionMetadataHandler.fileArtifactValueFromArtifact(
                 output,
@@ -907,17 +902,27 @@
         FileArtifactValue withDigest =
             FileArtifactValue.createFromInjectedDigest(
                 noDigest, path.getDigest(), !output.isConstantMetadata());
-        dirDatum.put(output, withDigest);
+        treeArtifacts.putChild(output, withDigest);
       } catch (IOException e) {
         throw new IllegalStateException(e);
       }
     }
 
     Map<Artifact, TreeArtifactValue> treeArtifactData = new HashMap<>();
-    for (Map.Entry<Artifact, Map<TreeFileArtifact, FileArtifactValue>> dirDatum :
-        directoryData.entrySet()) {
-      treeArtifactData.put(dirDatum.getKey(), TreeArtifactValue.create(dirDatum.getValue()));
-    }
+    treeArtifacts.injectTo(
+        new MetadataInjector() {
+          @Override
+          public void injectFile(Artifact output, FileArtifactValue metadata) {}
+
+          @Override
+          public void injectDirectory(
+              SpecialArtifact output, Map<TreeFileArtifact, FileArtifactValue> children) {
+            treeArtifactData.put(output, TreeArtifactValue.create(children));
+          }
+
+          @Override
+          public void markOmitted(Artifact output) {}
+        });
 
     return ActionExecutionValue.create(
         /*artifactData=*/ ImmutableMap.of(),
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactValueTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactValueTest.java
new file mode 100644
index 0000000..bd5c475
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactValueTest.java
@@ -0,0 +1,148 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.skyframe;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Artifact.SpecialArtifact;
+import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact;
+import com.google.devtools.build.lib.actions.ArtifactRoot;
+import com.google.devtools.build.lib.actions.FileArtifactValue;
+import com.google.devtools.build.lib.actions.FileArtifactValue.RemoteFileArtifactValue;
+import com.google.devtools.build.lib.actions.cache.MetadataInjector;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+/** Tests for {@link TreeArtifactValue}. */
+@RunWith(Parameterized.class)
+public final class TreeArtifactValueTest {
+
+  private static final ArtifactRoot ROOT =
+      ArtifactRoot.asDerivedRoot(new InMemoryFileSystem().getPath("/root"), "bin");
+
+  private enum MultiBuilderType {
+    BASIC {
+      @Override
+      TreeArtifactValue.MultiBuilder newMultiBuilder() {
+        return TreeArtifactValue.newMultiBuilder();
+      }
+    },
+    CONCURRENT {
+      @Override
+      TreeArtifactValue.MultiBuilder newMultiBuilder() {
+        return TreeArtifactValue.newConcurrentMultiBuilder();
+      }
+    };
+
+    abstract TreeArtifactValue.MultiBuilder newMultiBuilder();
+  }
+
+  @Parameter public MultiBuilderType multiBuilderType;
+  private final FakeMetadataInjector metadataInjector = new FakeMetadataInjector();
+
+  @Parameters(name = "{0}")
+  public static MultiBuilderType[] params() {
+    return MultiBuilderType.values();
+  }
+
+  @Test
+  public void empty() {
+    TreeArtifactValue.MultiBuilder treeArtifacts = multiBuilderType.newMultiBuilder();
+
+    treeArtifacts.injectTo(metadataInjector);
+
+    assertThat(metadataInjector.injectedTreeArtifacts).isEmpty();
+  }
+
+  @Test
+  public void singleTreeArtifact() {
+    TreeArtifactValue.MultiBuilder treeArtifacts = multiBuilderType.newMultiBuilder();
+    SpecialArtifact parent = createTreeArtifact("tree");
+    TreeFileArtifact child1 = TreeFileArtifact.createTreeOutput(parent, "child1");
+    TreeFileArtifact child2 = TreeFileArtifact.createTreeOutput(parent, "child2");
+
+    treeArtifacts.putChild(child1, metadataWithId(1));
+    treeArtifacts.putChild(child2, metadataWithId(2));
+    treeArtifacts.injectTo(metadataInjector);
+
+    assertThat(metadataInjector.injectedTreeArtifacts)
+        .containsExactly(
+            parent,
+            TreeArtifactValue.create(
+                ImmutableMap.of(child1, metadataWithId(1), child2, metadataWithId(2))));
+  }
+
+  @Test
+  public void multipleTreeArtifacts() {
+    TreeArtifactValue.MultiBuilder treeArtifacts = multiBuilderType.newMultiBuilder();
+    SpecialArtifact parent1 = createTreeArtifact("tree1");
+    TreeFileArtifact parent1Child1 = TreeFileArtifact.createTreeOutput(parent1, "child1");
+    TreeFileArtifact parent1Child2 = TreeFileArtifact.createTreeOutput(parent1, "child2");
+    SpecialArtifact parent2 = createTreeArtifact("tree2");
+    TreeFileArtifact parent2Child = TreeFileArtifact.createTreeOutput(parent2, "child");
+
+    treeArtifacts.putChild(parent1Child1, metadataWithId(1));
+    treeArtifacts.putChild(parent2Child, metadataWithId(3));
+    treeArtifacts.putChild(parent1Child2, metadataWithId(2));
+    treeArtifacts.injectTo(metadataInjector);
+
+    assertThat(metadataInjector.injectedTreeArtifacts)
+        .containsExactly(
+            parent1,
+            TreeArtifactValue.create(
+                ImmutableMap.of(
+                    parent1Child1, metadataWithId(1), parent1Child2, metadataWithId(2))),
+            parent2,
+            TreeArtifactValue.create(ImmutableMap.of(parent2Child, metadataWithId(3))));
+  }
+
+  private static SpecialArtifact createTreeArtifact(String execPath) {
+    return ActionsTestUtil.createTreeArtifactWithGeneratingAction(
+        ROOT, PathFragment.create(execPath));
+  }
+
+  private static FileArtifactValue metadataWithId(int id) {
+    return new RemoteFileArtifactValue(new byte[] {(byte) id}, id, id);
+  }
+
+  private static final class FakeMetadataInjector implements MetadataInjector {
+    private final Map<SpecialArtifact, TreeArtifactValue> injectedTreeArtifacts = new HashMap<>();
+
+    @Override
+    public void injectFile(Artifact output, FileArtifactValue metadata) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void injectDirectory(
+        SpecialArtifact output, Map<TreeFileArtifact, FileArtifactValue> children) {
+      injectedTreeArtifacts.put(output, TreeArtifactValue.create(children));
+    }
+
+    @Override
+    public void markOmitted(Artifact output) {
+      throw new UnsupportedOperationException();
+    }
+  }
+}