Use `SkyKeyComputeState` to address a major inefficiency in `ConfiguredTargetFunction` and `AspectFunction`.

In my benchmarks of a diverse set of targets, this reduces the analysis phase wall time by between 3% and 6.5%.

See the description of https://github.com/bazelbuild/bazel/commit/ed279ab4fa2d4356be00b54266f56edcc5aeae78 for some background on the inefficiency. And see the description of https://github.com/bazelbuild/bazel/commit/343ba438a93f8c56a7b524ac7a54666c57a969d9 for how this CL here is able to be a strict performance win even when Blaze is memory constrained.

PiperOrigin-RevId: 419918993
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 ef8c2f1..e10d746 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
@@ -208,20 +208,23 @@
    * Keep this in mind when making modifications and performance-test any changes you make.
    *
    * @param dependencyKeys the transition requests for each dep and each dependency kind
+   * @param eventHandler the handler for events
    * @return a mapping from each dependency kind in the source target to the {@link
    *     BuildConfigurationValue}s and {@link Label}s for the deps under that dependency kind .
    *     Returns null if not all Skyframe dependencies are available.
    */
   @Nullable
   public OrderedSetMultimap<DependencyKind, Dependency> resolveConfigurations(
-      OrderedSetMultimap<DependencyKind, DependencyKey> dependencyKeys)
+      OrderedSetMultimap<DependencyKind, DependencyKey> dependencyKeys,
+      ExtendedEventHandler eventHandler)
       throws ConfiguredValueCreationException, InterruptedException {
     OrderedSetMultimap<DependencyKind, Dependency> resolvedDeps = OrderedSetMultimap.create();
     boolean needConfigsFromSkyframe = false;
     for (Map.Entry<DependencyKind, DependencyKey> entry : dependencyKeys.entries()) {
       DependencyKind dependencyKind = entry.getKey();
       DependencyKey dependencyKey = entry.getValue();
-      ImmutableList<Dependency> depConfig = resolveConfiguration(dependencyKind, dependencyKey);
+      ImmutableList<Dependency> depConfig =
+          resolveConfiguration(dependencyKind, dependencyKey, eventHandler);
       if (depConfig == null) {
         // Instead of returning immediately, give the loop a chance to queue up every missing
         // dependency, then return all at once. That prevents re-executing this code an unnecessary
@@ -243,7 +246,7 @@
    */
   @Nullable
   public ImmutableList<Dependency> resolveConfiguration(
-      DependencyKind dependencyKind, DependencyKey dependencyKey)
+      DependencyKind dependencyKind, DependencyKey dependencyKey, ExtendedEventHandler eventHandler)
       throws ConfiguredValueCreationException, InterruptedException {
 
     Dependency.Builder dependencyBuilder = dependencyKey.getDependencyBuilder();
@@ -251,7 +254,8 @@
     ConfigurationTransition transition = dependencyKey.getTransition();
 
     if (transition == NullTransition.INSTANCE) {
-      Dependency resolvedDep = resolveNullTransition(dependencyBuilder, dependencyKind);
+      Dependency resolvedDep =
+          resolveNullTransition(dependencyBuilder, dependencyKind, eventHandler);
       if (resolvedDep == null) {
         return null; // Need Skyframe deps.
       }
@@ -260,19 +264,22 @@
       return ImmutableList.of(resolveHostTransition(dependencyBuilder, dependencyKey));
     }
 
-    return resolveGenericTransition(dependencyBuilder, dependencyKey);
+    return resolveGenericTransition(dependencyBuilder, dependencyKey, eventHandler);
   }
 
   @Nullable
   private Dependency resolveNullTransition(
-      Dependency.Builder dependencyBuilder, DependencyKind dependencyKind)
+      Dependency.Builder dependencyBuilder,
+      DependencyKind dependencyKind,
+      ExtendedEventHandler eventHandler)
       throws ConfiguredValueCreationException, 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) {
-      ImmutableList<String> transitionKeys = collectTransitionKeys(dependencyKind.getAttribute());
+      ImmutableList<String> transitionKeys =
+          collectTransitionKeys(dependencyKind.getAttribute(), eventHandler);
       if (transitionKeys == null) {
         return null; // Need Skyframe deps.
       }
@@ -293,7 +300,8 @@
   @Nullable
   private ImmutableList<Dependency> resolveGenericTransition(
       Dependency.Builder dependencyBuilder,
-      DependencyKey dependencyKey)
+      DependencyKey dependencyKey,
+      ExtendedEventHandler eventHandler)
       throws ConfiguredValueCreationException, InterruptedException {
     Map<String, BuildOptions> toOptions;
     try {
@@ -302,7 +310,7 @@
               getCurrentConfiguration().getOptions(),
               dependencyKey.getTransition(),
               env,
-              env.getListener());
+              eventHandler);
       if (toOptions == null) {
         return null; // Need more Skyframe deps for a Starlark transition.
       }
