Skip to content

Commit 0f55d12

Browse files
tjgqcopybara-github
authored andcommitted
Cache Merkle trees for tree artifacts.
Currently, a large tree artifact cannot benefit from the Merkle tree cache if it always appears on a nested set together with other (unique per-action) files. To improve this, modify SpawnInputExpander to treat the tree as a distinct node in the input hierarchy that can be cached separately. Also simplify the cache keys for filesets and runfiles, since the SpawnInputExpander is a per-build singleton, and this cache is only shared by actions within a single build. Progress on bazelbuild#17923. Closes bazelbuild#17929. PiperOrigin-RevId: 522039585 Change-Id: Ia4f2603325acfd4400239894214f2884a71d69cf
1 parent 278988c commit 0f55d12

File tree

3 files changed

+89
-33
lines changed

3 files changed

+89
-33
lines changed

src/main/java/com/google/devtools/build/lib/exec/SpawnInputExpander.java

+57-27
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414
package com.google.devtools.build.lib.exec;
1515

16+
import static com.google.common.collect.ImmutableList.toImmutableList;
1617

1718
import com.google.common.annotations.VisibleForTesting;
1819
import com.google.common.base.Preconditions;
@@ -22,6 +23,7 @@
2223
import com.google.devtools.build.lib.actions.Artifact;
2324
import com.google.devtools.build.lib.actions.Artifact.ArtifactExpander;
2425
import com.google.devtools.build.lib.actions.Artifact.MissingExpansionException;
26+
import com.google.devtools.build.lib.actions.Artifact.SpecialArtifact;
2527
import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact;
2628
import com.google.devtools.build.lib.actions.FileArtifactValue;
2729
import com.google.devtools.build.lib.actions.FilesetManifest;
@@ -39,7 +41,6 @@
3941
import com.google.devtools.build.lib.vfs.Path;
4042
import com.google.devtools.build.lib.vfs.PathFragment;
4143
import java.io.IOException;
42-
import java.util.Arrays;
4344
import java.util.HashMap;
4445
import java.util.List;
4546
import java.util.Map;
@@ -266,14 +267,19 @@ public SortedMap<PathFragment, ActionInput> getInputMapping(
266267

267268
/** The interface for accessing part of the input hierarchy. */
268269
public interface InputWalker {
270+
271+
/** Returns the leaf nodes at this point in the hierarchy. */
269272
SortedMap<PathFragment, ActionInput> getLeavesInputMapping()
270273
throws IOException, ForbiddenActionInputException;
271274

272-
void visitNonLeaves(InputVisitor visitor) throws IOException, ForbiddenActionInputException;
275+
/** Invokes the visitor on the non-leaf nodes at this point in the hierarchy. */
276+
default void visitNonLeaves(InputVisitor visitor)
277+
throws IOException, ForbiddenActionInputException {}
273278
}
274279

275280
/** The interface for visiting part of the input hierarchy. */
276281
public interface InputVisitor {
282+
277283
/**
278284
* Visits a part of the input hierarchy.
279285
*
@@ -305,13 +311,8 @@ public void walkInputs(
305311

306312
RunfilesSupplier runfilesSupplier = spawn.getRunfilesSupplier();
307313
visitor.visit(
308-
// The list of variables affecting the functional expressions below.
309-
Arrays.asList(
310-
// Assuming that artifactExpander and actionInputFileCache, different for each spawn,
311-
// always expand the same way.
312-
this, // For accessing addRunfilesToInputs.
313-
runfilesSupplier,
314-
baseDirectory),
314+
// Cache key for the sub-mapping containing the runfiles inputs for this spawn.
315+
ImmutableList.of(runfilesSupplier, baseDirectory),
315316
new InputWalker() {
316317
@Override
317318
public SortedMap<PathFragment, ActionInput> getLeavesInputMapping()
@@ -321,20 +322,14 @@ public SortedMap<PathFragment, ActionInput> getLeavesInputMapping()
321322
inputMap, runfilesSupplier, actionInputFileCache, artifactExpander, baseDirectory);
322323
return inputMap;
323324
}
324-
325-
@Override
326-
public void visitNonLeaves(InputVisitor childVisitor) {}
327325
});
328326

329327
Map<Artifact, ImmutableList<FilesetOutputSymlink>> filesetMappings = spawn.getFilesetMappings();
330328
// filesetMappings is assumed to be very small, so no need to implement visitNonLeaves() for
331329
// improved runtime.
332330
visitor.visit(
333-
// The list of variables affecting the functional expressions below.
334-
Arrays.asList(
335-
this, // For accessing addFilesetManifests.
336-
filesetMappings,
337-
baseDirectory),
331+
// Cache key for the sub-mapping containing the fileset inputs for this spawn.
332+
ImmutableList.of(filesetMappings, baseDirectory),
338333
new InputWalker() {
339334
@Override
340335
public SortedMap<PathFragment, ActionInput> getLeavesInputMapping()
@@ -343,32 +338,32 @@ public SortedMap<PathFragment, ActionInput> getLeavesInputMapping()
343338
addFilesetManifests(filesetMappings, inputMap, baseDirectory);
344339
return inputMap;
345340
}
346-
347-
@Override
348-
public void visitNonLeaves(InputVisitor childVisitor) {}
349341
});
350342
}
351343

352-
/** Walks through one level of a {@link NestedSet} of {@link ActionInput}s. */
344+
/** Visits a {@link NestedSet} occurring in {@link Spawn#getInputFiles}. */
353345
private void walkNestedSetInputs(
354346
PathFragment baseDirectory,
355347
NestedSet<? extends ActionInput> someInputFiles,
356348
ArtifactExpander artifactExpander,
357349
InputVisitor visitor)
358350
throws IOException, ForbiddenActionInputException {
359351
visitor.visit(
360-
// addInputs is static so no need to add 'this' as dependent key.
361-
Arrays.asList(
362-
// Assuming that artifactExpander, different for each spawn, always expands the same
363-
// way.
364-
someInputFiles.toNode(), baseDirectory),
352+
// Cache key for the sub-mapping containing the files in this nested set.
353+
ImmutableList.of(someInputFiles.toNode(), baseDirectory),
365354
new InputWalker() {
366355
@Override
367356
public SortedMap<PathFragment, ActionInput> getLeavesInputMapping() {
368357
TreeMap<PathFragment, ActionInput> inputMap = new TreeMap<>();
358+
// Consider files inside tree artifacts to be non-leaves. This caches better when a
359+
// large tree is not the sole direct child of a nested set.
360+
ImmutableList<? extends ActionInput> leaves =
361+
someInputFiles.getLeaves().stream()
362+
.filter(a -> !isTreeArtifact(a))
363+
.collect(toImmutableList());
369364
addInputs(
370365
inputMap,
371-
NestedSetBuilder.wrap(someInputFiles.getOrder(), someInputFiles.getLeaves()),
366+
NestedSetBuilder.wrap(someInputFiles.getOrder(), leaves),
372367
artifactExpander,
373368
baseDirectory);
374369
return inputMap;
@@ -377,18 +372,53 @@ public SortedMap<PathFragment, ActionInput> getLeavesInputMapping() {
377372
@Override
378373
public void visitNonLeaves(InputVisitor childVisitor)
379374
throws IOException, ForbiddenActionInputException {
375+
for (ActionInput input : someInputFiles.getLeaves()) {
376+
if (isTreeArtifact(input)) {
377+
walkTreeInputs(
378+
baseDirectory, (SpecialArtifact) input, artifactExpander, childVisitor);
379+
}
380+
}
380381
for (NestedSet<? extends ActionInput> subInputs : someInputFiles.getNonLeaves()) {
381382
walkNestedSetInputs(baseDirectory, subInputs, artifactExpander, childVisitor);
382383
}
383384
}
384385
});
385386
}
386387

388+
/** Visits a tree artifact occurring in {@link Spawn#getInputFiles}. */
389+
private void walkTreeInputs(
390+
PathFragment baseDirectory,
391+
SpecialArtifact tree,
392+
ArtifactExpander artifactExpander,
393+
InputVisitor visitor)
394+
throws IOException, ForbiddenActionInputException {
395+
visitor.visit(
396+
// Cache key for the sub-mapping containing the files in this tree artifact.
397+
ImmutableList.of(tree, baseDirectory),
398+
new InputWalker() {
399+
@Override
400+
public SortedMap<PathFragment, ActionInput> getLeavesInputMapping() {
401+
TreeMap<PathFragment, ActionInput> inputMap = new TreeMap<>();
402+
addInputs(
403+
inputMap,
404+
NestedSetBuilder.create(Order.STABLE_ORDER, tree),
405+
artifactExpander,
406+
baseDirectory);
407+
return inputMap;
408+
}
409+
});
410+
}
411+
412+
private static boolean isTreeArtifact(ActionInput input) {
413+
return input instanceof SpecialArtifact && ((SpecialArtifact) input).isTreeArtifact();
414+
}
415+
387416
/**
388417
* Exception signaling that an input was not a regular file: most likely a directory. This
389418
* exception is currently never thrown in practice since we do not enforce "strict" mode.
390419
*/
391420
private static final class ForbiddenNonFileException extends ForbiddenActionInputException {
421+
392422
ForbiddenNonFileException(ActionInput input) {
393423
super("Not a file: " + input.getExecPathString());
394424
}

src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionService.java

+5
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,11 @@ public boolean mayBeExecutedRemotely(Spawn spawn) {
343343
&& Spawns.mayBeExecutedRemotely(spawn);
344344
}
345345

346+
@VisibleForTesting
347+
Cache<Object, MerkleTree> getMerkleTreeCache() {
348+
return merkleTreeCache;
349+
}
350+
346351
private SortedMap<PathFragment, ActionInput> buildOutputDirMap(Spawn spawn) {
347352
TreeMap<PathFragment, ActionInput> outputDirMap = new TreeMap<>();
348353
for (ActionInput output : spawn.getOutputFiles()) {

src/test/java/com/google/devtools/build/lib/remote/RemoteExecutionServiceTest.java

+27-6
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
import com.google.devtools.build.lib.actions.Artifact;
6666
import com.google.devtools.build.lib.actions.ArtifactRoot;
6767
import com.google.devtools.build.lib.actions.ArtifactRoot.RootType;
68+
import com.google.devtools.build.lib.actions.EmptyRunfilesSupplier;
6869
import com.google.devtools.build.lib.actions.ExecutionRequirements;
6970
import com.google.devtools.build.lib.actions.ResourceSet;
7071
import com.google.devtools.build.lib.actions.SimpleSpawn;
@@ -1808,8 +1809,10 @@ public void buildMerkleTree_withMemoization_works() throws Exception {
18081809

18091810
// arrange
18101811
// Single node NestedSets are folded, so always add a dummy file everywhere.
1811-
ActionInput dummyFile = ActionInputHelper.fromPath("dummy");
1812-
fakeFileCache.createScratchInput(dummyFile, "dummy");
1812+
ActionInput dummyFile = ActionInputHelper.fromPath("file");
1813+
fakeFileCache.createScratchInput(dummyFile, "file");
1814+
1815+
ActionInput tree = ActionsTestUtil.createTreeArtifactWithGeneratingAction(artifactRoot, "tree");
18131816

18141817
ActionInput barFile = ActionInputHelper.fromPath("bar/file");
18151818
NestedSet<ActionInput> nodeBar =
@@ -1829,12 +1832,14 @@ public void buildMerkleTree_withMemoization_works() throws Exception {
18291832
NestedSet<ActionInput> nodeRoot1 =
18301833
new NestedSetBuilder<ActionInput>(Order.STABLE_ORDER)
18311834
.add(dummyFile)
1835+
.add(tree)
18321836
.addTransitive(nodeBar)
18331837
.addTransitive(nodeFoo1)
18341838
.build();
18351839
NestedSet<ActionInput> nodeRoot2 =
18361840
new NestedSetBuilder<ActionInput>(Order.STABLE_ORDER)
18371841
.add(dummyFile)
1842+
.add(tree)
18381843
.addTransitive(nodeBar)
18391844
.addTransitive(nodeFoo2)
18401845
.build();
@@ -1869,15 +1874,31 @@ public void buildMerkleTree_withMemoization_works() throws Exception {
18691874
service.buildRemoteAction(spawn1, context1);
18701875

18711876
// assert first time
1872-
// Called for: manifests, runfiles, nodeRoot1, nodeFoo1 and nodeBar.
1873-
verify(service, times(5)).uncachedBuildMerkleTreeVisitor(any(), any(), any());
1877+
verify(service, times(6)).uncachedBuildMerkleTreeVisitor(any(), any(), any());
1878+
assertThat(service.getMerkleTreeCache().asMap().keySet())
1879+
.containsExactly(
1880+
ImmutableList.of(ImmutableMap.of(), PathFragment.EMPTY_FRAGMENT), // fileset mapping
1881+
ImmutableList.of(EmptyRunfilesSupplier.INSTANCE, PathFragment.EMPTY_FRAGMENT),
1882+
ImmutableList.of(tree, PathFragment.EMPTY_FRAGMENT),
1883+
ImmutableList.of(nodeRoot1.toNode(), PathFragment.EMPTY_FRAGMENT),
1884+
ImmutableList.of(nodeFoo1.toNode(), PathFragment.EMPTY_FRAGMENT),
1885+
ImmutableList.of(nodeBar.toNode(), PathFragment.EMPTY_FRAGMENT));
18741886

18751887
// act second time
18761888
service.buildRemoteAction(spawn2, context2);
18771889

18781890
// assert second time
1879-
// Called again for: manifests, runfiles, nodeRoot2 and nodeFoo2 but not nodeBar (cached).
1880-
verify(service, times(5 + 4)).uncachedBuildMerkleTreeVisitor(any(), any(), any());
1891+
verify(service, times(6 + 2)).uncachedBuildMerkleTreeVisitor(any(), any(), any());
1892+
assertThat(service.getMerkleTreeCache().asMap().keySet())
1893+
.containsExactly(
1894+
ImmutableList.of(ImmutableMap.of(), PathFragment.EMPTY_FRAGMENT), // fileset mapping
1895+
ImmutableList.of(EmptyRunfilesSupplier.INSTANCE, PathFragment.EMPTY_FRAGMENT),
1896+
ImmutableList.of(tree, PathFragment.EMPTY_FRAGMENT),
1897+
ImmutableList.of(nodeRoot1.toNode(), PathFragment.EMPTY_FRAGMENT),
1898+
ImmutableList.of(nodeRoot2.toNode(), PathFragment.EMPTY_FRAGMENT),
1899+
ImmutableList.of(nodeFoo1.toNode(), PathFragment.EMPTY_FRAGMENT),
1900+
ImmutableList.of(nodeFoo2.toNode(), PathFragment.EMPTY_FRAGMENT),
1901+
ImmutableList.of(nodeBar.toNode(), PathFragment.EMPTY_FRAGMENT));
18811902
}
18821903

18831904
@Test

0 commit comments

Comments
 (0)