/*
* Copyright 2015-present Facebook, Inc.
*
* 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.facebook.buck.event.listener;
import com.facebook.buck.event.BuckEventBus;
import com.facebook.buck.event.ProgressEvent;
import com.facebook.buck.log.Logger;
import com.facebook.buck.util.ObjectMappers;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.AtomicDouble;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;
public class ProgressEstimator {
private final Path storageFile;
private static final Logger LOG = Logger.get(ProgressEstimator.class);
public static final String EXPECTED_NUMBER_OF_PARSED_RULES = "expectedNumberOfParsedRules";
public static final String EXPECTED_NUMBER_OF_PARSED_BUCK_FILES =
"expectedNumberOfParsedBUCKFiles";
public static final String EXPECTED_NUMBER_OF_GENERATED_PROJECT_FILES =
"expectedNumberOfGeneratedProjectFiles";
public static final String PROGRESS_ESTIMATIONS_JSON = ".progressestimations.json";
@Nullable private String command;
private BuckEventBus buckEventBus;
@Nullable private Map<String, Map<String, Number>> expectationsStorage;
private final AtomicInteger numberOfParsedRules = new AtomicInteger(0);
private final AtomicInteger numberOfParsedBUCKFiles = new AtomicInteger(0);
private final AtomicInteger numberOfGeneratedProjectFiles = new AtomicInteger(0);
private final AtomicInteger expectedNumberOfParsedRules = new AtomicInteger(0);
private final AtomicInteger expectedNumberOfParsedBUCKFiles = new AtomicInteger(0);
private final AtomicInteger expectedNumberOfGeneratedProjectFiles = new AtomicInteger(0);
private final AtomicInteger numberOfRules = new AtomicInteger(0);
private final AtomicInteger numberOfStartedRules = new AtomicInteger(0);
private final AtomicInteger numberOfFinishedRules = new AtomicInteger(0);
private final AtomicDouble processingFilesProgress = new AtomicDouble(-1.0);
private final AtomicDouble projectGenerationProgress = new AtomicDouble(-1.0);
private final AtomicDouble buildProgress = new AtomicDouble(-1.0);
public ProgressEstimator(Path storageFile, BuckEventBus buckEventBus) {
this.storageFile = storageFile;
this.command = null;
this.buckEventBus = buckEventBus;
this.expectationsStorage = null;
}
public void setCurrentCommand(String commandName, ImmutableList<String> commandArgs) {
command = commandName + " " + Joiner.on(" ").join(commandArgs);
fillEstimationsForCommand(command);
}
public void didParseBuckRules(int amount) {
numberOfParsedRules.addAndGet(amount);
numberOfParsedBUCKFiles.incrementAndGet();
calculateProcessingFilesEstimatedProgress();
}
public void didFinishParsing() {
if (command != null) {
expectedNumberOfParsedRules.set(numberOfParsedRules.get());
expectedNumberOfParsedBUCKFiles.set(numberOfParsedBUCKFiles.get());
calculateProcessingFilesEstimatedProgress();
updateEstimatedBuckFilesParsingValues(command);
}
}
public void didGenerateProjectForTarget() {
numberOfGeneratedProjectFiles.incrementAndGet();
calculateProjectFilesGenerationEstimatedProgress();
}
public void didFinishProjectGeneration() {
if (command != null) {
expectedNumberOfGeneratedProjectFiles.set(numberOfGeneratedProjectFiles.get());
calculateProjectFilesGenerationEstimatedProgress();
updateEstimatedProjectGenerationValues(command);
}
}
public void setNumberOfRules(int count) {
numberOfRules.set(Math.max(count, 0));
if (numberOfRules.intValue() == 0) {
LOG.warn("Got 0 rules to process, progress will not be computed");
}
calculateBuildProgress();
}
public void didStartRule() {
numberOfStartedRules.incrementAndGet();
calculateBuildProgress();
}
public void didResumeRule() {
calculateBuildProgress();
}
public void didSuspendRule() {
calculateBuildProgress();
}
public void didFinishRule() {
numberOfFinishedRules.incrementAndGet();
calculateBuildProgress();
}
public void didStartBuild() {
numberOfStartedRules.set(0);
numberOfFinishedRules.set(0);
}
public void didFinishBuild() {
int rulesCount = numberOfRules.intValue();
if (rulesCount > 0) {
numberOfStartedRules.set(rulesCount);
numberOfFinishedRules.set(rulesCount);
calculateBuildProgress();
}
}
private void fillEstimationsForCommand(String aCommand) {
preloadEstimationsFromStorageFile();
Map<String, Number> commandEstimations = getEstimationsForCommand(aCommand);
if (commandEstimations != null) {
if (commandEstimations.containsKey(EXPECTED_NUMBER_OF_PARSED_RULES)
&& commandEstimations.containsKey(EXPECTED_NUMBER_OF_PARSED_BUCK_FILES)) {
expectedNumberOfParsedRules.set(
commandEstimations.get(EXPECTED_NUMBER_OF_PARSED_RULES).intValue());
expectedNumberOfParsedBUCKFiles.set(
commandEstimations.get(EXPECTED_NUMBER_OF_PARSED_BUCK_FILES).intValue());
}
if (commandEstimations.containsKey(EXPECTED_NUMBER_OF_GENERATED_PROJECT_FILES)) {
expectedNumberOfGeneratedProjectFiles.set(
commandEstimations.get(EXPECTED_NUMBER_OF_GENERATED_PROJECT_FILES).intValue());
}
calculateProcessingFilesEstimatedProgress();
}
}
private void preloadEstimationsFromStorageFile() {
Map<String, Map<String, Number>> map = null;
if (Files.exists(storageFile)) {
try {
byte[] bytes = Files.readAllBytes(storageFile);
map =
ObjectMappers.READER.readValue(
ObjectMappers.createParser(bytes),
new TypeReference<HashMap<String, Map<String, Number>>>() {});
} catch (Exception e) {
LOG.warn("Unable to load progress estimations from file: " + e.getMessage());
}
}
if (map == null) {
map = new HashMap<>();
}
expectationsStorage = map;
}
@Nullable
private Map<String, Number> getEstimationsForCommand(String aCommand) {
if (expectationsStorage == null || aCommand == null) {
return null;
}
Map<String, Number> commandEstimations = expectationsStorage.get(aCommand);
if (commandEstimations == null) {
commandEstimations = new HashMap<>();
expectationsStorage.put(aCommand, commandEstimations);
}
return commandEstimations;
}
private void updateEstimatedProjectGenerationValues(String aCommand) {
Map<String, Number> commandEstimations = getEstimationsForCommand(aCommand);
if (commandEstimations != null) {
commandEstimations.put(
EXPECTED_NUMBER_OF_GENERATED_PROJECT_FILES, numberOfGeneratedProjectFiles);
saveEstimatedValues();
}
}
private void updateEstimatedBuckFilesParsingValues(String aCommand) {
Map<String, Number> commandEstimations = getEstimationsForCommand(aCommand);
if (commandEstimations != null) {
commandEstimations.put(EXPECTED_NUMBER_OF_PARSED_RULES, numberOfParsedRules);
commandEstimations.put(EXPECTED_NUMBER_OF_PARSED_BUCK_FILES, numberOfParsedBUCKFiles);
saveEstimatedValues();
}
}
private void saveEstimatedValues() {
if (expectationsStorage == null) {
return;
}
try {
Files.createDirectories(storageFile.getParent());
} catch (IOException e) {
LOG.warn(
"Unable to make path for storage %s: %s",
storageFile.toString(), e.getLocalizedMessage());
return;
}
try {
ObjectMappers.WRITER.writeValue(storageFile.toFile(), expectationsStorage);
} catch (IOException e) {
LOG.warn("Unable to save progress expectations: " + e.getLocalizedMessage());
}
}
/**
* @return Estimated progress of processing files stage. If return value is absent, it's
* impossible to compute the estimated progress.
*/
public Optional<Double> getEstimatedProgressOfProcessingBuckFiles() {
return wrapValueIntoOptional(processingFilesProgress.get());
}
private Optional<Double> wrapValueIntoOptional(double value) {
if (value == -1.0) {
return Optional.empty();
} else {
return Optional.of(value);
}
}
private void calculateProcessingFilesEstimatedProgress() {
double expectedNumberRules = expectedNumberOfParsedRules.doubleValue();
double expectedNumberOfBUCKFiles = expectedNumberOfParsedBUCKFiles.doubleValue();
double newValue;
if (expectedNumberRules == 0.0 || expectedNumberOfBUCKFiles == 0.0) {
newValue = -1.0;
} else {
double rulesProgress = numberOfParsedRules.doubleValue() / expectedNumberRules;
double filesProgress = numberOfParsedBUCKFiles.doubleValue() / expectedNumberOfBUCKFiles;
newValue = Math.min((rulesProgress + filesProgress) / 2.0, 1.0);
newValue = Math.floor(newValue * 100.0) / 100.0;
}
double oldValue = processingFilesProgress.getAndSet(newValue);
if (oldValue != newValue) {
buckEventBus.post(ProgressEvent.parsingProgressUpdated(newValue));
}
}
/**
* @return Estimated progress of generating projects stage. If return value is absent, it's
* impossible to compute the estimated progress.
*/
public Optional<Double> getEstimatedProgressOfGeneratingProjectFiles() {
return wrapValueIntoOptional(projectGenerationProgress.get());
}
private void calculateProjectFilesGenerationEstimatedProgress() {
double numberOfProcessedProjectFiles = numberOfGeneratedProjectFiles.doubleValue();
double expectedNumberOfProjectFiles = expectedNumberOfGeneratedProjectFiles.doubleValue();
double newValue;
if (numberOfProcessedProjectFiles == 0.0 || expectedNumberOfProjectFiles == 0.0) {
newValue = -1.0;
} else {
newValue = Math.min((numberOfProcessedProjectFiles / expectedNumberOfProjectFiles), 1.0);
newValue = Math.floor(newValue * 100.0) / 100.0;
}
double oldValue = projectGenerationProgress.getAndSet(newValue);
if (oldValue != newValue) {
buckEventBus.post(ProgressEvent.projectGenerationProgressUpdated(newValue));
}
}
/**
* @return Approximated progress of current build. Returns absent value if number of rules wasn't
* determined.
*/
public Optional<Double> getApproximateBuildProgress() {
return wrapValueIntoOptional(buildProgress.get());
}
private void calculateBuildProgress() {
double ruleCount = numberOfRules.doubleValue();
double newValue;
if (ruleCount == 0.0) {
newValue = -1.0;
} else {
double buildProgress = numberOfFinishedRules.get() / ruleCount;
newValue = Math.floor(buildProgress * 100.0) / 100.0;
}
double oldValue = buildProgress.getAndSet(newValue);
if (oldValue != newValue) {
buckEventBus.post(ProgressEvent.buildProgressUpdated(newValue));
}
}
}