blob: 5f666b893f300e0b205d827a27bb8d091efe5e43 [file] [log] [blame]
// Copyright 2018 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.util;
import com.google.common.collect.ImmutableMap;
import com.google.common.primitives.Ints;
import com.google.devtools.build.lib.actions.LocalHostCapacity;
import com.google.devtools.common.options.Converters;
import com.google.devtools.common.options.OptionsParsingException;
import java.util.Map;
import java.util.function.DoubleBinaryOperator;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
/**
* Converter for options that configure Bazel's resource usage.
*
* <p>The option can take either a value or one of the keywords {@code auto}, {@code HOST_CPUS}, or
* {@code HOST_RAM}, followed by an optional operator in the form {@code [-|*]<float>}.
*
* <p>If a keyword is passed, the converter returns the keyword's value in the {@link #keywords}
* map, scaled by the operation that follows if there is one. All values, explicit and derived, are
* adjusted for validity.
*
* <p>The supplier of the auto value, and, optionally, a max or min allowed value (inclusive), are
* passed to the constructor.
*/
public class ResourceConverter extends Converters.IntegerConverter {
private static final ImmutableMap<String, DoubleBinaryOperator> OPERATORS =
ImmutableMap.<String, DoubleBinaryOperator>builder()
.put("-", (l, r) -> l - r)
.put("*", (l, r) -> l * r)
.build();
/** Description of the accepted inputs to the converter. */
public static final String FLAG_SYNTAX =
"an integer, or a keyword (\"auto\", \"HOST_CPUS\", \"HOST_RAM\"), optionally followed by "
+ "an operation ([-|*]<float>) eg. \"auto\", \"HOST_CPUS*.5\"";
private final ImmutableMap<String, Supplier<Integer>> keywords;
private final Pattern validInputPattern;
protected final int minValue;
protected final int maxValue;
/**
* Constructs a ResourceConverter for options that take {@value FLAG_SYNTAX}
*
* @param autoSupplier a supplier for the value of the auto keyword
* @param minValue the minimum allowed value
* @param maxValue the maximum allowed value
*/
public ResourceConverter(Supplier<Integer> autoSupplier, int minValue, int maxValue) {
this(
ImmutableMap.<String, Supplier<Integer>>builder()
.put("auto", autoSupplier)
.put(
"HOST_CPUS",
() -> (int) Math.ceil(LocalHostCapacity.getLocalHostCapacity().getCpuUsage()))
.put(
"HOST_RAM",
() -> (int) Math.ceil(LocalHostCapacity.getLocalHostCapacity().getMemoryMb()))
.build(),
minValue,
maxValue);
}
/**
* Constructs a ResourceConverter for options that take {@value FLAG_SYNTAX} and accept any value
* greater than 1.
*
* @param autoSupplier a supplier for the value of the auto keyword
*/
public ResourceConverter(Supplier<Integer> autoSupplier) {
this(autoSupplier, 1, Integer.MAX_VALUE);
}
/**
* Constructs a ResourceConverter for options that take keywords other than the default set.
*
* @param keywords a map of keyword to the suppliers of their values
*/
public ResourceConverter(
ImmutableMap<String, Supplier<Integer>> keywords, int minValue, int maxValue) {
this.keywords = keywords;
this.validInputPattern =
Pattern.compile(
String.format(
"(?<keyword>%s)(?<expression>[%s][0-9]?(?:.[0-9]+)?)?",
String.join("|", this.keywords.keySet()), String.join("", OPERATORS.keySet())));
this.minValue = minValue;
this.maxValue = maxValue;
}
@Override
public final Integer convert(String input) throws OptionsParsingException {
int value;
if (Ints.tryParse(input) != null) {
value = super.convert(input);
return checkAndLimit(value);
}
Matcher matcher = validInputPattern.matcher(input);
if (matcher.matches()) {
Supplier<Integer> resourceSupplier = keywords.get(matcher.group("keyword"));
if (resourceSupplier != null) {
value = applyOperator(matcher.group("expression"), resourceSupplier);
return checkAndLimit(value);
}
}
throw new OptionsParsingException(
String.format(
"Parameter '%s' does not follow correct syntax. This flag takes %s.",
input, getTypeDescription()));
}
/** Applies function designated in {@code expression} ([-|*]<float>) to value. */
private Integer applyOperator(@Nullable String expression, Supplier<Integer> firstOperandSupplier)
throws OptionsParsingException {
if (expression == null) {
return firstOperandSupplier.get();
}
for (Map.Entry<String, DoubleBinaryOperator> operator : OPERATORS.entrySet()) {
if (expression.startsWith(operator.getKey())) {
float secondOperand;
try {
secondOperand = Float.parseFloat(expression.substring(operator.getKey().length()));
} catch (NumberFormatException e) {
throw new OptionsParsingException(
String.format("'%s is not a float", expression.substring(operator.getKey().length())),
e);
}
return (int)
Math.round(
operator
.getValue()
.applyAsDouble((float) firstOperandSupplier.get(), secondOperand));
}
}
// This should never happen because we've checked for a valid operator already.
throw new OptionsParsingException(
String.format("Parameter value '%s' does not contain a valid operator.", expression));
}
/**
* Checks validity of a resource value against min/max constraints. Implementations may choose to
* either raise an exception on out-of-bounds values, or adjust them to within the constraints.
*/
public int checkAndLimit(int value) throws OptionsParsingException {
if (value < minValue) {
throw new OptionsParsingException(
String.format("Value '(%d)' must be at least %d.", value, minValue));
}
if (value > maxValue) {
throw new OptionsParsingException(
String.format("Value '(%d)' cannot be greater than %d.", value, maxValue));
}
return value;
}
@Override
public String getTypeDescription() {
return FLAG_SYNTAX;
}
}