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
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionCacheChecker.java b/src/main/java/com/google/devtools/build/lib/actions/ActionCacheChecker.java
index 1a6c4c8..fc64c75 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/ActionCacheChecker.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionCacheChecker.java
@@ -24,6 +24,7 @@
import com.google.devtools.build.lib.actions.cache.DigestUtils;
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.MissReason;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.events.EventKind;
@@ -285,32 +286,38 @@
if (unconditionalExecution(action)) {
Preconditions.checkState(action.isVolatile());
reportUnconditionalExecution(handler, action);
- return true; // must execute - unconditional execution is requested.
+ actionCache.accountMiss(MissReason.UNCONDITIONAL_EXECUTION);
+ return true;
}
if (entry == null) {
reportNewAction(handler, action);
- return true; // must execute -- no cache entry (e.g. first build)
+ actionCache.accountMiss(MissReason.NOT_CACHED);
+ return true;
}
if (entry.isCorrupted()) {
reportCorruptedCacheEntry(handler, action);
- return true; // cache entry is corrupted - must execute
+ actionCache.accountMiss(MissReason.CORRUPTED_CACHE_ENTRY);
+ return true;
} else if (validateArtifacts(entry, action, actionInputs, metadataHandler, true)) {
reportChanged(handler, action);
- return true; // files have changed
+ actionCache.accountMiss(MissReason.DIFFERENT_FILES);
+ return true;
} else if (!entry.getActionKey().equals(action.getKey())) {
reportCommand(handler, action);
- return true; // must execute -- action key is different
+ actionCache.accountMiss(MissReason.DIFFERENT_ACTION_KEY);
+ return true;
}
Map<String, String> usedClientEnv = computeUsedClientEnv(action, clientEnv);
if (!entry.getUsedClientEnvDigest().equals(DigestUtils.fromEnv(usedClientEnv))) {
reportClientEnv(handler, action, usedClientEnv);
- return true; // different values taken from the environment -- must execute
+ actionCache.accountMiss(MissReason.DIFFERENT_ENVIRONMENT);
+ return true;
}
-
entry.getFileDigest();
- return false; // cache hit
+ actionCache.accountHit();
+ return false;
}
private static Metadata getMetadataOrConstant(MetadataHandler metadataHandler, Artifact artifact)
@@ -460,13 +467,16 @@
if (entry != null) {
if (entry.isCorrupted()) {
reportCorruptedCacheEntry(handler, action);
+ actionCache.accountMiss(MissReason.CORRUPTED_CACHE_ENTRY);
changed = true;
} else if (validateArtifacts(entry, action, action.getInputs(), metadataHandler, false)) {
reportChanged(handler, action);
+ actionCache.accountMiss(MissReason.DIFFERENT_FILES);
changed = true;
}
} else {
reportChangedDeps(handler, action);
+ actionCache.accountMiss(MissReason.DIFFERENT_DEPS);
changed = true;
}
if (changed) {
@@ -482,6 +492,8 @@
metadataHandler.setDigestForVirtualArtifact(middleman, entry.getFileDigest());
if (changed) {
actionCache.put(cacheKey, entry);
+ } else {
+ actionCache.accountHit();
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/BUILD b/src/main/java/com/google/devtools/build/lib/actions/BUILD
index db44f36..f481e5f 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/actions/BUILD
@@ -35,6 +35,7 @@
"//src/main/java/com/google/devtools/build/skyframe",
"//src/main/java/com/google/devtools/build/skyframe:skyframe-objects",
"//src/main/java/com/google/devtools/common/options",
+ "//src/main/protobuf:action_cache_java_proto",
"//src/main/protobuf:extra_actions_base_java_proto",
"//third_party:auto_value",
"//third_party:guava",
diff --git a/src/main/java/com/google/devtools/build/lib/actions/cache/ActionCache.java b/src/main/java/com/google/devtools/build/lib/actions/cache/ActionCache.java
index 59eb31f..fd1756a0 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/cache/ActionCache.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/cache/ActionCache.java
@@ -15,7 +15,10 @@
package com.google.devtools.build.lib.actions.cache;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.actions.cache.Protos.ActionCacheStatistics;
+import com.google.devtools.build.lib.actions.cache.Protos.ActionCacheStatistics.MissReason;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.build.lib.vfs.PathFragment;
@@ -67,6 +70,10 @@
* will continue to return same result regardless of internal data transformations).
*/
final class Entry {
+ /** Unique instance to represent a corrupted cache entry. */
+ public static final ActionCache.Entry CORRUPTED =
+ new ActionCache.Entry(null, ImmutableMap.<String, String>of(), false);
+
private final String actionKey;
@Nullable
// Null iff the corresponding action does not do input discovery.
@@ -141,7 +148,7 @@
* Returns true if this cache entry is corrupted and should be ignored.
*/
public boolean isCorrupted() {
- return actionKey == null;
+ return this == CORRUPTED;
}
/**
@@ -194,4 +201,22 @@
* Dumps action cache content into the given PrintStream.
*/
void dump(PrintStream out);
+
+ /** Accounts one cache hit. */
+ void accountHit();
+
+ /** Accounts one cache miss for the given reason. */
+ void accountMiss(MissReason reason);
+
+ /**
+ * Populates the given builder with statistics.
+ *
+ * <p>The extracted values are not guaranteed to be a consistent snapshot of the metrics tracked
+ * by the action cache. Therefore, even if it is safe to call this function at any point in time,
+ * this should only be called once there are no actions running.
+ */
+ void mergeIntoActionCacheStatistics(ActionCacheStatistics.Builder builder);
+
+ /** Resets the current statistics to zero. */
+ void resetStatistics();
}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/cache/CompactPersistentActionCache.java b/src/main/java/com/google/devtools/build/lib/actions/cache/CompactPersistentActionCache.java
index 70015af..52cca10 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/cache/CompactPersistentActionCache.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/cache/CompactPersistentActionCache.java
@@ -16,7 +16,8 @@
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.cache.Protos.ActionCacheStatistics;
+import com.google.devtools.build.lib.actions.cache.Protos.ActionCacheStatistics.MissReason;
import com.google.devtools.build.lib.clock.Clock;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ConditionallyThreadSafe;
import com.google.devtools.build.lib.profiler.AutoProfiler;
@@ -35,9 +36,11 @@
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.Collection;
+import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;
/**
@@ -154,8 +157,9 @@
private final PersistentMap<Integer, byte[]> map;
private final PersistentStringIndexer indexer;
- static final ActionCache.Entry CORRUPTED =
- new ActionCache.Entry(null, ImmutableMap.<String, String>of(), false);
+
+ private final AtomicInteger hits = new AtomicInteger();
+ private final Map<MissReason, AtomicInteger> misses = new EnumMap<>(MissReason.class);
public CompactPersistentActionCache(Path cacheRoot, Clock clock) throws IOException {
Path cacheFile = cacheFile(cacheRoot);
@@ -187,6 +191,15 @@
throw new IOException("Failed action cache referential integrity check: " + integrityError);
}
}
+
+ for (MissReason reason : MissReason.values()) {
+ if (reason == MissReason.UNRECOGNIZED) {
+ // The presence of this enum value is a protobuf artifact and confuses our metrics
+ // externalization code below. Just skip it.
+ continue;
+ }
+ misses.put(reason, new AtomicInteger(0));
+ }
}
/**
@@ -254,7 +267,7 @@
return data != null ? CompactPersistentActionCache.decode(indexer, data) : null;
} catch (IOException e) {
// return entry marked as corrupted.
- return CORRUPTED;
+ return ActionCache.Entry.CORRUPTED;
}
}
@@ -420,7 +433,7 @@
if (source.remaining() > 0) {
throw new IOException("serialized entry data has not been fully decoded");
}
- return new Entry(
+ return new ActionCache.Entry(
actionKey,
usedClientEnvDigest,
count == NO_INPUT_DISCOVERY_COUNT ? null : builder.build(),
@@ -429,4 +442,38 @@
throw new IOException("encoded entry data is incomplete", e);
}
}
+
+ @Override
+ public void accountHit() {
+ hits.incrementAndGet();
+ }
+
+ @Override
+ public void accountMiss(MissReason reason) {
+ AtomicInteger counter = misses.get(reason);
+ Preconditions.checkNotNull(counter, "Miss reason %s was not registered in the misses map "
+ + "during cache construction", reason);
+ counter.incrementAndGet();
+ }
+
+ @Override
+ public void mergeIntoActionCacheStatistics(ActionCacheStatistics.Builder builder) {
+ builder.setHits(hits.get());
+
+ int totalMisses = 0;
+ for (Map.Entry<MissReason, AtomicInteger> entry : misses.entrySet()) {
+ int count = entry.getValue().get();
+ builder.addMissDetailsBuilder().setReason(entry.getKey()).setCount(count);
+ totalMisses += count;
+ }
+ builder.setMisses(totalMisses);
+ }
+
+ @Override
+ public void resetStatistics() {
+ hits.set(0);
+ for (Map.Entry<MissReason, AtomicInteger> entry : misses.entrySet()) {
+ entry.getValue().set(0);
+ }
+ }
}
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java b/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java
index 1285961..af7659b 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java
@@ -380,6 +380,7 @@
request.getBuildOptions().getSymlinkPrefix(productName), productName);
ActionCache actionCache = getActionCache();
+ actionCache.resetStatistics();
SkyframeExecutor skyframeExecutor = env.getSkyframeExecutor();
Builder builder = createBuilder(
request, actionCache, skyframeExecutor, modifiedOutputFiles);
@@ -734,6 +735,7 @@
*/
private void saveActionCache(ActionCache actionCache) {
ActionCacheStatistics.Builder builder = ActionCacheStatistics.newBuilder();
+ actionCache.mergeIntoActionCacheStatistics(builder);
AutoProfiler p =
AutoProfiler.profiledAndLogged("Saving action cache", ProfilerTask.INFO, logger);
diff --git a/src/main/protobuf/action_cache.proto b/src/main/protobuf/action_cache.proto
index 429990d..5694ebe 100644
--- a/src/main/protobuf/action_cache.proto
+++ b/src/main/protobuf/action_cache.proto
@@ -31,5 +31,29 @@
// Time it took to save the action cache to disk.
uint64 save_time_in_ms = 2;
- // NEXT TAG: 3
+ // Reasons for not finding an action in the cache.
+ enum MissReason {
+ DIFFERENT_ACTION_KEY = 0;
+ DIFFERENT_DEPS = 1;
+ DIFFERENT_ENVIRONMENT = 2;
+ DIFFERENT_FILES = 3;
+ CORRUPTED_CACHE_ENTRY = 4;
+ NOT_CACHED = 5;
+ UNCONDITIONAL_EXECUTION = 6;
+ }
+
+ // Detailed information for a particular miss reason.
+ message MissDetail {
+ MissReason reason = 1;
+ int32 count = 2;
+ }
+
+ // Cache counters.
+ int32 hits = 3;
+ int32 misses = 4;
+
+ // Breakdown of the cache misses based on the reasons behind them.
+ repeated MissDetail miss_details = 5;
+
+ // NEXT TAG: 6
}
diff --git a/src/test/java/com/google/devtools/build/lib/BUILD b/src/test/java/com/google/devtools/build/lib/BUILD
index 403db1d..a3542c5 100644
--- a/src/test/java/com/google/devtools/build/lib/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/BUILD
@@ -289,6 +289,7 @@
"//src/main/java/com/google/devtools/build/skyframe",
"//src/main/java/com/google/devtools/build/skyframe:skyframe-objects",
"//src/main/java/com/google/devtools/common/options",
+ "//src/main/protobuf:action_cache_java_proto",
"//third_party:guava",
"//third_party:guava-testlib",
"//third_party:jsr305",
@@ -326,6 +327,7 @@
"//src/main/java/com/google/devtools/build/lib/vfs",
"//src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs",
"//src/main/java/com/google/devtools/common/options",
+ "//src/main/protobuf:action_cache_java_proto",
"//third_party:guava",
"//third_party:guava-testlib",
"//third_party:jsr305",
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 0000000..4da84f8
--- /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 1415c81..93fd34f 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 @@
@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 d1b796b..2fc80f5 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.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.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.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.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 @@
})
.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();
+ }
+ }
}
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/BUILD b/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
index a25cdbd..259a508 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
@@ -82,6 +82,7 @@
"//src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs",
"//src/main/java/com/google/devtools/build/skyframe",
"//src/main/java/com/google/devtools/build/skyframe:skyframe-objects",
+ "//src/main/protobuf:action_cache_java_proto",
"//src/test/java/com/google/devtools/build/lib:actions_testutil",
"//src/test/java/com/google/devtools/build/lib:analysis_testutil",
"//src/test/java/com/google/devtools/build/lib:foundations_testutil",
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTestCase.java b/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTestCase.java
index 22033a3..15d0b04 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTestCase.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTestCase.java
@@ -44,6 +44,8 @@
import com.google.devtools.build.lib.actions.Root;
import com.google.devtools.build.lib.actions.TestExecException;
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 com.google.devtools.build.lib.actions.util.DummyExecutor;
import com.google.devtools.build.lib.actions.util.TestAction;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
@@ -458,6 +460,26 @@
public void dump(PrintStream out) {
out.println("In-memory action cache has " + actionCache.size() + " records");
}
+
+ @Override
+ public void accountHit() {
+ // Not needed for these tests.
+ }
+
+ @Override
+ public void accountMiss(MissReason reason) {
+ // Not needed for these tests.
+ }
+
+ @Override
+ public void mergeIntoActionCacheStatistics(ActionCacheStatistics.Builder builder) {
+ // Not needed for these tests.
+ }
+
+ @Override
+ public void resetStatistics() {
+ // Not needed for these tests.
+ }
}
private static class SingletonActionLookupKey extends ActionLookupValue.ActionLookupKey {