@@ -380,7 +388,8 @@
   }
 
   @Nullable
-  private ImmutableList<String> collectTransitionKeys(Attribute attribute)
+  private ImmutableList<String> collectTransitionKeys(
+      Attribute attribute, ExtendedEventHandler eventHandler)
       throws ConfiguredValueCreationException, InterruptedException {
     TransitionFactory<AttributeTransitionData> transitionFactory = attribute.getTransitionFactory();
     if (transitionFactory.isSplit()) {
@@ -397,7 +406,7 @@
       try {
         toOptions =
             applyTransitionWithSkyframe(
-                getCurrentConfiguration().getOptions(), baseTransition, env, env.getListener());
+                getCurrentConfiguration().getOptions(), baseTransition, env, eventHandler);
         if (toOptions == null) {
           return null; // Need more Skyframe deps for a Starlark transition.
         }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/AspectFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/AspectFunction.java
index fe141a9..dcb2189 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/AspectFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/AspectFunction.java
@@ -315,8 +315,11 @@
     }
 
     SkyframeDependencyResolver resolver = new SkyframeDependencyResolver(env);
-    NestedSetBuilder<Package> transitivePackagesForPackageRootResolution =
-        storeTransitivePackagesForPackageRootResolution ? NestedSetBuilder.stableOrder() : null;
+    ConfiguredTargetFunction.State state =
+        env.getState(
+            () ->
+                new ConfiguredTargetFunction.State(
+                    storeTransitivePackagesForPackageRootResolution));
 
     TargetAndConfiguration originalTargetAndConfiguration =
         new TargetAndConfiguration(target, configuration);
@@ -327,16 +330,14 @@
         return null;
       }
 
