Add support for feature splits in R8 partial

Bug: b/388737195
Change-Id: I3f43f1119f4ba510c40b65a4ae9b69a6f4598e64
diff --git a/src/main/java/com/android/tools/r8/FeatureSplit.java b/src/main/java/com/android/tools/r8/FeatureSplit.java
index 03f99fa..1478bff 100644
--- a/src/main/java/com/android/tools/r8/FeatureSplit.java
+++ b/src/main/java/com/android/tools/r8/FeatureSplit.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8;
 
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import com.android.tools.r8.utils.FeatureSplitConsumers;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -40,9 +41,9 @@
       };
 
   private ProgramConsumer programConsumer;
-  private final List<ProgramResourceProvider> programResourceProviders;
+  private List<ProgramResourceProvider> programResourceProviders;
   private final AndroidResourceProvider androidResourceProvider;
-  private final AndroidResourceConsumer androidResourceConsumer;
+  private AndroidResourceConsumer androidResourceConsumer;
 
   private FeatureSplit(
       ProgramConsumer programConsumer,
@@ -55,14 +56,31 @@
     this.androidResourceConsumer = androidResourceConsumer;
   }
 
-  public boolean isBase() {
-    return false;
-  }
-
   void internalSetProgramConsumer(ProgramConsumer consumer) {
     this.programConsumer = consumer;
   }
 
+  void internalSetProgramResourceProviders(List<ProgramResourceProvider> programResourceProviders) {
+    this.programResourceProviders = programResourceProviders;
+  }
+
+  FeatureSplitConsumers internalClearConsumers() {
+    FeatureSplitConsumers consumers =
+        new FeatureSplitConsumers(programConsumer, androidResourceConsumer);
+    programConsumer = null;
+    androidResourceConsumer = null;
+    return consumers;
+  }
+
+  void internalSetConsumers(FeatureSplitConsumers consumers) {
+    programConsumer = consumers.getProgramConsumer();
+    androidResourceConsumer = consumers.getAndroidResourceConsumer();
+  }
+
+  public boolean isBase() {
+    return false;
+  }
+
   public List<ProgramResourceProvider> getProgramResourceProviders() {
     return programResourceProviders;
   }
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index ed07e79..a6a1b23 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -230,7 +230,7 @@
       } else {
         if (options.partialSubCompilationConfiguration != null) {
           R8PartialR8SubCompilationConfiguration r8SubCompilationConfiguration =
-              options.partialSubCompilationConfiguration.asR8SubCompilationConfiguration();
+              options.partialSubCompilationConfiguration.asR8();
           r8SubCompilationConfiguration.commitDexingOutputClasses(appView.withClassHierarchy());
         }
         ApplicationWriter.create(appView, marker).write(executorService, inputApp);
diff --git a/src/main/java/com/android/tools/r8/R8Partial.java b/src/main/java/com/android/tools/r8/R8Partial.java
index e0bf09a..eccfd59 100644
--- a/src/main/java/com/android/tools/r8/R8Partial.java
+++ b/src/main/java/com/android/tools/r8/R8Partial.java
@@ -5,6 +5,8 @@
 
 import com.android.tools.r8.dex.ApplicationReader;
 import com.android.tools.r8.diagnostic.R8VersionDiagnostic;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.features.FeatureSplitConfiguration;
 import com.android.tools.r8.graph.DirectMappedDexApplication;
 import com.android.tools.r8.partial.R8PartialD8Result;
 import com.android.tools.r8.partial.R8PartialInput;
@@ -13,14 +15,19 @@
 import com.android.tools.r8.partial.R8PartialSubCompilationConfiguration.R8PartialR8SubCompilationConfiguration;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.ExceptionUtils;
+import com.android.tools.r8.utils.FeatureSplitConsumers;
 import com.android.tools.r8.utils.ForwardingDiagnosticsHandler;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.InternalProgramClassProvider;
+import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.concurrent.ExecutorService;
 
 class R8Partial {
@@ -55,14 +62,24 @@
     if (!options.getStartupOptions().getStartupProfileProviders().isEmpty()) {
       throw options.reporter.fatalError("Partial shrinking does not support startup profiles");
     }
+    if (options.getProguardConfiguration().isProtoShrinkingEnabled()) {
+      throw options.reporter.fatalError("Partial shrinking does not support proto shrinking");
+    }
+
+    Map<FeatureSplit, FeatureSplitConsumers> featureSplitConsumers =
+        getAndClearFeatureSplitConsumers();
 
     timing.begin("Process input");
     R8PartialInput input = runProcessInputStep(app, executor);
 
     timing.end().begin("Run D8");
     R8PartialD8Result d8Result = runD8Step(input, executor);
+    timing.end();
 
-    timing.end().begin("Run R8");
+    setFeatureSplitConsumers(featureSplitConsumers);
+    lockFeatureSplitProgramResourceProviders();
+
+    timing.begin("Run R8");
     runR8Step(app, input, d8Result, executor);
     timing.end();
 
@@ -103,9 +120,11 @@
     options.partialCompilationConfiguration.d8DexOptionsConsumer.accept(d8Options);
     R8PartialD8SubCompilationConfiguration subCompilationConfiguration =
         new R8PartialD8SubCompilationConfiguration(input.getD8Types(), input.getR8Types(), timing);
+    d8Options.setFeatureSplitConfiguration(options.getFeatureSplitConfiguration());
     d8Options.partialSubCompilationConfiguration = subCompilationConfiguration;
     D8.runInternal(d8App, d8Options, executor);
     return new R8PartialD8Result(
+        subCompilationConfiguration.getClassToFeatureSplitMap(),
         subCompilationConfiguration.getDexedOutputClasses(),
         subCompilationConfiguration.getDesugaredOutputClasses());
   }
@@ -171,7 +190,9 @@
     InternalOptions r8Options = r8Command.getInternalOptions();
     options.partialCompilationConfiguration.r8OptionsConsumer.accept(r8Options);
     r8Options.partialSubCompilationConfiguration =
-        new R8PartialR8SubCompilationConfiguration(d8Result.getDexedClasses(), timing);
+        new R8PartialR8SubCompilationConfiguration(
+            d8Result.getClassToFeatureSplitMap(), d8Result.getDexedClasses(), timing);
+    r8Options.setFeatureSplitConfiguration(options.getFeatureSplitConfiguration());
     r8Options.mapConsumer = options.mapConsumer;
     if (options.androidResourceProvider != null) {
       r8Options.androidResourceProvider = options.androidResourceProvider;
@@ -180,4 +201,61 @@
     }
     R8.runInternal(r8App, r8Options, executor);
   }
