blob: 3ecf6fe24b60e259f23d9ad9cec97e1602cc0f85 [file] [log] [blame]
// Copyright 2016 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.remote;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Interner;
import com.google.common.collect.Iterables;
import com.google.common.collect.TreeTraverser;
import com.google.devtools.build.lib.actions.ActionInput;
import com.google.devtools.build.lib.concurrent.BlazeInterners;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.remote.RemoteProtocol.ContentDigest;
import com.google.devtools.build.lib.remote.RemoteProtocol.FileNode;
import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import javax.annotation.Nullable;
/**
* A factory and repository for {@link TreeNode} objects. Provides directory structure traversals,
* computing and caching Merkle hashes on all objects.
*/
@ThreadSafe
public final class TreeNodeRepository extends TreeTraverser<TreeNodeRepository.TreeNode> {
/**
* A single node in a hierarchical directory structure. Leaves are the Artifacts, although we only
* use the ActionInput interface. We assume that the objects used for the ActionInputs are unique
* (same data corresponds to a canonical object in memory).
*/
@Immutable
@ThreadSafe
public static final class TreeNode {
private final int hashCode;
private final ImmutableList<ChildEntry> childEntries; // no need to make it a map thus far.
@Nullable private final ActionInput actionInput; // Null iff this is a directory.
/** A pair of path segment, TreeNode. */
@Immutable
public static final class ChildEntry {
private final String segment;
private final TreeNode child;
public ChildEntry(String segment, TreeNode child) {
this.segment = segment;
this.child = child;
}
public TreeNode getChild() {
return child;
}
public String getSegment() {
return segment;
}
@Override
@SuppressWarnings("ReferenceEquality")
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof ChildEntry)) {
return false;
}
ChildEntry other = (ChildEntry) o;
// Pointer comparisons only, because both the Path segments and the TreeNodes are interned.
return other.segment == segment && other.child == child;
}
@Override
public int hashCode() {
return Objects.hash(segment, child);
}
}
// Should only be called by the TreeNodeRepository.
private TreeNode(Iterable<ChildEntry> childEntries) {
this.actionInput = null;
this.childEntries = ImmutableList.copyOf(childEntries);
hashCode = Arrays.hashCode(this.childEntries.toArray());
}
// Should only be called by the TreeNodeRepository.
private TreeNode(ActionInput actionInput) {
this.actionInput = actionInput;
this.childEntries = ImmutableList.of();
hashCode = actionInput.hashCode(); // This will ensure efficient interning of TreeNodes as
// long as all ActionInputs either implement data-based hashCode or are interned themselves.
}
public ActionInput getActionInput() {
return actionInput;
}
public ImmutableList<ChildEntry> getChildEntries() {
return childEntries;
}
public boolean isLeaf() {
return actionInput != null;
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof TreeNode)) {
return false;
}
TreeNode otherNode = (TreeNode) o;
// Full comparison of ActionInputs. If pointers are different, will compare paths.
return Objects.equals(otherNode.actionInput, actionInput)
&& childEntries.equals(otherNode.childEntries);
}
private String toDebugStringAtLevel(int level) {
char[] prefix = new char[level];
Arrays.fill(prefix, ' ');
StringBuilder sb = new StringBuilder();
if (isLeaf()) {
sb.append('\n');
sb.append(prefix);
sb.append("leaf: ");
sb.append(actionInput);
} else {
for (ChildEntry entry : childEntries) {
sb.append('\n');
sb.append(prefix);
sb.append(entry.segment);
sb.append(entry.child.toDebugStringAtLevel(level + 1));
}
}
return sb.toString();
}
public String toDebugString() {
return toDebugStringAtLevel(0);
}
}
// Keep only one canonical instance of every TreeNode in the repository.
private final Interner<TreeNode> interner = BlazeInterners.newWeakInterner();
// Merkle hashes are computed and cached by the repository, therefore execRoot must
// be part of the state.
private final Path execRoot;
private final Map<ActionInput, ContentDigest> fileContentsDigestCache = new HashMap<>();
private final Map<ContentDigest, ActionInput> digestFileContentsCache = new HashMap<>();
private final Map<TreeNode, ContentDigest> treeNodeDigestCache = new HashMap<>();
private final Map<ContentDigest, TreeNode> digestTreeNodeCache = new HashMap<>();
private final Map<TreeNode, FileNode> fileNodeCache = new HashMap<>();
public TreeNodeRepository(Path execRoot) {
this.execRoot = execRoot;
}
@Override
public Iterable<TreeNode> children(TreeNode node) {
return Iterables.transform(
node.getChildEntries(),
new Function<TreeNode.ChildEntry, TreeNode>() {
@Override
public TreeNode apply(TreeNode.ChildEntry entry) {
return entry.getChild();
}
});
}
/** Traverse the directory structure in order (pre-order tree traversal). */
public Iterable<TreeNode> descendants(TreeNode node) {
return preOrderTraversal(node);
}
/**
* Traverse the directory structure in order (pre-order tree traversal), return only the leaves.
*/
public Iterable<TreeNode> leaves(TreeNode node) {
return Iterables.filter(
descendants(node),
new Predicate<TreeNode>() {
@Override
public boolean apply(TreeNode node) {
return node.isLeaf();
}
});
}
/**
* This function is a temporary and highly inefficient hack! It builds the tree from a ready list
* of input files. TODO(olaola): switch to creating and maintaining the TreeNodeRepository based
* on the build graph structure.
*/
public TreeNode buildFromActionInputs(Iterable<ActionInput> actionInputs) {
TreeMap<PathFragment, ActionInput> sortedMap = new TreeMap<>();
for (ActionInput input : actionInputs) {
sortedMap.put(new PathFragment(input.getExecPathString()), input);
}
ImmutableList.Builder<ImmutableList<String>> segments = ImmutableList.builder();
for (PathFragment path : sortedMap.keySet()) {
segments.add(path.getSegments());
}
ImmutableList<ActionInput> inputs = ImmutableList.copyOf(sortedMap.values());
return buildParentNode(inputs, segments.build(), 0, inputs.size(), 0);
}
@SuppressWarnings("ReferenceEquality") // Segments are interned.
private TreeNode buildParentNode(
ImmutableList<ActionInput> inputs,
ImmutableList<ImmutableList<String>> segments,
int inputsStart,
int inputsEnd,
int segmentIndex) {
if (segmentIndex == segments.get(inputsStart).size()) {
// Leaf node reached. Must be unique.
Preconditions.checkArgument(
inputsStart == inputsEnd - 1, "Encountered two inputs with the same path.");
// TODO: check that the actionInput is a single file!
return interner.intern(new TreeNode(inputs.get(inputsStart)));
}
ArrayList<TreeNode.ChildEntry> entries = new ArrayList<>();
String segment = segments.get(inputsStart).get(segmentIndex);
for (int inputIndex = inputsStart; inputIndex < inputsEnd; ++inputIndex) {
if (inputIndex + 1 == inputsEnd
|| segment != segments.get(inputIndex + 1).get(segmentIndex)) {
entries.add(
new TreeNode.ChildEntry(
segment,
buildParentNode(inputs, segments, inputsStart, inputIndex + 1, segmentIndex + 1)));
if (inputIndex + 1 < inputsEnd) {
inputsStart = inputIndex + 1;
segment = segments.get(inputsStart).get(segmentIndex);
}
}
}
return interner.intern(new TreeNode(entries));
}
private synchronized ContentDigest getOrComputeActionInputDigest(ActionInput actionInput)
throws IOException {
ContentDigest digest = fileContentsDigestCache.get(actionInput);
if (digest == null) {
digest = ContentDigests.computeDigest(execRoot.getRelative(actionInput.getExecPathString()));
fileContentsDigestCache.put(actionInput, digest);
digestFileContentsCache.put(digest, actionInput);
}
return digest;
}
private synchronized FileNode getOrComputeFileNode(TreeNode node) throws IOException {
// Assumes all child digests have already been computed!
FileNode fileNode = fileNodeCache.get(node);
if (fileNode == null) {
FileNode.Builder b = FileNode.newBuilder();
if (node.isLeaf()) {
ContentDigest fileDigest = fileContentsDigestCache.get(node.getActionInput());
Preconditions.checkState(fileDigest != null);
b.getFileMetadataBuilder()
.setDigest(fileDigest)
.setExecutable(
execRoot.getRelative(node.getActionInput().getExecPathString()).isExecutable());
} else {
for (TreeNode.ChildEntry entry : node.getChildEntries()) {
ContentDigest childDigest = treeNodeDigestCache.get(entry.getChild());
Preconditions.checkState(childDigest != null);
b.addChildBuilder().setPath(entry.getSegment()).setDigest(childDigest);
}
}
fileNode = b.build();
fileNodeCache.put(node, fileNode);
ContentDigest digest = ContentDigests.computeDigest(fileNode);
treeNodeDigestCache.put(node, digest);
digestTreeNodeCache.put(digest, node);
}
return fileNode;
}
// Recursively traverses the tree, expanding and computing Merkle digests for nodes for which
// they have not yet been computed and cached.
public void computeMerkleDigests(TreeNode root) throws IOException {
synchronized (this) {
if (fileNodeCache.get(root) != null) {
// Strong assumption: the cache is valid, i.e. parent present implies children present.
return;
}
}
if (root.isLeaf()) {
getOrComputeActionInputDigest(root.getActionInput());
} else {
for (TreeNode child : children(root)) {
computeMerkleDigests(child);
}
}
getOrComputeFileNode(root);
}
/**
* Should only be used after computeMerkleDigests has been called on one of the node ancestors.
* Returns the precomputed digest.
*/
public ContentDigest getMerkleDigest(TreeNode node) {
return treeNodeDigestCache.get(node);
}
/**
* Returns the precomputed digests for both data and metadata. Should only be used after
* computeMerkleDigests has been called on one of the node ancestors.
*/
public ImmutableCollection<ContentDigest> getAllDigests(TreeNode root) {
ImmutableSet.Builder<ContentDigest> digests = ImmutableSet.builder();
for (TreeNode node : descendants(root)) {
digests.add(Preconditions.checkNotNull(treeNodeDigestCache.get(node)));
if (node.isLeaf()) {
digests.add(Preconditions.checkNotNull(fileContentsDigestCache.get(node.getActionInput())));
}
}
return digests.build();
}
/**
* Serializes all of the subtree to the file node list. TODO(olaola): add a version that only
* copies a part of the tree that we are interested in. Should only be used after
* computeMerkleDigests has been called on one of the node ancestors.
*/
// Note: this is not, strictly speaking, thread safe. If someone is deleting cached Merkle hashes
// while this is executing, it will trigger an exception. But I think this is WAI.
public ImmutableList<FileNode> treeToFileNodes(TreeNode root) throws IOException {
ImmutableList.Builder<FileNode> fileNodes = ImmutableList.builder();
for (TreeNode node : descendants(root)) {
fileNodes.add(Preconditions.checkNotNull(fileNodeCache.get(node)));
}
return fileNodes.build();
}
/**
* Should only be used on digests created by a call to computeMerkleDigests. Looks up ActionInputs
* or FileNodes by cached digests and adds them to the lists.
*/
public void getDataFromDigests(
Iterable<ContentDigest> digests, List<ActionInput> actionInputs, List<FileNode> nodes) {
for (ContentDigest digest : digests) {
TreeNode treeNode = digestTreeNodeCache.get(digest);
if (treeNode != null) {
nodes.add(Preconditions.checkNotNull(fileNodeCache.get(treeNode)));
} else { // If not there, it must be an ActionInput.
actionInputs.add(Preconditions.checkNotNull(digestFileContentsCache.get(digest)));
}
}
}
}