-      NestedSetBuilder<Cause> transitiveRootCauses = NestedSetBuilder.stableOrder();
-
       // Get the configuration targets that trigger this rule's configurable attributes.
       ConfigConditions configConditions =
           ConfiguredTargetFunction.getConfigConditions(
               env,
               originalTargetAndConfiguration,
-              transitivePackagesForPackageRootResolution,
+              state.transitivePackagesForPackageRootResolution,
               unloadedToolchainContext == null ? null : unloadedToolchainContext.targetPlatform(),
-              transitiveRootCauses);
+              state.transitiveRootCauses);
       if (configConditions == null) {
         // Those targets haven't yet been resolved.
         return null;
@@ -346,6 +347,7 @@
       try {
         depValueMap =
             ConfiguredTargetFunction.computeDependencies(
+                state,
                 env,
                 resolver,
                 originalTargetAndConfiguration,
@@ -358,9 +360,7 @@
                         .build(),
                 shouldUseToolchainTransition(configuration, aspect.getDefinition()),
                 ruleClassProvider,
-                buildViewProvider.getSkyframeBuildView().getHostConfiguration(),
-                transitivePackagesForPackageRootResolution,
-                transitiveRootCauses);
+                buildViewProvider.getSkyframeBuildView().getHostConfiguration());
       } catch (ConfiguredValueCreationException e) {
         throw new AspectCreationException(
             e.getMessage(), key.getLabel(), configuration, e.getDetailedExitCode());
@@ -368,8 +368,8 @@
       if (depValueMap == null) {
         return null;
       }
-      if (!transitiveRootCauses.isEmpty()) {
-        NestedSet<Cause> causes = transitiveRootCauses.build();
+      if (!state.transitiveRootCauses.isEmpty()) {
+        NestedSet<Cause> causes = state.transitiveRootCauses.build();
         throw new AspectFunctionException(
             new AspectCreationException(
                 "Loading failed",
@@ -403,7 +403,7 @@
           configConditions,
           toolchainContext,
           depValueMap,
-          transitivePackagesForPackageRootResolution);
+          state.transitivePackagesForPackageRootResolution);
     } catch (DependencyEvaluationException e) {
       // TODO(bazel-team): consolidate all env.getListener().handle() calls in this method, like in
       // ConfiguredTargetFunction. This encourages clear, consistent user messages (ideally without
@@ -636,7 +636,8 @@
               originalTargetAndAspectConfiguration,
               hostConfiguration,
               configConditions.asProviders());
-      ImmutableList<Dependency> deps = resolver.resolveConfiguration(depKind, depKey);
+      ImmutableList<Dependency> deps =
+          resolver.resolveConfiguration(depKind, depKey, env.getListener());
       if (deps == null) {
         return null;
       }
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 2d9ee1d..e5ef4c9 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
@@ -84,6 +84,7 @@
 import com.google.devtools.build.lib.util.DetailedExitCode.DetailedExitCodeComparator;
 import com.google.devtools.build.lib.util.OrderedSetMultimap;
 import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunction.Environment.SkyKeyComputeState;
 import com.google.devtools.build.skyframe.SkyFunctionException;
 import com.google.devtools.build.skyframe.SkyKey;
 import com.google.devtools.build.skyframe.SkyValue;
@@ -183,9 +184,61 @@
     }
   }
 
