| // Copyright 2018 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 com.google.common.base.Joiner; | |
| import com.google.common.base.Preconditions; | |
| import java.time.Duration; | |
| import java.util.ArrayList; | |
| import java.util.HashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| /** Timing, size, and memory statistics for a Spawn execution. */ | |
| public final class SpawnMetrics { | |
| /** Indicates whether the metrics correspond to the remote, local or worker execution. */ | |
| public static enum ExecKind { | |
| REMOTE("Remote"), | |
| LOCAL("Local"), | |
| WORKER("Worker"), | |
| /** | |
| * Other kinds of execution (or when it's not clear whether something happened locally or | |
| * remotely). | |
| */ | |
| OTHER("Other"); | |
| private final String name; | |
| private ExecKind(String name) { | |
| this.name = name; | |
| } | |
| @Override | |
| public String toString() { | |
| return name; | |
| } | |
| } | |
| /** Any non important stats < than 10% will not be shown in the summary. */ | |
| private static final double STATS_SHOW_THRESHOLD = 0.10; | |
| public static SpawnMetrics forLocalExecution(Duration wallTime) { | |
| return Builder.forLocalExec().setTotalTime(wallTime).setExecutionWallTime(wallTime).build(); | |
| } | |
| private final ExecKind execKind; | |
| private final Duration totalTime; | |
| private final Duration parseTime; | |
| private final Duration fetchTime; | |
| private final Duration queueTime; | |
| private final Duration uploadTime; | |
| private final Duration setupTime; | |
| private final Duration executionWallTime; | |
| private final Duration processOutputsTime; | |
| private final Duration networkTime; | |
| private final Map<Integer, Duration> retryTime; | |
| private final long inputBytes; | |
| private final long inputFiles; | |
| private final long memoryEstimateBytes; | |
| private SpawnMetrics(Builder builder) { | |
| this.execKind = builder.execKind; | |
| this.totalTime = builder.totalTime; | |
| this.parseTime = builder.parseTime; | |
| this.networkTime = builder.networkTime; | |
| this.fetchTime = builder.fetchTime; | |
| this.queueTime = builder.queueTime; | |
| this.setupTime = builder.setupTime; | |
| this.uploadTime = builder.uploadTime; | |
| this.executionWallTime = builder.executionWallTime; | |
| this.retryTime = builder.retryTime; | |
| this.processOutputsTime = builder.processOutputsTime; | |
| this.inputBytes = builder.inputBytes; | |
| this.inputFiles = builder.inputFiles; | |
| this.memoryEstimateBytes = builder.memoryEstimateBytes; | |
| } | |
| /** The kind of execution the metrics refer to (remote/local/worker). */ | |
| public ExecKind execKind() { | |
| return execKind; | |
| } | |
| /** Returns true if {@link #totalTime()} is zero. */ | |
| public boolean isEmpty() { | |
| return totalTime.isZero(); | |
| } | |
| /** | |
| * Total (measured locally) wall time spent running a spawn. This should be at least as large as | |
| * all the other times summed together. | |
| */ | |
| public Duration totalTime() { | |
| return totalTime; | |
| } | |
| /** | |
| * Total time spent getting on network. This includes time getting network-side errors and the | |
| * time of the round-trip, found by taking the difference of wall time here and the server time | |
| * reported by the RPC. This is 0 for locally executed spawns. | |
| */ | |
| public Duration networkTime() { | |
| return networkTime; | |
| } | |
| /** Total time waiting in queues. Includes queue time for any failed attempts. */ | |
| public Duration queueTime() { | |
| return queueTime; | |
| } | |
| /** The time spent transferring files to the backends. This is 0 for locally executed spawns. */ | |
| public Duration uploadTime() { | |
| return uploadTime; | |
| } | |
| /** | |
| * The time required to setup the environment in which the spawn is run. This may be 0 for locally | |
| * executed spawns, or may include time to setup a sandbox or other environment. Does not include | |
| * failed attempts. | |
| */ | |
| public Duration setupTime() { | |
| return setupTime; | |
| } | |
| /** Time spent running the subprocess. */ | |
| public Duration executionWallTime() { | |
| return executionWallTime; | |
| } | |
| /** | |
| * The time taken to convert the spawn into a network request, e.g., collecting runfiles, and | |
| * digests for all input files. | |
| */ | |
| public Duration parseTime() { | |
| return parseTime; | |
| } | |
| /** Total time spent fetching remote outputs. */ | |
| public Duration fetchTime() { | |
| return fetchTime; | |
| } | |
| /** Time spent in previous failed attempts. Does not include queue time. */ | |
| public Duration retryTime() { | |
| return retryTime.values().stream().reduce(Duration.ZERO, Duration::plus); | |
| } | |
| /** Time spent in previous failed attempts, keyed by error code. Does not include queue time. */ | |
| public Map<Integer, Duration> retryTimeByError() { | |
| return retryTime; | |
| } | |
| /** Time spend by the execution framework on processing outputs. */ | |
| public Duration processOutputsTime() { | |
| return processOutputsTime; | |
| } | |
| /** Any time that is not measured by a more specific component, out of {@code totalTime()}. */ | |
| public Duration otherTime() { | |
| return totalTime | |
| .minus(parseTime) | |
| .minus(networkTime) | |
| .minus(queueTime) | |
| .minus(uploadTime) | |
| .minus(setupTime) | |
| .minus(executionWallTime) | |
| .minus(fetchTime) | |
| .minus(retryTime()) | |
| .minus(processOutputsTime); | |
| } | |
| /** Total size in bytes of inputs or 0 if unavailable. */ | |
| public long inputBytes() { | |
| return inputBytes; | |
| } | |
| /** Total number of input files or 0 if unavailable. */ | |
| public long inputFiles() { | |
| return inputFiles; | |
| } | |
| /** Estimated memory usage or 0 if unavailable. */ | |
| public long memoryEstimate() { | |
| return memoryEstimateBytes; | |
| } | |
| /** | |
| * Generates a String representation of the stats. | |
| * | |
| * @param total total time used to compute the percentages | |
| * @param summary whether to exclude input file count and sizes, and memory estimates | |
| */ | |
| public String toString(Duration total, boolean summary) { | |
| StringBuilder sb = new StringBuilder(); | |
| sb.append("("); | |
| sb.append(prettyPercentage(totalTime, total)); | |
| sb.append(" of the time): ["); | |
| List<String> stats = new ArrayList<>(8); | |
| addStatToString(stats, "parse", !summary, parseTime, total); | |
| addStatToString(stats, "queue", true, queueTime, total); | |
| addStatToString(stats, "network", !summary, networkTime, total); | |
| addStatToString(stats, "upload", !summary, uploadTime, total); | |
| addStatToString(stats, "setup", true, setupTime, total); | |
| addStatToString(stats, "process", true, executionWallTime, total); | |
| addStatToString(stats, "fetch", !summary, fetchTime, total); | |
| addStatToString(stats, "retry", !summary, retryTime(), total); | |
| addStatToString(stats, "processOutputs", !summary, processOutputsTime, total); | |
| addStatToString(stats, "other", !summary, otherTime(), total); | |
| if (!summary) { | |
| stats.add("input files: " + inputFiles); | |
| stats.add("input bytes: " + inputBytes); | |
| stats.add("memory bytes: " + memoryEstimateBytes); | |
| } | |
| Joiner.on(", ").appendTo(sb, stats); | |
| sb.append("]"); | |
| return sb.toString(); | |
| } | |
| /** | |
| * Add to {@code strings} the string representation of {@code name} component. If {@code | |
| * forceShow} is set to false it will only show if it is above certain threshold. | |
| */ | |
| private static void addStatToString( | |
| List<String> strings, String name, boolean forceShow, Duration time, Duration totalTime) { | |
| if (forceShow || isAboveThreshold(time, totalTime)) { | |
| strings.add(name + ": " + prettyPercentage(time, totalTime)); | |
| } | |
| } | |
| private static boolean isAboveThreshold(Duration time, Duration totalTime) { | |
| return totalTime.toMillis() > 0 | |
| && (((float) time.toMillis() / totalTime.toMillis()) >= STATS_SHOW_THRESHOLD); | |
| } | |
| /** | |
| * Converts relative duration to the percentage string. | |
| * | |
| * @return formatted percentage string or "N/A" if result is undefined | |
| */ | |
| private static String prettyPercentage(Duration duration, Duration total) { | |
| // Duration.toMillis() != 0 does not imply !Duration.isZero() (due to truncation). | |
| if (total.toMillis() == 0) { | |
| // Return "not available" string if total is 0 and result is undefined. | |
| return "N/A"; | |
| } | |
| return String.format("%.2f%%", duration.toMillis() * 100.0 / total.toMillis()); | |
| } | |
| /** Builder class for SpawnMetrics. */ | |
| public static class Builder { | |
| private ExecKind execKind = null; | |
| private Duration totalTime = Duration.ZERO; | |
| private Duration parseTime = Duration.ZERO; | |
| private Duration networkTime = Duration.ZERO; | |
| private Duration fetchTime = Duration.ZERO; | |
| private Duration queueTime = Duration.ZERO; | |
| private Duration setupTime = Duration.ZERO; | |
| private Duration uploadTime = Duration.ZERO; | |
| private Duration executionWallTime = Duration.ZERO; | |
| private Duration processOutputsTime = Duration.ZERO; | |
| private Map<Integer, Duration> retryTime = new HashMap<>(); | |
| private long inputBytes = 0; | |
| private long inputFiles = 0; | |
| private long memoryEstimateBytes = 0; | |
| public static Builder forLocalExec() { | |
| return forExec(ExecKind.LOCAL); | |
| } | |
| public static Builder forRemoteExec() { | |
| return forExec(ExecKind.REMOTE); | |
| } | |
| public static Builder forWorkerExec() { | |
| return forExec(ExecKind.WORKER); | |
| } | |
| public static Builder forOtherExec() { | |
| return forExec(ExecKind.OTHER); | |
| } | |
| public static Builder forExec(ExecKind kind) { | |
| return new Builder().setExecKind(kind); | |
| } | |
| // Make the constructor private to force users to set the ExecKind by using one of the factory | |
| // methods. | |
| private Builder() {} | |
| public SpawnMetrics build() { | |
| Preconditions.checkNotNull(execKind, "ExecKind must be explicitly set using `setExecKind`"); | |
| // TODO(ulfjack): Add consistency checks here? | |
| return new SpawnMetrics(this); | |
| } | |
| public Builder setExecKind(ExecKind execKind) { | |
| this.execKind = execKind; | |
| return this; | |
| } | |
| public Builder setTotalTime(Duration totalTime) { | |
| this.totalTime = totalTime; | |
| return this; | |
| } | |
| public Builder setParseTime(Duration parseTime) { | |
| this.parseTime = parseTime; | |
| return this; | |
| } | |
| public Builder setNetworkTime(Duration networkTime) { | |
| this.networkTime = networkTime; | |
| return this; | |
| } | |
| public Builder setFetchTime(Duration fetchTime) { | |
| this.fetchTime = fetchTime; | |
| return this; | |
| } | |
| public Builder setQueueTime(Duration queueTime) { | |
| this.queueTime = queueTime; | |
| return this; | |
| } | |
| public Builder setSetupTime(Duration setupTime) { | |
| this.setupTime = setupTime; | |
| return this; | |
| } | |
| public Builder setUploadTime(Duration uploadTime) { | |
| this.uploadTime = uploadTime; | |
| return this; | |
| } | |
| public Builder setExecutionWallTime(Duration executionWallTime) { | |
| this.executionWallTime = executionWallTime; | |
| return this; | |
| } | |
| public Builder addRetryTime(int errorCode, Duration retryTime) { | |
| Duration d = this.retryTime.getOrDefault(errorCode, Duration.ZERO); | |
| this.retryTime.put(errorCode, d.plus(retryTime)); | |
| return this; | |
| } | |
| public Builder setRetryTime(Map<Integer, Duration> retryTime) { | |
| this.retryTime = new HashMap<>(retryTime); | |
| return this; | |
| } | |
| public Builder setProcessOutputsTime(Duration processOutputsTime) { | |
| this.processOutputsTime = processOutputsTime; | |
| return this; | |
| } | |
| public Builder setInputBytes(long inputBytes) { | |
| this.inputBytes = inputBytes; | |
| return this; | |
| } | |
| public Builder setInputFiles(long inputFiles) { | |
| this.inputFiles = inputFiles; | |
| return this; | |
| } | |
| public Builder setMemoryEstimateBytes(long memoryEstimateBytes) { | |
| this.memoryEstimateBytes = memoryEstimateBytes; | |
| return this; | |
| } | |
| public Builder addDurations(SpawnMetrics metric) { | |
| totalTime = totalTime.plus(metric.totalTime()); | |
| parseTime = parseTime.plus(metric.parseTime()); | |
| networkTime = networkTime.plus(metric.networkTime()); | |
| fetchTime = fetchTime.plus(metric.fetchTime()); | |
| queueTime = queueTime.plus(metric.queueTime()); | |
| uploadTime = uploadTime.plus(metric.uploadTime()); | |
| setupTime = setupTime.plus(metric.setupTime()); | |
| executionWallTime = executionWallTime.plus(metric.executionWallTime()); | |
| for (Map.Entry<Integer, Duration> entry : metric.retryTime.entrySet()) { | |
| addRetryTime(entry.getKey().intValue(), entry.getValue()); | |
| } | |
| processOutputsTime = processOutputsTime.plus(metric.processOutputsTime()); | |
| return this; | |
| } | |
| public Builder addNonDurations(SpawnMetrics metric) { | |
| inputFiles += metric.inputFiles(); | |
| inputBytes += metric.inputBytes(); | |
| memoryEstimateBytes += metric.memoryEstimate(); | |
| return this; | |
| } | |
| public Builder maxNonDurations(SpawnMetrics metric) { | |
| inputFiles = Long.max(inputFiles, metric.inputFiles()); | |
| inputBytes = Long.max(inputBytes, metric.inputBytes()); | |
| memoryEstimateBytes = Long.max(memoryEstimateBytes, metric.memoryEstimate()); | |
| return this; | |
| } | |
| } | |
| } |