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);
+ }
+}