blob: 3d4da2afe0e500ebad97e84c6bf81627a444bfda [file] [log] [blame]
// Copyright 2014 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 static java.util.Comparator.comparing;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.eventbus.AllowConcurrentEvents;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import com.google.devtools.build.lib.bugreport.BugReport;
import com.google.devtools.build.lib.clock.BlazeClock;
import com.google.devtools.build.lib.clock.Clock;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.util.Pair;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.Nullable;
/**
* Implements "Still waiting..." message functionality, displaying current status for "in-flight"
* actions. Used by the ParallelBuilder.
*
* TODO(bazel-team): (2010) It would be nice if "duplicated" actions (e.g. test shards and multiple
* test runs) were merged into the single line.
*/
@ThreadSafe
public final class ActionExecutionStatusReporter {
// Maximum number of lines to output per each status category before truncation.
private static final int MAX_LINES = 10;
private static final String PREPARING_MESSAGE = "Preparing";
private final EventHandler eventHandler;
private final EventBus eventBus;
private final Clock clock;
/**
* The status of each action "in flight", i.e. whose ExecuteBuildAction.call() method is active.
* Used for implementing the "still waiting" message.
*/
private final Map<ActionExecutionMetadata, Pair<String, Long>> actionStatus =
new ConcurrentHashMap<>(100);
public static ActionExecutionStatusReporter create(EventHandler eventHandler) {
return create(eventHandler, null, null);
}
@VisibleForTesting
static ActionExecutionStatusReporter create(EventHandler eventHandler, @Nullable Clock clock) {
return create(eventHandler, null, clock);
}
public static ActionExecutionStatusReporter create(
EventHandler eventHandler, @Nullable EventBus eventBus) {
return create(eventHandler, eventBus, null);
}
private static ActionExecutionStatusReporter create(
EventHandler eventHandler, @Nullable EventBus eventBus, @Nullable Clock clock) {
ActionExecutionStatusReporter result =
new ActionExecutionStatusReporter(
eventHandler, eventBus, clock == null ? BlazeClock.instance() : clock);
if (eventBus != null) {
eventBus.register(result);
}
return result;
}
private ActionExecutionStatusReporter(
EventHandler eventHandler, @Nullable EventBus eventBus, Clock clock) {
this.eventHandler = Preconditions.checkNotNull(eventHandler);
this.eventBus = eventBus;
this.clock = Preconditions.checkNotNull(clock);
}
public void unregisterFromEventBus() {
if (eventBus != null) {
eventBus.unregister(this);
}
}
private void setStatus(ActionExecutionMetadata action, String message) {
actionStatus.put(action, Pair.of(message, clock.nanoTime()));
}
/** Remove action from the list of active actions. Action must be present. */
public void remove(Action action) {
Pair<String, Long> status = actionStatus.remove(action);
if (status == null) {
BugReport.sendBugReport(
new IllegalStateException("Action not present: " + action.prettyPrint()));
}
}
@Subscribe
@AllowConcurrentEvents
public void updateStatus(ActionStartedEvent event) {
ActionExecutionMetadata action = event.getAction();
setStatus(action, PREPARING_MESSAGE);
}
@Subscribe
@AllowConcurrentEvents
public void updateStatus(ScanningActionEvent event) {
ActionExecutionMetadata action = event.getActionMetadata();
setStatus(action, "Scanning");
}
@Subscribe
@AllowConcurrentEvents
public void updateStatus(SchedulingActionEvent event) {
ActionExecutionMetadata action = event.getActionMetadata();
setStatus(action, "Scheduling");
}
@Subscribe
@AllowConcurrentEvents
public void updateStatus(RunningActionEvent event) {
ActionExecutionMetadata action = event.getActionMetadata();
setStatus(action, String.format("Running (%s)", event.getStrategy()));
}
@Subscribe
@AllowConcurrentEvents
public void updateStatus(StoppedScanningActionEvent event) {
remove(event.getAction());
}
public int getCount() {
return actionStatus.size();
}
private static void appendGroupStatus(StringBuilder buffer,
Map<ActionExecutionMetadata, Pair<String, Long>> statusMap, String status,
long currentTime) {
List<Pair<Long, ActionExecutionMetadata>> actions = new ArrayList<>();
for (Map.Entry<ActionExecutionMetadata, Pair<String, Long>> entry : statusMap.entrySet()) {
if (entry.getValue().first.equals(status)) {
actions.add(Pair.of(entry.getValue().second, entry.getKey()));
}
}
if (actions.isEmpty()) {
return;
}
Collections.sort(actions, comparing(arg -> arg.first));
buffer.append("\n " + status + ":");
boolean truncateList = actions.size() > MAX_LINES;
for (Pair<Long, ActionExecutionMetadata> entry : actions.subList(0,
truncateList ? MAX_LINES - 1 : actions.size())) {
String message = entry.second.getProgressMessage();
if (message == null) {
// Actions will a null progress message should run so
// fast we never see them here. In any case...
message = entry.second.prettyPrint();
}
buffer.append("\n ").append(message);
long runTime = (currentTime - entry.first) / 1000000000L; // Convert to seconds.
buffer.append(", ").append(runTime).append(" s");
}
if (truncateList) {
buffer.append("\n ... ").append(actions.size() - MAX_LINES + 1).append(" more jobs");
}
}
/**
* Get message showing currently executing actions.
*/
private String getExecutionStatusMessage(
Map<ActionExecutionMetadata, Pair<String, Long>> statusMap) {
int count = statusMap.size();
StringBuilder s = count != 1
? new StringBuilder("Still waiting for ").append(count).append(" jobs to complete:")
: new StringBuilder("Still waiting for 1 job to complete:");
long currentTime = clock.nanoTime();
// A tree is just as fast as HashSet for small data sets.
Set<String> statuses = new TreeSet<>();
for (Map.Entry<ActionExecutionMetadata, Pair<String, Long>> entry : statusMap.entrySet()) {
statuses.add(entry.getValue().first);
}
for (String status : statuses) {
appendGroupStatus(s, statusMap, status, currentTime);
}
return s.toString();
}
/**
* Show currently executing actions.
*/
public void showCurrentlyExecutingActions(String progressPercentageMessage) {
// Defensive copy to ensure thread safety.
Map<ActionExecutionMetadata, Pair<String, Long>> statusMap = new HashMap<>(actionStatus);
if (!statusMap.isEmpty()) {
eventHandler.handle(
Event.progress(progressPercentageMessage + getExecutionStatusMessage(statusMap)));
}
}
/**
* Warn about actions that are still being executed.
* Method is used to produce informative message when build is interrupted.
*/
void warnAboutCurrentlyExecutingActions() {
// Defensive copy to ensure thread safety.
Map<ActionExecutionMetadata, Pair<String, Long>> statusMap = new HashMap<>(actionStatus);
if (statusMap.isEmpty()) {
// There are no tasks in the queue so there is nothing to report.
eventHandler.handle(Event.warn("There are no active jobs - stopping the build"));
return;
}
Iterator<ActionExecutionMetadata> iterator = statusMap.keySet().iterator();
while (iterator.hasNext()) {
// Filter out actions that are not executed yet.
if (PREPARING_MESSAGE.equals(statusMap.get(iterator.next()).first)) {
iterator.remove();
}
}
if (!statusMap.isEmpty()) {
eventHandler.handle(Event.warn(getExecutionStatusMessage(statusMap)
+ "\nBuild will be stopped after these tasks terminate"));
} else {
// It is possible that one or more tasks in "Preparing" state just started being executed.
// So warn user just in case.
eventHandler.handle(Event.warn("Still waiting for unfinished jobs"));
}
}
/**
* Returns the number of seconds to wait before reporting slow progress again.
*
* @param userSpecifiedProgressInterval value of the --progress_report_interval flag; 0 means
* use default 10, then 30, then 60 seconds wait times
* @param previousWaitTime previous value returned by this method
*/
public static int getWaitTime(int userSpecifiedProgressInterval, int previousWaitTime) {
if (userSpecifiedProgressInterval > 0) {
return userSpecifiedProgressInterval;
}
// Increase waitTime to 10, then to 30 and then to 60 seconds to reduce
// spamming during long wait periods. If the user specified a
// waitTime directly through progressReportInterval, then use
// that value.
if (previousWaitTime == 0) {
return 10;
} else if (previousWaitTime == 10) {
return 30;
} else {
return 60;
}
}
}