Invalidate the analysis cache if a starlark option changes.
Without this change it was possible for an action to be in the skyframe cache whose outputs' contents had changed based on a starlark option without invalidating the action. Then if the action was requested again Bazel would assume that its outputs were fine and not re-run the action.
RELNOTES: None.
PiperOrigin-RevId: 281962111
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildOptions.java b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildOptions.java
index 9bee098..2682ad3 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildOptions.java
@@ -739,7 +739,7 @@
// specific options differ between the first and second built options.
private final Map<OptionDefinition, Object> first = new LinkedHashMap<>();
// Since this class can be used to track the result of transitions, {@link second} is a multimap
- // to be able to handle [@link SplitTransition}s.
+ // to be able to handle {@link SplitTransition}s.
private final Multimap<OptionDefinition, Object> second = OrderedSetMultimap.create();
// List of "extra" fragments for each BuildOption aka fragments that were trimmed off one
// BuildOption but not the other.
@@ -808,6 +808,19 @@
hasStarlarkOptions = true;
}
+ /**
+ * Returns the labels of all starlark options that caused a difference between the first and
+ * second options set.
+ */
+ public Set<Label> getChangedStarlarkOptions() {
+ return ImmutableSet.<Label>builder()
+ .addAll(skylarkFirst.keySet())
+ .addAll(skylarkSecond.keySet())
+ .addAll(extraStarlarkOptionsFirst)
+ .addAll(extraStarlarkOptionsSecond.keySet())
+ .build();
+ }
+
@VisibleForTesting
Map<Label, Object> getStarlarkFirstForTesting() {
return skylarkFirst;
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeBuildView.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeBuildView.java
index 7701ee3..201882e 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeBuildView.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeBuildView.java
@@ -27,6 +27,7 @@
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
+import com.google.common.collect.Streams;
import com.google.common.eventbus.EventBus;
import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
import com.google.devtools.build.lib.actions.ActionKeyContext;
@@ -179,90 +180,60 @@
}
/**
- * Describes the change between the current configuration collection and the incoming one,
- * limiting the number of options listed based on maxDifferencesToShow. Returns {@code null} if
- * the configurations have not changed in a way that requires the analysis cache to be
- * invalidated.
+ * Returns a description of the analysis-cache affecting changes between the current configuration
+ * collection and the incoming one.
+ *
+ * @param maxDifferencesToShow the maximum number of change-affecting options to include in the
+ * returned description
+ * @return a description or {@code null} if the configurations have not changed in a way that
+ * requires the analysis cache to be invalidated
*/
+ @Nullable
private String describeConfigurationDifference(
BuildConfigurationCollection configurations, int maxDifferencesToShow) {
if (this.configurations == null) {
- // no configurations currently, no need to drop anything
return null;
}
if (configurations.equals(this.configurations)) {
- // exact same configurations, no need to drop anything
return null;
}
+
ImmutableList<BuildConfiguration> oldTargetConfigs =
this.configurations.getTargetConfigurations();
ImmutableList<BuildConfiguration> newTargetConfigs = configurations.getTargetConfigurations();
- // Only compare the first configurations of each set regardless of whether there are more. All
- // options other than cpu will be identical across configurations; only --experimental_multi_cpu
- // (which is checked for below) can add more configurations, and it only sets --cpu.
- // We don't need to check the host configuration because it's derived from the target options.
+
+ // TODO(schmitt): We are only checking the first of the new configurations, even though (through
+ // split transitions) we could have more than one. There is some special handling for
+ // --cpu changing below but other options may also be changed and should be covered.
BuildConfiguration oldConfig = oldTargetConfigs.get(0);
BuildConfiguration newConfig = newTargetConfigs.get(0);
OptionsDiff diff = BuildOptions.diff(oldConfig.getOptions(), newConfig.getOptions());
- Stream<OptionDefinition> optionsWithCacheInvalidatingDifferences =
- diff.getFirst().keySet().stream()
- .filter(
- (definition) ->
- ruleClassProvider.shouldInvalidateCacheForOptionDiff(
- newConfig.getOptions(),
- definition,
- diff.getFirst().get(definition),
- Iterables.getOnlyElement(diff.getSecond().get(definition))));
- // --experimental_multi_cpu is currently the only way to have multiple configurations, but this
- // code is unable to see whether or how it is set, only infer it from the presence of multiple
- // configurations before or after the values changed and look at what the cpus of those
- // configurations are set to.
- if (Math.max(oldTargetConfigs.size(), newTargetConfigs.size()) > 1) {
- // Ignore changes to --cpu for consistency - depending on the old and new values of
- // --experimental_multi_cpu and how the order of configurations falls, we may or may not
- // register a --cpu change in the diff, and --experimental_multi_cpu overrides --cpu
- // anyway so it's redundant information as long as we have --experimental_multi_cpu change
- // detection.
- optionsWithCacheInvalidatingDifferences =
- optionsWithCacheInvalidatingDifferences.filter(
- (definition) -> !CoreOptions.CPU.equals(definition));
- ImmutableSet<String> oldCpus =
- oldTargetConfigs.stream().map(BuildConfiguration::getCpu).collect(toImmutableSet());
- ImmutableSet<String> newCpus =
- newTargetConfigs.stream().map(BuildConfiguration::getCpu).collect(toImmutableSet());
- if (!Objects.equals(oldCpus, newCpus)) {
- // --experimental_multi_cpu has changed, so inject that in the diff stream.
- optionsWithCacheInvalidatingDifferences =
- Stream.concat(
- Stream.of(BuildRequestOptions.EXPERIMENTAL_MULTI_CPU),
- optionsWithCacheInvalidatingDifferences);
- }
+ ImmutableSet<OptionDefinition> nativeCacheInvalidatingDifferences =
+ getNativeCacheInvalidatingDifferences(oldTargetConfigs, newTargetConfigs, newConfig, diff);
+ if (nativeCacheInvalidatingDifferences.isEmpty()
+ && diff.getChangedStarlarkOptions().isEmpty()) {
+ // The configuration may have changed, but none of the changes required a cache reset. For
+ // example, test trimming was turned on and a test option changed. In this case, nothing needs
+ // to be done.
+ return null;
}
+
if (maxDifferencesToShow == 0) {
- // with maxDifferencesToShow = 0, we're only concerned with whether _any_ option has changed,
- // so we don't need to do the option name transformation, and we only need to apply the
- // predicate until it finds one option that needs to be invalidated. Laziness go!
- return optionsWithCacheInvalidatingDifferences.findAny().isPresent()
- ? "Build options have changed"
- : null;
+ return "Build options have changed";
}
- // Otherwise, we go through the entire diff to generate a complete sorted list of option names.
- // Being lazy by applying a limit here could lead to inconsistent options being shown for
- // different values of maxDifferencesToShow here - the options displayed in the truncated list
- // would be dependent on the order in which the diff items are returned from keySet(), even
- // though the list is sorted.
+
ImmutableList<String> relevantDifferences =
- optionsWithCacheInvalidatingDifferences
- .map((definition) -> "--" + definition.getOptionName())
+ Streams.concat(
+ diff.getChangedStarlarkOptions().stream().map(Label::getCanonicalForm),
+ nativeCacheInvalidatingDifferences.stream().map(OptionDefinition::getOptionName))
+ .map(s -> "--" + s)
+ // Sorting the list to ensure that (if truncated through maxDifferencesToShow) the
+ // options in the message remain stable.
.sorted()
.collect(toImmutableList());
- if (relevantDifferences.isEmpty()) {
- // The configuration may have changed, but the predicate didn't think any of the changes
- // required a cache reset. For example, test trimming was turned on and a test option changed.
- // In this case, nothing needs to be done.
- return null;
- } else if (maxDifferencesToShow > 0 && relevantDifferences.size() > maxDifferencesToShow) {
+
+ if (maxDifferencesToShow > 0 && relevantDifferences.size() > maxDifferencesToShow) {
return String.format(
"Build options %s%s and %d more have changed",
Joiner.on(", ").join(relevantDifferences.subList(0, maxDifferencesToShow)),
@@ -282,6 +253,51 @@
}
}
+ // TODO(schmitt): This method assumes that the only option that can cause multiple target
+ // configurations is --cpu which (with the presence of split transitions) is no longer true.
+ private ImmutableSet<OptionDefinition> getNativeCacheInvalidatingDifferences(
+ ImmutableList<BuildConfiguration> oldTargetConfigs,
+ ImmutableList<BuildConfiguration> newTargetConfigs,
+ BuildConfiguration newConfig,
+ OptionsDiff diff) {
+ Stream<OptionDefinition> nativeCacheInvalidatingDifferences =
+ diff.getFirst().keySet().stream()
+ .filter(
+ (definition) ->
+ ruleClassProvider.shouldInvalidateCacheForOptionDiff(
+ newConfig.getOptions(),
+ definition,
+ diff.getFirst().get(definition),
+ Iterables.getOnlyElement(diff.getSecond().get(definition))));
+
+ // --experimental_multi_cpu is currently the only way to have multiple configurations, but this
+ // code is unable to see whether or how it is set, only infer it from the presence of multiple
+ // configurations before or after the values changed and look at what the cpus of those
+ // configurations are set to.
+ if (Math.max(oldTargetConfigs.size(), newTargetConfigs.size()) > 1) {
+ // Ignore changes to --cpu for consistency - depending on the old and new values of
+ // --experimental_multi_cpu and how the order of configurations falls, we may or may not
+ // register a --cpu change in the diff, and --experimental_multi_cpu overrides --cpu
+ // anyway so it's redundant information as long as we have --experimental_multi_cpu change
+ // detection.
+ nativeCacheInvalidatingDifferences =
+ nativeCacheInvalidatingDifferences.filter(
+ (definition) -> !CoreOptions.CPU.equals(definition));
+ ImmutableSet<String> oldCpus =
+ oldTargetConfigs.stream().map(BuildConfiguration::getCpu).collect(toImmutableSet());
+ ImmutableSet<String> newCpus =
+ newTargetConfigs.stream().map(BuildConfiguration::getCpu).collect(toImmutableSet());
+ if (!Objects.equals(oldCpus, newCpus)) {
+ // --experimental_multi_cpu has changed, so inject that in the diff stream.
+ nativeCacheInvalidatingDifferences =
+ Stream.concat(
+ Stream.of(BuildRequestOptions.EXPERIMENTAL_MULTI_CPU),
+ nativeCacheInvalidatingDifferences);
+ }
+ }
+ return nativeCacheInvalidatingDifferences.collect(toImmutableSet());
+ }
+
/** Sets the configurations. Not thread-safe. DO NOT CALL except from tests! */
@VisibleForTesting
public void setConfigurations(
@@ -297,9 +313,13 @@
} else {
String diff = describeConfigurationDifference(configurations, maxDifferencesToShow);
if (diff != null) {
- // Clearing cached ConfiguredTargets when the configuration changes is not required for
- // correctness, but prevents unbounded memory usage.
eventHandler.handle(Event.info(diff + ", discarding analysis cache."));
+ // Note that clearing the analysis cache is currently required for correctness. It is also
+ // helpful to save memory.
+ //
+ // If we had more memory, fixing the correctness issue (see also b/144932999) would allow us
+ // to not invalidate the cache, leading to potentially better performance on incremental
+ // builds.
skyframeExecutor.handleAnalysisInvalidatingChange();
}
}
diff --git a/src/test/shell/integration/starlark_configurations_test.sh b/src/test/shell/integration/starlark_configurations_test.sh
index 71c6c29..1551428 100755
--- a/src/test/shell/integration/starlark_configurations_test.sh
+++ b/src/test/shell/integration/starlark_configurations_test.sh
@@ -522,4 +522,71 @@
assert_equals $OUTPUT_CONFIG $TARGET_CONFIG
}
+function test_starlark_flag_change_causes_action_rerun() {
+ local -r pkg="$FUNCNAME"
+ mkdir -p "$pkg"
+
+ cat > "$pkg/rules.bzl" <<EOF
+BuildSettingInfo = provider(fields = ["value"])
+
+def _impl(ctx):
+ return BuildSettingInfo(value = ctx.build_setting_value)
+
+bool_flag = rule(
+ implementation = _impl,
+ build_setting = config.bool(flag = True),
+)
+EOF
+
+ cat > "$pkg/BUILD" <<EOF
+load(":rules.bzl", "bool_flag")
+
+bool_flag(
+ name = "myflag",
+ build_setting_default = True,
+)
+
+config_setting(
+ name = "myflag_selectable",
+ flag_values = {":myflag": "True"},
+)
+
+genrule(
+ name = "test_flag",
+ outs = ["out-flag.txt"],
+ cmd = "echo Value=" + select({
+ ":myflag_selectable": "True",
+ "//conditions:default": "False",
+ }) + " | tee \$@",
+)
+EOF
+
+ bazel shutdown
+
+ # Setting --noexperimental_check_output_files is required to reproduce the
+ # issue: The bug this test covers was caused by starlark flag changes not
+ # invalidating the analysis cache, resulting in Bazel not re-running the
+ # genrule action during the third invocation (because it sees the action in
+ # skyframe as already executed from the first invocation). Bazel will by
+ # default also invalidate actions by checking all output files for
+ # modification *unless* an output service is registered (which can -
+ # correctly - indicate that the output hasn't changed since the last build,
+ # here between the second and third runs). As we don't have an output service
+ # available in this test we disable output file checking entirely instead.
+ bazel build "//$pkg:test_flag" "--//$pkg:myflag=true" \
+ --noexperimental_check_output_files \
+ 2>>"$TEST_log" || fail "Expected build to succeed"
+ assert_equals "Value=True" "$(cat bazel-genfiles/$pkg/out-flag.txt)"
+
+ bazel build "//$pkg:test_flag" "--//$pkg:myflag=false" \
+ --noexperimental_check_output_files \
+ 2>>"$TEST_log" || fail "Expected build to succeed"
+ assert_equals "Value=False" "$(cat bazel-genfiles/$pkg/out-flag.txt)"
+
+ bazel build "//$pkg:test_flag" "--//$pkg:myflag=true" \
+ --noexperimental_check_output_files \
+ 2>>"$TEST_log" || fail "Expected build to succeed"
+ assert_equals "Value=True" "$(cat bazel-genfiles/$pkg/out-flag.txt)"
+}
+
run_suite "${PRODUCT_NAME} starlark configurations tests"