blob: c8fab64cd5fe70eaa06bce22859b0abafd2624b3 [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 com.google.devtools.build.lib.actions.Action;
import com.google.devtools.build.lib.actions.ActionCompletionEvent;
import com.google.devtools.build.lib.actions.ActionStartedEvent;
import com.google.devtools.build.lib.analysis.AnalysisPhaseCompleteEvent;
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.cmdline.Label;
import com.google.devtools.build.lib.pkgcache.LoadingPhaseCompleteEvent;
import com.google.devtools.build.lib.skyframe.LoadingPhaseStartedEvent;
import com.google.devtools.build.lib.skyframe.LoadingProgressReceiver;
import com.google.devtools.build.lib.util.Clock;
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.Deque;
import java.util.Map;
import java.util.TreeMap;
/**
* An experimental state tracker for the new experimental UI.
*/
class ExperimentalStateTracker {
static final int SAMPLE_SIZE = 3;
static final long SHOW_TIME_THRESHOLD_SECONDS = 3;
static final String ELLIPSIS = "...";
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;
// currently running actions, using the path of the primary
// output as unique identifier.
private final Deque<String> runningActions;
private final Map<String, Action> actions;
private final Map<String, Long> actionNanoStartTimes;
private int actionsCompleted;
private int totalTests;
private int completedTests;
private TestSummary mostRecentTest;
private int failedTests;
private boolean ok;
private ExecutionProgressReceiver executionProgressReceiver;
private LoadingProgressReceiver loadingProgressReceiver;
ExperimentalStateTracker(Clock clock, int targetWidth) {
this.runningActions = new ArrayDeque<>();
this.actions = new TreeMap<>();
this.actionNanoStartTimes = new TreeMap<>();
this.ok = true;
this.clock = clock;
this.targetWidth = targetWidth;
}
ExperimentalStateTracker(Clock clock) {
this(clock, 0);
}
void buildStarted(BuildStartingEvent event) {
status = "Loading";
additionalMessage = "";
}
void loadingStarted(LoadingPhaseStartedEvent event) {
status = null;
loadingProgressReceiver = event.getLoadingProgressReceiver();
}
void loadingComplete(LoadingPhaseCompleteEvent event) {
loadingProgressReceiver = null;
int count = event.getTargets().size();
status = "Analysing";
additionalMessage = "" + count + " targets";
}
void analysisComplete(AnalysisPhaseCompleteEvent event) {
status = null;
}
void progressReceiverAvailable(ExecutionProgressReceiverAvailableEvent event) {
executionProgressReceiver = event.getExecutionProgressReceiver();
}
void buildComplete(BuildCompleteEvent event) {
if (event.getResult().getSuccess()) {
status = "INFO";
additionalMessage = "Build completed successfully, " + actionsCompleted + " total actions";
} else {
ok = false;
status = "FAILED";
additionalMessage = "Build did NOT complete successfully";
}
}
synchronized void actionStarted(ActionStartedEvent event) {
Action action = event.getAction();
String name = action.getPrimaryOutput().getPath().getPathString();
Long nanoStartTime = event.getNanoTimeStart();
runningActions.addLast(name);
actions.put(name, action);
actionNanoStartTimes.put(name, nanoStartTime);
}
synchronized void actionCompletion(ActionCompletionEvent event) {
actionsCompleted++;
Action action = event.getAction();
String name = action.getPrimaryOutput().getPath().getPathString();
runningActions.remove(name);
actions.remove(name);
actionNanoStartTimes.remove(name);
// 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(action);
}
}
/**
* From a string, take a suffix of at most the given length.
*/
private String suffix(String s, int len) {
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 describeAction(String name, long nanoTime, int desiredWidth) {
Action action = actions.get(name);
String postfix = "";
long nanoRuntime = nanoTime - actionNanoStartTimes.get(name);
long runtimeSeconds = nanoRuntime / 1000000000;
if (runtimeSeconds > SHOW_TIME_THRESHOLD_SECONDS) {
postfix = " " + runtimeSeconds + "s";
}
String message = action.getProgressMessage();
if (message == null) {
message = action.prettyPrint();
}
if (desiredWidth <= 0) {
return message + postfix;
}
if (message.length() + postfix.length() <= desiredWidth) {
return message + postfix;
}
if (action.getOwner() != null) {
if (action.getOwner().getLabel() != null) {
String shortLabel =
shortenedLabelString(action.getOwner().getLabel(), desiredWidth - postfix.length());
if (shortLabel.length() + postfix.length() <= desiredWidth) {
return shortLabel + postfix;
}
}
}
if (3 * ELLIPSIS.length() <= desiredWidth) {
message = ELLIPSIS + suffix(message, desiredWidth - ELLIPSIS.length() - postfix.length());
}
return message + postfix;
}
private void sampleOldestActions(AnsiTerminalWriter terminalWriter) throws IOException {
int count = 0;
long nanoTime = clock.nanoTime();
int actionCount = runningActions.size();
for (String action : runningActions) {
count++;
int width = (count >= SAMPLE_SIZE && count < actionCount) ? targetWidth - 8 : targetWidth - 4;
terminalWriter.newline().append(" " + describeAction(action, nanoTime, width));
if (count >= SAMPLE_SIZE) {
break;
}
}
if (count < actionCount) {
terminalWriter.append(" ...");
}
}
public void testFilteringComplete(TestFilteringCompleteEvent event) {
if (event.getTestTargets() != null) {
totalTests = event.getTestTargets().size();
}
}
public synchronized void testSummary(TestSummary summary) {
completedTests++;
mostRecentTest = summary;
if (summary.getStatus() != BlazeTestStatus.PASSED) {
failedTests++;
}
}
/***
* 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 (status != null) {
return false;
}
if (runningActions.size() >= 1) {
return true;
}
if (loadingProgressReceiver != null) {
// This is kind-of a hack: since the event handler does not get informed about updates
// in the loading phase, indicate that the progress bar might change even though no
// explicit update event is known to the event handler.
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 wether a note would be added.
*
* The width parameter gives advice on to which length the 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 + shortenedLabelString(
mostRecentTest.getTarget().getLabel(), width - prefix.length()));
}
return true;
} else {
return false;
}
}
synchronized void writeProgressBar(AnsiTerminalWriter rawTerminalWriter, boolean shortVersion)
throws IOException {
PositionAwareAnsiTerminalWriter terminalWriter =
new PositionAwareAnsiTerminalWriter(rawTerminalWriter);
if (status != null) {
if (ok) {
terminalWriter.okStatus();
} else {
terminalWriter.failStatus();
}
terminalWriter.append(status + ":").normal().append(" " + additionalMessage);
return;
}
if (loadingProgressReceiver != null) {
terminalWriter
.okStatus()
.append("Loading:")
.normal()
.append(" " + loadingProgressReceiver.progressState());
return;
}
if (executionProgressReceiver != null) {
terminalWriter.okStatus().append(executionProgressReceiver.getProgressString());
} else {
terminalWriter.okStatus().append("Building:");
}
if (completedTests > 0) {
terminalWriter.normal().append(" " + completedTests + " / " + totalTests + " tests");
if (failedTests > 0) {
terminalWriter.append(", ").failStatus().append("" + failedTests + " failed").normal();
}
terminalWriter.append(";");
}
if (runningActions.size() == 0) {
terminalWriter.normal().append(" no action");
maybeShowRecentTest(terminalWriter, shortVersion, targetWidth - terminalWriter.getPosition());
} else if (runningActions.size() == 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(runningActions.peekFirst(), clock.nanoTime(), targetWidth - 4);
terminalWriter.normal().newline().append(" " + statusMessage);
} else {
String statusMessage =
describeAction(
runningActions.peekFirst(),
clock.nanoTime(),
targetWidth - terminalWriter.getPosition() - 1);
terminalWriter.normal().append(" " + statusMessage);
}
} else {
if (shortVersion) {
String statusMessage =
describeAction(
runningActions.peekFirst(),
clock.nanoTime(),
targetWidth - terminalWriter.getPosition());
statusMessage += " ... (" + runningActions.size() + " actions)";
terminalWriter.normal().append(" " + statusMessage);
} else {
String statusMessage = "" + runningActions.size() + " actions";
terminalWriter.normal().append(" " + statusMessage);
maybeShowRecentTest(
terminalWriter, shortVersion, targetWidth - terminalWriter.getPosition());
sampleOldestActions(terminalWriter);
}
}
}
void writeProgressBar(AnsiTerminalWriter terminalWriter) throws IOException {
writeProgressBar(terminalWriter, false);
}
}