aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/test/java/com/google/devtools/build/lib/actions
diff options
context:
space:
mode:
authorGravatar jmmv <jmmv@google.com>2017-09-26 11:59:22 -0400
committerGravatar John Cater <jcater@google.com>2017-09-27 10:00:47 -0400
commit9573a0d3d2a30b795ac096ff8acb59db561db7f6 (patch)
tree3478cc04009086a01d8d13cd26a2a52887d79000 /src/test/java/com/google/devtools/build/lib/actions
parent940ce20bb6045a8b0a09856f312991ba48ef2a7c (diff)
Collect action cache hits, misses, and reasons for the misses.
As a bonus, this brings in a bunch of new unit tests for the ActionCacheChecker. RELNOTES: None. PiperOrigin-RevId: 170059577
Diffstat (limited to 'src/test/java/com/google/devtools/build/lib/actions')
-rw-r--r--src/test/java/com/google/devtools/build/lib/actions/ActionCacheCheckerTest.java340
-rw-r--r--src/test/java/com/google/devtools/build/lib/actions/util/ActionCacheTestHelper.java14
-rw-r--r--src/test/java/com/google/devtools/build/lib/actions/util/ActionsTestUtil.java128
3 files changed, 482 insertions, 0 deletions
diff --git a/src/test/java/com/google/devtools/build/lib/actions/ActionCacheCheckerTest.java b/src/test/java/com/google/devtools/build/lib/actions/ActionCacheCheckerTest.java
new file mode 100644
index 0000000000..4da84f8923
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/actions/ActionCacheCheckerTest.java
@@ -0,0 +1,340 @@
+// Copyright 2017 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.actions;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.ActionCacheChecker.Token;
+import com.google.devtools.build.lib.actions.cache.ActionCache;
+import com.google.devtools.build.lib.actions.cache.CompactPersistentActionCache;
+import com.google.devtools.build.lib.actions.cache.Md5Digest;
+import com.google.devtools.build.lib.actions.cache.Metadata;
+import com.google.devtools.build.lib.actions.cache.MetadataHandler;
+import com.google.devtools.build.lib.actions.cache.Protos.ActionCacheStatistics;
+import com.google.devtools.build.lib.actions.cache.Protos.ActionCacheStatistics.MissDetail;
+import com.google.devtools.build.lib.actions.cache.Protos.ActionCacheStatistics.MissReason;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil.FakeArtifactResolverBase;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil.FakeMetadataHandlerBase;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil.MissDetailsBuilder;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil.NullAction;
+import com.google.devtools.build.lib.clock.Clock;
+import com.google.devtools.build.lib.skyframe.FileArtifactValue;
+import com.google.devtools.build.lib.testutil.ManualClock;
+import com.google.devtools.build.lib.testutil.Scratch;
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.Path;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ActionCacheCheckerTest {
+ private CorruptibleCompactPersistentActionCache cache;
+ private ActionCacheChecker cacheChecker;
+ private Set<Path> filesToDelete;
+
+ @Before
+ public void setupCache() throws Exception {
+ Scratch scratch = new Scratch();
+ Clock clock = new ManualClock();
+ ArtifactResolver artifactResolver = new FakeArtifactResolverBase();
+
+ cache = new CorruptibleCompactPersistentActionCache(scratch.resolve("/cache/test.dat"), clock);
+ cacheChecker = new ActionCacheChecker(cache, artifactResolver, Predicates.alwaysTrue(), null);
+ }
+
+ @Before
+ public void clearFilesToDeleteAfterTest() throws Exception {
+ filesToDelete = new HashSet<>();
+ }
+
+ @After
+ public void deleteFilesCreatedDuringTest() throws Exception {
+ for (Path path : filesToDelete) {
+ path.delete();
+ }
+ }
+
+ /** "Executes" the given action from the point of view of the cache's lifecycle. */
+ private void runAction(Action action) throws Exception {
+ runAction(action, new HashMap<>());
+ }
+
+ /**
+ * "Executes" the given action from the point of view of the cache's lifecycle with a custom
+ * client environment.
+ */
+ private void runAction(Action action, Map<String, String> clientEnv) throws Exception {
+ MetadataHandler metadataHandler = new FakeMetadataHandler();
+
+ for (Artifact artifact : action.getOutputs()) {
+ Path path = artifact.getPath();
+
+ // Record all action outputs as files to be deleted across tests to prevent cross-test
+ // pollution. We need to do this on a path basis because we don't know upfront which file
+ // system they live in so we cannot just recreate the file system. (E.g. all NullActions
+ // share an in-memory file system to hold dummy outputs.)
+ filesToDelete.add(path);
+
+ if (!path.exists()) {
+ FileSystemUtils.writeContentAsLatin1(path, "");
+ }
+ }
+
+ Token token = cacheChecker.getTokenIfNeedToExecute(
+ action, null, clientEnv, null, metadataHandler);
+ if (token != null) {
+ // Real action execution would happen here.
+ cacheChecker.afterExecution(action, token, metadataHandler, clientEnv);
+ }
+ }
+
+ /** Ensures that the cache statistics match exactly the given values. */
+ private void assertStatistics(int hits, Iterable<MissDetail> misses) {
+ ActionCacheStatistics.Builder builder = ActionCacheStatistics.newBuilder();
+ cache.mergeIntoActionCacheStatistics(builder);
+ ActionCacheStatistics stats = builder.build();
+
+ assertThat(stats.getHits()).isEqualTo(hits);
+ assertThat(stats.getMissDetailsList()).containsExactlyElementsIn(misses);
+ }
+
+ private void doTestNotCached(Action action, MissReason missReason) throws Exception {
+ runAction(action);
+
+ assertStatistics(0, new MissDetailsBuilder().set(missReason, 1).build());
+ }
+
+ private void doTestCached(Action action, MissReason missReason) throws Exception {
+ int runs = 5;
+ for (int i = 0; i < runs; i++) {
+ runAction(action);
+ }
+
+ assertStatistics(runs - 1, new MissDetailsBuilder().set(missReason, 1).build());
+ }
+
+ private void doTestCorruptedCacheEntry(Action action) throws Exception {
+ cache.corruptAllEntries();
+ runAction(action);
+
+ assertStatistics(
+ 0,
+ new MissDetailsBuilder().set(MissReason.CORRUPTED_CACHE_ENTRY, 1).build());
+ }
+
+ @Test
+ public void testNoActivity() throws Exception {
+ assertStatistics(0, new MissDetailsBuilder().build());
+ }
+
+ @Test
+ public void testNotCached() throws Exception {
+ doTestNotCached(new NullAction(), MissReason.NOT_CACHED);
+ }
+
+ @Test
+ public void testCached() throws Exception {
+ doTestCached(new NullAction(), MissReason.NOT_CACHED);
+ }
+
+ @Test
+ public void testCorruptedCacheEntry() throws Exception {
+ doTestCorruptedCacheEntry(new NullAction());
+ }
+
+ @Test
+ public void testDifferentActionKey() throws Exception {
+ Action action = new NullAction() {
+ @Override
+ protected String computeKey() {
+ return "key1";
+ }
+ };
+ runAction(action);
+ action = new NullAction() {
+ @Override
+ protected String computeKey() {
+ return "key2";
+ }
+ };
+ runAction(action);
+
+ assertStatistics(
+ 0,
+ new MissDetailsBuilder()
+ .set(MissReason.DIFFERENT_ACTION_KEY, 1)
+ .set(MissReason.NOT_CACHED, 1)
+ .build());
+ }
+
+ @Test
+ public void testDifferentEnvironment() throws Exception {
+ Action action = new NullAction() {
+ @Override
+ public Iterable<String> getClientEnvironmentVariables() {
+ return ImmutableList.of("used-var");
+ }
+ };
+ Map<String, String> clientEnv = new HashMap<>();
+ clientEnv.put("unused-var", "1");
+ runAction(action, clientEnv); // Not cached.
+ clientEnv.remove("unused-var");
+ runAction(action, clientEnv); // Cache hit because we only modified uninteresting variables.
+ clientEnv.put("used-var", "2");
+ runAction(action, clientEnv); // Cache miss because of different environment.
+ runAction(action, clientEnv); // Cache hit because we did not change anything.
+
+ assertStatistics(
+ 2,
+ new MissDetailsBuilder()
+ .set(MissReason.DIFFERENT_ENVIRONMENT, 1)
+ .set(MissReason.NOT_CACHED, 1)
+ .build());
+ }
+
+ @Test
+ public void testDifferentFiles() throws Exception {
+ Action action = new NullAction();
+ runAction(action); // Not cached.
+ FileSystemUtils.writeContentAsLatin1(action.getPrimaryOutput().getPath(), "modified");
+ runAction(action); // Cache miss because output files were modified.
+
+ assertStatistics(
+ 0,
+ new MissDetailsBuilder()
+ .set(MissReason.DIFFERENT_FILES, 1)
+ .set(MissReason.NOT_CACHED, 1)
+ .build());
+ }
+
+ @Test
+ public void testUnconditionalExecution() throws Exception {
+ Action action = new NullAction() {
+ @Override
+ public boolean executeUnconditionally() {
+ return true;
+ }
+
+ @Override
+ public boolean isVolatile() {
+ return true;
+ }
+ };
+
+ int runs = 5;
+ for (int i = 0; i < runs; i++) {
+ runAction(action);
+ }
+
+ assertStatistics(
+ 0, new MissDetailsBuilder().set(MissReason.UNCONDITIONAL_EXECUTION, runs).build());
+ }
+
+ @Test
+ public void testMiddleman_NotCached() throws Exception {
+ doTestNotCached(new NullMiddlemanAction(), MissReason.DIFFERENT_DEPS);
+ }
+
+ @Test
+ public void testMiddleman_Cached() throws Exception {
+ doTestCached(new NullMiddlemanAction(), MissReason.DIFFERENT_DEPS);
+ }
+
+ @Test
+ public void testMiddleman_CorruptedCacheEntry() throws Exception {
+ doTestCorruptedCacheEntry(new NullMiddlemanAction());
+ }
+
+ @Test
+ public void testMiddleman_DifferentFiles() throws Exception {
+ Action action = new NullMiddlemanAction() {
+ @Override
+ public synchronized Iterable<Artifact> getInputs() {
+ FileSystem fileSystem = getPrimaryOutput().getPath().getFileSystem();
+ Path path = fileSystem.getPath("/input");
+ Root root = Root.asSourceRoot(fileSystem.getPath("/"));
+ return ImmutableList.of(new Artifact(path, root));
+ }
+ };
+ runAction(action); // Not cached so recorded as different deps.
+ FileSystemUtils.writeContentAsLatin1(action.getPrimaryInput().getPath(), "modified");
+ runAction(action); // Cache miss because input files were modified.
+ FileSystemUtils.writeContentAsLatin1(action.getPrimaryOutput().getPath(), "modified");
+ runAction(action); // Outputs are not considered for middleman actions, so this is a cache hit.
+ runAction(action); // Outputs are not considered for middleman actions, so this is a cache hit.
+
+ assertStatistics(
+ 2,
+ new MissDetailsBuilder()
+ .set(MissReason.DIFFERENT_DEPS, 1)
+ .set(MissReason.DIFFERENT_FILES, 1)
+ .build());
+ }
+
+ /** A {@link CompactPersistentActionCache} that allows injecting corruption for testing. */
+ private static class CorruptibleCompactPersistentActionCache
+ extends CompactPersistentActionCache {
+ private boolean corrupted = false;
+
+ CorruptibleCompactPersistentActionCache(Path cacheRoot, Clock clock) throws IOException {
+ super(cacheRoot, clock);
+ }
+
+ void corruptAllEntries() {
+ corrupted = true;
+ }
+
+ @Override
+ public Entry get(String key) {
+ if (corrupted) {
+ return ActionCache.Entry.CORRUPTED;
+ } else {
+ return super.get(key);
+ }
+ }
+ }
+
+ /** A null middleman action. */
+ private static class NullMiddlemanAction extends NullAction {
+ @Override
+ public MiddlemanType getActionType() {
+ return MiddlemanType.RUNFILES_MIDDLEMAN;
+ }
+ }
+
+ /** A fake metadata handler that is able to obtain metadata from the file system. */
+ private static class FakeMetadataHandler extends FakeMetadataHandlerBase {
+ @Override
+ public Metadata getMetadata(Artifact artifact) throws IOException {
+ return FileArtifactValue.create(artifact);
+ }
+
+ @Override
+ public void setDigestForVirtualArtifact(Artifact artifact, Md5Digest md5Digest) {
+
+ }
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/actions/util/ActionCacheTestHelper.java b/src/test/java/com/google/devtools/build/lib/actions/util/ActionCacheTestHelper.java
index 1415c810ed..93fd34f15b 100644
--- a/src/test/java/com/google/devtools/build/lib/actions/util/ActionCacheTestHelper.java
+++ b/src/test/java/com/google/devtools/build/lib/actions/util/ActionCacheTestHelper.java
@@ -14,6 +14,8 @@
package com.google.devtools.build.lib.actions.util;
import com.google.devtools.build.lib.actions.cache.ActionCache;
+import com.google.devtools.build.lib.actions.cache.Protos.ActionCacheStatistics;
+import com.google.devtools.build.lib.actions.cache.Protos.ActionCacheStatistics.MissReason;
import java.io.PrintStream;
/**
@@ -46,5 +48,17 @@ public class ActionCacheTestHelper {
@Override
public void dump(PrintStream out) {}
+
+ @Override
+ public void accountHit() {}
+
+ @Override
+ public void accountMiss(MissReason reason) {}
+
+ @Override
+ public void mergeIntoActionCacheStatistics(ActionCacheStatistics.Builder builder) {}
+
+ @Override
+ public void resetStatistics() {}
};
}
diff --git a/src/test/java/com/google/devtools/build/lib/actions/util/ActionsTestUtil.java b/src/test/java/com/google/devtools/build/lib/actions/util/ActionsTestUtil.java
index d1b796b5c0..2fc80f5bde 100644
--- a/src/test/java/com/google/devtools/build/lib/actions/util/ActionsTestUtil.java
+++ b/src/test/java/com/google/devtools/build/lib/actions/util/ActionsTestUtil.java
@@ -14,6 +14,7 @@
package com.google.devtools.build.lib.actions.util;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.devtools.build.lib.util.Preconditions.checkArgument;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
@@ -29,6 +30,7 @@ import com.google.devtools.build.lib.actions.Action;
import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
import com.google.devtools.build.lib.actions.ActionExecutionContext;
import com.google.devtools.build.lib.actions.ActionGraph;
+import com.google.devtools.build.lib.actions.ActionInput;
import com.google.devtools.build.lib.actions.ActionInputHelper;
import com.google.devtools.build.lib.actions.ActionInputPrefetcher;
import com.google.devtools.build.lib.actions.ActionOwner;
@@ -36,15 +38,22 @@ import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.Artifact.ArtifactExpander;
import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact;
import com.google.devtools.build.lib.actions.ArtifactOwner;
+import com.google.devtools.build.lib.actions.ArtifactResolver;
import com.google.devtools.build.lib.actions.Executor;
import com.google.devtools.build.lib.actions.MutableActionGraph;
import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException;
+import com.google.devtools.build.lib.actions.PackageRootResolver;
import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.actions.cache.Md5Digest;
+import com.google.devtools.build.lib.actions.cache.Metadata;
import com.google.devtools.build.lib.actions.cache.MetadataHandler;
+import com.google.devtools.build.lib.actions.cache.Protos.ActionCacheStatistics.MissDetail;
+import com.google.devtools.build.lib.actions.cache.Protos.ActionCacheStatistics.MissReason;
import com.google.devtools.build.lib.analysis.actions.CustomCommandLine;
import com.google.devtools.build.lib.analysis.actions.SpawnActionTemplate;
import com.google.devtools.build.lib.analysis.actions.SpawnActionTemplate.OutputPathMapper;
import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.events.ExtendedEventHandler;
import com.google.devtools.build.lib.events.Reporter;
@@ -54,6 +63,7 @@ import com.google.devtools.build.lib.util.FileType;
import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.build.lib.util.ResourceUsage;
import com.google.devtools.build.lib.util.io.FileOutErr;
+import com.google.devtools.build.lib.vfs.FileStatus;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
@@ -65,9 +75,11 @@ import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import com.google.devtools.build.skyframe.ValueOrExceptionUtils;
import com.google.devtools.build.skyframe.ValueOrUntypedException;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.EnumMap;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
@@ -570,4 +582,120 @@ public final class ActionsTestUtil {
})
.build(NULL_ACTION_OWNER);
}
+
+ /** Builder for a list of {@link MissDetail}s with defaults set to zero for all possible items. */
+ public static class MissDetailsBuilder {
+ private final Map<MissReason, Integer> details = new EnumMap<>(MissReason.class);
+
+ /** Constructs a new builder with all possible cache miss reasons set to zero counts. */
+ public MissDetailsBuilder() {
+ for (MissReason reason : MissReason.values()) {
+ if (reason == MissReason.UNRECOGNIZED) {
+ // The presence of this enum value is a protobuf artifact and not part of our metrics
+ // collection. Just skip it.
+ continue;
+ }
+ details.put(reason, 0);
+ }
+ }
+
+ /** Sets the count of the given miss reason to the given value. */
+ public MissDetailsBuilder set(MissReason reason, int count) {
+ checkArgument(details.containsKey(reason));
+ details.put(reason, count);
+ return this;
+ }
+
+ /** Constructs the list of {@link MissDetail}s. */
+ public Iterable<MissDetail> build() {
+ List<MissDetail> result = new ArrayList<>(details.size());
+ for (Map.Entry<MissReason, Integer> entry : details.entrySet()) {
+ MissDetail detail = MissDetail.newBuilder()
+ .setReason(entry.getKey())
+ .setCount(entry.getValue())
+ .build();
+ result.add(detail);
+ }
+ return result;
+ }
+ }
+
+ /**
+ * An {@link ArtifactResolver} all of whose operations throw an exception.
+ *
+ * <p>This is to be used as a base class by other test programs that need to implement only a
+ * few of the hooks required by the scenario under test.
+ */
+ public static class FakeArtifactResolverBase implements ArtifactResolver {
+ @Override
+ public Artifact getSourceArtifact(
+ PathFragment execPath, Root root, ArtifactOwner owner) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Artifact getSourceArtifact(PathFragment execPath, Root root) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Artifact resolveSourceArtifact(
+ PathFragment execPath, RepositoryName repositoryName) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Map<PathFragment, Artifact> resolveSourceArtifacts(
+ Iterable<PathFragment> execPaths, PackageRootResolver resolver) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ /**
+ * A {@link MetadataHandler} all of whose operations throw an exception.
+ *
+ * <p>This is to be used as a base class by other test programs that need to implement only a
+ * few of the hooks required by the scenario under test.
+ */
+ public static class FakeMetadataHandlerBase implements MetadataHandler {
+ @Override
+ public Metadata getMetadata(Artifact artifact) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setDigestForVirtualArtifact(Artifact artifact, Md5Digest md5Digest) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void addExpandedTreeOutput(TreeFileArtifact output) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Iterable<TreeFileArtifact> getExpandedOutputs(Artifact artifact) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void injectDigest(ActionInput output, FileStatus statNoFollow, byte[] digest) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void markOmitted(ActionInput output) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean artifactOmitted(Artifact artifact) {
+ return false;
+ }
+
+ @Override
+ public void discardOutputMetadata() {
+ throw new UnsupportedOperationException();
+ }
+ }
}