// 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 runningActions; private final Map actions; private final Map 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); } }