/*
* Copyright 2017 the original author or authors.
*
* 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 org.gradle.internal.logging.console;
import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import org.gradle.internal.logging.events.BatchOutputEventListener;
import org.gradle.internal.logging.events.EndOutputEvent;
import org.gradle.internal.logging.events.OperationIdentifier;
import org.gradle.internal.logging.events.OutputEvent;
import org.gradle.internal.logging.events.OutputEventListener;
import org.gradle.internal.logging.events.ProgressCompleteEvent;
import org.gradle.internal.logging.events.ProgressEvent;
import org.gradle.internal.logging.events.ProgressStartEvent;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static org.gradle.internal.logging.console.BuildStatusRenderer.BUILD_PROGRESS_CATEGORY;
public class WorkInProgressRenderer extends BatchOutputEventListener {
private final OutputEventListener listener;
private final ProgressOperations operations = new ProgressOperations();
private final BuildProgressArea progressArea;
private final DefaultWorkInProgressFormatter labelFormatter;
private final ConsoleLayoutCalculator consoleLayoutCalculator;
// Track all unused labels to display future progress operation
private final Deque<StyledLabel> unusedProgressLabels;
// Track currently associated label with its progress operation
private final Map<OperationIdentifier, AssociationLabel> operationIdToAssignedLabels = new HashMap<OperationIdentifier, AssociationLabel>();
// Track any progress operation that either can't be display due to label shortage or child progress operation is already been displayed
private final Deque<ProgressOperation> unassignedProgressOperations = new ArrayDeque<ProgressOperation>();
public WorkInProgressRenderer(OutputEventListener listener, BuildProgressArea progressArea, DefaultWorkInProgressFormatter labelFormatter, ConsoleLayoutCalculator consoleLayoutCalculator) {
this.listener = listener;
this.progressArea = progressArea;
this.labelFormatter = labelFormatter;
this.consoleLayoutCalculator = consoleLayoutCalculator;
this.unusedProgressLabels = new ArrayDeque<StyledLabel>(progressArea.getBuildProgressLabels());
}
@Override
public void onOutput(OutputEvent event) {
if (event instanceof ProgressStartEvent) {
progressArea.setVisible(true);
ProgressStartEvent startEvent = (ProgressStartEvent) event;
ProgressOperation op = operations.start(startEvent.getShortDescription(), startEvent.getStatus(), startEvent.getCategory(), startEvent.getProgressOperationId(), startEvent.getParentProgressOperationId());
attach(op);
} else if (event instanceof ProgressCompleteEvent) {
ProgressCompleteEvent completeEvent = (ProgressCompleteEvent) event;
detach(operations.complete(completeEvent.getProgressOperationId()));
} else if (event instanceof ProgressEvent) {
ProgressEvent progressEvent = (ProgressEvent) event;
operations.progress(progressEvent.getStatus(), progressEvent.getProgressOperationId());
} else if (event instanceof EndOutputEvent) {
progressArea.setVisible(false);
}
listener.onOutput(event);
}
@Override
public void onOutput(Iterable<OutputEvent> events) {
Set<OperationIdentifier> completeEventOperationIds = toOperationIdSet(Iterables.filter(events, ProgressCompleteEvent.class));
Set<OperationIdentifier> operationIdsToSkip = new HashSet<OperationIdentifier>();
for (OutputEvent event : events) {
if (event instanceof ProgressStartEvent && completeEventOperationIds.contains(((ProgressStartEvent) event).getProgressOperationId())) {
operationIdsToSkip.add(((ProgressStartEvent) event).getProgressOperationId());
listener.onOutput(event);
} else if ((event instanceof ProgressCompleteEvent && operationIdsToSkip.contains(((ProgressCompleteEvent) event).getProgressOperationId()))
|| (event instanceof ProgressEvent && operationIdsToSkip.contains(((ProgressEvent) event).getProgressOperationId()))) {
listener.onOutput(event);
} else {
onOutput(event);
}
}
renderNow();
}
// Transform ProgressCompleteEvent into their corresponding progress OperationIdentifier.
private Set<OperationIdentifier> toOperationIdSet(Iterable<ProgressCompleteEvent> events) {
return Sets.newHashSet(Iterables.transform(events, new Function<ProgressCompleteEvent, OperationIdentifier>() {
@Override
public OperationIdentifier apply(ProgressCompleteEvent event) {
return event.getProgressOperationId();
}
}));
}
private void resizeTo(int newBuildProgressLabelCount) {
int previousBuildProgressLabelCount = progressArea.getBuildProgressLabels().size();
newBuildProgressLabelCount = consoleLayoutCalculator.calculateNumWorkersForConsoleDisplay(newBuildProgressLabelCount);
if (previousBuildProgressLabelCount >= newBuildProgressLabelCount) {
// We don't support shrinking at the moment
return;
}
progressArea.resizeBuildProgressTo(newBuildProgressLabelCount);
// Add new labels to the unused queue
for (int i = newBuildProgressLabelCount - 1; i >= previousBuildProgressLabelCount; --i) {
unusedProgressLabels.push(progressArea.getBuildProgressLabels().get(i));
}
}
private void attach(ProgressOperation operation) {
if (operation.hasChildren() || !isRenderable(operation)) {
return;
}
// Reuse parent label if possible
if (operation.getParent() != null) {
detach(operation.getParent().getOperationId());
}
// No more unused label? Try to resize.
if (unusedProgressLabels.isEmpty()) {
int newValue = operationIdToAssignedLabels.size() + 1;
resizeTo(newValue);
// At this point, the work-in-progress area may or may not have been resized due to maximum size constraint.
}
// Try to use a new label
if (unusedProgressLabels.isEmpty()) {
unassignedProgressOperations.add(operation);
} else {
attach(operation, unusedProgressLabels.pop());
}
}
private void attach(ProgressOperation operation, StyledLabel label) {
AssociationLabel association = new AssociationLabel(operation, label);
operationIdToAssignedLabels.put(operation.getOperationId(), association);
}
private void detach(ProgressOperation operation) {
if (!isRenderable(operation)) {
return;
}
detach(operation.getOperationId());
unassignedProgressOperations.remove(operation);
if (operation.getParent() != null && isRenderable(operation.getParent())) {
attach(operation.getParent());
} else if (!unassignedProgressOperations.isEmpty()) {
attach(unassignedProgressOperations.pop());
}
}
private void detach(OperationIdentifier operationId) {
AssociationLabel association = operationIdToAssignedLabels.remove(operationId);
if (association != null) {
unusedProgressLabels.push(association.label);
}
}
// Any ProgressOperation in the parent chain has a message, the operation is considered renderable.
private boolean isRenderable(ProgressOperation operation) {
for (ProgressOperation current = operation;
current != null && !BUILD_PROGRESS_CATEGORY.equals(current.getCategory());
current = current.getParent()) {
if (current.getMessage() != null) {
return true;
}
}
return false;
}
private void renderNow() {
for (AssociationLabel associatedLabel : operationIdToAssignedLabels.values()) {
associatedLabel.renderNow();
}
for (StyledLabel emptyLabel : unusedProgressLabels) {
emptyLabel.setText(labelFormatter.format());
}
}
private class AssociationLabel {
final ProgressOperation operation;
final StyledLabel label;
AssociationLabel(ProgressOperation operation, StyledLabel label) {
this.operation = operation;
this.label = label;
}
void renderNow() {
label.setText(labelFormatter.format(operation));
}
}
}