Java tutorial
// Copyright 2014 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.skyframe; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Predicate; 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.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import com.google.devtools.build.lib.actions.ActionAnalysisMetadata; import com.google.devtools.build.lib.actions.Actions; import com.google.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException; import com.google.devtools.build.lib.analysis.CachingAnalysisEnvironment; import com.google.devtools.build.lib.analysis.ConfiguredAspect; import com.google.devtools.build.lib.analysis.ConfiguredTarget; import com.google.devtools.build.lib.analysis.Dependency; import com.google.devtools.build.lib.analysis.LabelAndConfiguration; import com.google.devtools.build.lib.analysis.MergedConfiguredTarget; import com.google.devtools.build.lib.analysis.MergedConfiguredTarget.DuplicateException; import com.google.devtools.build.lib.analysis.TargetAndConfiguration; import com.google.devtools.build.lib.analysis.TransitiveInfoProvider; import com.google.devtools.build.lib.analysis.config.BuildConfiguration; import com.google.devtools.build.lib.analysis.config.BuildOptions; import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider; import com.google.devtools.build.lib.analysis.config.HostTransition; import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException; import com.google.devtools.build.lib.analysis.config.PatchTransition; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.collect.nestedset.NestedSet; import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; import com.google.devtools.build.lib.collect.nestedset.Order; import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable; import com.google.devtools.build.lib.events.Event; import com.google.devtools.build.lib.events.StoredEventHandler; import com.google.devtools.build.lib.packages.Aspect; import com.google.devtools.build.lib.packages.AspectDescriptor; import com.google.devtools.build.lib.packages.Attribute; import com.google.devtools.build.lib.packages.BuildType; import com.google.devtools.build.lib.packages.NoSuchTargetException; import com.google.devtools.build.lib.packages.NoSuchThingException; import com.google.devtools.build.lib.packages.Package; import com.google.devtools.build.lib.packages.RawAttributeMapper; import com.google.devtools.build.lib.packages.Rule; import com.google.devtools.build.lib.packages.RuleClassProvider; import com.google.devtools.build.lib.packages.SkylarkProviderIdentifier; import com.google.devtools.build.lib.packages.Target; import com.google.devtools.build.lib.packages.TargetUtils; import com.google.devtools.build.lib.skyframe.AspectFunction.AspectCreationException; import com.google.devtools.build.lib.skyframe.AspectValue.AspectKey; import com.google.devtools.build.lib.skyframe.SkyframeExecutor.BuildViewProvider; import com.google.devtools.build.lib.syntax.EvalException; import com.google.devtools.build.lib.util.OrderedSetMultimap; import com.google.devtools.build.lib.util.Preconditions; import com.google.devtools.build.skyframe.SkyFunction; import com.google.devtools.build.skyframe.SkyFunctionException; import com.google.devtools.build.skyframe.SkyKey; import com.google.devtools.build.skyframe.SkyValue; import com.google.devtools.build.skyframe.ValueOrException; import com.google.devtools.build.skyframe.ValueOrException2; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import java.util.concurrent.Semaphore; import javax.annotation.Nullable; /** * SkyFunction for {@link ConfiguredTargetValue}s. * * This class, together with {@link AspectFunction} drives the analysis phase. For more information, * see {@link com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory}. * * @see com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory */ final class ConfiguredTargetFunction implements SkyFunction { // This construction is a bit funky, but guarantees that the Object reference here is globally // unique. static final ImmutableMap<Label, ConfigMatchingProvider> NO_CONFIG_CONDITIONS = ImmutableMap .<Label, ConfigMatchingProvider>of(); /** * Exception class that signals an error during the evaluation of a dependency. */ public static class DependencyEvaluationException extends Exception { public DependencyEvaluationException(InvalidConfigurationException cause) { super(cause); } public DependencyEvaluationException(ConfiguredValueCreationException cause) { super(cause); } @Override public synchronized Exception getCause() { return (Exception) super.getCause(); } } private static final Function<Dependency, SkyKey> TO_KEYS = new Function<Dependency, SkyKey>() { @Override public SkyKey apply(Dependency input) { return ConfiguredTargetValue.key(input.getLabel(), input.getConfiguration()); } }; private final BuildViewProvider buildViewProvider; private final RuleClassProvider ruleClassProvider; private final Semaphore cpuBoundSemaphore; ConfiguredTargetFunction(BuildViewProvider buildViewProvider, RuleClassProvider ruleClassProvider, Semaphore cpuBoundSemaphore) { this.buildViewProvider = buildViewProvider; this.ruleClassProvider = ruleClassProvider; this.cpuBoundSemaphore = cpuBoundSemaphore; } private static boolean useDynamicConfigurations(BuildConfiguration config) { return config != null && config.useDynamicConfigurations(); } @Override public SkyValue compute(SkyKey key, Environment env) throws ConfiguredTargetFunctionException, InterruptedException { SkyframeBuildView view = buildViewProvider.getSkyframeBuildView(); NestedSetBuilder<Package> transitivePackages = NestedSetBuilder.stableOrder(); NestedSetBuilder<Label> transitiveLoadingRootCauses = NestedSetBuilder.stableOrder(); ConfiguredTargetKey configuredTargetKey = (ConfiguredTargetKey) key.argument(); LabelAndConfiguration lc = LabelAndConfiguration.of(configuredTargetKey.getLabel(), configuredTargetKey.getConfiguration()); BuildConfiguration configuration = lc.getConfiguration(); PackageValue packageValue = (PackageValue) env .getValue(PackageValue.key(lc.getLabel().getPackageIdentifier())); if (packageValue == null) { return null; } // TODO(ulfjack): This tries to match the logic in TransitiveTargetFunction / // TargetMarkerFunction. Maybe we can merge the two? Package pkg = packageValue.getPackage(); Target target; try { target = pkg.getTarget(lc.getLabel().getName()); } catch (NoSuchTargetException e) { throw new ConfiguredTargetFunctionException(new ConfiguredValueCreationException(e.getMessage())); } if (pkg.containsErrors()) { transitiveLoadingRootCauses.add(lc.getLabel()); } transitivePackages.add(pkg); // TODO(bazel-team): This is problematic - we create the right key, but then end up with a value // that doesn't match; we can even have the same value multiple times. However, I think it's // only triggered in tests (i.e., in normal operation, the configuration passed in is already // null). if (!target.isConfigurable()) { configuration = null; } // This line is only needed for accurate error messaging. Say this target has a circular // dependency with one of its deps. With this line, loading this target fails so Bazel // associates the corresponding error with this target, as expected. Without this line, // the first TransitiveTargetValue call happens on its dep (in trimConfigurations), so Bazel // associates the error with the dep, which is misleading. if (useDynamicConfigurations(configuration) && configuration.trimConfigurations() && env.getValue(TransitiveTargetValue.key(lc.getLabel())) == null) { return null; } TargetAndConfiguration ctgValue = new TargetAndConfiguration(target, configuration); SkyframeDependencyResolver resolver = view.createDependencyResolver(env); // TODO(janakr): this acquire() call may tie up this thread indefinitely, reducing the // parallelism of Skyframe. This is a strict improvement over the prior state of the code, in // which we ran with #processors threads, but ideally we would call #tryAcquire here, and if we // failed, would exit this SkyFunction and restart it when permits were available. cpuBoundSemaphore.acquire(); try { // Get the configuration targets that trigger this rule's configurable attributes. ImmutableMap<Label, ConfigMatchingProvider> configConditions = getConfigConditions(ctgValue.getTarget(), env, resolver, ctgValue, transitivePackages, transitiveLoadingRootCauses); if (env.valuesMissing()) { return null; } // TODO(ulfjack): ConfiguredAttributeMapper (indirectly used from computeDependencies) isn't // safe to use if there are missing config conditions, so we stop here, but only if there are // config conditions - though note that we can't check if configConditions is non-empty - it // may be empty for other reasons. It would be better to continue here so that we can collect // more root causes during computeDependencies. // Note that this doesn't apply to AspectFunction, because aspects can't have configurable // attributes. if (!transitiveLoadingRootCauses.isEmpty() && configConditions != NO_CONFIG_CONDITIONS) { throw new ConfiguredTargetFunctionException( new ConfiguredValueCreationException(transitiveLoadingRootCauses.build())); } OrderedSetMultimap<Attribute, ConfiguredTarget> depValueMap = computeDependencies(env, resolver, ctgValue, ImmutableList.<Aspect>of(), configConditions, ruleClassProvider, view.getHostConfiguration(configuration), transitivePackages, transitiveLoadingRootCauses); if (env.valuesMissing()) { return null; } if (!transitiveLoadingRootCauses.isEmpty()) { throw new ConfiguredTargetFunctionException( new ConfiguredValueCreationException(transitiveLoadingRootCauses.build())); } Preconditions.checkNotNull(depValueMap); ConfiguredTargetValue ans = createConfiguredTarget(view, env, target, configuration, depValueMap, configConditions, transitivePackages); return ans; } catch (DependencyEvaluationException e) { if (e.getCause() instanceof ConfiguredValueCreationException) { throw new ConfiguredTargetFunctionException((ConfiguredValueCreationException) e.getCause()); } else { // Cast to InvalidConfigurationException as a consistency check. If you add any // DependencyEvaluationException constructors, you may need to change this code, too. InvalidConfigurationException cause = (InvalidConfigurationException) e.getCause(); throw new ConfiguredTargetFunctionException( new ConfiguredValueCreationException(cause.getMessage(), target.getLabel())); } } catch (AspectCreationException e) { // getAnalysisRootCause may be null if the analysis of the aspect itself failed. Label analysisRootCause = target.getLabel(); if (e.getAnalysisRootCause() != null) { analysisRootCause = e.getAnalysisRootCause(); } throw new ConfiguredTargetFunctionException( new ConfiguredValueCreationException(e.getMessage(), analysisRootCause)); } finally { cpuBoundSemaphore.release(); } } /** * Computes the direct dependencies of a node in the configured target graph (a configured * target or an aspects). * * <p>Returns null if Skyframe hasn't evaluated the required dependencies yet. In this case, the * caller should also return null to Skyframe. * @param env the Skyframe environment * @param resolver the dependency resolver * @param ctgValue the label and the configuration of the node * @param aspects * @param configConditions the configuration conditions for evaluating the attributes of the node * @param ruleClassProvider rule class provider for determining the right configuration fragments * to apply to deps * @param hostConfiguration the host configuration. There's a noticeable performance hit from * instantiating this on demand for every dependency that wants it, so it's best to compute * the host configuration as early as possible and pass this reference to all consumers * */ @Nullable static OrderedSetMultimap<Attribute, ConfiguredTarget> computeDependencies(Environment env, SkyframeDependencyResolver resolver, TargetAndConfiguration ctgValue, Iterable<Aspect> aspects, ImmutableMap<Label, ConfigMatchingProvider> configConditions, RuleClassProvider ruleClassProvider, BuildConfiguration hostConfiguration, NestedSetBuilder<Package> transitivePackages, NestedSetBuilder<Label> transitiveLoadingRootCauses) throws DependencyEvaluationException, ConfiguredTargetFunctionException, AspectCreationException, InterruptedException { // Create the map from attributes to set of (target, configuration) pairs. OrderedSetMultimap<Attribute, Dependency> depValueNames; try { depValueNames = resolver.dependentNodeMap(ctgValue, hostConfiguration, aspects, configConditions, transitiveLoadingRootCauses); } catch (EvalException e) { // EvalException can only be thrown by computed Skylark attributes in the current rule. env.getListener().handle(Event.error(e.getLocation(), e.getMessage())); throw new DependencyEvaluationException( new ConfiguredValueCreationException(e.print(), ctgValue.getLabel())); } catch (InvalidConfigurationException e) { throw new DependencyEvaluationException(e); } // Trim each dep's configuration so it only includes the fragments needed by its transitive // closure (only dynamic configurations support this). if (useDynamicConfigurations(ctgValue.getConfiguration())) { depValueNames = getDynamicConfigurations(env, ctgValue, depValueNames, hostConfiguration, ruleClassProvider); // It's important that we don't use "if (env.missingValues()) { return null }" here (or // in the following lines). See the comments in getDynamicConfigurations' Skyframe call // for explanation. if (depValueNames == null) { return null; } } // Resolve configured target dependencies and handle errors. Map<SkyKey, ConfiguredTarget> depValues = resolveConfiguredTargetDependencies(env, depValueNames.values(), transitivePackages, transitiveLoadingRootCauses); if (depValues == null) { return null; } // Resolve required aspects. OrderedSetMultimap<SkyKey, ConfiguredAspect> depAspects = resolveAspectDependencies(env, depValues, depValueNames.values(), transitivePackages); if (depAspects == null) { return null; } // Merge the dependent configured targets and aspects into a single map. try { return mergeAspects(depValueNames, depValues, depAspects); } catch (DuplicateException e) { env.getListener().handle(Event.error(ctgValue.getTarget().getLocation(), e.getMessage())); throw new ConfiguredTargetFunctionException( new ConfiguredValueCreationException(e.getMessage(), ctgValue.getLabel())); } } /** * Helper class for {@link #getDynamicConfigurations} - encapsulates a set of config fragments and * a dynamic transition. This can be used to determine the exact build options needed to * set a dynamic configuration. */ @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 BuildConfiguration.Fragment>> fragments; final Attribute.Transition transition; private final int hashCode; FragmentsAndTransition(Set<Class<? extends BuildConfiguration.Fragment>> fragments, Attribute.Transition 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 { FragmentsAndTransition other = (FragmentsAndTransition) o; return other.transition.equals(transition) && other.fragments.equals(fragments); } } @Override public int hashCode() { return hashCode; } } /** * Helper class for {@link #getDynamicConfigurations} - encapsulates an <attribute, label> pair * that can be used to map from an input dependency to a trimmed dependency. */ @Immutable private static final class AttributeAndLabel { final Attribute attribute; final Label label; Integer hashCode; AttributeAndLabel(Attribute attribute, Label label) { this.attribute = attribute; this.label = label; } @Override public boolean equals(Object o) { if (!(o instanceof AttributeAndLabel)) { return false; } AttributeAndLabel other = (AttributeAndLabel) o; return Objects.equals(other.attribute, attribute) && 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.attribute, this.label); } return hashCode; } } /** * Variation of {@link Multimap#put} that triggers an exception if a value already exists. */ @VisibleForTesting 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 key %s", value.toString(), key.toString())); } map.put(key, value); } /** * Creates a dynamic configuration for each dep that's custom-fitted specifically for that dep. * * <p>More specifically: given a set of {@link Dependency} instances holding dynamic config * transition requests (e.g. {@link Dependency#hasStaticConfiguration()} == false}), returns * equivalent dependencies containing dynamically created configurations applying those * transitions. If {@link BuildConfiguration.Options#trimConfigurations()} is true, these * configurations only contain the fragments needed by the dep and its transitive closure. Else * the configurations unconditionally include all fragments. * * <p>This method is heavily performance-optimized. Because it, in aggregate, reads over every * edge in 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 the configuration of the node * @param originalDeps the set of configuration transition requests for this target's attributes * @param hostConfiguration the host configuration * @param ruleClassProvider the rule class provider for determining the right configuration * fragments to apply to deps * * @return a mapping from each attribute to the {@link BuildConfiguration}s and {@link Label}s * to use for that attribute's deps. Returns null if not all Skyframe dependencies are * available yet. */ @Nullable static OrderedSetMultimap<Attribute, Dependency> getDynamicConfigurations(Environment env, TargetAndConfiguration ctgValue, OrderedSetMultimap<Attribute, Dependency> originalDeps, BuildConfiguration hostConfiguration, RuleClassProvider ruleClassProvider) throws DependencyEvaluationException, InterruptedException { // Maps each Skyframe-evaluated BuildConfiguration to the dependencies that need that // configuration. 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, Map.Entry<Attribute, Dependency>> keysToEntries = LinkedListMultimap.create(); // Stores the result of applying a dynamic 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 multiple values. All other transitions map to one. Map<FragmentsAndTransition, List<BuildOptions>> transitionsMap = new LinkedHashMap<>(); // The fragments used by the current target's configuration. Set<Class<? extends BuildConfiguration.Fragment>> ctgFragments = ctgValue.getConfiguration() .fragmentClasses(); BuildOptions ctgOptions = ctgValue.getConfiguration().getOptions(); // Stores the dynamically configured versions of each dependency. This method must preserve the // original label ordering of each attribute. For example, if originalDeps.get("data") is // [":a", ":b"], the dynamic 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, originaldeps.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<AttributeAndLabel, Dependency> dynamicDeps = LinkedHashMultimap.create(); // Performance optimization: This method iterates over originalDeps twice. By storing // AttributeAndLabel 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<AttributeAndLabel> attributesAndLabels = new ArrayList<>(originalDeps.size()); for (Map.Entry<Attribute, Dependency> depsEntry : originalDeps.entries()) { Dependency dep = depsEntry.getValue(); AttributeAndLabel attributeAndLabel = new AttributeAndLabel(depsEntry.getKey(), dep.getLabel()); attributesAndLabels.add(attributeAndLabel); // Certain targets (like output files) trivially re-use their input configuration. Likewise, // deps with null configurations (e.g. source files), can be trivially computed. So we skip // all logic in this method for these cases and just reinsert their original configurations // back at the end (note that null-configured targets will have a static // NullConfigurationDependency instead of dynamic // Dependency(label, transition=Attribute.Configuration.Transition.NULL)). // // 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 (dep.hasStaticConfiguration()) { continue; } // Figure out the required fragments for this dep and its transitive closure. Set<Class<? extends BuildConfiguration.Fragment>> depFragments = getTransitiveFragments(env, dep.getLabel(), ctgValue.getConfiguration()); if (depFragments == null) { return null; } // TODO(gregce): remove the below call once we have confidence dynamic 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, attributeAndLabel.attribute.getName(), dep, depFragments); } boolean sameFragments = depFragments.equals(ctgFragments); Attribute.Transition transition = dep.getTransition(); if (sameFragments) { if (transition == Attribute.ConfigurationTransition.NONE) { // The dep uses the same exact configuration. putOnlyEntry(dynamicDeps, attributeAndLabel, Dependency.withConfigurationAndAspects( dep.getLabel(), ctgValue.getConfiguration(), dep.getAspects())); continue; } else if (transition == HostTransition.INSTANCE) { // 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. putOnlyEntry(dynamicDeps, attributeAndLabel, Dependency .withConfigurationAndAspects(dep.getLabel(), hostConfiguration, dep.getAspects())); continue; } } // Apply the transition or use the cached result if it was already applied. FragmentsAndTransition transitionKey = new FragmentsAndTransition(depFragments, transition); List<BuildOptions> toOptions = transitionsMap.get(transitionKey); if (toOptions == null) { toOptions = getDynamicTransitionOptions(ctgOptions, transition, depFragments, ruleClassProvider, !sameFragments); 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).equals(ctgOptions)) { putOnlyEntry(dynamicDeps, attributeAndLabel, Dependency.withConfigurationAndAspects(dep.getLabel(), ctgValue.getConfiguration(), dep.getAspects())); continue; } // If we get here, we have to get the configuration from Skyframe. for (BuildOptions options : toOptions) { keysToEntries.put(BuildConfigurationValue.key(depFragments, options), depsEntry); } } // 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 ConfiguredTargetFucntion to make that impractical. Map<SkyKey, ValueOrException<InvalidConfigurationException>> depConfigValues = env .getValuesOrThrow(keysToEntries.keySet(), InvalidConfigurationException.class); // Now fill in the remaining unresolved deps with the now-resolved configurations. try { for (Map.Entry<SkyKey, ValueOrException<InvalidConfigurationException>> entry : depConfigValues .entrySet()) { SkyKey key = entry.getKey(); ValueOrException<InvalidConfigurationException> valueOrException = 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; } BuildConfigurationValue trimmedConfig = (BuildConfigurationValue) valueOrException.get(); for (Map.Entry<Attribute, Dependency> info : keysToEntries.get(key)) { Dependency originalDep = info.getValue(); AttributeAndLabel attr = new AttributeAndLabel(info.getKey(), originalDep.getLabel()); Dependency resolvedDep = Dependency.withConfigurationAndAspects(originalDep.getLabel(), trimmedConfig.getConfiguration(), originalDep.getAspects()); if (attr.attribute.hasSplitConfigurationTransition()) { dynamicDeps.put(attr, resolvedDep); } else { putOnlyEntry(dynamicDeps, attr, resolvedDep); } } } } catch (InvalidConfigurationException e) { throw new DependencyEvaluationException(e); } return sortDynamicallyConfiguredDeps(originalDeps, dynamicDeps, attributesAndLabels); } /** * 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 BuildConfiguration.Fragment>> getTransitiveFragments(Environment env, Label dep, BuildConfiguration parentConfig) throws InterruptedException { Preconditions.checkArgument(parentConfig.useDynamicConfigurations()); if (!parentConfig.trimConfigurations()) { return parentConfig.getAllFragments().keySet(); } SkyKey fragmentsKey = TransitiveTargetValue.key(dep); TransitiveTargetValue transitiveDepInfo = (TransitiveTargetValue) env.getValue(fragmentsKey); if (transitiveDepInfo == null) { // 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; } return transitiveDepInfo.getTransitiveConfigFragments().toSet(); } /** * Applies a dynamic configuration transition over a set of build options. * * @return the build options for the transitioned configuration. If trimResults is true, * only options needed by the required fragments are included. Else the same options as the * original input are included (with different possible values, of course). */ static List<BuildOptions> getDynamicTransitionOptions(BuildOptions fromOptions, Attribute.Transition transition, Iterable<Class<? extends BuildConfiguration.Fragment>> requiredFragments, RuleClassProvider ruleClassProvider, boolean trimResults) { List<BuildOptions> result; if (transition == Attribute.ConfigurationTransition.NONE) { result = ImmutableList.<BuildOptions>of(fromOptions); } else if (transition instanceof PatchTransition) { // TODO(bazel-team): safety-check that this never mutates fromOptions. result = ImmutableList.<BuildOptions>of(((PatchTransition) transition).apply(fromOptions)); } else if (transition instanceof Attribute.SplitTransition) { @SuppressWarnings("unchecked") // Attribute.java doesn't have the BuildOptions symbol. List<BuildOptions> toOptions = ((Attribute.SplitTransition<BuildOptions>) transition) .split(fromOptions); if (toOptions.isEmpty()) { // When the split returns an empty list, it's signaling it doesn't apply to this instance. // Check that it's safe to skip the transition and return the original options. Verify.verify(transition.defaultsToSelf()); result = ImmutableList.<BuildOptions>of(fromOptions); } else { result = toOptions; } } else { throw new IllegalStateException( String.format("unsupported dynamic transition type: %s", transition.getClass().getName())); } if (!trimResults) { return result; } else { ImmutableList.Builder<BuildOptions> trimmedOptions = ImmutableList.builder(); for (BuildOptions toOptions : result) { trimmedOptions.add( toOptions.trim(BuildConfiguration.getOptionsClasses(requiredFragments, ruleClassProvider))); } return trimmedOptions.build(); } } /** * Diagnostic helper method for dynamic configurations: 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(Environment env, TargetAndConfiguration ctgValue, String attribute, Dependency dep, Set<Class<? extends BuildConfiguration.Fragment>> expectedDepFragments) throws DependencyEvaluationException { Set<String> ctgFragmentNames = new HashSet<>(); for (BuildConfiguration.Fragment fragment : ctgValue.getConfiguration().getAllFragments().values()) { ctgFragmentNames.add(fragment.getClass().getSimpleName()); } Set<String> depFragmentNames = new HashSet<>(); for (Class<? extends BuildConfiguration.Fragment> fragmentClass : expectedDepFragments) { depFragmentNames.add(fragmentClass.getSimpleName()); } Set<String> missing = Sets.difference(depFragmentNames, ctgFragmentNames); if (!missing.isEmpty()) { String msg = String.format( "%s: dependency %s from attribute \"%s\" is missing required config fragments: %s", ctgValue.getLabel(), dep.getLabel(), attribute, Joiner.on(", ").join(missing)); env.getListener().handle(Event.error(msg)); throw new DependencyEvaluationException(new InvalidConfigurationException(msg)); } } /** * Determines the output ordering of each <attribute, depLabel> -> * [dep<config1>, dep<config2>, ...] collection produced by a split transition. */ @VisibleForTesting static final Comparator<Dependency> DYNAMIC_SPLIT_DEP_ORDERING = new Comparator<Dependency>() { @Override public int compare(Dependency d1, Dependency d2) { return d1.getConfiguration().getMnemonic().compareTo(d2.getConfiguration().getMnemonic()); } }; /** * Helper method for {@link #getDynamicConfigurations}: returns a copy of the output deps * using the same key and value ordering as the input deps. * * @param originalDeps the input deps with the ordering to preserve * @param dynamicDeps the unordered output deps * @param attributesAndLabels collection of <attribute, depLabel> pairs guaranteed to match * the ordering of originalDeps.entries(). This is a performance optimization: see * {@link #getDynamicConfigurations#attributesAndLabels} for details. */ private static OrderedSetMultimap<Attribute, Dependency> sortDynamicallyConfiguredDeps( OrderedSetMultimap<Attribute, Dependency> originalDeps, Multimap<AttributeAndLabel, Dependency> dynamicDeps, ArrayList<AttributeAndLabel> attributesAndLabels) { Iterator<AttributeAndLabel> iterator = attributesAndLabels.iterator(); OrderedSetMultimap<Attribute, Dependency> result = OrderedSetMultimap.create(); for (Map.Entry<Attribute, Dependency> depsEntry : originalDeps.entries()) { AttributeAndLabel attrAndLabel = iterator.next(); if (depsEntry.getValue().hasStaticConfiguration()) { result.put(attrAndLabel.attribute, depsEntry.getValue()); } else { Collection<Dependency> dynamicAttrDeps = dynamicDeps.get(attrAndLabel); Verify.verify(!dynamicAttrDeps.isEmpty()); if (dynamicAttrDeps.size() > 1) { List<Dependency> sortedSplitList = new ArrayList<>(dynamicAttrDeps); Collections.sort(sortedSplitList, DYNAMIC_SPLIT_DEP_ORDERING); dynamicAttrDeps = sortedSplitList; } result.putAll(depsEntry.getKey(), dynamicAttrDeps); } } return result; } /** * Merges the each direct dependency configured target with the aspects associated with it. * * <p>Note that the combination of a configured target and its associated aspects are not * represented by a Skyframe node. This is because there can possibly be many different * combinations of aspects for a particular configured target, so it would result in a * combinatiorial explosion of Skyframe nodes. */ private static OrderedSetMultimap<Attribute, ConfiguredTarget> mergeAspects( OrderedSetMultimap<Attribute, Dependency> depValueNames, Map<SkyKey, ConfiguredTarget> depConfiguredTargetMap, OrderedSetMultimap<SkyKey, ConfiguredAspect> depAspectMap) throws DuplicateException { OrderedSetMultimap<Attribute, ConfiguredTarget> result = OrderedSetMultimap.create(); for (Map.Entry<Attribute, Dependency> entry : depValueNames.entries()) { Dependency dep = entry.getValue(); SkyKey depKey = TO_KEYS.apply(dep); ConfiguredTarget depConfiguredTarget = depConfiguredTargetMap.get(depKey); result.put(entry.getKey(), MergedConfiguredTarget.of(depConfiguredTarget, depAspectMap.get(depKey))); } return result; } /** * Given a list of {@link Dependency} objects, returns a multimap from the {@link SkyKey} of the * dependency to the {@link ConfiguredAspect} instances that should be merged into it. * * <p>Returns null if the required aspects are not computed yet. */ @Nullable private static OrderedSetMultimap<SkyKey, ConfiguredAspect> resolveAspectDependencies(Environment env, Map<SkyKey, ConfiguredTarget> configuredTargetMap, Iterable<Dependency> deps, NestedSetBuilder<Package> transitivePackages) throws AspectCreationException, InterruptedException { OrderedSetMultimap<SkyKey, ConfiguredAspect> result = OrderedSetMultimap.create(); Set<SkyKey> aspectKeys = new HashSet<>(); for (Dependency dep : deps) { AspectKey key = null; for (Entry<AspectDescriptor, BuildConfiguration> depAspect : dep.getAspectConfigurations().entrySet()) { key = getNextAspectKey(key, dep, depAspect); aspectKeys.add(key.getSkyKey()); } } Map<SkyKey, ValueOrException2<AspectCreationException, NoSuchThingException>> depAspects = env .getValuesOrThrow(aspectKeys, AspectCreationException.class, NoSuchThingException.class); for (Dependency dep : deps) { SkyKey depKey = TO_KEYS.apply(dep); // If the same target was declared in different attributes of rule, we should not process it // twice. if (result.containsKey(depKey)) { continue; } AspectKey key = null; ConfiguredTarget depConfiguredTarget = configuredTargetMap.get(depKey); for (Entry<AspectDescriptor, BuildConfiguration> depAspect : dep.getAspectConfigurations().entrySet()) { key = getNextAspectKey(key, dep, depAspect); SkyKey aspectKey = key.getSkyKey(); AspectValue aspectValue; try { // TODO(ulfjack): Catch all thrown AspectCreationException and NoSuchThingException // instances and merge them into a single Exception to get full root cause data. aspectValue = (AspectValue) depAspects.get(aspectKey).get(); } catch (NoSuchThingException e) { throw new AspectCreationException(String.format("Evaluation of aspect %s on %s failed: %s", depAspect.getKey().getAspectClass().getName(), dep.getLabel(), e.toString())); } if (aspectValue == null) { // Dependent aspect has either not been computed yet or is in error. return null; } if (!aspectMatchesConfiguredTarget(depConfiguredTarget, aspectValue.getAspect())) { continue; } result.put(depKey, aspectValue.getConfiguredAspect()); transitivePackages.addTransitive(aspectValue.getTransitivePackages()); } } return result; } private static AspectKey getNextAspectKey(AspectKey key, Dependency dep, Entry<AspectDescriptor, BuildConfiguration> depAspect) { if (key == null) { key = AspectValue.createAspectKey(dep.getLabel(), dep.getConfiguration(), depAspect.getKey(), depAspect.getValue()); } else { key = AspectValue.createAspectKey(key, depAspect.getKey(), depAspect.getValue()); } return key; } private static boolean aspectMatchesConfiguredTarget(final ConfiguredTarget dep, Aspect aspect) { return aspect.getDefinition().getRequiredProviders().isSatisfiedBy(new Predicate<Class<?>>() { @Override public boolean apply(Class<?> provider) { return dep.getProvider(provider.asSubclass(TransitiveInfoProvider.class)) != null; } }, new Predicate<SkylarkProviderIdentifier>() { @Override public boolean apply(SkylarkProviderIdentifier skylarkProviderIdentifier) { return dep.get(skylarkProviderIdentifier) != null; } }); } /** * Returns the set of {@link ConfigMatchingProvider}s that key the configurable attributes used by * this rule. * * <p>>If the configured targets supplying those providers aren't yet resolved by the dependency * resolver, returns null. */ @Nullable static ImmutableMap<Label, ConfigMatchingProvider> getConfigConditions(Target target, Environment env, SkyframeDependencyResolver resolver, TargetAndConfiguration ctgValue, NestedSetBuilder<Package> transitivePackages, NestedSetBuilder<Label> transitiveLoadingRootCauses) throws DependencyEvaluationException, InterruptedException { if (!(target instanceof Rule)) { return NO_CONFIG_CONDITIONS; } Map<Label, ConfigMatchingProvider> configConditions = new LinkedHashMap<>(); // Collect the labels of the configured targets we need to resolve. OrderedSetMultimap<Attribute, Label> configLabelMap = OrderedSetMultimap.create(); RawAttributeMapper attributeMap = RawAttributeMapper.of(((Rule) target)); for (Attribute a : ((Rule) target).getAttributes()) { for (Label configLabel : attributeMap.getConfigurabilityKeys(a.getName(), a.getType())) { if (!BuildType.Selector.isReservedLabel(configLabel)) { configLabelMap.put(a, target.getLabel().resolveRepositoryRelative(configLabel)); } } } if (configLabelMap.isEmpty()) { return NO_CONFIG_CONDITIONS; } // Collect the corresponding Skyframe configured target values. Abort early if they haven't // been computed yet. Collection<Dependency> configValueNames = resolver.resolveRuleLabels(ctgValue, configLabelMap, transitiveLoadingRootCauses); if (env.valuesMissing()) { return null; } // No need to get new configs from Skyframe - config_setting rules always use the current // target's config. // TODO(bazel-team): remove the need for this special transformation. We can probably do this by // simply passing this through trimConfigurations. BuildConfiguration targetConfig = ctgValue.getConfiguration(); if (useDynamicConfigurations(targetConfig)) { ImmutableList.Builder<Dependency> staticConfigs = ImmutableList.builder(); for (Dependency dep : configValueNames) { staticConfigs.add( Dependency.withConfigurationAndAspects(dep.getLabel(), targetConfig, dep.getAspects())); } configValueNames = staticConfigs.build(); } Map<SkyKey, ConfiguredTarget> configValues = resolveConfiguredTargetDependencies(env, configValueNames, transitivePackages, transitiveLoadingRootCauses); if (configValues == null) { return null; } // Get the configured targets as ConfigMatchingProvider interfaces. for (Dependency entry : configValueNames) { ConfiguredTarget value = configValues.get(TO_KEYS.apply(entry)); // The code above guarantees that value is non-null here. ConfigMatchingProvider provider = value.getProvider(ConfigMatchingProvider.class); if (provider != null) { configConditions.put(entry.getLabel(), provider); } else { // Not a valid provider for configuration conditions. String message = entry.getLabel() + " is not a valid configuration key for " + target.getLabel(); env.getListener().handle(Event.error(TargetUtils.getLocationMaybe(target), message)); throw new DependencyEvaluationException( new ConfiguredValueCreationException(message, target.getLabel())); } } return ImmutableMap.copyOf(configConditions); } /** * * Resolves the targets referenced in depValueNames and returns their ConfiguredTarget * instances. * * <p>Returns null if not all instances are available yet. */ @Nullable private static Map<SkyKey, ConfiguredTarget> resolveConfiguredTargetDependencies(Environment env, Collection<Dependency> deps, NestedSetBuilder<Package> transitivePackages, NestedSetBuilder<Label> transitiveLoadingRootCauses) throws DependencyEvaluationException, InterruptedException { boolean missedValues = env.valuesMissing(); boolean failed = false; Iterable<SkyKey> depKeys = Iterables.transform(deps, TO_KEYS); Map<SkyKey, ValueOrException<ConfiguredValueCreationException>> depValuesOrExceptions = env .getValuesOrThrow(depKeys, ConfiguredValueCreationException.class); Map<SkyKey, ConfiguredTarget> result = Maps.newHashMapWithExpectedSize(depValuesOrExceptions.size()); for (Map.Entry<SkyKey, ValueOrException<ConfiguredValueCreationException>> entry : depValuesOrExceptions .entrySet()) { try { ConfiguredTargetValue depValue = (ConfiguredTargetValue) entry.getValue().get(); if (depValue == null) { missedValues = true; } else { result.put(entry.getKey(), depValue.getConfiguredTarget()); transitivePackages.addTransitive(depValue.getTransitivePackages()); } } catch (ConfiguredValueCreationException e) { // TODO(ulfjack): If there is an analysis root cause, we drop all loading root causes. if (e.getAnalysisRootCause() != null) { throw new DependencyEvaluationException(e); } transitiveLoadingRootCauses.addTransitive(e.loadingRootCauses); failed = true; } } if (missedValues) { return null; } else if (failed) { throw new DependencyEvaluationException( new ConfiguredValueCreationException(transitiveLoadingRootCauses.build())); } else { return result; } } @Override public String extractTag(SkyKey skyKey) { return Label.print(((ConfiguredTargetKey) skyKey.argument()).getLabel()); } @Nullable private ConfiguredTargetValue createConfiguredTarget(SkyframeBuildView view, Environment env, Target target, BuildConfiguration configuration, OrderedSetMultimap<Attribute, ConfiguredTarget> depValueMap, ImmutableMap<Label, ConfigMatchingProvider> configConditions, NestedSetBuilder<Package> transitivePackages) throws ConfiguredTargetFunctionException, InterruptedException { StoredEventHandler events = new StoredEventHandler(); BuildConfiguration ownerConfig = (configuration == null) ? null : configuration.getArtifactOwnerConfiguration(); CachingAnalysisEnvironment analysisEnvironment = view.createAnalysisEnvironment( new ConfiguredTargetKey(target.getLabel(), ownerConfig), false, events, env, configuration); if (env.valuesMissing()) { return null; } Preconditions.checkNotNull(depValueMap); ConfiguredTarget configuredTarget = view.createConfiguredTarget(target, configuration, analysisEnvironment, depValueMap, configConditions); events.replayOn(env.getListener()); if (events.hasErrors()) { analysisEnvironment.disable(target); throw new ConfiguredTargetFunctionException(new ConfiguredValueCreationException( "Analysis of target '" + target.getLabel() + "' failed; build aborted", target.getLabel())); } Preconditions.checkState(!analysisEnvironment.hasErrors(), "Analysis environment hasError() but no errors reported"); if (env.valuesMissing()) { return null; } analysisEnvironment.disable(target); Preconditions.checkNotNull(configuredTarget, target); ImmutableMap<Artifact, ActionAnalysisMetadata> generatingActions; // Check for conflicting actions within this configured target (that indicates a bug in the // rule implementation). try { generatingActions = Actions .filterSharedActionsAndThrowActionConflict(analysisEnvironment.getRegisteredActions()); } catch (ActionConflictException e) { throw new ConfiguredTargetFunctionException(e); } return new ConfiguredTargetValue(configuredTarget, generatingActions, transitivePackages.build()); } /** * An exception indicating that there was a problem during the construction of * a ConfiguredTargetValue. */ public static final class ConfiguredValueCreationException extends Exception { private final NestedSet<Label> loadingRootCauses; // TODO(ulfjack): Collect all analysis root causes, not just the first one. @Nullable private final Label analysisRootCause; public ConfiguredValueCreationException(String message, Label currentTarget) { super(message); this.loadingRootCauses = NestedSetBuilder.<Label>emptySet(Order.STABLE_ORDER); this.analysisRootCause = Preconditions.checkNotNull(currentTarget); } public ConfiguredValueCreationException(String message, NestedSet<Label> rootCauses) { super(message); this.loadingRootCauses = rootCauses; this.analysisRootCause = null; } public ConfiguredValueCreationException(NestedSet<Label> rootCauses) { this("Loading failed", rootCauses); } public ConfiguredValueCreationException(String message) { this(message, NestedSetBuilder.<Label>emptySet(Order.STABLE_ORDER)); } public NestedSet<Label> getRootCauses() { return loadingRootCauses; } public Label getAnalysisRootCause() { return analysisRootCause; } } /** * Used to declare all the exception types that can be wrapped in the exception thrown by * {@link ConfiguredTargetFunction#compute}. */ public static final class ConfiguredTargetFunctionException extends SkyFunctionException { public ConfiguredTargetFunctionException(NoSuchThingException e) { super(e, Transience.PERSISTENT); } private ConfiguredTargetFunctionException(ConfiguredValueCreationException e) { super(e, Transience.PERSISTENT); } private ConfiguredTargetFunctionException(ActionConflictException e) { super(e, Transience.PERSISTENT); } } }