blob: 078316c9a103b89217a143f8bf7eb9dbbd1450a0 [file] [log] [blame]
// Copyright 2022 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.common.annotations.VisibleForTesting;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.buildtool.buildevent.BuildCompleteEvent;
import com.google.devtools.build.lib.buildtool.buildevent.ExecutionProgressReceiverAvailableEvent;
import com.google.devtools.build.lib.clock.Clock;
import com.google.devtools.build.lib.pkgcache.LoadingPhaseCompleteEvent;
import com.google.devtools.build.lib.skyframe.ConfigurationPhaseStartedEvent;
import com.google.devtools.build.lib.skyframe.LoadingPhaseStartedEvent;
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.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
import java.time.Instant;
/** Tracks the state of Skymeld builds and determines what to display at each state in the UI. */
final class SkymeldUiStateTracker extends UiStateTracker {
enum BuildStatus {
// We explicitly define a starting status, which can be used to determine what to display in
// cases before the build has started.
BUILD_NOT_STARTED,
COMPUTING_MAIN_REPO_MAPPING,
BUILD_STARTED,
TARGET_PATTERN_PARSING,
LOADING_COMPLETE,
CONFIGURATION, // Analysis with configuration.
// The order of the AnalysisCompleteEvent and ExecutionProgressReceiverAvailableEvent is not
// certain, this splits the possible paths of the change in BuildStatus into two.
ANALYSIS_COMPLETE, // After analysis but before execution.
ANALYSIS_AND_EXECUTION, // During analysis and execution.
EXECUTION, // Only execution.
BUILD_COMPLETED;
}
@VisibleForTesting BuildStatus buildStatus = BuildStatus.BUILD_NOT_STARTED;
SkymeldUiStateTracker(Clock clock, int targetWidth) {
super(clock, targetWidth);
}
SkymeldUiStateTracker(Clock clock) {
super(clock);
}
/**
* Main method that writes the progress of the build.
*
* @param rawTerminalWriter used to write to the terminal.
* @param shortVersion whether to write a short version of the output.
* @param timestamp null if the UiOptions specifies not to show timestamps.
* @throws IOException when attempting to write to the terminal writer.
*/
@Override
synchronized void writeProgressBar(
AnsiTerminalWriter rawTerminalWriter, boolean shortVersion, String timestamp)
throws IOException {
PositionAwareAnsiTerminalWriter terminalWriter =
new PositionAwareAnsiTerminalWriter(rawTerminalWriter);
if (timestamp != null) {
terminalWriter.append(timestamp);
}
switch (buildStatus) {
case BUILD_NOT_STARTED:
return;
case COMPUTING_MAIN_REPO_MAPPING:
writeBaseProgress("Computing main repo mapping", "", terminalWriter);
break;
case BUILD_STARTED:
writeBaseProgress("Loading", "", terminalWriter);
break;
case TARGET_PATTERN_PARSING:
writeLoadingAnalysisPhaseProgress("Loading", "", terminalWriter, false);
break;
case LOADING_COMPLETE:
case CONFIGURATION:
writeLoadingAnalysisPhaseProgress(
"Analyzing", additionalMessage, terminalWriter, shortVersion);
break;
case ANALYSIS_COMPLETE:
// Currently, regular Blaze does not add additional information in this phase, and this is
// left empty on purpose to mimic the same behavior.
break;
case ANALYSIS_AND_EXECUTION:
writeLoadingAnalysisPhaseProgress(
"Analyzing", additionalMessage, terminalWriter, shortVersion);
terminalWriter.newline();
writeExecutionProgress(terminalWriter, shortVersion);
break;
case EXECUTION:
writeExecutionProgress(terminalWriter, shortVersion);
break;
case BUILD_COMPLETED:
writeBaseProgress(ok ? "INFO" : "FAILED", additionalMessage, terminalWriter);
break;
}
if (!shortVersion) {
reportOnDownloads(terminalWriter);
maybeReportActiveUploadsOrDownloads(terminalWriter);
maybeReportBepTransports(terminalWriter);
}
}
void writeBaseProgress(
String status, String message, PositionAwareAnsiTerminalWriter terminalWriter)
throws IOException {
if (ok) {
terminalWriter.okStatus();
} else {
terminalWriter.failStatus();
}
terminalWriter.append(status + ":").normal().append(" " + message);
}
void writeLoadingAnalysisPhaseProgress(
String status,
String message,
PositionAwareAnsiTerminalWriter terminalWriter,
boolean shortVersion)
throws IOException {
writeBaseProgress(status, message, terminalWriter);
if (packageProgressReceiver != null) {
Pair<String, String> progress = packageProgressReceiver.progressState();
String analysisProgress = progress.getFirst();
if (configuredTargetProgressReceiver != null) {
analysisProgress += ", " + configuredTargetProgressReceiver.getProgressString();
}
if (message.isEmpty()) {
terminalWriter.append(analysisProgress);
} else {
terminalWriter.append(" (" + analysisProgress + ")");
}
if (!progress.getSecond().isEmpty() && !shortVersion) {
terminalWriter.newline().append(" " + progress.getSecond());
}
}
}
@Override
void mainRepoMappingComputationStarted() {
buildStatus = BuildStatus.COMPUTING_MAIN_REPO_MAPPING;
}
@Override
void buildStarted() {
buildStatus = BuildStatus.BUILD_STARTED;
}
@Override
void loadingStarted(LoadingPhaseStartedEvent event) {
buildStatus = BuildStatus.TARGET_PATTERN_PARSING;
packageProgressReceiver = event.getPackageProgressReceiver();
}
@Override
void loadingComplete(LoadingPhaseCompleteEvent event) {
buildStatus = BuildStatus.LOADING_COMPLETE;
int labelsCount = event.getLabels().size();
if (labelsCount == 1) {
additionalMessage = "target " + Iterables.getOnlyElement(event.getLabels());
} else {
additionalMessage = labelsCount + " targets";
}
mainRepositoryMapping = event.getMainRepositoryMapping();
}
@Override
void configurationStarted(ConfigurationPhaseStartedEvent event) {
buildStatus = BuildStatus.CONFIGURATION;
configuredTargetProgressReceiver = event.getConfiguredTargetProgressReceiver();
}
/**
* Make the state tracker aware of the fact that the analysis has finished. Return a summary of
* the work done in the analysis phase.
*/
@Override
@CanIgnoreReturnValue
synchronized String analysisComplete() {
// This is where the path of the BuildStatus splits, the BuildStatus at this point could be
// either CONFIGURATION or ANALYSIS_AND_EXECUTION.
buildStatus =
BuildStatus.CONFIGURATION.equals(buildStatus)
? BuildStatus.ANALYSIS_COMPLETE
: BuildStatus.EXECUTION;
String workDone = "Analyzed " + additionalMessage;
if (packageProgressReceiver != null) {
Pair<String, String> progress = packageProgressReceiver.progressState();
workDone += " (" + progress.getFirst();
if (configuredTargetProgressReceiver != null) {
workDone += ", " + configuredTargetProgressReceiver.getProgressString();
}
workDone += ")";
}
workDone += ".";
packageProgressReceiver = null;
configuredTargetProgressReceiver = null;
return workDone;
}
@Override
synchronized void progressReceiverAvailable(ExecutionProgressReceiverAvailableEvent event) {
executionProgressReceiver = event.getExecutionProgressReceiver();
// This is where the path of the BuildStatus splits, the BuildStatus at this point could be
// either CONFIGURATION or ANALYSIS_COMPLETE.
buildStatus =
BuildStatus.CONFIGURATION.equals(buildStatus)
? BuildStatus.ANALYSIS_AND_EXECUTION
: BuildStatus.EXECUTION;
}
@Override
void buildComplete(BuildCompleteEvent event) {
buildStatus = BuildStatus.BUILD_COMPLETED;
buildCompleteAt = Instant.ofEpochMilli(clock.currentTimeMillis());
if (event.getResult().getSuccess()) {
int actionsCompleted = this.actionsCompleted.get();
if (failedTests == 0) {
additionalMessage =
"Build completed successfully, "
+ actionsCompleted
+ pluralize(" total action", actionsCompleted);
} else {
additionalMessage =
"Build completed, "
+ failedTests
+ pluralize(" test", failedTests)
+ " FAILED, "
+ actionsCompleted
+ pluralize(" total action", actionsCompleted);
}
} else {
ok = false;
additionalMessage = "Build did NOT complete successfully";
}
}
@Override
protected boolean buildCompleted() {
return BuildStatus.BUILD_COMPLETED.equals(buildStatus);
}
}