Prototype for R8 partial compilation

Bug: b/309743298
Change-Id: I7799a2b90f7258a1f2795eb58d14db3f3fdeb2f1
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 886f985..8fccc8d 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -190,8 +190,8 @@
     return timing.time("Create app-view", () -> AppView.createForD8(appInfo, typeRewriter, timing));
   }
 
-  private static void runInternal(
-      AndroidApp inputApp, InternalOptions options, ExecutorService executor) throws IOException {
+  static void runInternal(AndroidApp inputApp, InternalOptions options, ExecutorService executor)
+      throws IOException {
     if (options.printMemory) {
       // Run GC twice to remove objects with finalizers.
       System.gc();
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 76a03cf..24d8545 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -171,7 +171,7 @@
   private final Timing timing;
   private final InternalOptions options;
 
-  private R8(InternalOptions options) {
+  R8(InternalOptions options) {
     this.options = options;
     if (options.printMemory) {
       System.gc();
@@ -245,9 +245,17 @@
         });
   }
 
-  private static void runInternal(AndroidApp app, InternalOptions options, ExecutorService executor)
+  static void runInternal(AndroidApp app, InternalOptions options, ExecutorService executor)
       throws IOException {
-    new R8(options).runInternal(app, executor);
+    if (options.r8PartialCompilationOptions.enabled) {
+      try {
+        new R8Partial(options).runInternal(app, executor);
+      } catch (ResourceException e) {
+        throw new RuntimeException(e);
+      }
+    } else {
+      new R8(options).runInternal(app, executor);
+    }
   }
 
   private static DirectMappedDexApplication getDirectApp(AppView<?> appView) {
@@ -255,8 +263,7 @@
   }
 
   @SuppressWarnings("DefaultCharset")
-  private void runInternal(AndroidApp inputApp, ExecutorService executorService)
-      throws IOException {
+  void runInternal(AndroidApp inputApp, ExecutorService executorService) throws IOException {
     timing.begin("Run prelude");
     assert options.programConsumer != null;
     if (options.quiet) {
diff --git a/src/main/java/com/android/tools/r8/R8Partial.java b/src/main/java/com/android/tools/r8/R8Partial.java
new file mode 100644
index 0000000..0d75c31
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/R8Partial.java
@@ -0,0 +1,232 @@
+// Copyright (c) 2024, 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;
+
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.android.tools.r8.StringConsumer.FileConsumer;
+import com.android.tools.r8.dex.ApplicationReader;
+import com.android.tools.r8.dump.CompilerDump;
+import com.android.tools.r8.dump.DumpOptions;
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.naming.MapConsumer;
+import com.android.tools.r8.synthesis.SyntheticItems.GlobalSyntheticsStrategy;
+import com.android.tools.r8.tracereferences.TraceReferencesBridge;
+import com.android.tools.r8.tracereferences.TraceReferencesCommand;
+import com.android.tools.r8.tracereferences.TraceReferencesKeepRules;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DumpInputFlags;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.ZipUtils;
+import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
+import com.google.common.io.ByteStreams;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+class R8Partial {
+
+  private final InternalOptions options;
+
+  R8Partial(InternalOptions options) {
+    this.options = options;
+  }
+
+  void runInternal(AndroidApp app, ExecutorService executor) throws IOException, ResourceException {
+    Timing timing = Timing.create("R8 partial " + Version.LABEL, options);
+
+    ProgramConsumer originalProgramConsumer = options.programConsumer;
+    MapConsumer originalMapConsumer = options.mapConsumer;
+
+    Path tmp = options.r8PartialCompilationOptions.getTemp();
+    Path dumpFile = options.r8PartialCompilationOptions.getDumpFile();
+
+    // Create a dump of the compiler input.
+    // TODO(b/309743298): Do not use compiler dump to handle splitting the compilation. This should
+    // be all in memory.
+    ApplicationReader applicationReader = new ApplicationReader(app, options, timing);
+    applicationReader.dump(
+        new DumpInputFlags() {
+
+          @Override
+          public Path getDumpPath() {
+            return dumpFile;
+          }
+
+          @Override
+          public boolean shouldDump(DumpOptions options) {
+            return true;
+          }
+
+          @Override
+          public boolean shouldFailCompilation() {
+            return false;
+          }
+
+          @Override
+          public boolean shouldLogDumpInfoMessage() {
+            return false;
+          }
+        });
+    CompilerDump dump = CompilerDump.fromArchive(dumpFile, tmp);
+    if (dump.getBuildProperties().hasMainDexKeepRules()
+        || dump.getBuildProperties().hasArtProfileProviders()
+        || dump.getBuildProperties().hasStartupProfileProviders()) {
+      throw options.reporter.fatalError(
+          "Split compilation does not support legacy multi-dex, baseline or startup profiles");
+    }
+
+    DexApplication dapp = applicationReader.read().toDirect();
+    AppInfoWithClassHierarchy appInfo =
+        AppInfoWithClassHierarchy.createForDesugaring(
+            AppInfo.createInitialAppInfo(dapp, GlobalSyntheticsStrategy.forNonSynthesizing()));
+
+    Predicate<String> isR8 = options.r8PartialCompilationOptions.isR8;
+    Set<String> d8classes = new HashSet<>();
+    appInfo
+        .classes()
+        .forEach(
+            clazz -> {
+              String key = clazz.toSourceString();
+              if (!d8classes.contains(key) && !isR8.test(key)) {
+                d8classes.add(key);
+                // TODO(b/309743298): Improve this to only visit each class once and stop at
+                //  library boundary.
+                appInfo.forEachSuperType(
+                    clazz,
+                    (superType, subclass, ignored) -> {
+                      DexProgramClass superClass =
+                          asProgramClassOrNull(appInfo.definitionFor(superType));
+                      if (superClass != null) {
+                        d8classes.add(superClass.toSourceString());
+                      }
+                    });
+              }
+            });
+
+    // Filter the program input into the D8 and R8 parts.
+    Set<String> d8ZipEntries =
+        d8classes.stream()
+            .map(name -> name.replace('.', '/') + ".class")
+            .collect(Collectors.toSet());
+    ZipBuilder d8ProgramBuilder = ZipBuilder.builder(tmp.resolve("d8-program.jar"));
+    ZipBuilder r8ProgramBuilder = ZipBuilder.builder(tmp.resolve("r8-program.jar"));
+    ZipUtils.iter(
+        dump.getProgramArchive(),
+        (entry, input) -> {
+          if (d8ZipEntries.contains(entry.getName())) {
+            d8ProgramBuilder.addBytes(entry.getName(), ByteStreams.toByteArray(input));
+          } else {
+            r8ProgramBuilder.addBytes(entry.getName(), ByteStreams.toByteArray(input));
+          }
+        });
+    Path d8Program = d8ProgramBuilder.build();
+    Path r8Program = r8ProgramBuilder.build();
+
+    // Compile D8 input with D8.
+    Path d8Output = tmp.resolve("d8-output.zip");
+    D8Command.Builder d8Builder =
+        D8Command.builder()
+            .setMinApiLevel(dump.getBuildProperties().getMinApi())
+            .addLibraryFiles(dump.getLibraryArchive())
+            .addClasspathFiles(dump.getClasspathArchive())
+            .addClasspathFiles(r8Program)
+            .addProgramFiles(d8Program)
+            .setMode(dump.getBuildProperties().getCompilationMode())
+            .setOutput(d8Output, OutputMode.DexIndexed);
+    if (dump.hasDesugaredLibrary()) {
+      d8Builder.addDesugaredLibraryConfiguration(
+          Files.readString(dump.getDesugaredLibraryFile(), UTF_8));
+    }
+    d8Builder.validate();
+    D8Command d8command = d8Builder.makeCommand();
+    AndroidApp d8App = d8command.getInputApp();
+    InternalOptions d8Options = d8command.getInternalOptions();
+    assert d8Options.getMinApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.N)
+        : "Default interface methods not yet supported";
+    D8.runInternal(d8App, d8Options, executor);
+
+    // Run trace references to produce keep rules for the D8 compiled part.
+    // TODO(b/309743298): Do not emit keep rules into a file.
+    Path traceReferencesRules = tmp.resolve("tr.rules");
+    TraceReferencesKeepRules keepRulesConsumer =
+        TraceReferencesKeepRules.builder()
+            .setOutputConsumer(new FileConsumer(traceReferencesRules))
+            .build();
+    TraceReferencesCommand.Builder trBuilder =
+        TraceReferencesCommand.builder()
+            .setConsumer(keepRulesConsumer)
+            .addLibraryFiles(dump.getLibraryArchive())
+            .addTargetFiles(r8Program)
+            .addSourceFiles(d8Program);
+    TraceReferencesCommand tr = TraceReferencesBridge.makeCommand(trBuilder);
+    TraceReferencesBridge.runInternal(tr);
+
+    // Compile R8 input with R8 using the keep rules from trace references.
+    Path r8Output = tmp.resolve("r8-output.zip");
+    R8Command.Builder r8Builder =
+        R8Command.builder()
+            .setMinApiLevel(dump.getBuildProperties().getMinApi())
+            .addLibraryFiles(dump.getLibraryArchive())
+            .addClasspathFiles(dump.getClasspathArchive())
+            .addClasspathFiles(d8Program)
+            .addProgramFiles(r8Program)
+            .addProguardConfigurationFiles(dump.getProguardConfigFile(), traceReferencesRules)
+            .setEnableEmptyMemberRulesToDefaultInitRuleConversion(true)
+            .setMode(dump.getBuildProperties().getCompilationMode())
+            .setOutput(r8Output, OutputMode.DexIndexed);
+    if (dump.hasDesugaredLibrary()) {
+      r8Builder.addDesugaredLibraryConfiguration(
+          Files.readString(dump.getDesugaredLibraryFile(), UTF_8));
+    }
+    R8Command r8Command = r8Builder.makeCommand();
+    AndroidApp r8App = r8Command.getInputApp();
+    InternalOptions r8Options = r8Command.getInternalOptions();
+    r8Options.mapConsumer = originalMapConsumer;
+    r8Options.quiet = true; // Don't write the R8 version.
+    R8.runInternal(r8App, r8Options, executor);
+
+    // Emit resources and merged DEX to the output consumer.
+    // TODO(b/309743298): Consider passing the DataResourceConsumer to the R8 invocation above.
+    DataResourceConsumer dataResourceConsumer = originalProgramConsumer.getDataResourceConsumer();
+    if (dataResourceConsumer != null) {
+      ZipUtils.iter(
+          r8Output,
+          (zip, entry, is) -> {
+            if (entry.getName().endsWith(FileUtils.DEX_EXTENSION)) {
+              return;
+            }
+            dataResourceConsumer.accept(
+                DataEntryResource.fromZip(zip, entry), new DiagnosticsHandler() {});
+          });
+    }
+    // TODO(b/309743298): Handle jumbo string rewriting with PCs in mapping file.
+    D8Command.Builder mergerBuilder =
+        D8Command.builder()
+            .setMinApiLevel(dump.getBuildProperties().getMinApi())
+            .addLibraryFiles(dump.getLibraryArchive())
+            .addClasspathFiles(dump.getClasspathArchive())
+            .addProgramFiles(d8Output, r8Output)
+            .setMode(dump.getBuildProperties().getCompilationMode())
+            .setProgramConsumer(originalProgramConsumer);
+    mergerBuilder.validate();
+    D8Command mergeCommand = mergerBuilder.makeCommand();
+    AndroidApp mergeApp = mergeCommand.getInputApp();
+    InternalOptions mergeOptions = mergeCommand.getInternalOptions();
+    D8.runInternal(mergeApp, mergeOptions, executor);
+    timing.end();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
index 5e31c47..8890de3 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -157,6 +157,11 @@
     return builder.build();
   }
 
+  public final void dump(DumpInputFlags dumpInputFlags) {
+    assert verifyMainDexOptionsCompatible(inputApp, options);
+    dumpApplication(dumpInputFlags);
+  }
+
   private void dumpApplication(DumpInputFlags dumpInputFlags) {
     DumpOptions dumpOptions = options.dumpOptions;
     if (dumpOptions == null || !dumpInputFlags.shouldDump(dumpOptions)) {
diff --git a/src/test/testbase/java/com/android/tools/r8/dump/CompilerDump.java b/src/main/java/com/android/tools/r8/dump/CompilerDump.java
similarity index 100%
rename from src/test/testbase/java/com/android/tools/r8/dump/CompilerDump.java
rename to src/main/java/com/android/tools/r8/dump/CompilerDump.java
diff --git a/src/main/java/com/android/tools/r8/tracereferences/TraceReferences.java b/src/main/java/com/android/tools/r8/tracereferences/TraceReferences.java
index 50a016c..c186bb9 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/TraceReferences.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/TraceReferences.java
@@ -67,7 +67,7 @@
         command.getReporter(), () -> runInternal(command, options));
   }
 
-  private static void runInternal(TraceReferencesCommand command, InternalOptions options)
+  static void runInternal(TraceReferencesCommand command, InternalOptions options)
       throws IOException, ResourceException {
     AndroidApp.Builder builder = AndroidApp.builder();
     command.getLibrary().forEach(builder::addLibraryResourceProvider);
diff --git a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesBridge.java b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesBridge.java
new file mode 100644
index 0000000..5c0b302
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesBridge.java
@@ -0,0 +1,20 @@
+// Copyright (c) 2024, 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.tracereferences;
+
+import com.android.tools.r8.ResourceException;
+import java.io.IOException;
+
+// Provide access to some package private APIs.
+public class TraceReferencesBridge {
+
+  public static TraceReferencesCommand makeCommand(TraceReferencesCommand.Builder builder) {
+    return builder.makeCommand();
+  }
+
+  public static void runInternal(TraceReferencesCommand command)
+      throws IOException, ResourceException {
+    TraceReferences.runInternal(command, command.getInternalOptions());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommand.java b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommand.java
index 28deeb3..429b912 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommand.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommand.java
@@ -337,7 +337,7 @@
       return this;
     }
 
-    private TraceReferencesCommand makeCommand() {
+    TraceReferencesCommand makeCommand() {
       if (isPrintHelp() || isPrintVersion()) {
         return new TraceReferencesCommand(isPrintHelp(), isPrintVersion());
       }
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index da0da31..7d9439b 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -114,11 +114,14 @@
 import com.android.tools.r8.verticalclassmerging.VerticallyMergedClasses;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Predicates;
+import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -1019,6 +1022,9 @@
   private final ArtProfileOptions artProfileOptions = new ArtProfileOptions(this);
   private final StartupOptions startupOptions = new StartupOptions();
   private final InstrumentationOptions instrumentationOptions;
+  public final R8PartialCompilationOptions r8PartialCompilationOptions =
+      new R8PartialCompilationOptions(
+          System.getProperty("com.android.tools.r8.r8PartialCompilation"));
   public final TestingOptions testing = new TestingOptions();
 
   public List<ProguardConfigurationRule> mainDexKeepRules = ImmutableList.of();
@@ -2276,6 +2282,39 @@
     }
   }
 
+  public static class R8PartialCompilationOptions {
+    public boolean enabled;
+    public Path tempDir = null;
+    public Predicate<String> isR8 = null;
+
+    R8PartialCompilationOptions(String partialR8) {
+      this.enabled = partialR8 != null;
+      if (this.enabled) {
+        final List<String> prefixes = Splitter.on(",").splitToList(partialR8);
+        this.isR8 =
+            name -> {
+              for (int i = 0; i < prefixes.size(); i++) {
+                if (name.startsWith(prefixes.get(i))) {
+                  return true;
+                }
+              }
+              return false;
+            };
+      }
+    }
+
+    public synchronized Path getTemp() throws IOException {
+      if (tempDir == null) {
+        tempDir = Files.createTempDirectory("r8PartialCompilation");
+      }
+      return tempDir;
+    }
+
+    public Path getDumpFile() throws IOException {
+      return getTemp().resolve("dump.zip");
+    }
+  }
+
   public static class TestingOptions {
 
     public boolean enableEmbeddedKeepAnnotations =
diff --git a/src/main/java/com/android/tools/r8/utils/ZipUtils.java b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
index 2b11a9f..59724de 100644
--- a/src/main/java/com/android/tools/r8/utils/ZipUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
@@ -89,13 +89,21 @@
     iter(Paths.get(zipFileStr), handler);
   }
 
-  public static void iter(Path zipFilePath, OnEntryHandler handler) throws IOException {
+  public static void iter(Path zipFile, OnEntryHandler handler) throws IOException {
+    iter(zipFile, (zip, entry, input) -> handler.onEntry(entry, input));
+  }
+
+  public interface OnEntryHandlerWithZipFile {
+    void onEntry(ZipFile zip, ZipEntry entry, InputStream input) throws IOException;
+  }
+
+  public static void iter(Path zipFilePath, OnEntryHandlerWithZipFile handler) throws IOException {
     try (ZipFile zipFile = new ZipFile(zipFilePath.toFile(), StandardCharsets.UTF_8)) {
       final Enumeration<? extends ZipEntry> entries = zipFile.entries();
       while (entries.hasMoreElements()) {
         ZipEntry entry = entries.nextElement();
         try (InputStream entryStream = zipFile.getInputStream(entry)) {
-          handler.onEntry(entry, entryStream);
+          handler.onEntry(zipFile, entry, entryStream);
         }
       }
     }
diff --git a/src/test/java/com/android/tools/r8/partial/ClassHierarchyInterleavedD8AndR8Test.java b/src/test/java/com/android/tools/r8/partial/ClassHierarchyInterleavedD8AndR8Test.java
new file mode 100644
index 0000000..0af90b1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/partial/ClassHierarchyInterleavedD8AndR8Test.java
@@ -0,0 +1,105 @@
+// Copyright (c) 2024, 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.partial;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.DexVm;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.ThrowingConsumer;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.util.function.Predicate;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ClassHierarchyInterleavedD8AndR8Test extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  // Test with min API level 24 where default interface methods are supported instead of using
+  // dump.getBuildProperties().getMinApi(). Tivi has min API 23 and there are currently trace
+  // references issues with CC classes.
+  public static TestParametersCollection data() {
+    return getTestParameters()
+        .withDexRuntime(DexVm.Version.V7_0_0)
+        .withApiLevel(AndroidApiLevel.N)
+        .build();
+  }
+
+  private void runTest(
+      Predicate<String> isR8, ThrowingConsumer<CodeInspector, RuntimeException> inspector)
+      throws Exception {
+    testForR8(parameters.getBackend())
+        .addOptionsModification(
+            options -> {
+              options.r8PartialCompilationOptions.enabled = true;
+              options.r8PartialCompilationOptions.isR8 = isR8;
+            })
+        .setMinApi(parameters)
+        .addProgramClasses(A.class, B.class, C.class, Main.class)
+        .addKeepMainRule(Main.class)
+        .compile()
+        .inspect(inspector)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithEmptyOutput();
+  }
+
+  @Test
+  public void testD8Top() throws Exception {
+    runTest(
+        name -> !name.equals(A.class.getTypeName()),
+        inspector -> {
+          assertThat(inspector.clazz(A.class), isPresentAndNotRenamed());
+          assertThat(inspector.clazz(B.class), isAbsent()); // Merged into C.
+          assertThat(inspector.clazz(C.class), isPresentAndRenamed());
+        });
+  }
+
+  @Test
+  public void testD8Middle() throws Exception {
+    runTest(
+        name -> !name.equals(B.class.getTypeName()),
+        inspector -> {
+          assertThat(inspector.clazz(A.class), isPresentAndNotRenamed());
+          assertThat(inspector.clazz(B.class), isPresentAndNotRenamed());
+          assertThat(inspector.clazz(C.class), isPresentAndRenamed());
+        });
+  }
+
+  @Test
+  public void testD8Bottom() throws Exception {
+    runTest(
+        name -> !name.equals(C.class.getTypeName()),
+        inspector -> {
+          assertThat(inspector.clazz(A.class), isPresentAndNotRenamed());
+          assertThat(inspector.clazz(B.class), isPresentAndNotRenamed());
+          assertThat(inspector.clazz(C.class), isPresentAndNotRenamed());
+        });
+  }
+
+  public static class A {}
+
+  public static class B extends A {}
+
+  public static class C extends B {}
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      new C();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/partial/PartialCompilationBasicTest.java b/src/test/java/com/android/tools/r8/partial/PartialCompilationBasicTest.java
new file mode 100644
index 0000000..929c0e2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/partial/PartialCompilationBasicTest.java
@@ -0,0 +1,105 @@
+// Copyright (c) 2024, 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.partial;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.DexVm;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class PartialCompilationBasicTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  // Test with min API level 24.
+  public static TestParametersCollection data() {
+    return getTestParameters()
+        .withDexRuntime(DexVm.Version.V7_0_0)
+        .withApiLevel(AndroidApiLevel.N)
+        .build();
+  }
+
+  @Test
+  public void runTestClassAIsCompiledWithD8() throws Exception {
+    testForR8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(A.class, B.class, Main.class)
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(
+            options -> {
+              options.r8PartialCompilationOptions.enabled = true;
+              // Run R8 on all classes except class A.
+              options.r8PartialCompilationOptions.isR8 =
+                  name -> !name.equals(A.class.getTypeName());
+            })
+        .compile()
+        .inspect(
+            inspector -> {
+              assertThat(inspector.clazz(A.class), isPresent());
+              assertThat(inspector.clazz(B.class), isAbsent());
+            })
+        .run(parameters.getRuntime(), Main.class, getClass().getTypeName())
+        .assertSuccessWithOutputLines("Instantiated", "Not instantiated");
+  }
+
+  @Test
+  public void runTestClassBIsCompiledWithD8() throws Exception {
+    testForR8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(A.class, B.class, Main.class)
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(
+            options -> {
+              options.r8PartialCompilationOptions.enabled = true;
+              // Run R8 on all classes except class A.
+              options.r8PartialCompilationOptions.isR8 =
+                  name -> !name.equals(B.class.getTypeName());
+            })
+        .compile()
+        .inspect(
+            inspector -> {
+              assertThat(inspector.clazz(A.class), isAbsent());
+              assertThat(inspector.clazz(B.class), isPresent());
+            })
+        .run(parameters.getRuntime(), Main.class, getClass().getTypeName())
+        .assertSuccessWithOutputLines("Not instantiated", "Instantiated");
+  }
+
+  public static class A {}
+
+  public static class B {}
+
+  public static class Main {
+
+    public static void main(String[] args) throws Exception {
+      // Instantiate class A.
+      try {
+        Class.forName(new String(args[0] + "$" + new String(new byte[] {65})));
+        System.out.println("Instantiated");
+      } catch (ClassNotFoundException e) {
+        System.out.println("Not instantiated");
+      }
+      // Instantiate class B.
+      try {
+        Class.forName(new String(args[0] + "$" + new String(new byte[] {66})));
+        System.out.println("Instantiated");
+      } catch (ClassNotFoundException e) {
+        System.out.println("Not instantiated");
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/partial/PartialCompilationDemoTest.java b/src/test/java/com/android/tools/r8/partial/PartialCompilationDemoTest.java
new file mode 100644
index 0000000..f527e25
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/partial/PartialCompilationDemoTest.java
@@ -0,0 +1,310 @@
+// Copyright (c) 2024, 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.partial;
+
+import static com.android.tools.r8.ToolHelper.DESUGARED_JDK_11_LIB_JAR;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.L8;
+import com.android.tools.r8.L8Command;
+import com.android.tools.r8.LibraryDesugaringTestConfiguration;
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.StringConsumer.FileConsumer;
+import com.android.tools.r8.StringResource;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestCompilerBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm;
+import com.android.tools.r8.desugar.desugaredlibrary.jdk11.DesugaredLibraryJDK11Undesugarer;
+import com.android.tools.r8.dump.CompilerDump;
+import com.android.tools.r8.tracereferences.TraceReferences;
+import com.android.tools.r8.tracereferences.TraceReferencesCommand;
+import com.android.tools.r8.tracereferences.TraceReferencesKeepRules;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.Pair;
+import com.android.tools.r8.utils.ZipUtils;
+import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.function.Predicate;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+// This test is currently for demonstrating R8 partial compilation, and for now all tests should
+// be @Ignore(d) and only enabled for local experiments.
+@RunWith(Parameterized.class)
+public class PartialCompilationDemoTest extends TestBase {
+
+  private static final Path TIVI_DUMP_PATH =
+      Paths.get(ToolHelper.THIRD_PARTY_DIR, "opensource-apps", "tivi", "dump_app.zip");
+  private static final Path NOWINANDROID_DUMP_PATH =
+      Paths.get(
+          ToolHelper.THIRD_PARTY_DIR, "opensource-apps", "android", "nowinandroid", "dump_app.zip");
+  // When using with the desugar_jdk_libs.jar in third_party (DESUGARED_JDK_11_LIB_JAR) for L8
+  // compilation then the configuration from the dump cannot be used for L8, as the configuration
+  // in the dump is the "machine specification" which only works with the specific version it was
+  // built for.
+  private static final boolean useDesugaredLibraryConfigurationFromDump = false;
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  // Test with min API level 24 where default interface methods are supported instead fo using
+  // dump.getBuildProperties().getMinApi(). Tivi has min API 23 and there are currently trace
+  // references issues with CC classes for default interface methods.
+  public static TestParametersCollection data() {
+    return getTestParameters()
+        .withDexRuntime(DexVm.Version.V7_0_0)
+        .withApiLevel(AndroidApiLevel.N)
+        .build();
+  }
+
+  private void configureDesugaredLibrary(
+      TestCompilerBuilder<?, ?, ?, ?, ?> builder, CompilerDump dump) {
+    if (useDesugaredLibraryConfigurationFromDump) {
+      builder.enableCoreLibraryDesugaring(
+          LibraryDesugaringTestConfiguration.forSpecification(dump.getDesugaredLibraryFile()));
+    } else {
+      builder.enableCoreLibraryDesugaring(
+          LibraryDesugaringTestConfiguration.forSpecification(
+              Paths.get(ToolHelper.LIBRARY_DESUGAR_SOURCE_DIR, "jdk11", "desugar_jdk_libs.json")));
+    }
+  }
+
+  private void configureDesugaredLibrary(L8Command.Builder builder, CompilerDump dump) {
+    if (useDesugaredLibraryConfigurationFromDump) {
+      builder.addDesugaredLibraryConfiguration(
+          StringResource.fromFile(dump.getDesugaredLibraryFile()));
+    } else {
+      builder.addDesugaredLibraryConfiguration(
+          StringResource.fromFile(
+              Paths.get(ToolHelper.LIBRARY_DESUGAR_SOURCE_DIR, "jdk11", "desugar_jdk_libs.json")));
+    }
+  }
+
+  @Test
+  @Ignore("Will be removed, only present to easily compare with partial compilation")
+  public void testD8() throws Exception {
+    Path tempDir = temp.newFolder().toPath();
+
+    CompilerDump dump = CompilerDump.fromArchive(TIVI_DUMP_PATH, temp.newFolder().toPath());
+    Path output = tempDir.resolve("tivid8.zip");
+    testForD8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addLibraryFiles(dump.getLibraryArchive())
+        .addClasspathFiles(dump.getClasspathArchive())
+        .addProgramFiles(dump.getProgramArchive())
+        .setMode(CompilationMode.RELEASE)
+        .apply(b -> configureDesugaredLibrary(b, dump))
+        .compile()
+        .writeToZip(output);
+
+    Path l8Output = tempDir.resolve("tivid8l8.zip");
+    runL8(tempDir, dump, output, l8Output);
+  }
+
+  @Test
+  @Ignore("Will be removed, only present to easily compare with partial compilation")
+  public void testR8() throws Exception {
+    Path tempDir = temp.newFolder().toPath();
+
+    CompilerDump dump = CompilerDump.fromArchive(TIVI_DUMP_PATH, temp.newFolder().toPath());
+    Path output = tempDir.resolve("tivir8.zip");
+    testForR8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addLibraryFiles(dump.getLibraryArchive())
+        .addClasspathFiles(dump.getClasspathArchive())
+        .addProgramFiles(dump.getProgramArchive())
+        .addKeepRuleFiles(dump.getProguardConfigFile())
+        .apply(b -> configureDesugaredLibrary(b, dump))
+        .setMode(CompilationMode.RELEASE)
+        .addOptionsModification(
+            options -> options.getOpenClosedInterfacesOptions().suppressAllOpenInterfaces())
+        .allowDiagnosticMessages()
+        .allowUnnecessaryDontWarnWildcards()
+        .allowUnusedDontWarnPatterns()
+        .allowUnusedProguardConfigurationRules()
+        .compile()
+        .writeToZip(output);
+
+    Path l8Output = tempDir.resolve("tivir8l8.zip");
+    runL8(tempDir, dump, output, l8Output);
+  }
+
+  private void testDump(CompilerDump dump, String appNamespace) throws Exception {
+    assert !appNamespace.endsWith(".");
+    Path tempDir = temp.newFolder().toPath();
+
+    // Different sets of namespaces to shrink.
+    ImmutableMap<String, Predicate<String>> splits =
+        ImmutableMap.of(
+            "androidx", name -> name.startsWith("androidx."),
+            "androidx_kotlin_and_kotlinx",
+                name ->
+                    name.startsWith("androidx.")
+                        || name.startsWith("kotlin.")
+                        || name.startsWith("kotlinx."),
+            "more_libraries",
+                name ->
+                    name.startsWith("androidx.")
+                        || name.startsWith("kotlin.")
+                        || name.startsWith("kotlinx.")
+                        || name.startsWith("android.support.")
+                        || name.startsWith("io.ktor.")
+                        || name.startsWith("com.google.android.gms.")
+                        || name.startsWith("com.google.firebase."),
+            "all_but_app namespace", name -> !name.startsWith(appNamespace + "."));
+
+    // Compile with each set of namespaces to shrink and collect DEX size.
+    Map<String, Pair<Long, Long>> dexSize = new LinkedHashMap<>();
+    for (Entry<String, Predicate<String>> entry : splits.entrySet()) {
+      long size = runR8PartialAndL8(tempDir, dump, entry.getKey(), entry.getValue());
+      dexSize.put(entry.getKey(), new Pair<>(size, 0L));
+    }
+    dexSize.forEach(
+        (name, size) ->
+            System.out.println(name + ": " + size.getFirst() + ", " + size.getSecond()));
+
+    // Check that sizes for increased namespaces to shrink does not increase size.
+    Pair<Long, Long> previousSize = null;
+    for (Entry<String, Pair<Long, Long>> entry : dexSize.entrySet()) {
+      if (previousSize != null) {
+        assertTrue(entry.getKey(), entry.getValue().getFirst() <= previousSize.getFirst());
+        assertTrue(entry.getKey(), entry.getValue().getSecond() <= previousSize.getSecond());
+      }
+      previousSize = entry.getValue();
+    }
+  }
+
+  @Test
+  @Ignore("Still experimental, do not run this test by default")
+  public void testTivi() throws Exception {
+    Path tempDir = temp.newFolder().toPath();
+    Path dumpDir = tempDir.resolve("dump");
+    testDump(CompilerDump.fromArchive(TIVI_DUMP_PATH, dumpDir), "app.tivi");
+  }
+
+  @Test
+  @Ignore("Still experimental, do not run this test by default")
+  public void testNowinandroid() throws Exception {
+    Path tempDir = temp.newFolder().toPath();
+    Path dumpDir = tempDir.resolve("dump");
+    testDump(
+        CompilerDump.fromArchive(NOWINANDROID_DUMP_PATH, dumpDir),
+        "com.google.samples.apps.nowinandroid");
+  }
+
+  private long runR8PartialAndL8(
+      Path tempDir, CompilerDump dump, String name, Predicate<String> isR8) throws Exception {
+    Path tmp = tempDir.resolve(name);
+    Files.createDirectory(tmp);
+    Path output = tmp.resolve("tivix8.zip");
+    runR8Partial(tempDir, dump, output, isR8);
+    Path l8Output = tmp.resolve("tivix8l8.zip");
+    runL8(tmp, dump, output, l8Output);
+    Box<Long> size = new Box<>(0L);
+    for (Path zipWithDex : new Path[] {output, l8Output}) {
+      ZipUtils.iter(
+          zipWithDex,
+          (zipEntry, input) ->
+              size.set(
+                  size.get()
+                      + (zipEntry.getName().endsWith(FileUtils.DEX_EXTENSION)
+                          ? zipEntry.getSize()
+                          : 0L)));
+    }
+    return size.get();
+  }
+
+  private void runR8Partial(Path tempDir, CompilerDump dump, Path output, Predicate<String> isR8)
+      throws IOException, CompilationFailedException {
+    testForR8(parameters.getBackend())
+        .addOptionsModification(
+            options -> {
+              options.r8PartialCompilationOptions.enabled = true;
+              options.r8PartialCompilationOptions.tempDir = tempDir;
+              options.r8PartialCompilationOptions.isR8 = isR8;
+
+              // For compiling nowonandroid.
+              options.testing.allowUnnecessaryDontWarnWildcards = true;
+              options.testing.allowUnusedDontWarnRules = true;
+              options.getOpenClosedInterfacesOptions().suppressAllOpenInterfaces();
+            })
+        .setMinApi(parameters)
+        .addLibraryFiles(dump.getLibraryArchive())
+        .addClasspathFiles(dump.getClasspathArchive())
+        .addProgramFiles(dump.getProgramArchive())
+        .addKeepRuleFiles(dump.getProguardConfigFile())
+        // Add this keep rules as trace references does not trace annotations. The consumer rules
+        // explicitly leaves out the annotation class Navigator.Name:
+        //
+        // # A -keep rule for the Navigator.Name annotation class is not required
+        // # since the annotation is referenced from the code.
+        //
+        // As this code reference is not in the D8 part of the code Navigator.Name is removed.
+        .addKeepRules("-keep class androidx.navigation.Navigator$Name { *; }")
+        .apply(b -> configureDesugaredLibrary(b, dump))
+        .setMode(CompilationMode.RELEASE)
+        .allowStdoutMessages()
+        .allowStderrMessages()
+        .allowUnusedProguardConfigurationRules() // nowonandroid
+        .enableEmptyMemberRulesToDefaultInitRuleConversion(true) // avoid warnings
+        .allowDiagnosticInfoMessages()
+        .compile()
+        .writeToZip(output);
+  }
+
+  private void runL8(Path tempDir, CompilerDump dump, Path appDexCode, Path output)
+      throws Exception {
+    Path desugaredLibraryRules = tempDir.resolve("desugared_library.rules");
+    TraceReferencesKeepRules keepRulesConsumer =
+        TraceReferencesKeepRules.builder()
+            .setOutputConsumer(new FileConsumer(desugaredLibraryRules))
+            .build();
+
+    AndroidApiLevel apiLevel = AndroidApiLevel.N;
+    Path path = tempDir.resolve("desugared_library.jar");
+    Path dd =
+        DesugaredLibraryJDK11Undesugarer.undesugaredJarJDK11(tempDir, DESUGARED_JDK_11_LIB_JAR);
+    L8Command.Builder commandBuilder =
+        L8Command.builder()
+            .setMinApiLevel(apiLevel.getLevel())
+            .addLibraryFiles(dump.getLibraryArchive())
+            .addProgramFiles(dd)
+            .setOutput(path, OutputMode.ClassFile);
+    configureDesugaredLibrary(commandBuilder, dump);
+    L8.run(commandBuilder.build());
+
+    TraceReferencesCommand.Builder traceReferencesCommandBuilder =
+        TraceReferencesCommand.builder()
+            .addLibraryFiles(dump.getLibraryArchive())
+            .addSourceFiles(appDexCode)
+            .addTargetFiles(path)
+            .setConsumer(keepRulesConsumer);
+    TraceReferences.run(traceReferencesCommandBuilder.build());
+
+    testForR8(Backend.DEX)
+        .addLibraryFiles(dump.getLibraryArchive())
+        .addProgramFiles(path)
+        .addKeepRuleFiles(desugaredLibraryRules)
+        .compile()
+        .writeToZip(output);
+  }
+}