+  static class State implements SkyKeyComputeState {
+    /** Null if ConfiguredTargetFuncton is not storing this information. */
+    @Nullable NestedSetBuilder<Package> transitivePackagesForPackageRootResolution;
+
+    NestedSetBuilder<Cause> transitiveRootCauses = NestedSetBuilder.stableOrder();
+
+    /** Null if not yet computed or if {@link #resolveConfigurationsResult} is non-null. */
+    @Nullable private OrderedSetMultimap<DependencyKind, DependencyKey> dependentNodeMapResult;
+
+    /** Null if not yet computed or if {@link #computeDependenciesResult} is non-null. */
+    @Nullable private OrderedSetMultimap<DependencyKind, Dependency> resolveConfigurationsResult;
+
+    /** Null if not yet computed or if {@link #computeDependenciesResult} is non-null. */
+    @Nullable
+    private Map<SkyKey, ConfiguredTargetAndData> resolveConfiguredTargetDependenciesResult;
+
+    /** Null if not yet computed or if {@link #computeDependenciesResult} is non-null. */
+    @Nullable
+    private OrderedSetMultimap<Dependency, ConfiguredAspect> resolveAspectDependenciesResult;
+
+    /**
+     * Non-null if all the work in {@link #computeDependencies} is already done. This field contains
+     * the result.
+     */
+    @Nullable
+    private OrderedSetMultimap<DependencyKind, ConfiguredTargetAndData> computeDependenciesResult;
+
+    /**
+     * Non-null if either {@link #resolveConfigurationsResult} or {@link #computeDependenciesResult}
+     * are non-null. This field contains events (from {@link
+     * ConfigurationResolver#resolveConfigurations}) that should be replayed.
+     *
+     * <p>When {@link #resolveConfigurationsResult} or {@link #computeDependenciesResult} are
+     * non-null (e.g. populated on a previous call to {@link #computeDependencies} on a previous
+     * call to {@link #compute}), we don't freshly do the work that would cause these events to be
+     * freshly emitted. So instead we replay these events from the actual call to {@link
+     * ConfigurationResolver#resolveConfigurations} we did in the past. This is important because
+     * Skyframe retains and uses only the events emitted to {@code env.getListener()} on a call to
+     * {@link #compute} that had no missing deps. That is, if our earlier {@link #compute}'s call to
+     * {@link ConfigurationResolver#resolveConfigurations} emitted events to {@code
+     * env.getListener()}, and that {@link #compute} call returned null, then those events would be
+     * thrown away.
+     */
+    @Nullable private StoredEventHandler storedEventHandlerFromResolveConfigurations;
+
+    State(boolean storeTransitivePackagesForPackageRootResolution) {
+      this.transitivePackagesForPackageRootResolution =
+          storeTransitivePackagesForPackageRootResolution ? NestedSetBuilder.stableOrder() : null;
+    }
+  }
+
   @Override
   public SkyValue compute(SkyKey key, Environment env)
       throws ReportedException, UnreportedException, InterruptedException {
+    State state = env.getState(() -> new State(storeTransitivePackagesForPackageRootResolution));
     if (shouldUnblockCpuWorkWhenFetchingDeps) {
       env =
           new StateInformingSkyFunctionEnvironment(
@@ -194,9 +247,6 @@
               /*postFetch=*/ () -> maybeAcquireSemaphoreWithLogging(key));
     }
     SkyframeBuildView view = buildViewProvider.getSkyframeBuildView();
-    NestedSetBuilder<Package> transitivePackagesForPackageRootResolution =
-        storeTransitivePackagesForPackageRootResolution ? NestedSetBuilder.stableOrder() : null;
-    NestedSetBuilder<Cause> transitiveRootCauses = NestedSetBuilder.stableOrder();
 
     ConfiguredTargetKey configuredTargetKey = (ConfiguredTargetKey) key.argument();
     Label label = configuredTargetKey.getLabel();
@@ -241,10 +291,11 @@
     }
     if (pkg.containsErrors()) {
       FailureDetail failureDetail = pkg.contextualizeFailureDetailForTarget(target);
-      transitiveRootCauses.add(new LoadingFailedCause(label, DetailedExitCode.of(failureDetail)));
+      state.transitiveRootCauses.add(
+          new LoadingFailedCause(label, DetailedExitCode.of(failureDetail)));
     }
-    if (transitivePackagesForPackageRootResolution != null) {
-      transitivePackagesForPackageRootResolution.add(pkg);
+    if (state.transitivePackagesForPackageRootResolution != null) {
+      state.transitivePackagesForPackageRootResolution.add(pkg);
     }
     if (target.isConfigurable() == (configuredTargetKey.getConfigurationKey() == null)) {
       // We somehow ended up in a target that requires a non-null configuration as a dependency of
@@ -254,9 +305,9 @@
       // not prepared for it.
       return new NonRuleConfiguredTargetValue(
           new EmptyConfiguredTarget(target.getLabel(), configuredTargetKey.getConfigurationKey()),
-          transitivePackagesForPackageRootResolution == null
+          state.transitivePackagesForPackageRootResolution == null
               ? null
-              : transitivePackagesForPackageRootResolution.build());
+              : state.transitivePackagesForPackageRootResolution.build());
     }
 
     TargetAndConfiguration ctgValue = new TargetAndConfiguration(target, configuration);
@@ -287,11 +338,11 @@
           getConfigConditions(
               env,
               ctgValue,
-              transitivePackagesForPackageRootResolution,
+              state.transitivePackagesForPackageRootResolution,
               unloadedToolchainContexts == null
                   ? null
                   : unloadedToolchainContexts.getTargetPlatform(),
-              transitiveRootCauses);
+              state.transitiveRootCauses);
       if (env.valuesMissing()) {
         return null;
       }
@@ -302,9 +353,9 @@
       // more root causes during computeDependencies.
       // Note that this doesn't apply to AspectFunction, because aspects can't have configurable
       // attributes.
