Rewrite the ConfigurationResolver for simplicity.
This removes a large number of obsolete optimizations and reduces the code size and complexity drastically.
Part of work on toolchain transitions, #10523.
Closes #11441.
PiperOrigin-RevId: 315482263
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/ConfigurationResolver.java b/src/main/java/com/google/devtools/build/lib/analysis/config/ConfigurationResolver.java
index dc27ad9..242cdd0 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/ConfigurationResolver.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/ConfigurationResolver.java
@@ -17,13 +17,9 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Functions;
import com.google.common.base.Joiner;
-import com.google.common.base.Verify;
-import com.google.common.base.VerifyException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.LinkedHashMultimap;
-import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.analysis.ConfigurationsCollector;
@@ -34,7 +30,6 @@
import com.google.devtools.build.lib.analysis.PlatformOptions;
import com.google.devtools.build.lib.analysis.TargetAndConfiguration;
import com.google.devtools.build.lib.analysis.config.transitions.ConfigurationTransition;
-import com.google.devtools.build.lib.analysis.config.transitions.NoTransition;
import com.google.devtools.build.lib.analysis.config.transitions.NullTransition;
import com.google.devtools.build.lib.analysis.config.transitions.SplitTransition;
import com.google.devtools.build.lib.analysis.config.transitions.TransitionFactory;
@@ -42,7 +37,6 @@
import com.google.devtools.build.lib.analysis.skylark.StarlarkTransition;
import com.google.devtools.build.lib.analysis.skylark.StarlarkTransition.TransitionException;
import com.google.devtools.build.lib.cmdline.Label;
-import com.google.devtools.build.lib.concurrent.ThreadSafety;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.ExtendedEventHandler;
import com.google.devtools.build.lib.events.StoredEventHandler;
@@ -57,7 +51,6 @@
import com.google.devtools.build.lib.skyframe.TransitiveTargetKey;
import com.google.devtools.build.lib.skyframe.TransitiveTargetValue;
import com.google.devtools.build.lib.util.OrderedSetMultimap;
-import com.google.devtools.build.lib.util.Pair;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.skyframe.SkyFunction;
import com.google.devtools.build.skyframe.SkyKey;
@@ -69,12 +62,10 @@
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import java.util.Set;
import javax.annotation.Nullable;
@@ -82,21 +73,76 @@
* Turns configuration transition requests into actual configurations.
*
* <p>This involves:
+ *
* <ol>
* <li>Patching a source configuration's options with the transition
* <li>If {@link BuildConfiguration#trimConfigurations} is true, trimming configuration fragments
* to only those needed by the destination target and its transitive dependencies
* <li>Getting the destination configuration from Skyframe
- * </ol>
+ * </ol>
*
- * <p>For the work of determining the transition requests themselves, see
- * {@link TransitionResolver}.
+ * <p>For the work of determining the transition requests themselves, see {@link
+ * TransitionResolver}.
*/
public final class ConfigurationResolver {
+
/**
- * Translates a set of {@link Dependency} objects with configuration transition requests to the
+ * Determines the output ordering of each {@code <attribute, depLabel> -> [dep<config1>,
+ * dep<config2>, ...]} collection produced by a split transition.
+ */
+ @VisibleForTesting
+ public static final Comparator<Dependency> SPLIT_DEP_ORDERING =
+ Comparator.comparing(
+ Functions.compose(BuildConfiguration::getMnemonic, Dependency::getConfiguration))
+ .thenComparing(
+ Functions.compose(BuildConfiguration::checksum, Dependency::getConfiguration));
+
+ // Signals that a Skyframe restart is needed.
+ private static class ValueMissingException extends Exception {
+ private ValueMissingException() {
+ super();
+ }
+ }
+
+ private final SkyFunction.Environment env;
+ private final TargetAndConfiguration ctgValue;
+ private final BuildConfiguration hostConfiguration;
+ private final BuildOptions defaultBuildOptions;
+ private final ImmutableMap<Label, ConfigMatchingProvider> configConditions;
+
+ public ConfigurationResolver(
+ SkyFunction.Environment env,
+ TargetAndConfiguration ctgValue,
+ BuildConfiguration hostConfiguration,
+ BuildOptions defaultBuildOptions,
+ ImmutableMap<Label, ConfigMatchingProvider> configConditions) {
+ this.env = env;
+ this.ctgValue = ctgValue;
+ this.hostConfiguration = hostConfiguration;
+ this.defaultBuildOptions = defaultBuildOptions;
+ this.configConditions = configConditions;
+ }
+
+ private BuildConfiguration getCurrentConfiguration() {
+ return ctgValue.getConfiguration();
+ }
+
+ /**
+ * Translates a set of {@link DependencyKey} objects with configuration transition requests to the
* same objects with resolved configurations.
*
+ * <p>This method must preserve the original label ordering of each attribute. For example, if
+ * {@code dependencyKeys.get("data")} is {@code [":a", ":b"]}, the resolved variant must also be
+ * {@code [":a", ":b"]} in the same order.
+ *
+ * <p>For split transitions, {@code dependencyKeys.get("data") = [":a", ":b"]} can produce the
+ * output {@code [":a"<config1>, ":a"<config2>, ..., ":b"<config1>, ":b"<config2>, ...]}. All
+ * instances of ":a" still appear before all instances of ":b". But the {@code [":a"<config1>,
+ * ":a"<config2>"]} subset may be in any (deterministic) order. In particular, this may not be the
+ * same order as {@link SplitTransition#split}. If needed, this code can be modified to use that
+ * order, but that involves more runtime in performance-critical code, so we won't make that
+ * change without a clear need.
+ *
* <p>If {@link BuildConfiguration#trimConfigurations()} is true, these configurations only
* contain the fragments needed by the dep and its transitive closure. Else they unconditionally
* include all fragments.
@@ -106,465 +152,237 @@
* the configured target graph, small inefficiencies can have observable impact on analysis time.
* Keep this in mind when making modifications and performance-test any changes you make.
*
- * @param env Skyframe evaluation environment
- * @param ctgValue the label and configuration of the source target
* @param dependencyKeys the transition requests for each dep and each dependency kind
- * @param hostConfiguration the host configuration
- * @param defaultBuildOptions default build options to diff options against for optimization
- * @param configConditions {@link ConfigMatchingProvider} map for the rule
* @return a mapping from each dependency kind in the source target to the {@link
* BuildConfiguration}s and {@link Label}s for the deps under that dependency kind . Returns
* null if not all Skyframe dependencies are available.
*/
@Nullable
- public static OrderedSetMultimap<DependencyKind, Dependency> resolveConfigurations(
- SkyFunction.Environment env,
- TargetAndConfiguration ctgValue,
- OrderedSetMultimap<DependencyKind, DependencyKey> dependencyKeys,
- BuildConfiguration hostConfiguration,
- BuildOptions defaultBuildOptions,
- ImmutableMap<Label, ConfigMatchingProvider> configConditions)
+ public OrderedSetMultimap<DependencyKind, Dependency> resolveConfigurations(
+ OrderedSetMultimap<DependencyKind, DependencyKey> dependencyKeys)
throws DependencyEvaluationException, InterruptedException {
-
- // Maps each Skyframe-evaluated BuildConfiguration to the dependencies that need that
- // configuration paired with a transition key corresponding to the BuildConfiguration. For cases
- // where Skyframe isn't needed to get the configuration (e.g. when we just re-used the original
- // rule's configuration), we should skip this outright.
- Multimap<SkyKey, Pair<Map.Entry<DependencyKind, DependencyKey>, String>> keysToEntries =
- LinkedListMultimap.create();
-
- // Stores the result of applying a transition to the current configuration using a
- // particular subset of fragments. By caching this, we save from redundantly computing the
- // same transition for every dependency edge that requests that transition. This can have
- // real effect on analysis time for commonly triggered transitions.
- //
- // Split transitions may map to one or multiple values. Patch transitions map to exactly one.
- Map<FragmentsAndTransition, Map<String, BuildOptions>> transitionsMap = new LinkedHashMap<>();
-
- BuildConfiguration currentConfiguration = ctgValue.getConfiguration();
-
- // Stores the configuration-resolved versions of each dependency. This method must preserve the
- // original label ordering of each attribute. For example, if dependencyKeys.get("data") is
- // [":a", ":b"], the resolved variant must also be [":a", ":b"] in the same order. Because we
- // may not actualize the results in order (some results need Skyframe-evaluated configurations
- // while others can be computed trivially), we dump them all into this map, then as a final step
- // iterate through the original list and pluck out values from here for the final value.
- //
- // For split transitions, dependencyKeys.get("data") = [":a", ":b"] can produce the output
- // [":a"<config1>, ":a"<config2>, ..., ":b"<config1>, ":b"<config2>, ...]. All instances of ":a"
- // still appear before all instances of ":b". But the [":a"<config1>, ":a"<config2>"] subset may
- // be in any (deterministic) order. In particular, this may not be the same order as
- // SplitTransition.split. If needed, this code can be modified to use that order, but that
- // involves more runtime in performance-critical code, so we won't make that change without a
- // clear need.
- //
- // This map is used heavily by all builds. Inserts and gets should be as fast as possible.
- Multimap<DependencyEdge, Dependency> resolvedDeps = LinkedHashMultimap.create();
-
- // Performance optimization: This method iterates over dependencyKeys twice. By storing
- // DependencyEdge instances in this list, we avoid having to recreate them the second time
- // (particularly avoid recomputing their hash codes). Profiling shows this shaves 25% off this
- // method's execution time (at the time of this comment).
- ArrayList<DependencyEdge> attributesAndLabels = new ArrayList<>(dependencyKeys.size());
-
- for (Map.Entry<DependencyKind, DependencyKey> depsEntry : dependencyKeys.entries()) {
- DependencyKey dep = depsEntry.getValue();
- DependencyKind depKind = depsEntry.getKey();
- DependencyEdge dependencyEdge = new DependencyEdge(depKind, dep.getLabel());
- attributesAndLabels.add(dependencyEdge);
-
- // The null configuration can be trivially computed (it's, well, null), so special-case that
- // transition here and skip the rest of the logic. A *lot* of targets have null deps, so
- // this produces real savings. Profiling tests over a simple cc_binary show this saves ~1% of
- // total analysis phase time.
- ConfigurationTransition transition = dep.getTransition();
- if (transition == NullTransition.INSTANCE) {
- Dependency finalDependency =
- Dependency.builder().withNullConfiguration().setLabel(dep.getLabel()).build();
- // If the base transition is a split transition, execute the transition and store returned
- // transition keys along with the null configuration dependency, so that other code relying
- // on stored transition keys doesn't have to implement special handling logic just for this
- // kind of cases.
- if (depKind.getAttribute() != null) {
- TransitionFactory<AttributeTransitionData> transitionFactory =
- depKind.getAttribute().getTransitionFactory();
- if (transitionFactory.isSplit()) {
- AttributeTransitionData transitionData =
- AttributeTransitionData.builder()
- .attributes(
- ConfiguredAttributeMapper.of(
- ctgValue.getTarget().getAssociatedRule(), configConditions))
- .build();
- ConfigurationTransition baseTransition = transitionFactory.create(transitionData);
- Map<String, BuildOptions> toOptions;
- try {
- // TODO(jungjw): See if we can dedup getBuildSettingPackages implementations and put
- // this in applyTransition.
- HashMap<PackageValue.Key, PackageValue> buildSettingPackages =
- StarlarkTransition.getBuildSettingPackages(env, baseTransition);
- if (buildSettingPackages == null) {
- return null;
- }
- toOptions =
- applyTransition(
- currentConfiguration.getOptions(),
- baseTransition,
- buildSettingPackages,
- env.getListener());
- } catch (TransitionException e) {
- throw new DependencyEvaluationException(e);
- }
- if (!SplitTransition.equals(currentConfiguration.getOptions(), toOptions.values())) {
- finalDependency =
- Dependency.builder()
- .setLabel(dep.getLabel())
- .withNullConfiguration()
- .setTransitionKeys(ImmutableList.copyOf(toOptions.keySet()))
- .build();
- }
- }
- }
- putOnlyEntry(resolvedDeps, dependencyEdge, finalDependency);
- continue;
- } else if (transition.isHostTransition()) {
- // The current rule's host configuration can also be used for the dep. We short-circuit
- // the standard transition logic for host transitions because these transitions are
- // uniquely frequent. It's possible, e.g., for every node in the configured target graph
- // to incur multiple host transitions. So we aggressively optimize to avoid hurting
- // analysis time.
- if (hostConfiguration.trimConfigurationsRetroactively() && !dep.getAspects().isEmpty()) {
- String message =
- ctgValue.getLabel()
- + " has aspects attached, but these are not supported in retroactive"
- + " trimming mode.";
- env.getListener()
- .handle(Event.error(TargetUtils.getLocationMaybe(ctgValue.getTarget()), message));
- throw new DependencyEvaluationException(new InvalidConfigurationException(message));
- }
- putOnlyEntry(
- resolvedDeps,
- dependencyEdge,
- Dependency.builder()
- .setLabel(dep.getLabel())
- .setConfiguration(hostConfiguration)
- .setAspects(dep.getAspects())
- .build());
- continue;
+ try {
+ OrderedSetMultimap<DependencyKind, Dependency> resolvedDeps = OrderedSetMultimap.create();
+ for (Map.Entry<DependencyKind, DependencyKey> entry : dependencyKeys.entries()) {
+ DependencyKind dependencyKind = entry.getKey();
+ DependencyKey dependencyKey = entry.getValue();
+ resolvedDeps.putAll(dependencyKind, resolveConfiguration(dependencyKind, dependencyKey));
}
+ return resolvedDeps;
+ } catch (ValueMissingException e) {
+ return null;
+ }
+ }
- // Figure out the required fragments for this dep and its transitive closure.
- Set<Class<? extends Fragment>> depFragments =
- getTransitiveFragments(env, dep.getLabel(), ctgValue.getConfiguration());
- if (depFragments == null) {
- return null;
- }
- // TODO(gregce): remove the below call once we have confidence trimmed configurations always
- // provide needed fragments. This unnecessarily drags performance on the critical path (up
- // to 0.5% of total analysis time as profiled over a simple cc_binary).
- if (ctgValue.getConfiguration().trimConfigurations()) {
- checkForMissingFragments(
- env, ctgValue, dependencyEdge.dependencyKind.getAttribute(), dep, depFragments);
- }
+ private ImmutableList<Dependency> resolveConfiguration(
+ DependencyKind dependencyKind, DependencyKey dependencyKey)
+ throws DependencyEvaluationException, ValueMissingException, InterruptedException {
- boolean sameFragments =
- depFragments.equals(currentConfiguration.fragmentClasses().fragmentClasses());
+ Dependency.Builder dependencyBuilder = Dependency.builder().setLabel(dependencyKey.getLabel());
- if (sameFragments) {
- if (transition == NoTransition.INSTANCE) {
- if (ctgValue.getConfiguration().trimConfigurationsRetroactively()
- && !dep.getAspects().isEmpty()) {
- String message =
- ctgValue.getLabel()
- + " has aspects attached, but these are not supported in retroactive"
- + " trimming mode.";
- env.getListener()
- .handle(Event.error(TargetUtils.getLocationMaybe(ctgValue.getTarget()), message));
- throw new DependencyEvaluationException(new InvalidConfigurationException(message));
- }
- // The dep uses the same exact configuration. Let's re-use the current configuration and
- // skip adding a Skyframe dependency edge on it.
- putOnlyEntry(
- resolvedDeps,
- dependencyEdge,
- Dependency.builder()
- .setLabel(dep.getLabel())
- .setConfiguration(ctgValue.getConfiguration())
- .setAspects(dep.getAspects())
- .build());
- continue;
- }
- }
-
- // Apply the transition or use the cached result if it was already applied.
- FragmentsAndTransition transitionKey = new FragmentsAndTransition(depFragments, transition);
- Map<String, BuildOptions> toOptions = transitionsMap.get(transitionKey);
- if (toOptions == null) {
- try {
- HashMap<PackageValue.Key, PackageValue> buildSettingPackages =
- StarlarkTransition.getBuildSettingPackages(env, transition);
- if (buildSettingPackages == null) {
- return null;
- }
- toOptions =
- applyTransition(
- currentConfiguration.getOptions(),
- transition,
- buildSettingPackages,
- env.getListener());
- } catch (TransitionException e) {
- throw new DependencyEvaluationException(e);
- }
- transitionsMap.put(transitionKey, toOptions);
- }
- // If the transition doesn't change the configuration, trivially re-use the original
- // configuration.
- if (sameFragments
- && toOptions.size() == 1
- && Iterables.getOnlyElement(toOptions.values())
- .equals(currentConfiguration.getOptions())) {
- if (ctgValue.getConfiguration().trimConfigurationsRetroactively()
- && !dep.getAspects().isEmpty()) {
- String message =
- ctgValue.getLabel()
- + " has aspects attached, but these are not supported in retroactive"
- + " trimming mode.";
- env.getListener()
- .handle(Event.error(TargetUtils.getLocationMaybe(ctgValue.getTarget()), message));
- throw new DependencyEvaluationException(new InvalidConfigurationException(message));
- }
- putOnlyEntry(
- resolvedDeps,
- dependencyEdge,
- Dependency.builder()
- .setLabel(dep.getLabel())
- .setConfiguration(ctgValue.getConfiguration())
- .setAspects(dep.getAspects())
- // Explicitly do not set the transition key, since there is only one configuration
- // and it matches the current one. This ignores the transition key set if this
- // was a split transition.
- .build());
- continue;
- }
-
- // If we get here, we have to get the configuration from Skyframe.
- PathFragment platformMappingPath =
- currentConfiguration.getOptions().get(PlatformOptions.class).platformMappings;
- PlatformMappingValue platformMappingValue =
- (PlatformMappingValue) env.getValue(PlatformMappingValue.Key.create(platformMappingPath));
- if (platformMappingValue == null) {
- return null;
- }
-
- try {
- for (Map.Entry<String, BuildOptions> optionsEntry : toOptions.entrySet()) {
- if (sameFragments) {
- keysToEntries.put(
- BuildConfigurationValue.keyWithPlatformMapping(
- platformMappingValue,
- defaultBuildOptions,
- currentConfiguration.fragmentClasses(),
- BuildOptions.diffForReconstruction(
- defaultBuildOptions, optionsEntry.getValue())),
- new Pair<>(depsEntry, optionsEntry.getKey()));
-
- } else {
- keysToEntries.put(
- BuildConfigurationValue.keyWithPlatformMapping(
- platformMappingValue,
- defaultBuildOptions,
- depFragments,
- BuildOptions.diffForReconstruction(
- defaultBuildOptions, optionsEntry.getValue())),
- new Pair<>(depsEntry, optionsEntry.getKey()));
- }
- }
- } catch (OptionsParsingException e) {
- throw new DependencyEvaluationException(new InvalidConfigurationException(e));
- }
+ ConfigurationTransition transition = dependencyKey.getTransition();
+ if (transition == NullTransition.INSTANCE) {
+ return ImmutableList.of(resolveNullTransition(dependencyBuilder, dependencyKind));
+ } else if (transition.isHostTransition()) {
+ return ImmutableList.of(resolveHostTransition(dependencyBuilder, dependencyKey));
}
- // Get all BuildConfigurations we need from Skyframe. While not every value might be available,
- // we don't call env.valuesMissing() here because that could be true from the earlier
- // resolver.dependentNodeMap call in computeDependencies, which also calls Skyframe. This method
- // doesn't need those missing values, but it still has to be called after
- // resolver.dependentNodeMap because it consumes that method's output. The reason the missing
- // values don't matter is because resolver.dependentNodeMap still returns "partial" results
- // and this method runs over whatever's available.
- //
- // While there would be no *correctness* harm in nulling out early, there's significant
- // *performance* harm. Profiling shows that putting "if (env.valuesMissing()) { return null; }"
- // here (or even after resolver.dependentNodeMap) produces a ~30% performance hit on the
- // analysis phase. That's because resolveConfiguredTargetDependencies and
- // resolveAspectDependencies don't get a chance to make their own Skyframe requests before
- // bailing out of this ConfiguredTargetFunction call. Ideally we could batch all requests
- // from all methods into a single Skyframe call, but there are enough subtle data flow
- // dependencies in ConfiguredTargetFunction to make that impractical.
- Map<SkyKey, ValueOrException<InvalidConfigurationException>> depConfigValues =
- env.getValuesOrThrow(keysToEntries.keySet(), InvalidConfigurationException.class);
+ // Figure out the required fragments for this dep and its transitive closure.
+ Set<Class<? extends Fragment>> depFragments =
+ getTransitiveFragments(dependencyKey.getLabel(), getCurrentConfiguration());
- // Now fill in the remaining unresolved deps with the now-resolved configurations.
+ // TODO(gregce): remove the below call once we have confidence trimmed configurations always
+ // provide needed fragments. This unnecessarily drags performance on the critical path (up
+ // to 0.5% of total analysis time as profiled over a simple cc_binary).
+ if (getCurrentConfiguration().trimConfigurations()) {
+ checkForMissingFragments(dependencyKind.getAttribute(), dependencyKey, depFragments);
+ }
+
+ return resolveGenericTransition(depFragments, dependencyBuilder, dependencyKey);
+ }
+
+ private Dependency resolveNullTransition(
+ Dependency.Builder dependencyBuilder, DependencyKind dependencyKind)
+ throws DependencyEvaluationException, ValueMissingException, InterruptedException {
+ // The null configuration can be trivially computed (it's, well, null), so special-case that
+ // transition here and skip the rest of the logic. A *lot* of targets have null deps, so
+ // this produces real savings. Profiling tests over a simple cc_binary show this saves ~1% of
+ // total analysis phase time.
+ if (dependencyKind.getAttribute() != null) {
+ dependencyBuilder.setTransitionKeys(collectTransitionKeys(dependencyKind.getAttribute()));
+ }
+
+ return dependencyBuilder.withNullConfiguration().build();
+ }
+
+ private Dependency resolveHostTransition(
+ Dependency.Builder dependencyBuilder, DependencyKey dependencyKey)
+ throws DependencyEvaluationException {
+ // The current rule's host configuration can also be used for the dep. We short-circuit
+ // the standard transition logic for host transitions because these transitions are
+ // uniquely frequent. It's possible, e.g., for every node in the configured target graph
+ // to incur multiple host transitions. So we aggressively optimize to avoid hurting
+ // analysis time.
+ if (hostConfiguration.trimConfigurationsRetroactively()
+ && !dependencyKey.getAspects().isEmpty()) {
+ String message =
+ ctgValue.getLabel()
+ + " has aspects attached, but these are not supported in retroactive"
+ + " trimming mode.";
+ env.getListener()
+ .handle(Event.error(TargetUtils.getLocationMaybe(ctgValue.getTarget()), message));
+ throw new DependencyEvaluationException(new InvalidConfigurationException(message));
+ }
+
+ return dependencyBuilder
+ .setConfiguration(hostConfiguration)
+ .setAspects(dependencyKey.getAspects())
+ .build();
+ }
+
+ private ImmutableList<Dependency> resolveGenericTransition(
+ Set<Class<? extends Fragment>> depFragments,
+ Dependency.Builder dependencyBuilder,
+ DependencyKey dependencyKey)
+ throws DependencyEvaluationException, InterruptedException, ValueMissingException {
+ Map<String, BuildOptions> toOptions;
try {
- for (Map.Entry<SkyKey, ValueOrException<InvalidConfigurationException>> entry :
- depConfigValues.entrySet()) {
- SkyKey key = entry.getKey();
- ValueOrException<InvalidConfigurationException> valueOrException = entry.getValue();
+ HashMap<PackageValue.Key, PackageValue> buildSettingPackages =
+ StarlarkTransition.getBuildSettingPackages(env, dependencyKey.getTransition());
+ if (buildSettingPackages == null) {
+ throw new ValueMissingException();
+ }
+ toOptions =
+ applyTransition(
+ getCurrentConfiguration().getOptions(),
+ dependencyKey.getTransition(),
+ buildSettingPackages,
+ env.getListener());
+ } catch (TransitionException e) {
+ throw new DependencyEvaluationException(e);
+ }
+
+ if (depFragments.equals(getCurrentConfiguration().fragmentClasses().fragmentClasses())
+ && SplitTransition.equals(getCurrentConfiguration().getOptions(), toOptions.values())) {
+ // The dep uses the same exact configuration. Let's re-use the current configuration and
+ // skip adding a Skyframe dependency edge on it.
+ return ImmutableList.of(
+ dependencyBuilder
+ .setConfiguration(getCurrentConfiguration())
+ .setAspects(dependencyKey.getAspects())
+ // Explicitly do not set the transition key, since there is only one configuration
+ // and it matches the current one. This ignores the transition key set if this
+ // was a split transition.
+ .build());
+ }
+
+ PathFragment platformMappingPath =
+ getCurrentConfiguration().getOptions().get(PlatformOptions.class).platformMappings;
+ PlatformMappingValue platformMappingValue =
+ (PlatformMappingValue) env.getValue(PlatformMappingValue.Key.create(platformMappingPath));
+ if (platformMappingValue == null) {
+ throw new ValueMissingException();
+ }
+
+ Map<String, BuildConfigurationValue.Key> configurationKeys = new HashMap<>();
+ try {
+ for (Map.Entry<String, BuildOptions> optionsEntry : toOptions.entrySet()) {
+ String transitionKey = optionsEntry.getKey();
+ BuildConfigurationValue.Key buildConfigurationValueKey =
+ BuildConfigurationValue.keyWithPlatformMapping(
+ platformMappingValue,
+ defaultBuildOptions,
+ depFragments,
+ BuildOptions.diffForReconstruction(defaultBuildOptions, optionsEntry.getValue()));
+ configurationKeys.put(transitionKey, buildConfigurationValueKey);
+ }
+ } catch (OptionsParsingException e) {
+ throw new DependencyEvaluationException(new InvalidConfigurationException(e));
+ }
+
+ Map<SkyKey, ValueOrException<InvalidConfigurationException>> depConfigValues =
+ env.getValuesOrThrow(configurationKeys.values(), InvalidConfigurationException.class);
+ List<Dependency> dependencies = new ArrayList<>();
+ try {
+ for (Map.Entry<String, BuildConfigurationValue.Key> entry : configurationKeys.entrySet()) {
+ String transitionKey = entry.getKey();
+ ValueOrException<InvalidConfigurationException> valueOrException =
+ depConfigValues.get(entry.getValue());
if (valueOrException.get() == null) {
- // Instead of env.missingValues(), check for missing values here. This guarantees we only
- // null out on missing values from *this specific Skyframe request*.
- return null;
+ continue;
}
- BuildConfiguration trimmedConfig =
+ BuildConfiguration configuration =
((BuildConfigurationValue) valueOrException.get()).getConfiguration();
- for (Pair<Map.Entry<DependencyKind, DependencyKey>, String> info : keysToEntries.get(key)) {
- DependencyKey originalDep = info.first.getValue();
- if (trimmedConfig.trimConfigurationsRetroactively()
- && !originalDep.getAspects().isEmpty()) {
- String message =
- ctgValue.getLabel()
- + " has aspects attached, but these are not supported in retroactive"
- + " trimming mode.";
- env.getListener()
- .handle(Event.error(TargetUtils.getLocationMaybe(ctgValue.getTarget()), message));
- throw new DependencyEvaluationException(new InvalidConfigurationException(message));
- }
- DependencyEdge attr = new DependencyEdge(info.first.getKey(), originalDep.getLabel());
+ if (configuration != null) {
Dependency resolvedDep =
- Dependency.builder()
- .setLabel(originalDep.getLabel())
- .setConfiguration(trimmedConfig)
- .setAspects(originalDep.getAspects())
- .setTransitionKey(info.second)
+ dependencyBuilder
+ // Copy the builder so we don't overwrite the other dependencies.
+ .copy()
+ .setConfiguration(configuration)
+ .setAspects(dependencyKey.getAspects())
+ .setTransitionKey(transitionKey)
.build();
- Attribute attribute = attr.dependencyKind.getAttribute();
- if (attribute != null && attribute.getTransitionFactory().isSplit()) {
- resolvedDeps.put(attr, resolvedDep);
- } else {
- putOnlyEntry(resolvedDeps, attr, resolvedDep);
- }
+ dependencies.add(resolvedDep);
}
}
+ if (env.valuesMissing()) {
+ throw new ValueMissingException();
+ }
} catch (InvalidConfigurationException e) {
throw new DependencyEvaluationException(e);
}
- return sortResolvedDeps(dependencyKeys, resolvedDeps, attributesAndLabels);
+ Collections.sort(dependencies, SPLIT_DEP_ORDERING);
+ return ImmutableList.copyOf(dependencies);
}
- /**
- * Encapsulates a set of config fragments and a config transition. This can be used to determine
- * the exact build options needed to set a configuration.
- */
- @ThreadSafety.Immutable
- private static final class FragmentsAndTransition {
- // Treat this as immutable. The only reason this isn't an ImmutableSet is because it
- // gets bound to a NestedSet.toSet() reference, which returns a Set interface.
- final Set<Class<? extends Fragment>> fragments;
- final ConfigurationTransition transition;
- private final int hashCode;
-
- FragmentsAndTransition(
- Set<Class<? extends Fragment>> fragments, ConfigurationTransition transition) {
- this.fragments = fragments;
- this.transition = transition;
- hashCode = Objects.hash(this.fragments, this.transition);
- }
-
- @Override
- public boolean equals(Object o) {
- if (o == this) {
- return true;
- } else if (o == null) {
- return false;
- } else {
- if (!(o instanceof FragmentsAndTransition)) {
- return false;
+ private ImmutableList<String> collectTransitionKeys(Attribute attribute)
+ throws DependencyEvaluationException, ValueMissingException, InterruptedException {
+ TransitionFactory<AttributeTransitionData> transitionFactory = attribute.getTransitionFactory();
+ if (transitionFactory.isSplit()) {
+ AttributeTransitionData transitionData =
+ AttributeTransitionData.builder()
+ .attributes(
+ ConfiguredAttributeMapper.of(
+ ctgValue.getTarget().getAssociatedRule(), configConditions))
+ .build();
+ ConfigurationTransition baseTransition = transitionFactory.create(transitionData);
+ Map<String, BuildOptions> toOptions;
+ try {
+ // TODO(jungjw): See if we can dedup getBuildSettingPackages implementations and put
+ // this in applyTransition.
+ HashMap<PackageValue.Key, PackageValue> buildSettingPackages =
+ StarlarkTransition.getBuildSettingPackages(env, baseTransition);
+ if (buildSettingPackages == null) {
+ throw new ValueMissingException();
}
- FragmentsAndTransition other = (FragmentsAndTransition) o;
- return other.transition.equals(transition) && other.fragments.equals(fragments);
+ toOptions =
+ applyTransition(
+ getCurrentConfiguration().getOptions(),
+ baseTransition,
+ buildSettingPackages,
+ env.getListener());
+ } catch (TransitionException e) {
+ throw new DependencyEvaluationException(e);
+ }
+ if (!SplitTransition.equals(getCurrentConfiguration().getOptions(), toOptions.values())) {
+ return ImmutableList.copyOf(toOptions.keySet());
}
}
- @Override
- public int hashCode() {
- return hashCode;
- }
- }
-
- /**
- * Encapsulates an <attribute, label> pair that can be used to map from an input dependency to a
- * trimmed dependency.
- */
- @ThreadSafety.Immutable
- private static final class DependencyEdge {
- final DependencyKind dependencyKind;
- final Label label;
- Integer hashCode;
-
- DependencyEdge(DependencyKind dependencyKind, Label label) {
- this.dependencyKind = dependencyKind;
- this.label = label;
- }
-
- @Override
- public boolean equals(Object o) {
- if (!(o instanceof DependencyEdge)) {
- return false;
- }
- DependencyEdge other = (DependencyEdge) o;
- return Objects.equals(other.dependencyKind, dependencyKind) && other.label.equals(label);
- }
-
- @Override
- public int hashCode() {
- if (hashCode == null) {
- // Not every <Attribute, Label> pair gets hashed. So only evaluate for the instances that
- // need it. This can significantly reduce the number of evaluations.
- hashCode = Objects.hash(this.dependencyKind, this.label);
- }
- return hashCode;
- }
-
- @Override
- public String toString() {
- Attribute attribute = dependencyKind.getAttribute();
-
- return "DependencyEdge{attribute="
- + (attribute == null ? "(null)" : attribute)
- + ", label="
- + label
- + "}";
- }
- }
-
- /**
- * Variation of {@link Multimap#put} that triggers an exception if a value already exists.
- */
- @VisibleForTesting
- public static <K, V> void putOnlyEntry(Multimap<K, V> map, K key, V value) {
- // Performance note: while "Verify.verify(!map.containsKey(key, value), String.format(...)))"
- // is simpler code, profiling shows a substantial performance penalty to that approach
- // (~10% extra analysis phase time on a simple cc_binary). Most of that is from the cost of
- // evaluating value.toString() on every call. This approach essentially eliminates the overhead.
- if (map.containsKey(key)) {
- throw new VerifyException(
- String.format("couldn't insert %s: map already has values for key %s: %s",
- value.toString(), key.toString(), map.get(key).toString()));
- }
- map.put(key, value);
+ return ImmutableList.of();
}
/**
* Returns the configuration fragments required by a dep and its transitive closure. Returns null
* if Skyframe dependencies aren't yet available.
*
- * @param env Skyframe evaluation environment
* @param dep label of the dep to check
* @param parentConfig configuration of the rule depending on the dep
*/
- @Nullable
- private static Set<Class<? extends Fragment>> getTransitiveFragments(
- SkyFunction.Environment env, Label dep, BuildConfiguration parentConfig)
- throws InterruptedException {
+ private ImmutableSet<Class<? extends Fragment>> getTransitiveFragments(
+ Label dep, BuildConfiguration parentConfig)
+ throws InterruptedException, ValueMissingException {
if (!parentConfig.trimConfigurations()) {
return parentConfig.getFragmentsMap().keySet();
}
@@ -574,7 +392,7 @@
// This should only be possible for tests. In actual runs, this was already called
// as a routine part of the loading phase.
// TODO(bazel-team): check this only occurs in a test context.
- return null;
+ throw new ValueMissingException();
}
return transitiveDepInfo.getTransitiveConfigFragments().toSet();
}
@@ -646,15 +464,11 @@
* Checks the config fragments required by a dep against the fragments in its actual
* configuration. If any are missing, triggers a descriptive "missing fragments" error.
*/
- private static void checkForMissingFragments(
- SkyFunction.Environment env,
- TargetAndConfiguration ctgValue,
- Attribute attribute,
- DependencyKey dep,
- Set<Class<? extends Fragment>> expectedDepFragments)
+ private void checkForMissingFragments(
+ Attribute attribute, DependencyKey dep, Set<Class<? extends Fragment>> expectedDepFragments)
throws DependencyEvaluationException {
Set<String> ctgFragmentNames = new HashSet<>();
- for (Fragment fragment : ctgValue.getConfiguration().getFragmentsMap().values()) {
+ for (Fragment fragment : getCurrentConfiguration().getFragmentsMap().values()) {
ctgFragmentNames.add(fragment.getClass().getSimpleName());
}
Set<String> depFragmentNames = new HashSet<>();
@@ -676,46 +490,6 @@
}
/**
- * Determines the output ordering of each {@code <attribute, depLabel> -> [dep<config1>,
- * dep<config2>, ...]} collection produced by a split transition.
- */
- @VisibleForTesting
- public static final Comparator<Dependency> SPLIT_DEP_ORDERING =
- Comparator.comparing(
- Functions.compose(BuildConfiguration::getMnemonic, Dependency::getConfiguration))
- .thenComparing(
- Functions.compose(BuildConfiguration::checksum, Dependency::getConfiguration));
-
- /**
- * Returns a copy of the output deps using the same key and value ordering as the input deps.
- *
- * @param dependencyKeys the input deps with the ordering to preserve
- * @param resolvedDeps the unordered output deps
- * @param attributesAndLabels collection of <attribute, depLabel> pairs guaranteed to match the
- * ordering of dependencyKeys.entries(). This is a performance optimization: see {@link
- * #resolveConfigurations#attributesAndLabels} for details.
- */
- private static OrderedSetMultimap<DependencyKind, Dependency> sortResolvedDeps(
- OrderedSetMultimap<DependencyKind, DependencyKey> dependencyKeys,
- Multimap<DependencyEdge, Dependency> resolvedDeps,
- ArrayList<DependencyEdge> attributesAndLabels) {
- Iterator<DependencyEdge> iterator = attributesAndLabels.iterator();
- OrderedSetMultimap<DependencyKind, Dependency> result = OrderedSetMultimap.create();
- for (Map.Entry<DependencyKind, DependencyKey> depsEntry : dependencyKeys.entries()) {
- DependencyEdge edge = iterator.next();
- Collection<Dependency> resolvedDepWithSplit = resolvedDeps.get(edge);
- Verify.verify(!resolvedDepWithSplit.isEmpty());
- if (resolvedDepWithSplit.size() > 1) {
- List<Dependency> sortedSplitList = new ArrayList<>(resolvedDepWithSplit);
- Collections.sort(sortedSplitList, SPLIT_DEP_ORDERING);
- resolvedDepWithSplit = sortedSplitList;
- }
- result.putAll(depsEntry.getKey(), resolvedDepWithSplit);
- }
- return result;
- }
-
- /**
* This method allows resolution of configurations outside of a skyfunction call.
*
* <p>Unlike {@link #resolveConfigurations}, this doesn't expect the current context to be
@@ -825,4 +599,3 @@
}
}
}
-
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ConfiguredTargetFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/ConfiguredTargetFunction.java
index 8fc039e..55a4de5 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/ConfiguredTargetFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ConfiguredTargetFunction.java
@@ -634,14 +634,11 @@
}
// Trim each dep's configuration so it only includes the fragments needed by its transitive
// closure.
+ ConfigurationResolver configResolver =
+ new ConfigurationResolver(
+ env, ctgValue, hostConfiguration, defaultBuildOptions, configConditions);
OrderedSetMultimap<DependencyKind, Dependency> depValueNames =
- ConfigurationResolver.resolveConfigurations(
- env,
- ctgValue,
- initialDependencies,
- hostConfiguration,
- defaultBuildOptions,
- configConditions);
+ configResolver.resolveConfigurations(initialDependencies);
// Return early in case packages were not loaded yet. In theory, we could start configuring
// dependent targets in loaded packages. However, that creates an artificial sync boundary
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/ConfigurationsForTargetsTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/ConfigurationsForTargetsTest.java
index ae07bbf..2821a20 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/ConfigurationsForTargetsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/ConfigurationsForTargetsTest.java
@@ -15,19 +15,13 @@
package com.google.devtools.build.lib.skyframe;
import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.assertThrows;
import com.google.common.base.Supplier;
-import com.google.common.base.VerifyException;
-import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Collections2;
-import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
-import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
-import com.google.common.collect.SetMultimap;
import com.google.devtools.build.lib.analysis.AliasProvider;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
@@ -270,41 +264,6 @@
}
@Test
- public void putOnlyEntryCorrectWithSetMultimap() throws Exception {
- internalTestPutOnlyEntry(HashMultimap.<String, String>create());
- }
-
- /**
- * Unlike {@link SetMultimap}, {@link ListMultimap} allows duplicate <Key, value> pairs. Make
- * sure that doesn't fool {@link ConfigurationResolver#putOnlyEntry}.
- */
- @Test
- public void putOnlyEntryCorrectWithListMultimap() throws Exception {
- internalTestPutOnlyEntry(ArrayListMultimap.<String, String>create());
- }
-
- private static void internalTestPutOnlyEntry(Multimap<String, String> map) throws Exception {
- ConfigurationResolver.putOnlyEntry(map, "foo", "bar");
- ConfigurationResolver.putOnlyEntry(map, "baz", "bar");
- VerifyException e =
- assertThrows(
- "Expected an exception when trying to add a new value to an existing key",
- VerifyException.class,
- () -> ConfigurationResolver.putOnlyEntry(map, "foo", "baz"));
- assertThat(e)
- .hasMessageThat()
- .isEqualTo("couldn't insert baz: map already has values for key foo: [bar]");
- e =
- assertThrows(
- "Expected an exception when trying to add a pre-existing <key, value> pair",
- VerifyException.class,
- () -> ConfigurationResolver.putOnlyEntry(map, "foo", "bar"));
- assertThat(e)
- .hasMessageThat()
- .isEqualTo("couldn't insert bar: map already has values for key foo: [bar]");
- }
-
- @Test
public void nullConfiguredDepsHaveExpectedConfigs() throws Exception {
scratch.file(
"a/BUILD",