blob: 8bb7f9a13c117269a61edd9a7769aff14f01baf8 [file] [log] [blame]
// Copyright 2016 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.runtime;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Comparators;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multiset;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.actions.Action;
import com.google.devtools.build.lib.actions.ActionCompletionEvent;
import com.google.devtools.build.lib.actions.ActionExecutionMetadata;
import com.google.devtools.build.lib.actions.ActionScanningCompletedEvent;
import com.google.devtools.build.lib.actions.ActionStartedEvent;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.RunningActionEvent;
import com.google.devtools.build.lib.actions.ScanningActionEvent;
import com.google.devtools.build.lib.actions.SchedulingActionEvent;
import com.google.devtools.build.lib.actions.StoppedScanningActionEvent;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.buildeventstream.AnnounceBuildEventTransportsEvent;
import com.google.devtools.build.lib.buildeventstream.BuildEventTransport;
import com.google.devtools.build.lib.buildeventstream.BuildEventTransportClosedEvent;
import com.google.devtools.build.lib.buildtool.ExecutionProgressReceiver;
import com.google.devtools.build.lib.buildtool.buildevent.BuildCompleteEvent;
import com.google.devtools.build.lib.buildtool.buildevent.BuildStartingEvent;
import com.google.devtools.build.lib.buildtool.buildevent.ExecutionProgressReceiverAvailableEvent;
import com.google.devtools.build.lib.buildtool.buildevent.TestFilteringCompleteEvent;
import com.google.devtools.build.lib.clock.Clock;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.events.ExtendedEventHandler.FetchProgress;
import com.google.devtools.build.lib.pkgcache.LoadingPhaseCompleteEvent;
import com.google.devtools.build.lib.skyframe.ConfigurationPhaseStartedEvent;
import com.google.devtools.build.lib.skyframe.ConfiguredTargetProgressReceiver;
import com.google.devtools.build.lib.skyframe.LoadingPhaseStartedEvent;
import com.google.devtools.build.lib.skyframe.PackageProgressReceiver;
import com.google.devtools.build.lib.util.Pair;
import com.google.devtools.build.lib.util.io.AnsiTerminalWriter;
import com.google.devtools.build.lib.util.io.PositionAwareAnsiTerminalWriter;
import com.google.devtools.build.lib.view.test.TestStatus.BlazeTestStatus;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Comparator;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
/** An experimental state tracker for the new experimental UI. */
class UiStateTracker {
enum ProgressMode {
OLDEST_ACTIONS,
MNEMONIC_HISTOGRAM
}
static final long SHOW_TIME_THRESHOLD_SECONDS = 3;
static final String ELLIPSIS = "...";
static final String FETCH_PREFIX = " Fetching ";
static final String AND_MORE = " ...";
static final String NO_STATUS = "-----";
static final int STATUS_LENGTH = 5;
static final int NANOS_PER_SECOND = 1000000000;
static final String URL_PROTOCOL_SEP = "://";
private ProgressMode progressMode = ProgressMode.OLDEST_ACTIONS;
private int sampleSize = 3;
private String status;
private String additionalMessage;
private final Clock clock;
// Desired maximal width of the progress bar, if positive.
// Non-positive values indicate not to aim for a particular width.
private final int targetWidth;
/**
* Tracker of strategy names to unique IDs and viceversa.
*
* <p>The IDs generated by this class can be used to index bitmaps.
*
* <p>TODO(jmmv): Would be nice if the strategies themselves contained their IDs (initialized at
* construction time) and we passed around those IDs instead of strings.
*/
@ThreadSafe
@VisibleForTesting
static class StrategyIds {
/** Fallback name in case we exhaust our space for IDs. */
public static final String FALLBACK_NAME = "unknown";
/** Counter of unique strategies seen so far. */
private AtomicInteger counter = new AtomicInteger(0);
/** Mapping of strategy names to their unique IDs. */
private Map<String, Integer> strategyIds = new ConcurrentHashMap<>();
/** Mapping of strategy unique IDs to their names. */
private Map<Integer, String> strategyNames = new ConcurrentHashMap<>();
public final Integer fallbackId;
/** Constructs a new collection of strategy IDs. */
public StrategyIds() {
fallbackId = getId(FALLBACK_NAME);
}
/** Computes the ID of a strategy given its name. */
public Integer getId(String strategy) {
Integer id =
strategyIds.computeIfAbsent(
strategy,
(key) -> {
int value = counter.getAndIncrement();
if (value >= Integer.SIZE) {
return fallbackId;
} else {
return 1 << value;
}
});
strategyNames.putIfAbsent(id, strategy);
return id;
}
/** Flattens a bitmap of strategy IDs into a human-friendly string. */
public String formatNames(int bitmap) {
StringBuilder builder = new StringBuilder();
int mask = 0x1;
while (bitmap != 0) {
int id = bitmap & mask;
if (id != 0) {
String name = checkNotNull(strategyNames.get(id), "Unknown strategy with id " + id);
builder.append(name);
bitmap &= ~mask;
if (bitmap != 0) {
builder.append(", ");
}
}
mask <<= 1;
}
return builder.toString();
}
}
private static final StrategyIds strategyIds = new StrategyIds();
/**
* Tracks all details for an action that we have heard about.
*
* <p>We cannot make assumptions on the order in which action state events come in, so this class
* takes care of always "advancing" the state of an action.
*/
private static class ActionState {
/**
* The action this state belongs to.
*
* <p>We assume that all events related to the same action refer to the same {@link
* ActionExecutionMetadata} object.
*/
final ActionExecutionMetadata action;
/** Timestamp of the last state change. */
long nanoStartTime;
/**
* Whether this action is in the scanning state or not.
*
* <p>If true, implies that {@link #schedulingStrategiesBitmap} and {@link
* #runningStrategiesBitmap} are both zero. The opposite is not necessarily true: if false, the
* bitmaps can be zero as well to represent that the action is still in the preparation stage.
*/
boolean scanning;
/**
* Bitmap of strategies that are scheduling this action.
*
* <p>If non-zero, implies that {@link #scanning} is false.
*/
int schedulingStrategiesBitmap = 0;
/**
* Bitmap of strategies that are running this action.
*
* <p>If non-zero, implies that {@link #scanning} is false.
*/
int runningStrategiesBitmap = 0;
/** Starts tracking the state of an action. */
ActionState(ActionExecutionMetadata action, long nanoStartTime) {
this.action = action;
this.nanoStartTime = nanoStartTime;
}
/** Creates a deep copy of this action state. */
synchronized ActionState deepCopy() {
ActionState other = new ActionState(action, nanoStartTime);
other.scanning = scanning;
other.schedulingStrategiesBitmap = schedulingStrategiesBitmap;
other.runningStrategiesBitmap = runningStrategiesBitmap;
return other;
}
/** Computes the weight of this action for the global active actions counter. */
synchronized int countActions() {
int activeStrategies =
Integer.bitCount(schedulingStrategiesBitmap) + Integer.bitCount(runningStrategiesBitmap);
return activeStrategies > 0 ? activeStrategies : 1;
}
/**
* Marks the action as scanning.
*
* <p>Because we may receive events out of order, this does nothing if the action is already
* scheduled or running.
*/
synchronized void setScanning(long nanoChangeTime) {
if (schedulingStrategiesBitmap == 0 && runningStrategiesBitmap == 0) {
scanning = true;
nanoStartTime = nanoChangeTime;
}
}
/**
* Marks the action as no longer scanning.
*
* <p>Because we may receive events out of order, this does nothing if the action is already
* scheduled or running.
*/
synchronized void setStopScanning(long nanoChangeTime) {
if (schedulingStrategiesBitmap == 0 && runningStrategiesBitmap == 0) {
scanning = false;
nanoStartTime = nanoChangeTime;
}
}
/**
* Marks the action as scheduling with the given strategy.
*
* <p>Because we may receive events out of order, this does nothing if the action is already
* running with this strategy.
*/
synchronized void setScheduling(String strategy, long nanoChangeTime) {
int id = strategyIds.getId(strategy);
if ((runningStrategiesBitmap & id) == 0) {
scanning = false;
schedulingStrategiesBitmap |= id;
nanoStartTime = nanoChangeTime;
}
}
/**
* Marks the action as running with the given strategy.
*
* <p>Because "running" is a terminal state, this forcibly updates the state to running
* regardless of any other events (which may come out of order).
*/
synchronized void setRunning(String strategy, long nanoChangeTime) {
scanning = false;
int id = strategyIds.getId(strategy);
schedulingStrategiesBitmap &= ~id;
runningStrategiesBitmap |= id;
nanoStartTime = nanoChangeTime;
}
/** Generates a human-readable description of this action's state. */
synchronized String describe() {
if (runningStrategiesBitmap != 0) {
return "Running";
} else if (schedulingStrategiesBitmap != 0) {
return "Scheduling";
} else if (scanning) {
return "Scanning";
} else {
return "Preparing";
}
}
}
private final Map<Artifact, ActionState> activeActions;
// running downloads are identified by the original URL they were trying to access.
private final Deque<String> runningDownloads;
private final Map<String, Long> downloadNanoStartTimes;
private final Map<String, FetchProgress> downloads;
/**
* For each test, the list of actions (as identified by the primary output artifact) currently
* running for that test (identified by its label), in the order they got started. A key is
* present in the map if and only if that was discovered as a test.
*/
private final Map<Label, Set<Artifact>> testActions;
private final AtomicInteger actionsCompleted;
private int totalTests;
private int completedTests;
private TestSummary mostRecentTest;
private int failedTests;
private boolean ok;
private boolean buildComplete;
// These are only null between the completion of analysis and the beginning of execution.
@Nullable private String defaultStatus = "Loading";
@Nullable private String defaultActivity = "loading...";
private ExecutionProgressReceiver executionProgressReceiver;
private PackageProgressReceiver packageProgressReceiver;
private ConfiguredTargetProgressReceiver configuredTargetProgressReceiver;
// Set of build event protocol transports that need yet to be closed.
private Set<BuildEventTransport> bepOpenTransports = new HashSet<>();
// The point in time when closing of BEP transports was started.
private long bepTransportClosingStartTimeMillis;
UiStateTracker(Clock clock, int targetWidth) {
this.activeActions = new ConcurrentHashMap<>();
this.actionsCompleted = new AtomicInteger();
this.testActions = new ConcurrentHashMap<>();
this.runningDownloads = new ArrayDeque<>();
this.downloads = new TreeMap<>();
this.downloadNanoStartTimes = new TreeMap<>();
this.ok = true;
this.clock = clock;
this.targetWidth = targetWidth;
}
UiStateTracker(Clock clock) {
this(clock, 0);
}
/** Set the progress bar mode and sample size. */
void setProgressMode(ProgressMode progressMode, int sampleSize) {
this.progressMode = progressMode;
this.sampleSize = Math.max(1, sampleSize);
}
void buildStarted(BuildStartingEvent event) {
status = "Loading";
additionalMessage = "";
}
void loadingStarted(LoadingPhaseStartedEvent event) {
status = null;
packageProgressReceiver = event.getPackageProgressReceiver();
}
void configurationStarted(ConfigurationPhaseStartedEvent event) {
configuredTargetProgressReceiver = event.getConfiguredTargetProgressReceiver();
}
void loadingComplete(LoadingPhaseCompleteEvent event) {
int count = event.getLabels().size();
status = "Analyzing";
if (count == 1) {
additionalMessage = "target " + Iterables.getOnlyElement(event.getLabels());
} else {
additionalMessage = "" + count + " targets";
}
}
/**
* Make the state tracker aware of the fact that the analysis has finished. Return a summary of
* the work done in the analysis phase.
*/
synchronized String analysisComplete() {
String workDone = "Analyzed " + additionalMessage;
if (packageProgressReceiver != null) {
Pair<String, String> progress = packageProgressReceiver.progressState();
workDone += " (" + progress.getFirst();
if (configuredTargetProgressReceiver != null) {
workDone += ", " + configuredTargetProgressReceiver.getProgressString();
}
workDone += ")";
}
workDone += ".";
status = null;
packageProgressReceiver = null;
configuredTargetProgressReceiver = null;
defaultStatus = null;
defaultActivity = null;
return workDone;
}
synchronized void progressReceiverAvailable(ExecutionProgressReceiverAvailableEvent event) {
executionProgressReceiver = event.getExecutionProgressReceiver();
defaultStatus = "Building";
defaultActivity = "checking cached actions";
}
void buildComplete(BuildCompleteEvent event, String additionalInfo) {
buildComplete = true;
// Build event protocol transports are closed right after the build complete event.
bepTransportClosingStartTimeMillis = clock.currentTimeMillis();
if (event.getResult().getSuccess()) {
status = "INFO";
int actionsCompleted = this.actionsCompleted.get();
if (failedTests == 0) {
additionalMessage =
additionalInfo
+ "Build completed successfully, "
+ actionsCompleted
+ " total action"
+ (actionsCompleted == 1 ? "" : "s");
} else {
additionalMessage =
additionalInfo
+ "Build completed, "
+ failedTests
+ " test"
+ (failedTests == 1 ? "" : "s")
+ " FAILED, "
+ actionsCompleted
+ " total action"
+ (actionsCompleted == 1 ? "" : "s");
}
} else {
ok = false;
status = "FAILED";
additionalMessage = additionalInfo + "Build did NOT complete successfully";
}
}
void buildComplete(BuildCompleteEvent event) {
buildComplete(event, "");
}
synchronized void downloadProgress(FetchProgress event) {
String url = event.getResourceIdentifier();
if (event.isFinished()) {
// a download is finished, clean it up
runningDownloads.remove(url);
downloadNanoStartTimes.remove(url);
downloads.remove(url);
} else if (runningDownloads.contains(url)) {
// a new progress update on an already known, still running download
downloads.put(url, event);
} else {
// Start of a new download
long nanoTime = clock.nanoTime();
runningDownloads.add(url);
downloads.put(url, event);
downloadNanoStartTimes.put(url, nanoTime);
}
}
private ActionState getActionState(
ActionExecutionMetadata action, Artifact actionId, long nanoTimeNow) {
return activeActions.computeIfAbsent(actionId, (key) -> new ActionState(action, nanoTimeNow));
}
void actionStarted(ActionStartedEvent event) {
Action action = event.getAction();
Artifact actionId = action.getPrimaryOutput();
getActionState(action, actionId, event.getNanoTimeStart());
if (action.getOwner() != null) {
Label owner = action.getOwner().getLabel();
if (owner != null) {
Set<Artifact> testActionsForOwner = testActions.get(owner);
if (testActionsForOwner != null) {
testActionsForOwner.add(actionId);
}
}
}
}
void scanningAction(ScanningActionEvent event) {
ActionExecutionMetadata action = event.getActionMetadata();
Artifact actionId = event.getActionMetadata().getPrimaryOutput();
long now = clock.nanoTime();
getActionState(action, actionId, now).setScanning(now);
}
void stopScanningAction(StoppedScanningActionEvent event) {
Action action = event.getAction();
Artifact actionId = action.getPrimaryOutput();
long now = clock.nanoTime();
getActionState(action, actionId, now).setStopScanning(now);
}
void schedulingAction(SchedulingActionEvent event) {
ActionExecutionMetadata action = event.getActionMetadata();
Artifact actionId = event.getActionMetadata().getPrimaryOutput();
long now = clock.nanoTime();
getActionState(action, actionId, now).setScheduling(event.getStrategy(), now);
}
void runningAction(RunningActionEvent event) {
ActionExecutionMetadata action = event.getActionMetadata();
Artifact actionId = event.getActionMetadata().getPrimaryOutput();
long now = clock.nanoTime();
getActionState(action, actionId, now).setRunning(event.getStrategy(), now);
}
void actionCompletion(ActionScanningCompletedEvent event) {
Action action = event.getAction();
Artifact actionId = action.getPrimaryOutput();
checkState(activeActions.containsKey(actionId));
activeActions.remove(actionId);
// As callers to the experimental state tracker assume we will fully report the new state once
// informed of an action completion, we need to make sure the progress receiver is aware of the
// completion, even though it might be called later on the event bus.
if (executionProgressReceiver != null) {
executionProgressReceiver.actionCompleted(event.getActionLookupData());
}
}
void actionCompletion(ActionCompletionEvent event) {
actionsCompleted.incrementAndGet();
Action action = event.getAction();
Artifact actionId = action.getPrimaryOutput();
checkState(activeActions.containsKey(actionId));
activeActions.remove(actionId);
if (action.getOwner() != null) {
Label owner = action.getOwner().getLabel();
if (owner != null) {
Set<Artifact> testActionsForOwner = testActions.get(owner);
if (testActionsForOwner != null) {
testActionsForOwner.remove(actionId);
}
}
}
// As callers to the experimental state tracker assume we will fully report the new state once
// informed of an action completion, we need to make sure the progress receiver is aware of the
// completion, even though it might be called later on the event bus.
if (executionProgressReceiver != null) {
executionProgressReceiver.actionCompleted(event.getActionLookupData());
}
}
/** From a string, take a suffix of at most the given length. */
static String suffix(String s, int len) {
if (len <= 0) {
return "";
}
int startPos = s.length() - len;
if (startPos <= 0) {
return s;
}
return s.substring(startPos);
}
/**
* If possible come up with a human-readable description of the label that fits within the given
* width; a non-positive width indicates not no restriction at all.
*/
private String shortenedLabelString(Label label, int width) {
if (width <= 0) {
return label.toString();
}
String name = label.toString();
if (name.length() <= width) {
return name;
}
name = suffix(name, width - ELLIPSIS.length());
int slashPos = name.indexOf('/');
if (slashPos >= 0) {
return ELLIPSIS + name.substring(slashPos);
}
int colonPos = name.indexOf(':');
if (slashPos >= 0) {
return ELLIPSIS + name.substring(colonPos);
}
// no reasonable place found to shorten; as last resort, just truncate
if (3 * ELLIPSIS.length() <= width) {
return ELLIPSIS + suffix(label.toString(), width - ELLIPSIS.length());
}
return label.toString();
}
private String shortenedString(String s, int maxWidth) {
if (maxWidth <= 3 * ELLIPSIS.length() || s.length() <= maxWidth) {
return s;
}
return s.substring(0, maxWidth - ELLIPSIS.length()) + ELLIPSIS;
}
// Describe a group of actions running for the same test.
private String describeTestGroup(
Label owner, long nanoTime, int desiredWidth, Set<Artifact> allActions) {
String prefix = "Testing ";
String labelSep = " [";
String postfix = " (" + allActions.size() + " actions)]";
// Leave enough room for at least 3 samples of run times, each 4 characters
// (a digit, 's', comma, and space).
int labelWidth = desiredWidth - prefix.length() - labelSep.length() - postfix.length() - 12;
StringBuffer message =
new StringBuffer(prefix).append(shortenedLabelString(owner, labelWidth)).append(labelSep);
// Compute the remaining width for the sample times, but if the desired width is too small
// anyway, then show at least one sample.
int remainingWidth = desiredWidth - message.length() - postfix.length();
if (remainingWidth < 0) {
remainingWidth = "[1s], ".length() + 1;
}
String sep = "";
boolean allReported = true;
for (Artifact actionId : allActions) {
ActionState actionState = activeActions.get(actionId);
if (actionState == null) {
// This action must have completed while we were constructing this output. Skip it.
continue;
}
long nanoRuntime = nanoTime - actionState.nanoStartTime;
long runtimeSeconds = nanoRuntime / NANOS_PER_SECOND;
String text =
actionState.runningStrategiesBitmap == 0
? sep + "[" + runtimeSeconds + "s]"
: sep + runtimeSeconds + "s";
if (remainingWidth < text.length()) {
allReported = false;
break;
}
message.append(text);
remainingWidth -= text.length();
sep = ", ";
}
return message.append(allReported ? "]" : postfix).toString();
}
// Describe an action by a string of the desired length; if describing that action includes
// describing other actions, add those to the to set of actions to skip in further samples of
// actions.
private String describeAction(
ActionState actionState, long nanoTime, int desiredWidth, Set<Artifact> toSkip) {
ActionExecutionMetadata action = actionState.action;
if (action.getOwner() != null) {
Label owner = action.getOwner().getLabel();
if (owner != null) {
Set<Artifact> allRelatedActions = testActions.get(owner);
if (allRelatedActions != null && allRelatedActions.size() > 1) {
if (toSkip != null) {
toSkip.addAll(allRelatedActions);
}
return describeTestGroup(owner, nanoTime, desiredWidth, allRelatedActions);
}
}
}
String postfix = "";
String prefix = "";
long nanoRuntime = nanoTime - actionState.nanoStartTime;
long runtimeSeconds = nanoRuntime / NANOS_PER_SECOND;
String strategy = null;
if (actionState.runningStrategiesBitmap != 0) {
strategy = strategyIds.formatNames(actionState.runningStrategiesBitmap);
} else {
String status = actionState.describe();
if (status == null) {
status = NO_STATUS;
}
if (status.length() > STATUS_LENGTH) {
status = status.substring(0, STATUS_LENGTH);
}
prefix = prefix + "[" + status + "] ";
}
// To keep the UI appearance more stable, always show the elapsed
// time if we also show a strategy (otherwise the strategy will jump in
// the progress bar).
if (strategy != null || runtimeSeconds > SHOW_TIME_THRESHOLD_SECONDS) {
postfix = "; " + runtimeSeconds + "s";
}
if (strategy != null) {
postfix += " " + strategy;
}
String message = action.getProgressMessage();
if (message == null) {
message = action.prettyPrint();
}
if (desiredWidth <= 0) {
return prefix + message + postfix;
}
if (prefix.length() + message.length() + postfix.length() <= desiredWidth) {
return prefix + message + postfix;
}
// We have to shorten the message to fit into the line.
if (action.getOwner() != null) {
if (action.getOwner().getLabel() != null) {
// First attempt is to shorten the package path string in the messge, if it occurs there
String pathString = action.getOwner().getLabel().getPackageFragment().toString();
int pathIndex = message.indexOf(pathString);
if (pathIndex >= 0) {
String start = message.substring(0, pathIndex);
String end = message.substring(pathIndex + pathString.length());
int pathTargetLength =
desiredWidth - start.length() - end.length() - postfix.length() - prefix.length();
// This attempt of shortening is reasonable if what is left from the label
// is significantly longer (twice as long) as the ellipsis symbols introduced.
if (pathTargetLength >= 3 * ELLIPSIS.length()) {
String shortPath = suffix(pathString, pathTargetLength - ELLIPSIS.length());
int slashPos = shortPath.indexOf('/');
if (slashPos >= 0) {
return prefix + start + ELLIPSIS + shortPath.substring(slashPos) + end + postfix;
}
}
}
// Second attempt: just take a shortened version of the label.
String shortLabel =
shortenedLabelString(
action.getOwner().getLabel(), desiredWidth - postfix.length() - prefix.length());
if (prefix.length() + shortLabel.length() + postfix.length() <= desiredWidth) {
return prefix + shortLabel + postfix;
}
}
}
if (3 * ELLIPSIS.length() + postfix.length() + prefix.length() <= desiredWidth) {
message =
ELLIPSIS
+ suffix(
message, desiredWidth - ELLIPSIS.length() - postfix.length() - prefix.length());
}
return prefix + message + postfix;
}
private ActionState getOldestAction() {
long minStart = Long.MAX_VALUE;
ActionState result = null;
for (ActionState action : activeActions.values()) {
if (action.nanoStartTime < minStart) {
minStart = action.nanoStartTime;
result = action;
}
}
return result;
}
private String countActions() {
// TODO(djasper): Iterating over the actions here is slow, but it's only done once per refresh
// and thus might be faster than trying to update these values in the critical path.
// Re-investigate if this ever turns up in a profile.
int actionsCount = 0;
int executingActionsCount = 0;
for (ActionState actionState : activeActions.values()) {
actionsCount += actionState.countActions();
executingActionsCount += Integer.bitCount(actionState.runningStrategiesBitmap);
}
if (actionsCount == 1) {
return " 1 action running";
} else if (actionsCount == executingActionsCount) {
return "" + actionsCount + " actions running";
} else {
return "" + actionsCount + " actions, " + executingActionsCount + " running";
}
}
private void printActionState(AnsiTerminalWriter terminalWriter) throws IOException {
switch (progressMode) {
case OLDEST_ACTIONS:
sampleOldestActions(terminalWriter);
break;
case MNEMONIC_HISTOGRAM:
showMnemonicHistogram(terminalWriter);
break;
}
}
private void showMnemonicHistogram(AnsiTerminalWriter terminalWriter) throws IOException {
Multiset<String> mnemonicHistogram = HashMultiset.create();
for (Map.Entry<Artifact, ActionState> action : activeActions.entrySet()) {
mnemonicHistogram.add(action.getValue().action.getMnemonic());
}
List<Multiset.Entry<String>> sorted =
mnemonicHistogram.entrySet().stream()
.collect(
Comparators.greatest(sampleSize, Comparator.comparingLong((e) -> e.getCount())));
for (Multiset.Entry<String> entry : sorted) {
terminalWriter.newline().append(" " + entry.getElement() + " " + entry.getCount());
}
}
private void sampleOldestActions(AnsiTerminalWriter terminalWriter) throws IOException {
// This method can only be called if 'activeActions.size()' was observed to be larger than 1 at
// some point in the past. But the 'activeActions' map can grow and shrink concurrent with this
// code here so we need to be very careful lest we fall victim to a check-then-act race.
int racyActiveActionsCount = activeActions.size();
PriorityQueue<Map.Entry<Artifact, ActionState>> priorityHeap =
new PriorityQueue<>(
// The 'initialCapacity' parameter must be positive.
/*initialCapacity=*/ Math.max(racyActiveActionsCount, 1),
Comparator.comparing(
(Map.Entry<Artifact, ActionState> entry) ->
entry.getValue().runningStrategiesBitmap == 0)
.thenComparingLong(entry -> entry.getValue().nanoStartTime)
.thenComparingInt(entry -> entry.getValue().hashCode()));
priorityHeap.addAll(activeActions.entrySet());
// From this point on, we no longer consume 'activeActions'. So now it's sound to look at how
// many entries were added to 'priorityHeap' and act appropriately based on that count.
int actualObservedActiveActionsCount = priorityHeap.size();
int count = 0;
int totalCount = 0;
long nanoTime = clock.nanoTime();
Set<Artifact> toSkip = new HashSet<>();
while (!priorityHeap.isEmpty()) {
Map.Entry<Artifact, ActionState> entry = priorityHeap.poll();
totalCount++;
if (toSkip.contains(entry.getKey())) {
continue;
}
count++;
if (count > sampleSize) {
totalCount--;
break;
}
int width =
targetWidth
- 4
- ((count >= sampleSize && count < actualObservedActiveActionsCount)
? AND_MORE.length()
: 0);
terminalWriter
.newline()
.append(" " + describeAction(entry.getValue(), nanoTime, width, toSkip));
}
if (totalCount < actualObservedActiveActionsCount) {
terminalWriter.append(AND_MORE);
}
}
public synchronized void testFilteringComplete(TestFilteringCompleteEvent event) {
if (event.getTestTargets() != null) {
totalTests = event.getTestTargets().size();
for (ConfiguredTarget target : event.getTestTargets()) {
if (target.getLabel() != null) {
testActions.put(target.getLabel(), Sets.newConcurrentHashSet());
}
}
}
}
public synchronized void testSummary(TestSummary summary) {
completedTests++;
mostRecentTest = summary;
if ((summary.getStatus() != BlazeTestStatus.PASSED)
&& (summary.getStatus() != BlazeTestStatus.FLAKY)) {
failedTests++;
}
}
synchronized void buildEventTransportsAnnounced(AnnounceBuildEventTransportsEvent event) {
this.bepOpenTransports.addAll(event.transports());
}
synchronized void buildEventTransportClosed(BuildEventTransportClosedEvent event) {
bepOpenTransports.remove(event.transport());
}
synchronized int pendingTransports() {
return bepOpenTransports.size();
}
/**
* * Predicate indicating whether the contents of the progress bar can change, if the only thing
* that happens is that time passes; this is the case, e.g., if the progress bar shows time
* information relative to the current time.
*/
boolean progressBarTimeDependent() {
if (packageProgressReceiver != null) {
return true;
}
if (runningDownloads.size() >= 1) {
return true;
}
if (buildComplete && !bepOpenTransports.isEmpty()) {
return true;
}
if (status != null) {
return false;
}
if (activeActions.size() >= 1) {
return true;
}
return false;
}
/**
* Maybe add a note about the last test that passed. Return true, if the note was added (and hence
* a line break is appropriate if more data is to come. If a null value is provided for the
* terminal writer, only return whether a note would be added.
*
* <p>The width parameter gives advice on to which length the description of the test should the
* shortened to, if possible.
*/
private boolean maybeShowRecentTest(
AnsiTerminalWriter terminalWriter, boolean shortVersion, int width) throws IOException {
final String prefix = "; last test: ";
if (!shortVersion && mostRecentTest != null) {
if (terminalWriter != null) {
terminalWriter.normal().append(prefix);
if (mostRecentTest.getStatus() == BlazeTestStatus.PASSED) {
terminalWriter.okStatus();
} else {
terminalWriter.failStatus();
}
terminalWriter.append(
shortenedLabelString(mostRecentTest.getLabel(), width - prefix.length()));
terminalWriter.normal();
}
return true;
} else {
return false;
}
}
private String shortenUrl(String url, int width) {
if (url.length() < width) {
return url;
}
// Try to shorten to the form prot://host/.../rest/path/filename
String prefix = "";
int protocolIndex = url.indexOf(URL_PROTOCOL_SEP);
if (protocolIndex > 0) {
prefix = url.substring(0, protocolIndex + URL_PROTOCOL_SEP.length() + 1);
url = url.substring(protocolIndex + URL_PROTOCOL_SEP.length() + 1);
int hostIndex = url.indexOf("/");
if (hostIndex > 0) {
prefix = prefix + url.substring(0, hostIndex + 1);
url = url.substring(hostIndex + 1);
int targetLength = width - prefix.length();
// accept this form of shortening, if what is left from the filename is
// significantly longer (twice as long) as the ellipsis symbol introduced
if (targetLength > 3 * ELLIPSIS.length()) {
String shortPath = suffix(url, targetLength - ELLIPSIS.length());
int slashPos = shortPath.indexOf("/");
if (slashPos >= 0) {
return prefix + ELLIPSIS + shortPath.substring(slashPos);
} else {
return prefix + ELLIPSIS + shortPath;
}
}
}
}
// Last resort: just take a suffix
if (width <= ELLIPSIS.length()) {
// No chance to shorten anyway
return "";
}
return ELLIPSIS + suffix(url, width - ELLIPSIS.length());
}
private void reportOnOneDownload(
String url, long nanoTime, int width, AnsiTerminalWriter terminalWriter) throws IOException {
String postfix = "";
FetchProgress download = downloads.get(url);
long nanoDownloadTime = nanoTime - downloadNanoStartTimes.get(url);
long downloadSeconds = nanoDownloadTime / NANOS_PER_SECOND;
String progress = download.getProgress();
if (progress.length() > 0) {
postfix = postfix + " " + progress;
}
if (downloadSeconds > SHOW_TIME_THRESHOLD_SECONDS) {
postfix = postfix + " " + downloadSeconds + "s";
}
if (postfix.length() > 0) {
postfix = ";" + postfix;
}
url = shortenUrl(url, Math.max(width - postfix.length(), 3 * ELLIPSIS.length()));
terminalWriter.append(url + postfix);
}
private void reportOnDownloads(AnsiTerminalWriter terminalWriter) throws IOException {
int count = 0;
long nanoTime = clock.nanoTime();
int downloadCount = runningDownloads.size();
String suffix = AND_MORE + " (" + downloadCount + " fetches)";
for (String url : runningDownloads) {
if (count >= sampleSize) {
break;
}
count++;
terminalWriter.newline().append(FETCH_PREFIX);
reportOnOneDownload(
url,
nanoTime,
targetWidth
- FETCH_PREFIX.length()
- ((count >= sampleSize && count < downloadCount) ? suffix.length() : 0),
terminalWriter);
}
if (count < downloadCount) {
terminalWriter.append(suffix);
}
}
/**
* Display any BEP transports that are still open after the build. Most likely, because uploading
* build events takes longer than the build itself.
*/
private void maybeReportBepTransports(PositionAwareAnsiTerminalWriter terminalWriter)
throws IOException {
if (!buildComplete || bepOpenTransports.isEmpty()) {
return;
}
long sinceSeconds =
MILLISECONDS.toSeconds(clock.currentTimeMillis() - bepTransportClosingStartTimeMillis);
if (sinceSeconds == 0) {
// Special case for when bazel was interrupted, in which case we don't want to have
// a BEP upload message.
return;
}
int count = bepOpenTransports.size();
// Can just use targetWidth, because we always write to a new line
int maxWidth = targetWidth;
String waitMessage = "Waiting for build events upload: ";
String name = bepOpenTransports.iterator().next().name();
String line = waitMessage + name + " " + sinceSeconds + "s";
if (count == 1 && line.length() <= maxWidth) {
terminalWriter.newline().append(line);
} else if (count == 1) {
waitMessage = "Waiting for: ";
String waitSecs = " " + sinceSeconds + "s";
int maxNameWidth = maxWidth - waitMessage.length() - waitSecs.length();
terminalWriter.newline().append(waitMessage + shortenedString(name, maxNameWidth) + waitSecs);
} else {
terminalWriter.newline().append(waitMessage + sinceSeconds + "s");
for (BuildEventTransport transport : bepOpenTransports) {
name = " " + transport.name();
terminalWriter.newline().append(shortenedString(name, maxWidth));
}
}
}
synchronized void writeProgressBar(
AnsiTerminalWriter rawTerminalWriter, boolean shortVersion, String timestamp)
throws IOException {
PositionAwareAnsiTerminalWriter terminalWriter =
new PositionAwareAnsiTerminalWriter(rawTerminalWriter);
int actionsCount = activeActions.size();
if (timestamp != null) {
terminalWriter.append(timestamp);
}
if (status != null) {
if (ok) {
terminalWriter.okStatus();
} else {
terminalWriter.failStatus();
}
terminalWriter.append(status + ":").normal().append(" " + additionalMessage);
if (packageProgressReceiver != null) {
Pair<String, String> progress = packageProgressReceiver.progressState();
terminalWriter.append(" (" + progress.getFirst());
if (configuredTargetProgressReceiver != null) {
terminalWriter.append(", " + configuredTargetProgressReceiver.getProgressString());
}
terminalWriter.append(")");
if (progress.getSecond().length() > 0 && !shortVersion) {
terminalWriter.newline().append(" " + progress.getSecond());
}
}
if (!shortVersion) {
reportOnDownloads(terminalWriter);
maybeReportBepTransports(terminalWriter);
}
return;
}
if (packageProgressReceiver != null) {
Pair<String, String> progress = packageProgressReceiver.progressState();
terminalWriter.okStatus().append("Loading:").normal().append(" " + progress.getFirst());
if (progress.getSecond().length() > 0) {
terminalWriter.newline().append(" " + progress.getSecond());
}
if (!shortVersion) {
reportOnDownloads(terminalWriter);
}
return;
}
if (executionProgressReceiver != null) {
terminalWriter.okStatus().append(executionProgressReceiver.getProgressString());
} else if (defaultStatus == null) {
return;
} else {
terminalWriter.okStatus().append(defaultStatus).append(":");
}
if (completedTests > 0) {
terminalWriter.normal().append(" " + completedTests + " / " + totalTests + " tests");
if (failedTests > 0) {
terminalWriter.append(", ").failStatus().append("" + failedTests + " failed").normal();
}
terminalWriter.append(";");
}
// Get the oldest action. Note that actions might have finished in the meantime and thus there
// might not be one.
ActionState oldestAction = getOldestAction();
if (actionsCount == 0 || oldestAction == null) {
terminalWriter.normal().append(" ").append(defaultActivity);
maybeShowRecentTest(terminalWriter, shortVersion, targetWidth - terminalWriter.getPosition());
} else if (actionsCount == 1) {
if (maybeShowRecentTest(null, shortVersion, targetWidth - terminalWriter.getPosition())) {
// As we will break lines anyway, also show the number of running actions, to keep
// things stay roughly in the same place (also compensating for the missing plural-s
// in the word action).
terminalWriter.normal().append(" 1 action");
maybeShowRecentTest(
terminalWriter, shortVersion, targetWidth - terminalWriter.getPosition());
String statusMessage =
describeAction(oldestAction, clock.nanoTime(), targetWidth - 4, null);
terminalWriter.normal().newline().append(" " + statusMessage);
} else {
String statusMessage =
describeAction(
oldestAction,
clock.nanoTime(),
targetWidth - terminalWriter.getPosition() - 1,
null);
terminalWriter.normal().append(" " + statusMessage);
}
} else {
if (shortVersion) {
String statusMessage =
describeAction(
oldestAction, clock.nanoTime(), targetWidth - terminalWriter.getPosition(), null);
statusMessage += " ... (" + countActions() + ")";
terminalWriter.normal().append(" " + statusMessage);
} else {
String statusMessage = countActions();
terminalWriter.normal().append(" " + statusMessage);
maybeShowRecentTest(
terminalWriter, shortVersion, targetWidth - terminalWriter.getPosition());
printActionState(terminalWriter);
}
}
if (!shortVersion) {
reportOnDownloads(terminalWriter);
maybeReportBepTransports(terminalWriter);
}
}
void writeProgressBar(AnsiTerminalWriter terminalWriter, boolean shortVersion)
throws IOException {
writeProgressBar(terminalWriter, shortVersion, null);
}
void writeProgressBar(AnsiTerminalWriter terminalWriter) throws IOException {
writeProgressBar(terminalWriter, false);
}
}