+
+  private Map<FeatureSplit, FeatureSplitConsumers> getAndClearFeatureSplitConsumers() {
+    FeatureSplitConfiguration featureSplitConfiguration = options.getFeatureSplitConfiguration();
+    if (featureSplitConfiguration == null) {
+      return null;
+    }
+    Map<FeatureSplit, FeatureSplitConsumers> featureSplitConsumers = new IdentityHashMap<>();
+    for (FeatureSplit featureSplit : featureSplitConfiguration.getFeatureSplits()) {
+      featureSplitConsumers.put(featureSplit, featureSplit.internalClearConsumers());
+    }
+    return featureSplitConsumers;
+  }
+
+  private void setFeatureSplitConsumers(
+      Map<FeatureSplit, FeatureSplitConsumers> featureSplitConsumers) {
+    if (featureSplitConsumers == null) {
+      return;
+    }
+    FeatureSplitConfiguration featureSplitConfiguration = options.getFeatureSplitConfiguration();
+    for (FeatureSplit featureSplit : featureSplitConfiguration.getFeatureSplits()) {
+      featureSplit.internalSetConsumers(featureSplitConsumers.get(featureSplit));
+    }
+    featureSplitConsumers.clear();
+  }
+
+  private void lockFeatureSplitProgramResourceProviders() {
+    FeatureSplitConfiguration featureSplitConfiguration = options.getFeatureSplitConfiguration();
+    if (featureSplitConfiguration == null) {
+      return;
+    }
+    for (FeatureSplit featureSplit : featureSplitConfiguration.getFeatureSplits()) {
+      List<ProgramResourceProvider> programResourceProviders =
+          featureSplit.getProgramResourceProviders();
+      List<ProgramResourceProvider> replacementProgramResourceProviders =
+          ListUtils.map(
+              programResourceProviders,
+              programResourceProvider ->
+                  new ProgramResourceProvider() {
+
+                    @Override
+                    public Collection<ProgramResource> getProgramResources() {
+                      throw new Unreachable();
+                    }
+
+                    @Override
+                    public DataResourceProvider getDataResourceProvider() {
+                      return programResourceProvider.getDataResourceProvider();
+                    }
+
+                    @Override
+                    public void finished(DiagnosticsHandler handler) throws IOException {
+                      programResourceProvider.finished(handler);
+                    }
+                  });
+      featureSplit.internalSetProgramResourceProviders(replacementProgramResourceProviders);
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/features/ClassToFeatureSplitMap.java b/src/main/java/com/android/tools/r8/features/ClassToFeatureSplitMap.java
index f4a0bea..e3e251c 100644
--- a/src/main/java/com/android/tools/r8/features/ClassToFeatureSplitMap.java
+++ b/src/main/java/com/android/tools/r8/features/ClassToFeatureSplitMap.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.ProgramResource;
 import com.android.tools.r8.ProgramResourceProvider;
 import com.android.tools.r8.ResourceException;
+import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -17,6 +18,7 @@
 import com.android.tools.r8.graph.ProgramDefinition;
 import com.android.tools.r8.graph.PrunedItems;
 import com.android.tools.r8.graph.lens.GraphLens;
+import com.android.tools.r8.partial.R8PartialSubCompilationConfiguration.R8PartialR8SubCompilationConfiguration;
 import com.android.tools.r8.synthesis.SyntheticItems;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.Reporter;
@@ -44,16 +46,17 @@
 
   public static ClassToFeatureSplitMap createInitialD8ClassToFeatureSplitMap(
       InternalOptions options) {
-    // We only support feature splits in D8 when run through R8 partial.
-    if (options.partialSubCompilationConfiguration != null) {
-      return createInitialClassToFeatureSplitMap(
-          options.dexItemFactory(), options.getFeatureSplitConfiguration(), options.reporter);
-    }
-    return createEmptyClassToFeatureSplitMap();
+    return createInitialClassToFeatureSplitMap(
+        options.dexItemFactory(), options.getFeatureSplitConfiguration(), options.reporter);
   }
 
   public static ClassToFeatureSplitMap createInitialR8ClassToFeatureSplitMap(
       InternalOptions options) {
+    if (options.partialSubCompilationConfiguration != null) {
+      R8PartialR8SubCompilationConfiguration subCompilationConfiguration =
+          options.partialSubCompilationConfiguration.asR8();
+      return subCompilationConfiguration.getClassToFeatureSplitMap();
+    }
     return createInitialClassToFeatureSplitMap(
         options.dexItemFactory(), options.getFeatureSplitConfiguration(), options.reporter);
   }
@@ -93,6 +96,23 @@
     return new ClassToFeatureSplitMap(classToFeatureSplitMap, representativeStringsForFeatureSplit);
   }
 
+  public ClassToFeatureSplitMap commitSyntheticsForR8Partial(AppView<AppInfo> appView) {
+    Map<DexType, FeatureSplit> newClassToFeatureSplitMap =
+        new IdentityHashMap<>(classToFeatureSplitMap);
+    SyntheticItems syntheticItems = appView.getSyntheticItems();
+    assert !syntheticItems.hasPendingSyntheticClasses();
+    for (DexProgramClass clazz : appView.appInfo().classes()) {
+      if (syntheticItems.isSynthetic(clazz)) {
+        FeatureSplit featureSplit = getFeatureSplit(clazz, syntheticItems);
+        if (!featureSplit.isBase()) {
+          newClassToFeatureSplitMap.put(clazz.getType(), featureSplit);
+        }
+      }
+    }
+    return new ClassToFeatureSplitMap(
+        newClassToFeatureSplitMap, representativeStringsForFeatureSplit);
+  }
+
   public int compareFeatureSplits(FeatureSplit featureSplitA, FeatureSplit featureSplitB) {
     assert featureSplitA != null;
     assert featureSplitB != null;
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
index 901e1fa..b9c1ed7 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
@@ -636,7 +636,9 @@
         // Since we've copied the code object from an existing method from the same class, the
         // code is already processed, and thus we don't need to schedule it for processing in D8.
         assert !appView.options().isGeneratingClassFiles() || replacement.getCode().isCfCode();
-        assert !appView.options().isGeneratingDex() || replacement.getCode().isDexCode();
+        assert !appView.options().isGeneratingDex()
+            || replacement.getCode().isDexCode()
+            || appView.options().partialSubCompilationConfiguration != null;
         return new ProgramMethod(implMethodHolder, replacement);
       }
       // The method might already have been moved by another invoke-dynamic targeting it.
diff --git a/src/main/java/com/android/tools/r8/partial/R8PartialD8Result.java b/src/main/java/com/android/tools/r8/partial/R8PartialD8Result.java
index d698676..54ee91b 100644
--- a/src/main/java/com/android/tools/r8/partial/R8PartialD8Result.java
+++ b/src/main/java/com/android/tools/r8/partial/R8PartialD8Result.java
@@ -3,20 +3,29 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.partial;
 
+import com.android.tools.r8.features.ClassToFeatureSplitMap;
 import com.android.tools.r8.graph.DexProgramClass;
 import java.util.Collection;
 
 public class R8PartialD8Result {
 
+  private final ClassToFeatureSplitMap classToFeatureSplitMap;
   private final Collection<DexProgramClass> dexedClasses;
   private final Collection<DexProgramClass> desugaredClasses;
 
   public R8PartialD8Result(
-      Collection<DexProgramClass> dexedClasses, Collection<DexProgramClass> desugaredClasses) {
+      ClassToFeatureSplitMap classToFeatureSplitMap,
+      Collection<DexProgramClass> dexedClasses,
+      Collection<DexProgramClass> desugaredClasses) {
+    this.classToFeatureSplitMap = classToFeatureSplitMap;
     this.dexedClasses = dexedClasses;
     this.desugaredClasses = desugaredClasses;
   }
 
+  public ClassToFeatureSplitMap getClassToFeatureSplitMap() {
+    return classToFeatureSplitMap;
+  }
+
   public Collection<DexProgramClass> getDexedClasses() {
     return dexedClasses;
   }
diff --git a/src/main/java/com/android/tools/r8/partial/R8PartialResourceUseCollector.java b/src/main/java/com/android/tools/r8/partial/R8PartialResourceUseCollector.java
index ad8fcdf..ef26726 100644
--- a/src/main/java/com/android/tools/r8/partial/R8PartialResourceUseCollector.java
+++ b/src/main/java/com/android/tools/r8/partial/R8PartialResourceUseCollector.java
@@ -20,7 +20,7 @@
 
   public void run() {
     R8PartialR8SubCompilationConfiguration r8SubCompilationConfiguration =
-        appView.options().partialSubCompilationConfiguration.asR8SubCompilationConfiguration();
+        appView.options().partialSubCompilationConfiguration.asR8();
     ResourceShrinker.runForTesting(r8SubCompilationConfiguration.getDexingOutputClasses(), this);
   }
 
diff --git a/src/main/java/com/android/tools/r8/partial/R8PartialSubCompilationConfiguration.java b/src/main/java/com/android/tools/r8/partial/R8PartialSubCompilationConfiguration.java
index ec8b0a9..cce90ee 100644
--- a/src/main/java/com/android/tools/r8/partial/R8PartialSubCompilationConfiguration.java
+++ b/src/main/java/com/android/tools/r8/partial/R8PartialSubCompilationConfiguration.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.partial;
 
+import com.android.tools.r8.features.ClassToFeatureSplitMap;
+import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
@@ -40,7 +42,7 @@
     return false;
   }
 
-  public R8PartialR8SubCompilationConfiguration asR8SubCompilationConfiguration() {
+  public R8PartialR8SubCompilationConfiguration asR8() {
     return null;
   }
 
@@ -50,6 +52,7 @@
     private final Set<DexType> d8Types;
     private final Set<DexType> r8Types;
 
+    private ClassToFeatureSplitMap classToFeatureSplitMap;
     private Collection<DexProgramClass> dexedOutputClasses;
     private Collection<DexProgramClass> desugaredOutputClasses;
 
@@ -60,6 +63,10 @@
       this.r8Types = r8Types;
     }
 
+    public ClassToFeatureSplitMap getClassToFeatureSplitMap() {
+      return classToFeatureSplitMap;
+    }
+
     public Collection<DexProgramClass> getDexedOutputClasses() {
       assert dexedOutputClasses != null;
       return dexedOutputClasses;
@@ -103,7 +110,9 @@
       return this;
     }
 
-    public void writeApplication(AppView<?> appView) {
+    public void writeApplication(AppView<AppInfo> appView) {
+      classToFeatureSplitMap =
+          appView.appInfo().getClassToFeatureSplitMap().commitSyntheticsForR8Partial(appView);
       dexedOutputClasses = new ArrayList<>();
       desugaredOutputClasses = new ArrayList<>();
       for (DexProgramClass clazz : appView.appInfo().classes()) {
@@ -119,15 +128,22 @@
   public static class R8PartialR8SubCompilationConfiguration
       extends R8PartialSubCompilationConfiguration {
 
+    private ClassToFeatureSplitMap classToFeatureSplitMap;
     private Collection<DexProgramClass> dexingOutputClasses;
 
     public R8PartialR8SubCompilationConfiguration(
+        ClassToFeatureSplitMap classToFeatureSplitMap,
         Collection<DexProgramClass> dexingOutputClasses,
         Timing timing) {
       super(timing);
+      this.classToFeatureSplitMap = classToFeatureSplitMap;
       this.dexingOutputClasses = dexingOutputClasses;
     }
 
+    public ClassToFeatureSplitMap getClassToFeatureSplitMap() {
+      return classToFeatureSplitMap;
+    }
+
     public Collection<DexProgramClass> getDexingOutputClasses() {
       assert dexingOutputClasses != null;
       return dexingOutputClasses;
@@ -173,7 +189,7 @@
     }
 
     @Override
-    public R8PartialR8SubCompilationConfiguration asR8SubCompilationConfiguration() {
+    public R8PartialR8SubCompilationConfiguration asR8() {
       return this;
     }
   }
diff --git a/src/main/java/com/android/tools/r8/partial/R8PartialUseCollector.java b/src/main/java/com/android/tools/r8/partial/R8PartialUseCollector.java
index 0af71c2..5fe7e73 100644
--- a/src/main/java/com/android/tools/r8/partial/R8PartialUseCollector.java
+++ b/src/main/java/com/android/tools/r8/partial/R8PartialUseCollector.java
@@ -51,7 +51,7 @@
 
   public void run(ExecutorService executorService) throws ExecutionException {
     R8PartialR8SubCompilationConfiguration r8SubCompilationConfiguration =
-        appView.options().partialSubCompilationConfiguration.asR8SubCompilationConfiguration();
+        appView.options().partialSubCompilationConfiguration.asR8();
     traceClasses(r8SubCompilationConfiguration.getDexingOutputClasses(), executorService);
     commitPackagesToKeep();
   }
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index 242a7eb..3f5389f 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -1873,7 +1873,17 @@
           analyses.traceInstanceFieldRead(
               fieldReference, singleResolutionResult, currentMethod, worklist);
 
-          ProgramField field = singleResolutionResult.getProgramField();
+          DexClassAndField classField = singleResolutionResult.getResolutionPair();
+          assert classField != null;
+
+          DexClass initialResolutionHolder = singleResolutionResult.getInitialResolutionHolder();
+          if (initialResolutionHolder != classField.getHolder()) {
+            // Mark the initial resolution holder as live. Note that this should only be done if
+            // the field is not a dead proto field (in which case we bail-out above).
+            markTypeAsLive(initialResolutionHolder, currentMethod);
+          }
+
+          ProgramField field = classField.asProgramField();
           if (field == null) {
             // No need to trace into the non-program code.
             return;
@@ -1891,11 +1901,6 @@
             fieldAccessInfoCollection.get(field.getReference()).setReadFromRecordInvokeDynamic();
           }
 
-          if (field.getReference() != fieldReference) {
-            // Mark the initial resolution holder as live.
-            markTypeAsLive(singleResolutionResult.getInitialResolutionHolder(), currentMethod);
-          }
-
           worklist.enqueueMarkFieldAsReachableAction(
               field, currentMethod, KeepReason.fieldReferencedIn(currentMethod));
         },
@@ -1937,7 +1942,17 @@
           analyses.traceInstanceFieldWrite(
               fieldReference, singleResolutionResult, currentMethod, worklist);
 
-          ProgramField field = singleResolutionResult.getProgramField();
+          DexClassAndField classField = singleResolutionResult.getResolutionPair();
+          assert classField != null;
+
+          DexClass initialResolutionHolder = singleResolutionResult.getInitialResolutionHolder();
+          if (initialResolutionHolder != classField.getHolder()) {
+            // Mark the initial resolution holder as live. Note that this should only be done if
+            // the field is not a dead proto field (in which case we bail-out above).
+            markTypeAsLive(initialResolutionHolder, currentMethod);
+          }
+
+          ProgramField field = classField.asProgramField();
           if (field == null) {
             // No need to trace into the non-program code.
             return;
@@ -1953,11 +1968,6 @@
             fieldAccessInfoCollection.get(field.getReference()).setWrittenFromMethodHandle();
           }
 
-          if (field.getReference() != fieldReference) {
-            // Mark the initial resolution holder as live.
-            markTypeAsLive(singleResolutionResult.getInitialResolutionHolder(), currentMethod);
-          }
-
           KeepReason reason = KeepReason.fieldReferencedIn(currentMethod);
           worklist.enqueueMarkFieldAsReachableAction(field, currentMethod, reason);
         },
@@ -2014,7 +2024,17 @@
           analyses.traceStaticFieldRead(
               fieldReference, singleResolutionResult, currentMethod, worklist);
 
-          ProgramField field = singleResolutionResult.getProgramField();
+          DexClassAndField classField = singleResolutionResult.getResolutionPair();
+          assert classField != null;
+
+          DexClass initialResolutionHolder = singleResolutionResult.getInitialResolutionHolder();
+          if (initialResolutionHolder != classField.getHolder()) {
+            // Mark the initial resolution holder as live. Note that this should only be done if
+            // the field is not a dead proto field (in which case we bail-out above).
+            markTypeAsLive(initialResolutionHolder, currentMethod);
+          }
+
+          ProgramField field = classField.asProgramField();
           if (field == null) {
             // No need to trace into the non-program code.
             return;
@@ -2039,13 +2059,6 @@
                 joiner -> joiner.disallowMinification().disallowOptimization().disallowShrinking());
           }
 
-          if (field.getReference() != fieldReference) {
-            // Mark the initial resolution holder as live. Note that this should only be done if
-            // the field
-            // is not a dead proto field (in which case we bail-out above).
-            markTypeAsLive(singleResolutionResult.getInitialResolutionHolder(), currentMethod);
-          }
-
           markFieldAsLive(field, currentMethod);
         },
         failedResolution -> {
@@ -2102,7 +2115,17 @@
           analyses.traceStaticFieldWrite(
               fieldReference, singleResolutionResult, currentMethod, worklist);
 
-          ProgramField field = singleResolutionResult.getProgramField();
+          DexClassAndField classField = singleResolutionResult.getResolutionPair();
+          assert classField != null;
+
+          DexClass initialResolutionHolder = singleResolutionResult.getInitialResolutionHolder();
+          if (initialResolutionHolder != classField.getHolder()) {
+            // Mark the initial resolution holder as live. Note that this should only be done if
+            // the field is not a dead proto field (in which case we bail-out above).
+            markTypeAsLive(initialResolutionHolder, currentMethod);
+          }
+
+          ProgramField field = classField.asProgramField();
           if (field == null) {
             // No need to trace into the non-program code.
             return;
@@ -2118,13 +2141,6 @@
             fieldAccessInfoCollection.get(field.getReference()).setWrittenFromMethodHandle();
           }
 
-          if (field.getReference() != fieldReference) {
-            // Mark the initial resolution holder as live. Note that this should only be done if
-            // the field
-            // is not a dead proto field (in which case we bail-out above).
-            markTypeAsLive(singleResolutionResult.getInitialResolutionHolder(), currentMethod);
-          }
-
           markFieldAsLive(field, currentMethod);
         },
         failedResolution -> {
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
index 71121cd..b331bcb 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
@@ -12,7 +12,6 @@
 import com.android.tools.r8.errors.MissingGlobalSyntheticsConsumerDiagnostic;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.features.ClassToFeatureSplitMap;
-import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.ClassResolutionResult;
 import com.android.tools.r8.graph.ClasspathMethod;
@@ -666,12 +665,7 @@
 
   private SynthesizingContext getSynthesizingContext(
       ProgramDefinition context, AppView<?> appView) {
-    if (appView.hasClassHierarchy()) {
-      AppInfoWithClassHierarchy appInfo = appView.appInfoWithClassHierarchy();
-      return getSynthesizingContext(context, appInfo.getClassToFeatureSplitMap());
-    }
-    return getSynthesizingContext(
-        context, ClassToFeatureSplitMap.createEmptyClassToFeatureSplitMap());
+    return getSynthesizingContext(context, appView.appInfo().getClassToFeatureSplitMap());
   }
 
   /** Used to find the synthesizing context for a new synthetic that is about to be created. */
diff --git a/src/main/java/com/android/tools/r8/utils/FeatureSplitConsumers.java b/src/main/java/com/android/tools/r8/utils/FeatureSplitConsumers.java
new file mode 100644
index 0000000..610544b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/FeatureSplitConsumers.java
@@ -0,0 +1,27 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils;
+
+import com.android.tools.r8.AndroidResourceConsumer;
+import com.android.tools.r8.ProgramConsumer;
+
+public class FeatureSplitConsumers {
+
+  private final ProgramConsumer programConsumer;
+  private final AndroidResourceConsumer androidResourceConsumer;
+
+  public FeatureSplitConsumers(
+      ProgramConsumer programConsumer, AndroidResourceConsumer androidResourceConsumer) {
+    this.programConsumer = programConsumer;
+    this.androidResourceConsumer = androidResourceConsumer;
+  }
+
+  public ProgramConsumer getProgramConsumer() {
+    return programConsumer;
+  }
+
+  public AndroidResourceConsumer getAndroidResourceConsumer() {
+    return androidResourceConsumer;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/FileUtils.java b/src/main/java/com/android/tools/r8/utils/FileUtils.java
index efaeb5f..9153858 100644
--- a/src/main/java/com/android/tools/r8/utils/FileUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/FileUtils.java
@@ -93,6 +93,10 @@
         || name.endsWith(AAR_EXTENSION);
   }
 
+  public static String readTextFile(Path file) throws IOException {
+    return readTextFile(file, StandardCharsets.UTF_8);
+  }
+
   public static String readTextFile(Path file, Charset charset) throws IOException {
     return new String(Files.readAllBytes(file), charset);
   }
diff --git a/src/test/java/com/android/tools/r8/benchmarks/appdumps/AppDumpBenchmarkBuilder.java b/src/test/java/com/android/tools/r8/benchmarks/appdumps/AppDumpBenchmarkBuilder.java
index df805ff..b217907 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/appdumps/AppDumpBenchmarkBuilder.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/appdumps/AppDumpBenchmarkBuilder.java
@@ -30,6 +30,8 @@
 import com.android.tools.r8.keepanno.annotations.KeepEdge;
 import com.android.tools.r8.keepanno.annotations.KeepItemKind;
 import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableSet;
 import java.io.IOException;
 import java.lang.annotation.RetentionPolicy;
@@ -401,7 +403,12 @@
                   TestBase.testForR8Partial(environment.getTemp(), Backend.DEX)
                       .addProgramFiles(dump.getProgramArchive())
                       .addLibraryFiles(dump.getLibraryArchive())
-                      .addKeepRuleFiles(dump.getProguardConfigFile())
+                      .addKeepRules(
+                          // TODO(b/392529669): Add support for proto shrinking.
+                          StringUtils.replaceAll(
+                              FileUtils.readTextFile(dump.getProguardConfigFile()),
+                              "-shrinkunusedprotofields",
+                              ""))
                       .addR8PartialOptionsModification(
                           options -> {
                             options.apiModelingOptions().androidApiExtensionPackages =
diff --git a/src/test/java/com/android/tools/r8/benchmarks/appdumps/ChromeBenchmarks.java b/src/test/java/com/android/tools/r8/benchmarks/appdumps/ChromeBenchmarks.java
index b58c940..679c4a3 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/appdumps/ChromeBenchmarks.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/appdumps/ChromeBenchmarks.java
@@ -5,10 +5,12 @@
 
 import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.R8PartialTestBuilder;
+import com.android.tools.r8.R8PartialTestCompileResult;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.benchmarks.BenchmarkBase;
 import com.android.tools.r8.benchmarks.BenchmarkConfig;
+import com.android.tools.r8.utils.LibraryProvidedProguardRulesTestUtils;
 import com.google.common.collect.ImmutableList;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -44,7 +46,8 @@
             .setName("ChromeAppPartial")
             .setDumpDependencyPath(dir)
             .setFromRevision(16457)
-            .buildR8WithPartialShrinking(ChromeBenchmarks::configurePartial));
+            .buildR8WithPartialShrinking(
+                ChromeBenchmarks::configurePartial, ChromeBenchmarks::inspectPartial));
   }
 
   private static void configure(R8FullTestBuilder testBuilder) {
@@ -60,12 +63,21 @@
 
   private static void configurePartial(R8PartialTestBuilder testBuilder) {
     testBuilder
-        .allowDiagnosticInfoMessages()
+        .addDontWarn("android.adservices.common.AdServicesOutcomeReceiver")
+        .allowDiagnosticMessages()
         .allowUnusedDontWarnPatterns()
         .allowUnusedProguardConfigurationRules()
         .allowUnnecessaryDontWarnWildcards();
   }
 
+  private static void inspectPartial(R8PartialTestCompileResult compileResult) {
+    compileResult.inspectDiagnosticMessages(
+        diagnostics ->
+            diagnostics
+                .assertWarningsMatch(LibraryProvidedProguardRulesTestUtils.getDiagnosticMatcher())
+                .assertNoErrors());
+  }
+
   @Ignore
   @Test
   @Override
@@ -78,8 +90,6 @@
     testBenchmarkWithName("ChromeApp");
   }
 
-  // TODO(b/388421578): Add support for feature splits in R8 partial.
-  @Ignore
   @Test
   public void testChromeAppPartial() throws Exception {
     testBenchmarkWithName("ChromeAppPartial");
diff --git a/tools/perf.py b/tools/perf.py
index e891508..6215f3e 100755
--- a/tools/perf.py
+++ b/tools/perf.py
@@ -20,6 +20,9 @@
     'ChromeApp': {
         'targets': ['r8-full']
     },
+    'ChromeAppPartial': {
+        'targets': ['r8-full']
+    },
     'CraneApp': {
         'targets': ['r8-full']
     },