Populate SpawnMetrics and improve CP output
Prevent non-remote executions from remote stat output in
CriticalPathComponent.
Populate metrics recorded in remote execution.
Emit SpawnExecutedEvent to supply metrics to CriticalPathComputer
subscriber after 'Actual execution' in AbstractSpawnStrategy.
Closes #10361.
PiperOrigin-RevId: 304132580
diff --git a/src/main/java/com/google/devtools/build/lib/actions/SpawnResult.java b/src/main/java/com/google/devtools/build/lib/actions/SpawnResult.java
index 2d9be26..9ddb878 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/SpawnResult.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/SpawnResult.java
@@ -242,6 +242,9 @@
/** Returns a file path to the action metadata log. */
Optional<MetadataLog> getActionMetadataLog();
+ /** Whether the spawn result was obtained through remote strategy. */
+ boolean wasRemote();
+
/** Basic implementation of {@link SpawnResult}. */
@Immutable
@ThreadSafe
@@ -265,6 +268,7 @@
// Invariant: Either both have a value or both are null.
@Nullable private final ActionInput inMemoryOutputFile;
@Nullable private final ByteString inMemoryContents;
+ private final boolean remote;
SimpleSpawnResult(Builder builder) {
this.exitCode = builder.exitCode;
@@ -286,6 +290,7 @@
this.inMemoryOutputFile = builder.inMemoryOutputFile;
this.inMemoryContents = builder.inMemoryContents;
this.actionMetadataLog = builder.actionMetadataLog;
+ this.remote = builder.remote;
}
@Override
@@ -414,6 +419,11 @@
public Optional<MetadataLog> getActionMetadataLog() {
return actionMetadataLog;
}
+
+ @Override
+ public boolean wasRemote() {
+ return remote;
+ }
}
/** Builder class for {@link SpawnResult}. */
@@ -437,6 +447,7 @@
// Invariant: Either both have a value or both are null.
@Nullable private ActionInput inMemoryOutputFile;
@Nullable private ByteString inMemoryContents;
+ private boolean remote;
public SpawnResult build() {
Preconditions.checkArgument(!runnerName.isEmpty());
@@ -536,6 +547,11 @@
this.actionMetadataLog = Optional.of(actionMetadataLog);
return this;
}
+
+ public Builder setRemote(boolean remote) {
+ this.remote = remote;
+ return this;
+ }
}
/** A {@link Spawn}'s metadata name and {@link Path}. */
diff --git a/src/main/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategy.java b/src/main/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategy.java
index f9e8eeb..5eede88 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategy.java
@@ -32,6 +32,7 @@
import com.google.devtools.build.lib.actions.SandboxedSpawnStrategy;
import com.google.devtools.build.lib.actions.SchedulingActionEvent;
import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.actions.SpawnExecutedEvent;
import com.google.devtools.build.lib.actions.SpawnResult;
import com.google.devtools.build.lib.actions.SpawnResult.Status;
import com.google.devtools.build.lib.actions.Spawns;
@@ -49,6 +50,7 @@
import com.google.devtools.build.lib.vfs.PathFragment;
import java.io.IOException;
import java.time.Duration;
+import java.time.Instant;
import java.util.SortedMap;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;
@@ -122,6 +124,9 @@
} else {
// Actual execution.
spawnResult = spawnRunner.execAsync(spawn, context).get();
+ actionExecutionContext
+ .getEventHandler()
+ .post(new SpawnExecutedEvent(spawn, spawnResult, Instant.now()));
if (cacheHandle.willStore()) {
cacheHandle.store(spawnResult);
}
diff --git a/src/main/java/com/google/devtools/build/lib/remote/BUILD b/src/main/java/com/google/devtools/build/lib/remote/BUILD
index eed3e55..56ccaca 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/remote/BUILD
@@ -75,6 +75,7 @@
"//third_party:netty",
"//third_party/grpc:grpc-jar",
"//third_party/protobuf:protobuf_java",
+ "//third_party/protobuf:protobuf_java_util",
"@googleapis//:google_bytestream_bytestream_java_grpc",
"@googleapis//:google_bytestream_bytestream_java_proto",
"@googleapis//:google_longrunning_operations_java_proto",
diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java
index b834667..d8aaf0b 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java
@@ -52,6 +52,7 @@
import com.google.devtools.build.lib.remote.options.RemoteOptions;
import com.google.devtools.build.lib.remote.options.RemoteOutputsMode;
import com.google.devtools.build.lib.remote.util.DigestUtil;
+import com.google.devtools.build.lib.remote.util.NetworkTime;
import com.google.devtools.build.lib.remote.util.TracingMetadataUtils;
import com.google.devtools.build.lib.remote.util.Utils;
import com.google.devtools.build.lib.runtime.BlazeModule;
@@ -238,6 +239,7 @@
if (loggingInterceptor != null) {
interceptors.add(loggingInterceptor);
}
+ interceptors.add(new NetworkTime.Interceptor());
execChannel =
RemoteCacheClientFactory.createGrpcChannel(
remoteOptions.remoteExecutor,
@@ -258,6 +260,7 @@
if (loggingInterceptor != null) {
interceptors.add(loggingInterceptor);
}
+ interceptors.add(new NetworkTime.Interceptor());
cacheChannel =
RemoteCacheClientFactory.createGrpcChannel(
remoteOptions.remoteCache,
diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnCache.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnCache.java
index 54271d2..88023c1 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnCache.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnCache.java
@@ -25,11 +25,13 @@
import build.bazel.remote.execution.v2.Digest;
import build.bazel.remote.execution.v2.Platform;
import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.actions.ActionInput;
import com.google.devtools.build.lib.actions.ExecException;
import com.google.devtools.build.lib.actions.FileArtifactValue;
import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.actions.SpawnMetrics;
import com.google.devtools.build.lib.actions.SpawnResult;
import com.google.devtools.build.lib.actions.SpawnResult.Status;
import com.google.devtools.build.lib.actions.Spawns;
@@ -50,6 +52,7 @@
import com.google.devtools.build.lib.remote.options.RemoteOptions;
import com.google.devtools.build.lib.remote.options.RemoteOutputsMode;
import com.google.devtools.build.lib.remote.util.DigestUtil;
+import com.google.devtools.build.lib.remote.util.NetworkTime;
import com.google.devtools.build.lib.remote.util.TracingMetadataUtils;
import com.google.devtools.build.lib.remote.util.Utils;
import com.google.devtools.build.lib.remote.util.Utils.InMemoryOutput;
@@ -116,9 +119,16 @@
return SpawnCache.NO_RESULT_NO_STORE;
}
+ NetworkTime networkTime = new NetworkTime();
+ Stopwatch totalTime = Stopwatch.createStarted();
+
SortedMap<PathFragment, ActionInput> inputMap = context.getInputMapping(true);
MerkleTree merkleTree =
MerkleTree.build(inputMap, context.getMetadataProvider(), execRoot, digestUtil);
+ SpawnMetrics.Builder spawnMetrics =
+ new SpawnMetrics.Builder()
+ .setInputBytes(merkleTree.getInputBytes())
+ .setInputFiles(merkleTree.getInputFiles());
Digest merkleTreeRoot = merkleTree.getRootDigest();
// Get the remote platform properties.
@@ -138,7 +148,8 @@
// Look up action cache, and reuse the action output if it is found.
ActionKey actionKey = digestUtil.computeActionKey(action);
Context withMetadata =
- TracingMetadataUtils.contextWithMetadata(buildRequestId, commandId, actionKey);
+ TracingMetadataUtils.contextWithMetadata(buildRequestId, commandId, actionKey)
+ .withValue(NetworkTime.CONTEXT_KEY, networkTime);
Profiler prof = Profiler.instance();
if (options.remoteAcceptCached
@@ -161,6 +172,7 @@
remoteOutputsMode,
/* exitCode = */ 0,
hasFilesToDownload(spawn.getOutputFiles(), filesToDownload));
+ Stopwatch fetchTime = Stopwatch.createStarted();
if (downloadOutputs) {
try (SilentCloseable c =
prof.profile(ProfilerTask.REMOTE_DOWNLOAD, "download outputs")) {
@@ -183,9 +195,19 @@
context::lockOutputFiles);
}
}
+ fetchTime.stop();
+ totalTime.stop();
+ spawnMetrics
+ .setFetchTime(fetchTime.elapsed())
+ .setTotalTime(totalTime.elapsed())
+ .setNetworkTime(networkTime.getDuration());
SpawnResult spawnResult =
createSpawnResult(
- result.getExitCode(), /* cacheHit= */ true, "remote", inMemoryOutput);
+ result.getExitCode(),
+ /* cacheHit= */ true,
+ "remote",
+ inMemoryOutput,
+ spawnMetrics.build());
return SpawnCache.success(spawnResult);
}
} catch (CacheNotFoundException e) {
diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnRunner.java
index 118ce6c..b9abd58 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnRunner.java
@@ -30,10 +30,12 @@
import build.bazel.remote.execution.v2.Digest;
import build.bazel.remote.execution.v2.ExecuteRequest;
import build.bazel.remote.execution.v2.ExecuteResponse;
+import build.bazel.remote.execution.v2.ExecutedActionMetadata;
import build.bazel.remote.execution.v2.LogFile;
import build.bazel.remote.execution.v2.Platform;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
@@ -46,6 +48,7 @@
import com.google.devtools.build.lib.actions.CommandLines.ParamFileActionInput;
import com.google.devtools.build.lib.actions.ExecException;
import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.actions.SpawnMetrics;
import com.google.devtools.build.lib.actions.SpawnResult;
import com.google.devtools.build.lib.actions.SpawnResult.Status;
import com.google.devtools.build.lib.actions.Spawns;
@@ -67,6 +70,7 @@
import com.google.devtools.build.lib.remote.options.RemoteOptions;
import com.google.devtools.build.lib.remote.options.RemoteOutputsMode;
import com.google.devtools.build.lib.remote.util.DigestUtil;
+import com.google.devtools.build.lib.remote.util.NetworkTime;
import com.google.devtools.build.lib.remote.util.TracingMetadataUtils;
import com.google.devtools.build.lib.remote.util.Utils;
import com.google.devtools.build.lib.remote.util.Utils.InMemoryOutput;
@@ -77,6 +81,9 @@
import com.google.protobuf.Any;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Message;
+import com.google.protobuf.Timestamp;
+import com.google.protobuf.util.Durations;
+import com.google.protobuf.util.Timestamps;
import com.google.rpc.PreconditionFailure;
import com.google.rpc.PreconditionFailure.Violation;
import io.grpc.Context;
@@ -94,6 +101,7 @@
import java.util.SortedMap;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
import javax.annotation.Nullable;
/** A client for the remote execution service. */
@@ -192,7 +200,7 @@
@Override
public SpawnResult exec(Spawn spawn, SpawnExecutionContext context)
throws ExecException, InterruptedException, IOException {
-
+ Stopwatch totalTime = Stopwatch.createStarted();
boolean spawnCacheableRemotely = Spawns.mayBeCachedRemotely(spawn);
boolean uploadLocalResults = remoteOptions.remoteUploadLocalResults && spawnCacheableRemotely;
boolean acceptCachedResult = remoteOptions.remoteAcceptCached && spawnCacheableRemotely;
@@ -202,6 +210,10 @@
SortedMap<PathFragment, ActionInput> inputMap = context.getInputMapping(true);
final MerkleTree merkleTree =
MerkleTree.build(inputMap, context.getMetadataProvider(), execRoot, digestUtil);
+ SpawnMetrics.Builder spawnMetrics =
+ new SpawnMetrics.Builder()
+ .setInputBytes(merkleTree.getInputBytes())
+ .setInputFiles(merkleTree.getInputFiles());
maybeWriteParamFilesLocally(spawn);
// Get the remote platform properties.
@@ -218,13 +230,17 @@
Action action =
buildAction(
commandHash, merkleTree.getRootDigest(), context.getTimeout(), spawnCacheableRemotely);
- ActionKey actionKey = digestUtil.computeActionKey(action);
+
+ spawnMetrics.setParseTime(totalTime.elapsed());
Preconditions.checkArgument(
Spawns.mayBeExecutedRemotely(spawn), "Spawn can't be executed remotely. This is a bug.");
+ NetworkTime networkTime = new NetworkTime();
// Look up action cache, and reuse the action output if it is found.
+ ActionKey actionKey = digestUtil.computeActionKey(action);
Context withMetadata =
- TracingMetadataUtils.contextWithMetadata(buildRequestId, commandId, actionKey);
+ TracingMetadataUtils.contextWithMetadata(buildRequestId, commandId, actionKey)
+ .withValue(NetworkTime.CONTEXT_KEY, networkTime);
Context previous = withMetadata.attach();
Profiler prof = Profiler.instance();
try {
@@ -246,7 +262,14 @@
} else {
try {
return downloadAndFinalizeSpawnResult(
- cachedResult, /* cacheHit= */ true, spawn, context, remoteOutputsMode);
+ cachedResult,
+ /* cacheHit= */ true,
+ spawn,
+ context,
+ remoteOutputsMode,
+ totalTime,
+ networkTime::getDuration,
+ spawnMetrics);
} catch (CacheNotFoundException e) {
// No cache hit, so we fall through to local or remote execution.
// We set acceptCachedResult to false in order to force the action re-execution.
@@ -284,7 +307,14 @@
Map<Digest, Message> additionalInputs = Maps.newHashMapWithExpectedSize(2);
additionalInputs.put(actionKey.getDigest(), action);
additionalInputs.put(commandHash, command);
+ Duration networkTimeStart = networkTime.getDuration();
+ Stopwatch uploadTime = Stopwatch.createStarted();
remoteCache.ensureInputsPresent(merkleTree, additionalInputs);
+ // subtract network time consumed here to ensure wall clock during upload is not
+ // double
+ // counted, and metrics time computation does not exceed total time
+ spawnMetrics.setUploadTime(
+ uploadTime.elapsed().minus(networkTime.getDuration().minus(networkTimeStart)));
}
ExecuteResponse reply;
try (SilentCloseable c = prof.profile(REMOTE_EXECUTION, "execute remotely")) {
@@ -300,13 +330,22 @@
outErr.printErr(message + "\n");
}
+ spawnMetricsAccounting(spawnMetrics, actionResult.getExecutionMetadata());
+
try (SilentCloseable c = prof.profile(REMOTE_DOWNLOAD, "download server logs")) {
maybeDownloadServerLogs(reply, actionKey);
}
try {
return downloadAndFinalizeSpawnResult(
- actionResult, reply.getCachedResult(), spawn, context, remoteOutputsMode);
+ actionResult,
+ reply.getCachedResult(),
+ spawn,
+ context,
+ remoteOutputsMode,
+ totalTime,
+ networkTime::getDuration,
+ spawnMetrics);
} catch (CacheNotFoundException e) {
// No cache hit, so if we retry this execution, we must no longer accept
// cached results, it must be reexecuted
@@ -323,12 +362,60 @@
}
}
+ /** conversion utility for protobuf Timestamp difference to java.time.Duration */
+ private static Duration between(Timestamp from, Timestamp to) {
+ return Duration.ofNanos(Durations.toNanos(Timestamps.between(from, to)));
+ }
+
+ @VisibleForTesting
+ static void spawnMetricsAccounting(
+ SpawnMetrics.Builder spawnMetrics, ExecutedActionMetadata executionMetadata) {
+ // Expect that a non-empty worker indicates that all fields are populated.
+ // If the bounded sides of these checkpoints are default timestamps, i.e. unset,
+ // the phase durations can be extremely large. Unset pairs, or a fully unset
+ // collection of timestamps, will result in zeroed durations, and no metrics
+ // contributions for a phase or phases.
+ if (!executionMetadata.getWorker().isEmpty()) {
+ // Accumulate queueTime from any previous attempts
+ Duration remoteQueueTime =
+ spawnMetrics
+ .build()
+ .remoteQueueTime()
+ .plus(
+ between(
+ executionMetadata.getQueuedTimestamp(),
+ executionMetadata.getWorkerStartTimestamp()));
+ spawnMetrics.setRemoteQueueTime(remoteQueueTime);
+ // setup time does not include failed attempts
+ Duration setupTime =
+ between(
+ executionMetadata.getWorkerStartTimestamp(),
+ executionMetadata.getExecutionStartTimestamp());
+ spawnMetrics.setSetupTime(setupTime);
+ // execution time is unspecified for failures
+ Duration executionWallTime =
+ between(
+ executionMetadata.getExecutionStartTimestamp(),
+ executionMetadata.getExecutionCompletedTimestamp());
+ spawnMetrics.setExecutionWallTime(executionWallTime);
+ // remoteProcessOutputs time is unspecified for failures
+ Duration remoteProcessOutputsTime =
+ between(
+ executionMetadata.getOutputUploadStartTimestamp(),
+ executionMetadata.getOutputUploadCompletedTimestamp());
+ spawnMetrics.setRemoteProcessOutputsTime(remoteProcessOutputsTime);
+ }
+ }
+
private SpawnResult downloadAndFinalizeSpawnResult(
ActionResult actionResult,
boolean cacheHit,
Spawn spawn,
SpawnExecutionContext context,
- RemoteOutputsMode remoteOutputsMode)
+ RemoteOutputsMode remoteOutputsMode,
+ Stopwatch totalTime,
+ Supplier<Duration> networkTime,
+ SpawnMetrics.Builder spawnMetrics)
throws ExecException, IOException, InterruptedException {
boolean downloadOutputs =
shouldDownloadAllSpawnOutputs(
@@ -336,6 +423,8 @@
/* exitCode = */ actionResult.getExitCode(),
hasFilesToDownload(spawn.getOutputFiles(), filesToDownload));
InMemoryOutput inMemoryOutput = null;
+ Duration networkTimeStart = networkTime.get();
+ Stopwatch fetchTime = Stopwatch.createStarted();
if (downloadOutputs) {
try (SilentCloseable c = Profiler.instance().profile(REMOTE_DOWNLOAD, "download outputs")) {
remoteCache.download(
@@ -356,7 +445,21 @@
context::lockOutputFiles);
}
}
- return createSpawnResult(actionResult.getExitCode(), cacheHit, getName(), inMemoryOutput);
+ fetchTime.stop();
+ totalTime.stop();
+ Duration networkTimeEnd = networkTime.get();
+ // subtract network time consumed here to ensure wall clock during fetch is not double
+ // counted, and metrics time computation does not exceed total time
+ return createSpawnResult(
+ actionResult.getExitCode(),
+ cacheHit,
+ getName(),
+ inMemoryOutput,
+ spawnMetrics
+ .setFetchTime(fetchTime.elapsed().minus(networkTimeEnd.minus(networkTimeStart)))
+ .setTotalTime(totalTime.elapsed())
+ .setNetworkTime(networkTimeEnd)
+ .build());
}
@Override
diff --git a/src/main/java/com/google/devtools/build/lib/remote/merkletree/MerkleTree.java b/src/main/java/com/google/devtools/build/lib/remote/merkletree/MerkleTree.java
index 7115601..7fac053 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/merkletree/MerkleTree.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/merkletree/MerkleTree.java
@@ -33,6 +33,7 @@
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
+import java.util.concurrent.atomic.AtomicLong;
import javax.annotation.Nullable;
/** A merkle tree representation as defined by the remote execution api. */
@@ -68,14 +69,20 @@
private final Map<Digest, Directory> digestDirectoryMap;
private final Map<Digest, PathOrBytes> digestFileMap;
private final Digest rootDigest;
+ private final long inputFiles;
+ private final long inputBytes;
private MerkleTree(
Map<Digest, Directory> digestDirectoryMap,
Map<Digest, PathOrBytes> digestFileMap,
- Digest rootDigest) {
+ Digest rootDigest,
+ long inputFiles,
+ long inputBytes) {
this.digestDirectoryMap = digestDirectoryMap;
this.digestFileMap = digestFileMap;
this.rootDigest = rootDigest;
+ this.inputFiles = inputFiles;
+ this.inputBytes = inputBytes;
}
/** Returns the digest of the merkle tree's root. */
@@ -83,6 +90,16 @@
return rootDigest;
}
+ /** Returns the number of files represented by this merkle tree */
+ public long getInputFiles() {
+ return inputFiles;
+ }
+
+ /** Returns the sum of file sizes plus protobuf sizes used to represent this merkle tree */
+ public long getInputBytes() {
+ return inputBytes;
+ }
+
@Nullable
public Directory getDirectoryByDigest(Digest digest) {
return digestDirectoryMap.get(digest);
@@ -143,31 +160,38 @@
private static MerkleTree build(DirectoryTree tree, DigestUtil digestUtil) {
Preconditions.checkNotNull(tree);
if (tree.isEmpty()) {
- return new MerkleTree(ImmutableMap.of(), ImmutableMap.of(), digestUtil.compute(new byte[0]));
+ return new MerkleTree(
+ ImmutableMap.of(), ImmutableMap.of(), digestUtil.compute(new byte[0]), 0, 0);
}
Map<Digest, Directory> digestDirectoryMap =
Maps.newHashMapWithExpectedSize(tree.numDirectories());
Map<Digest, PathOrBytes> digestPathMap = Maps.newHashMapWithExpectedSize(tree.numFiles());
Map<PathFragment, Digest> m = new HashMap<>();
+ AtomicLong inputBytes = new AtomicLong(0);
tree.visit(
(dirname, files, dirs) -> {
Directory.Builder b = Directory.newBuilder();
for (DirectoryTree.FileNode file : files) {
b.addFiles(buildProto(file));
digestPathMap.put(file.getDigest(), toPathOrBytes(file));
+ inputBytes.addAndGet(file.getDigest().getSizeBytes());
}
for (DirectoryTree.DirectoryNode dir : dirs) {
PathFragment subDirname = dirname.getRelative(dir.getPathSegment());
Digest protoDirDigest =
Preconditions.checkNotNull(m.remove(subDirname), "protoDirDigest was null");
b.addDirectories(buildProto(dir, protoDirDigest));
+ inputBytes.addAndGet(protoDirDigest.getSizeBytes());
}
Directory protoDir = b.build();
Digest protoDirDigest = digestUtil.compute(protoDir);
digestDirectoryMap.put(protoDirDigest, protoDir);
m.put(dirname, protoDirDigest);
});
- return new MerkleTree(digestDirectoryMap, digestPathMap, m.get(PathFragment.EMPTY_FRAGMENT));
+ Digest rootDigest = m.get(PathFragment.EMPTY_FRAGMENT);
+ inputBytes.addAndGet(rootDigest.getSizeBytes());
+ return new MerkleTree(
+ digestDirectoryMap, digestPathMap, rootDigest, tree.numFiles(), inputBytes.get());
}
private static FileNode buildProto(DirectoryTree.FileNode file) {
diff --git a/src/main/java/com/google/devtools/build/lib/remote/util/BUILD b/src/main/java/com/google/devtools/build/lib/remote/util/BUILD
index ceb78d0..2ab7faa 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/util/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/remote/util/BUILD
@@ -16,6 +16,7 @@
deps = [
"//src/main/java/com/google/devtools/build/lib:build-base",
"//src/main/java/com/google/devtools/build/lib/actions",
+ "//src/main/java/com/google/devtools/build/lib/concurrent",
"//src/main/java/com/google/devtools/build/lib/remote/common",
"//src/main/java/com/google/devtools/build/lib/remote/options",
"//src/main/java/com/google/devtools/build/lib/vfs",
@@ -24,6 +25,7 @@
"//third_party:jsr305",
"//third_party/grpc:grpc-jar",
"//third_party/protobuf:protobuf_java",
+ "@remoteapis//:build_bazel_remote_execution_v2_remote_execution_java_grpc",
"@remoteapis//:build_bazel_remote_execution_v2_remote_execution_java_proto",
],
)
diff --git a/src/main/java/com/google/devtools/build/lib/remote/util/NetworkTime.java b/src/main/java/com/google/devtools/build/lib/remote/util/NetworkTime.java
new file mode 100644
index 0000000..1f51559
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/remote/util/NetworkTime.java
@@ -0,0 +1,108 @@
+// Copyright 2019 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.util;
+
+import build.bazel.remote.execution.v2.ExecutionGrpc;
+import com.google.common.base.Stopwatch;
+import com.google.devtools.build.lib.concurrent.ThreadSafety;
+import io.grpc.CallOptions;
+import io.grpc.Channel;
+import io.grpc.ClientCall;
+import io.grpc.ClientInterceptor;
+import io.grpc.Context;
+import io.grpc.ForwardingClientCall;
+import io.grpc.ForwardingClientCallListener;
+import io.grpc.Metadata;
+import io.grpc.MethodDescriptor;
+import io.grpc.Status;
+import java.time.Duration;
+
+/** Reentrant wall clock stopwatch and grpc interceptor for network waits. */
+@ThreadSafety.ThreadSafe
+public class NetworkTime {
+ public static final Context.Key<NetworkTime> CONTEXT_KEY = Context.key("remote-network-time");
+
+ private final Stopwatch wallTime = Stopwatch.createUnstarted();
+ private int outstanding = 0;
+
+ private synchronized void start() {
+ if (!wallTime.isRunning()) {
+ wallTime.start();
+ }
+ outstanding++;
+ }
+
+ private synchronized void stop() {
+ if (--outstanding == 0) {
+ wallTime.stop();
+ }
+ }
+
+ public Duration getDuration() {
+ return wallTime.elapsed();
+ }
+
+ /** The ClientInterceptor used to track network time. */
+ public static class Interceptor implements ClientInterceptor {
+ @Override
+ public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
+ MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
+ ClientCall<ReqT, RespT> call = next.newCall(method, callOptions);
+ // prevent accounting for execution wait time
+ if (method != ExecutionGrpc.getExecuteMethod()
+ && method != ExecutionGrpc.getWaitExecutionMethod()) {
+ NetworkTime networkTime = CONTEXT_KEY.get();
+ if (networkTime != null) {
+ call = new NetworkTimeCall<>(call, networkTime);
+ }
+ }
+ return call;
+ }
+ }
+
+ private static class NetworkTimeCall<ReqT, RespT>
+ extends ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT> {
+ private final NetworkTime networkTime;
+ private boolean firstMessage = true;
+
+ protected NetworkTimeCall(ClientCall<ReqT, RespT> delegate, NetworkTime networkTime) {
+ super(delegate);
+ this.networkTime = networkTime;
+ }
+
+ @Override
+ public void start(Listener<RespT> responseListener, Metadata headers) {
+ super.start(
+ new ForwardingClientCallListener.SimpleForwardingClientCallListener<RespT>(
+ responseListener) {
+
+ @Override
+ public void onClose(Status status, Metadata trailers) {
+ networkTime.stop();
+ super.onClose(status, trailers);
+ }
+ },
+ headers);
+ }
+
+ @Override
+ public void sendMessage(ReqT message) {
+ if (firstMessage) {
+ networkTime.start();
+ firstMessage = false;
+ }
+ super.sendMessage(message);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/remote/util/Utils.java b/src/main/java/com/google/devtools/build/lib/remote/util/Utils.java
index 79f7c68..abf3170 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/util/Utils.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/util/Utils.java
@@ -23,6 +23,7 @@
import com.google.devtools.build.lib.actions.ActionInput;
import com.google.devtools.build.lib.actions.ExecutionRequirements;
import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.actions.SpawnMetrics;
import com.google.devtools.build.lib.actions.SpawnResult;
import com.google.devtools.build.lib.actions.SpawnResult.Status;
import com.google.devtools.build.lib.remote.common.CacheNotFoundException;
@@ -83,13 +84,19 @@
/** Constructs a {@link SpawnResult}. */
public static SpawnResult createSpawnResult(
- int exitCode, boolean cacheHit, String runnerName, @Nullable InMemoryOutput inMemoryOutput) {
+ int exitCode,
+ boolean cacheHit,
+ String runnerName,
+ @Nullable InMemoryOutput inMemoryOutput,
+ SpawnMetrics spawnMetrics) {
SpawnResult.Builder builder =
new SpawnResult.Builder()
.setStatus(exitCode == 0 ? Status.SUCCESS : Status.NON_ZERO_EXIT)
.setExitCode(exitCode)
.setRunnerName(cacheHit ? runnerName + " cache hit" : runnerName)
- .setCacheHit(cacheHit);
+ .setCacheHit(cacheHit)
+ .setSpawnMetrics(spawnMetrics)
+ .setRemote(true);
if (inMemoryOutput != null) {
builder.setInMemoryOutput(inMemoryOutput.getOutput(), inMemoryOutput.getContents());
}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/CriticalPathComponent.java b/src/main/java/com/google/devtools/build/lib/runtime/CriticalPathComponent.java
index 2fef7c9..d18fc25 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/CriticalPathComponent.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/CriticalPathComponent.java
@@ -80,6 +80,9 @@
/** Child with the maximum critical path. */
@Nullable private CriticalPathComponent child;
+ /** Indication that there is at least one remote spawn metrics received. */
+ private boolean remote = false;
+
public CriticalPathComponent(int id, Action action, long startNanos) {
this.id = id;
this.action = Preconditions.checkNotNull(action);
@@ -188,7 +191,13 @@
* the longestPhaseSpawnRunnerName to the longest running spawn runner name across all phases if
* it exists.
*/
- void addSpawnResult(SpawnMetrics metrics, @Nullable String runnerName) {
+ void addSpawnResult(SpawnMetrics metrics, @Nullable String runnerName, boolean wasRemote) {
+ // Mark this component as having remote components if _any_ spawn result contributing
+ // to it contains meaningful remote metrics. Subsequent non-remote spawns in an action
+ // must not reset this flag.
+ if (wasRemote) {
+ this.remote = true;
+ }
if (this.phaseChange) {
this.totalSpawnMetrics =
SpawnMetrics.aggregateMetrics(
@@ -320,8 +329,10 @@
currentTime = String.format("%.2f", getElapsedTimeNoCheck().toMillis() / 1000.0) + "s";
}
sb.append(currentTime);
- sb.append(", Remote ");
- sb.append(getSpawnMetrics().toString(getElapsedTimeNoCheck(), /* summary= */ false));
+ if (remote) {
+ sb.append(", Remote ");
+ sb.append(getSpawnMetrics().toString(getElapsedTimeNoCheck(), /* summary= */ false));
+ }
sb.append(" ");
sb.append(getActionString());
return sb.toString();
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/CriticalPathComputer.java b/src/main/java/com/google/devtools/build/lib/runtime/CriticalPathComputer.java
index 8259aa1..7e64fad 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/CriticalPathComputer.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/CriticalPathComputer.java
@@ -160,7 +160,8 @@
Preconditions.checkNotNull(outputArtifactToComponent.get(primaryOutput));
SpawnResult spawnResult = event.getSpawnResult();
- stats.addSpawnResult(spawnResult.getMetrics(), spawnResult.getRunnerName());
+ stats.addSpawnResult(
+ spawnResult.getMetrics(), spawnResult.getRunnerName(), spawnResult.wasRemote());
}
/** Returns the list of components using the most memory. */
@@ -202,7 +203,7 @@
public void discoverInputs(DiscoveredInputsEvent event) {
CriticalPathComponent stats =
tryAddComponent(createComponent(event.getAction(), event.getStartTimeNanos()));
- stats.addSpawnResult(event.getMetrics(), null);
+ stats.addSpawnResult(event.getMetrics(), null, /* wasRemote=*/ false);
stats.changePhase();
}
diff --git a/src/test/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategyTest.java b/src/test/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategyTest.java
index 8585639..c469fb8 100644
--- a/src/test/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategyTest.java
+++ b/src/test/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategyTest.java
@@ -32,6 +32,7 @@
import com.google.devtools.build.lib.actions.SpawnResult.Status;
import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
import com.google.devtools.build.lib.analysis.platform.PlatformInfo;
+import com.google.devtools.build.lib.events.StoredEventHandler;
import com.google.devtools.build.lib.exec.Protos.Digest;
import com.google.devtools.build.lib.exec.Protos.EnvironmentVariable;
import com.google.devtools.build.lib.exec.Protos.File;
@@ -85,6 +86,8 @@
MockitoAnnotations.initMocks(this);
scratch = new Scratch(fs);
rootDir = ArtifactRoot.asSourceRoot(Root.fromPath(scratch.dir("/execroot")));
+ StoredEventHandler eventHandler = new StoredEventHandler();
+ when(actionExecutionContext.getEventHandler()).thenReturn(eventHandler);
}
@Test
@@ -146,7 +149,6 @@
verify(spawnRunner, never()).execAsync(any(Spawn.class), any(SpawnExecutionContext.class));
}
- @SuppressWarnings("unchecked")
@Test
public void testCacheMiss() throws Exception {
SpawnCache cache = mock(SpawnCache.class);
@@ -172,7 +174,6 @@
verify(entry).store(eq(spawnResult));
}
- @SuppressWarnings("unchecked")
@Test
public void testCacheMissWithNonZeroExit() throws Exception {
SpawnCache cache = mock(SpawnCache.class);
diff --git a/src/test/java/com/google/devtools/build/lib/remote/BUILD b/src/test/java/com/google/devtools/build/lib/remote/BUILD
index 290fe63..f8fca23 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/remote/BUILD
@@ -82,6 +82,7 @@
"//third_party:truth",
"//third_party/grpc:grpc-jar",
"//third_party/protobuf:protobuf_java",
+ "//third_party/protobuf:protobuf_java_util",
"@googleapis//:google_bytestream_bytestream_java_grpc",
"@googleapis//:google_bytestream_bytestream_java_proto",
"@googleapis//:google_longrunning_operations_java_proto",
diff --git a/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerTest.java b/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerTest.java
index 4082565..0a90dea 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerTest.java
@@ -26,13 +26,13 @@
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import build.bazel.remote.execution.v2.ActionResult;
import build.bazel.remote.execution.v2.Digest;
import build.bazel.remote.execution.v2.ExecuteRequest;
import build.bazel.remote.execution.v2.ExecuteResponse;
+import build.bazel.remote.execution.v2.ExecutedActionMetadata;
import build.bazel.remote.execution.v2.LogFile;
import com.google.common.collect.ClassToInstanceMap;
import com.google.common.collect.ImmutableClassToInstanceMap;
@@ -54,6 +54,7 @@
import com.google.devtools.build.lib.actions.ResourceSet;
import com.google.devtools.build.lib.actions.SimpleSpawn;
import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.actions.SpawnMetrics;
import com.google.devtools.build.lib.actions.SpawnResult;
import com.google.devtools.build.lib.actions.SpawnResult.Status;
import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
@@ -86,10 +87,14 @@
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
import com.google.devtools.common.options.Options;
+import com.google.protobuf.Timestamp;
+import com.google.protobuf.util.Durations;
+import com.google.protobuf.util.Timestamps;
import com.google.rpc.Code;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
+import java.time.Duration;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@@ -192,7 +197,7 @@
verify(cache, never())
.downloadActionResult(any(ActionKey.class), /* inlineOutErr= */ eq(false));
verify(cache, never()).upload(any(), any(), any(), any(), any(), any());
- verifyZeroInteractions(localRunner);
+ verifyNoMoreInteractions(localRunner);
}
private FakeSpawnExecutionContext getSpawnContext(Spawn spawn) {
@@ -965,6 +970,51 @@
.downloadMinimal(eq(succeededAction), anyCollection(), any(), any(), any(), any(), any());
}
+ @Test
+ public void accountingDisabledWithoutWorker() {
+ SpawnMetrics.Builder spawnMetrics = Mockito.mock(SpawnMetrics.Builder.class);
+ RemoteSpawnRunner.spawnMetricsAccounting(
+ spawnMetrics, ExecutedActionMetadata.getDefaultInstance());
+ verifyNoMoreInteractions(spawnMetrics);
+ }
+
+ @Test
+ public void accountingAddsDurationsForStages() {
+ SpawnMetrics.Builder builder =
+ new SpawnMetrics.Builder()
+ .setRemoteQueueTime(Duration.ofSeconds(1))
+ .setSetupTime(Duration.ofSeconds(2))
+ .setExecutionWallTime(Duration.ofSeconds(2))
+ .setRemoteProcessOutputsTime(Duration.ofSeconds(2));
+ Timestamp queued = Timestamp.getDefaultInstance();
+ com.google.protobuf.Duration oneSecond = Durations.fromMillis(1000);
+ Timestamp workerStart = Timestamps.add(queued, oneSecond);
+ Timestamp executionStart = Timestamps.add(workerStart, oneSecond);
+ Timestamp executionCompleted = Timestamps.add(executionStart, oneSecond);
+ Timestamp outputUploadStart = Timestamps.add(executionCompleted, oneSecond);
+ Timestamp outputUploadComplete = Timestamps.add(outputUploadStart, oneSecond);
+ ExecutedActionMetadata executedMetadata =
+ ExecutedActionMetadata.newBuilder()
+ .setWorker("test worker")
+ .setQueuedTimestamp(queued)
+ .setWorkerStartTimestamp(workerStart)
+ .setExecutionStartTimestamp(executionStart)
+ .setExecutionCompletedTimestamp(executionCompleted)
+ .setOutputUploadStartTimestamp(outputUploadStart)
+ .setOutputUploadCompletedTimestamp(outputUploadComplete)
+ .build();
+ RemoteSpawnRunner.spawnMetricsAccounting(builder, executedMetadata);
+ SpawnMetrics spawnMetrics = builder.build();
+ // remote queue time is accumulated
+ assertThat(spawnMetrics.remoteQueueTime()).isEqualTo(Duration.ofSeconds(2));
+ // setup time is substituted
+ assertThat(spawnMetrics.setupTime()).isEqualTo(Duration.ofSeconds(1));
+ // execution time is unspecified, assume substituted
+ assertThat(spawnMetrics.executionWallTime()).isEqualTo(Duration.ofSeconds(1));
+ // remoteProcessOutputs time is unspecified, assume substituted
+ assertThat(spawnMetrics.remoteProcessOutputsTime()).isEqualTo(Duration.ofSeconds(1));
+ }
+
private static Spawn newSimpleSpawn(Artifact... outputs) {
return simpleSpawnWithExecutionInfo(ImmutableMap.of(), outputs);
}