-      if (!transitiveRootCauses.isEmpty()
+      if (!state.transitiveRootCauses.isEmpty()
           && !Objects.equals(configConditions, ConfigConditions.EMPTY)) {
-        NestedSet<Cause> causes = transitiveRootCauses.build();
+        NestedSet<Cause> causes = state.transitiveRootCauses.build();
         env.getListener()
             .handle(Event.error(target.getLocation(), "Cannot compute config conditions"));
         throw new ReportedException(
@@ -318,6 +369,7 @@
       // Calculate the dependencies of this target.
       OrderedSetMultimap<DependencyKind, ConfiguredTargetAndData> depValueMap =
           computeDependencies(
+              state,
               env,
               resolver,
               ctgValue,
@@ -328,11 +380,9 @@
                   : unloadedToolchainContexts.asToolchainContexts(),
               DependencyResolver.shouldUseToolchainTransition(configuration, ctgValue.getTarget()),
               ruleClassProvider,
-              view.getHostConfiguration(),
-              transitivePackagesForPackageRootResolution,
-              transitiveRootCauses);
-      if (!transitiveRootCauses.isEmpty()) {
-        NestedSet<Cause> causes = transitiveRootCauses.build();
+              view.getHostConfiguration());
+      if (!state.transitiveRootCauses.isEmpty()) {
+        NestedSet<Cause> causes = state.transitiveRootCauses.build();
         // TODO(bazel-team): consider reporting the error in this class vs. exporting it for
         // BuildTool to handle. Calling code needs to be untangled for that to work and pass tests.
         throw new UnreportedException(
@@ -372,7 +422,7 @@
               configConditions,
               toolchainContexts,
               execGroupCollectionBuilder,
-              transitivePackagesForPackageRootResolution);
+              state.transitivePackagesForPackageRootResolution);
       if (ans != null && configuredTargetProgress != null) {
         configuredTargetProgress.doneConfigureTarget();
       }
@@ -615,6 +665,7 @@
    * <p>Returns null if Skyframe hasn't evaluated the required dependencies yet. In this case, the
    * caller should also return null to Skyframe.
    *
+   * @param state the compute state
    * @param env the Skyframe environment
    * @param resolver the dependency resolver
    * @param ctgValue the label and the configuration of the node
@@ -626,8 +677,11 @@
    *     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
    */
+  // TODO(b/213351014): Make the control flow of this helper function more readable. This will
+  //   involve making a corresponding change to State to match the control flow.
   @Nullable
   static OrderedSetMultimap<DependencyKind, ConfiguredTargetAndData> computeDependencies(
+      State state,
       Environment env,
       SkyframeDependencyResolver resolver,
       TargetAndConfiguration ctgValue,
@@ -636,42 +690,71 @@
       @Nullable ToolchainCollection<ToolchainContext> toolchainContexts,
       boolean useToolchainTransition,
       RuleClassProvider ruleClassProvider,
-      BuildConfigurationValue hostConfiguration,
-      @Nullable NestedSetBuilder<Package> transitivePackagesForPackageRootResolution,
-      NestedSetBuilder<Cause> transitiveRootCauses)
+      BuildConfigurationValue hostConfiguration)
       throws DependencyEvaluationException, ConfiguredValueCreationException,
           AspectCreationException, InterruptedException {
-    // Create the map from attributes to set of (target, transition) pairs.
-    OrderedSetMultimap<DependencyKind, DependencyKey> initialDependencies;
-    BuildConfigurationValue configuration = ctgValue.getConfiguration();
-    Label label = ctgValue.getLabel();
-    try {
-      initialDependencies =
-          resolver.dependentNodeMap(
-              ctgValue,
-              aspects,
-              configConditions,
-              toolchainContexts,
-              useToolchainTransition,
-              transitiveRootCauses,
-              ((ConfiguredRuleClassProvider) ruleClassProvider).getTrimmingTransitionFactory());
-    } catch (DependencyResolver.Failure e) {
-      env.getListener().post(new AnalysisRootCauseEvent(configuration, label, e.getMessage()));
-      throw new DependencyEvaluationException(
-          new ConfiguredValueCreationException(
-              e.getLocation(), e.getMessage(), label, configuration.getEventId(), null, null),
-          // These errors occur within DependencyResolver, which is attached to the current target.
-          // i.e. no dependent ConfiguredTargetFunction call happens to report its own error.
-          /*depReportedOwnError=*/ false);
-    } catch (InconsistentAspectOrderException e) {
-      throw new DependencyEvaluationException(e);
+    if (state.computeDependenciesResult != null) {
+      state.storedEventHandlerFromResolveConfigurations.replayOn(env.getListener());
+      return state.computeDependenciesResult;
     }
-    // Trim each dep's configuration so it only includes the fragments needed by its transitive
-    // closure.
-    ConfigurationResolver configResolver =
-        new ConfigurationResolver(env, ctgValue, hostConfiguration, configConditions);
-    OrderedSetMultimap<DependencyKind, Dependency> depValueNames =
-        configResolver.resolveConfigurations(initialDependencies);
+
+    OrderedSetMultimap<DependencyKind, Dependency> depValueNames;
+    if (state.resolveConfigurationsResult != null) {
+      depValueNames = state.resolveConfigurationsResult;
+    } else {
+      // Create the map from attributes to set of (target, transition) pairs.
+      OrderedSetMultimap<DependencyKind, DependencyKey> initialDependencies;
+      if (state.dependentNodeMapResult != null) {
+        initialDependencies = state.dependentNodeMapResult;
+      } else {
+        BuildConfigurationValue configuration = ctgValue.getConfiguration();
+        Label label = ctgValue.getLabel();
+        try {
+          initialDependencies =
+              resolver.dependentNodeMap(
+                  ctgValue,
+                  aspects,
+                  configConditions,
+                  toolchainContexts,
+                  useToolchainTransition,
+                  state.transitiveRootCauses,
+                  ((ConfiguredRuleClassProvider) ruleClassProvider).getTrimmingTransitionFactory());
+        } catch (DependencyResolver.Failure e) {
+          env.getListener().post(new AnalysisRootCauseEvent(configuration, label, e.getMessage()));
+          throw new DependencyEvaluationException(
+              new ConfiguredValueCreationException(
+                  e.getLocation(), e.getMessage(), label, configuration.getEventId(), null, null),
+              // These errors occur within DependencyResolver, which is attached to the current
+              // target. i.e. no dependent ConfiguredTargetFunction call happens to report its own
+              // error.
+              /*depReportedOwnError=*/ false);
+        } catch (InconsistentAspectOrderException e) {
+          throw new DependencyEvaluationException(e);
+        }
+        if (!env.valuesMissing()) {
+          state.dependentNodeMapResult = initialDependencies;
+        }
+      }
+      // Trim each dep's configuration so it only includes the fragments needed by its transitive
+      // closure.
+      ConfigurationResolver configResolver =
+          new ConfigurationResolver(env, ctgValue, hostConfiguration, configConditions);
+      StoredEventHandler storedEventHandler = new StoredEventHandler();
+      try {
+        depValueNames =
+            configResolver.resolveConfigurations(initialDependencies, storedEventHandler);
+      } catch (ConfiguredValueCreationException e) {
+        storedEventHandler.replayOn(env.getListener());
+        throw e;
+      }
+      if (!env.valuesMissing()) {
+        state.resolveConfigurationsResult = depValueNames;
+        state.storedEventHandlerFromResolveConfigurations = storedEventHandler;
+
+        // We won't need this anymore.
+        state.dependentNodeMapResult = null;
+      }
+    }
 
     // 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
@@ -682,33 +765,58 @@
     }
 
     // Resolve configured target dependencies and handle errors.
-    Map<SkyKey, ConfiguredTargetAndData> depValues =
-        resolveConfiguredTargetDependencies(
-            env,
-            ctgValue,
-            depValueNames.values(),
-            transitivePackagesForPackageRootResolution,
-            transitiveRootCauses);
-    if (depValues == null) {
-      return null;
+    Map<SkyKey, ConfiguredTargetAndData> depValues;
+    if (state.resolveConfiguredTargetDependenciesResult != null) {
+      depValues = state.resolveConfiguredTargetDependenciesResult;
+    } else {
+      depValues =
+          resolveConfiguredTargetDependencies(
+              env,
+              ctgValue,
+              depValueNames.values(),
+              state.transitivePackagesForPackageRootResolution,
+              state.transitiveRootCauses);
+      if (env.valuesMissing()) {
+        return null;
+      }
+      state.resolveConfiguredTargetDependenciesResult = depValues;
     }
 
     // Resolve required aspects.
-    OrderedSetMultimap<Dependency, ConfiguredAspect> depAspects =
-        AspectResolver.resolveAspectDependencies(
-            env, depValues, depValueNames.values(), transitivePackagesForPackageRootResolution);
-    if (depAspects == null) {
-      return null;
+    OrderedSetMultimap<Dependency, ConfiguredAspect> depAspects;
+    if (state.resolveAspectDependenciesResult != null) {
+      depAspects = state.resolveAspectDependenciesResult;
+    } else {
+      depAspects =
+          AspectResolver.resolveAspectDependencies(
+              env,
+              depValues,
+              depValueNames.values(),
+              state.transitivePackagesForPackageRootResolution);
+      if (env.valuesMissing()) {
+        return null;
+      }
+      state.resolveAspectDependenciesResult = depAspects;
     }
 
     // Merge the dependent configured targets and aspects into a single map.
+    OrderedSetMultimap<DependencyKind, ConfiguredTargetAndData> mergeAspectsResult;
     try {
-      return AspectResolver.mergeAspects(depValueNames, depValues, depAspects);
+      mergeAspectsResult = AspectResolver.mergeAspects(depValueNames, depValues, depAspects);
     } catch (DuplicateException e) {
       throw new DependencyEvaluationException(
           new ConfiguredValueCreationException(ctgValue, e.getMessage()),
           /*depReportedOwnError=*/ false);
     }
+    state.computeDependenciesResult = mergeAspectsResult;
+    state.storedEventHandlerFromResolveConfigurations.replayOn(env.getListener());
+
+    // We won't need these anymore.
+    state.resolveConfigurationsResult = null;
+    state.resolveConfiguredTargetDependenciesResult = null;
+    state.resolveAspectDependenciesResult = null;
+
+    return mergeAspectsResult;
   }
 
   /**
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 5080367..05a4f41 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
@@ -33,7 +33,6 @@
 import com.google.devtools.build.lib.analysis.config.ConfigurationResolver;
 import com.google.devtools.build.lib.analysis.util.AnalysisMock;
 import com.google.devtools.build.lib.analysis.util.AnalysisTestCase;
-import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
 import com.google.devtools.build.lib.packages.Attribute;
 import com.google.devtools.build.lib.packages.RuleClassProvider;
 import com.google.devtools.build.lib.packages.Target;
@@ -119,6 +118,8 @@
       try {
         OrderedSetMultimap<DependencyKind, ConfiguredTargetAndData> depMap =
             ConfiguredTargetFunction.computeDependencies(
+                new ConfiguredTargetFunction.State(
+                    /*storeTransitivePackagesForPackageRootResolution=*/ false),
                 env,
                 new SkyframeDependencyResolver(env),
                 (TargetAndConfiguration) skyKey.argument(),
@@ -127,9 +128,7 @@
                 /*toolchainContexts=*/ null,
                 /*useToolchainTransition=*/ false,
                 stateProvider.lateBoundRuleClassProvider(),
-                stateProvider.lateBoundHostConfig(),
-                NestedSetBuilder.stableOrder(),
-                NestedSetBuilder.stableOrder());
+                stateProvider.lateBoundHostConfig());
         return env.valuesMissing() ? null : new Value(depMap);
       } catch (RuntimeException e) {
         throw e;