| // Copyright (c) 2022, 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.startup; | |
| import static com.android.tools.r8.startup.utils.StartupTestingMatchers.isEqualToClassDataLayout; | |
| import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; | |
| import static com.android.tools.r8.utils.codeinspector.Matchers.notIf; | |
| import static org.hamcrest.MatcherAssert.assertThat; | |
| import static org.junit.Assert.assertEquals; | |
| import static org.junit.Assert.assertTrue; | |
| import com.android.tools.r8.D8TestCompileResult; | |
| import com.android.tools.r8.R8TestCompileResult; | |
| import com.android.tools.r8.TestBase; | |
| import com.android.tools.r8.TestCompilerBuilder; | |
| import com.android.tools.r8.TestParameters; | |
| import com.android.tools.r8.graph.DexProgramClass; | |
| import com.android.tools.r8.ir.desugar.LambdaClass; | |
| import com.android.tools.r8.profile.art.ArtProfileBuilderUtils.SyntheticToSyntheticContextGeneralization; | |
| import com.android.tools.r8.references.ClassReference; | |
| import com.android.tools.r8.references.Reference; | |
| import com.android.tools.r8.startup.profile.ExternalStartupClass; | |
| import com.android.tools.r8.startup.profile.ExternalStartupItem; | |
| import com.android.tools.r8.startup.profile.ExternalStartupMethod; | |
| import com.android.tools.r8.startup.profile.ExternalSyntheticStartupMethod; | |
| import com.android.tools.r8.startup.utils.MixedSectionLayoutInspector; | |
| import com.android.tools.r8.startup.utils.StartupTestingUtils; | |
| import com.android.tools.r8.synthesis.SyntheticItemsTestUtils; | |
| import com.android.tools.r8.utils.AndroidApiLevel; | |
| import com.android.tools.r8.utils.BooleanUtils; | |
| import com.android.tools.r8.utils.MethodReferenceUtils; | |
| import com.android.tools.r8.utils.codeinspector.ClassSubject; | |
| import com.android.tools.r8.utils.codeinspector.CodeInspector; | |
| import com.android.tools.r8.utils.codeinspector.MethodSubject; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableSet; | |
| import java.nio.file.Path; | |
| import java.util.Collection; | |
| import java.util.LinkedHashSet; | |
| import java.util.List; | |
| import java.util.Set; | |
| import java.util.function.Consumer; | |
| 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 StartupSyntheticPlacementTest extends TestBase { | |
| @Parameter(0) | |
| public TestParameters parameters; | |
| @Parameter(1) | |
| public boolean enableMinimalStartupDex; | |
| @Parameter(2) | |
| public boolean enableStartupCompletenessCheck; | |
| @Parameter(3) | |
| public boolean useLambda; | |
| @Parameters(name = "{0}, minimal startup dex: {1}, completeness check: {2}, use lambda: {3}") | |
| public static List<Object[]> data() { | |
| return buildParameters( | |
| // N so that java.util.function.Consumer is present. | |
| getTestParameters().withDexRuntimes().withApiLevel(AndroidApiLevel.N).build(), | |
| BooleanUtils.values(), | |
| BooleanUtils.values(), | |
| BooleanUtils.values()); | |
| } | |
| @Test | |
| public void testLayoutUsingD8() throws Exception { | |
| // First build the app using R8. | |
| R8TestCompileResult r8CompileResult = | |
| testForR8(parameters.getBackend()) | |
| .addInnerClasses(getClass()) | |
| .addKeepMainRule(Main.class) | |
| .addKeepClassAndMembersRules(A.class, B.class, C.class) | |
| .addDontOptimize() | |
| .setMinApi(parameters.getApiLevel()) | |
| .compile(); | |
| // Verify that the build works. | |
| r8CompileResult | |
| .run(parameters.getRuntime(), Main.class, Boolean.toString(useLambda)) | |
| .assertSuccessWithOutputLines(getExpectedOutput()); | |
| Path optimizedApp = r8CompileResult.writeToZip(); | |
| // Then instrument the app to generate a startup list for the minified app. | |
| Set<ExternalStartupItem> startupList = new LinkedHashSet<>(); | |
| testForD8(parameters.getBackend()) | |
| .addProgramFiles(optimizedApp) | |
| .apply( | |
| StartupTestingUtils.enableStartupInstrumentationForOptimizedAppUsingLogcat(parameters)) | |
| .release() | |
| .setMinApi(parameters.getApiLevel()) | |
| .compile() | |
| .addRunClasspathFiles(StartupTestingUtils.getAndroidUtilLog(temp)) | |
| .run(parameters.getRuntime(), Main.class, Boolean.toString(useLambda)) | |
| .apply( | |
| StartupTestingUtils.removeStartupListFromStdout( | |
| startupList::add, SyntheticToSyntheticContextGeneralization.createForD8())) | |
| .assertSuccessWithOutputLines(getExpectedOutput()) | |
| .apply( | |
| runResult -> | |
| assertEquals( | |
| getExpectedStartupList(r8CompileResult.inspector(), false), startupList)); | |
| // Finally rebuild the minified app using D8 and the startup list. | |
| testForD8(parameters.getBackend()) | |
| .addProgramFiles(optimizedApp) | |
| .apply( | |
| testBuilder -> | |
| configureStartupOptions(testBuilder, r8CompileResult.inspector(), startupList)) | |
| .release() | |
| .setMinApi(parameters.getApiLevel()) | |
| .compile() | |
| .inspectMultiDex( | |
| r8CompileResult.writeProguardMap(), this::inspectPrimaryDex, this::inspectSecondaryDex) | |
| .run(parameters.getRuntime(), Main.class, Boolean.toString(useLambda)) | |
| .assertSuccessWithOutputLines(getExpectedOutput()); | |
| } | |
| @Test | |
| public void testLayoutUsingR8() throws Exception { | |
| // First generate a startup list for the original app. | |
| Set<ExternalStartupItem> startupList = new LinkedHashSet<>(); | |
| D8TestCompileResult instrumentationCompileResult = | |
| testForD8(parameters.getBackend()) | |
| .addInnerClasses(getClass()) | |
| .apply( | |
| StartupTestingUtils.enableStartupInstrumentationForOriginalAppUsingLogcat( | |
| parameters)) | |
| .release() | |
| .setMinApi(parameters.getApiLevel()) | |
| .compile(); | |
| instrumentationCompileResult | |
| .addRunClasspathFiles(StartupTestingUtils.getAndroidUtilLog(temp)) | |
| .run(parameters.getRuntime(), Main.class, Boolean.toString(useLambda)) | |
| .apply( | |
| StartupTestingUtils.removeStartupListFromStdout( | |
| startupList::add, SyntheticToSyntheticContextGeneralization.createForR8())) | |
| .assertSuccessWithOutputLines(getExpectedOutput()) | |
| .apply( | |
| runResult -> | |
| assertEquals(getExpectedStartupList(runResult.inspector(), true), startupList)); | |
| // Then build the app using the startup list that is based on original names. | |
| testForR8(parameters.getBackend()) | |
| .addInnerClasses(getClass()) | |
| .addKeepMainRule(Main.class) | |
| .addKeepClassAndMembersRules(A.class, B.class, C.class) | |
| .apply( | |
| testBuilder -> | |
| configureStartupOptions( | |
| testBuilder, instrumentationCompileResult.inspector(), startupList)) | |
| .setMinApi(parameters.getApiLevel()) | |
| .compile() | |
| .inspectMultiDex(this::inspectPrimaryDex, this::inspectSecondaryDex) | |
| .run(parameters.getRuntime(), Main.class, Boolean.toString(useLambda)) | |
| .applyIf( | |
| enableStartupCompletenessCheck && useLambda, | |
| runResult -> runResult.assertFailureWithErrorThatThrows(NullPointerException.class), | |
| runResult -> runResult.assertSuccessWithOutputLines(getExpectedOutput())); | |
| } | |
| private void configureStartupOptions( | |
| TestCompilerBuilder<?, ?, ?, ?, ?> testBuilder, | |
| CodeInspector inspector, | |
| Collection<ExternalStartupItem> startupList) { | |
| testBuilder | |
| .addOptionsModification( | |
| options -> { | |
| options | |
| .getStartupOptions() | |
| .setEnableMinimalStartupDex(enableMinimalStartupDex) | |
| .setEnableStartupCompletenessCheckForTesting(enableStartupCompletenessCheck); | |
| options | |
| .getTestingOptions() | |
| .setMixedSectionLayoutStrategyInspector( | |
| getMixedSectionLayoutInspector(inspector)); | |
| }) | |
| .apply(ignore -> StartupTestingUtils.setStartupConfiguration(testBuilder, startupList)); | |
| } | |
| private List<String> getExpectedOutput() { | |
| return ImmutableList.of("A", "B", "C"); | |
| } | |
| @SuppressWarnings("unchecked") | |
| private Set<ExternalStartupItem> getExpectedStartupList( | |
| CodeInspector inspector, boolean isStartupListForOriginalApp) throws NoSuchMethodException { | |
| ImmutableSet.Builder<ExternalStartupItem> builder = ImmutableSet.builder(); | |
| builder.add( | |
| ExternalStartupClass.builder() | |
| .setClassReference(Reference.classFromClass(Main.class)) | |
| .build(), | |
| ExternalStartupMethod.builder() | |
| .setMethodReference(MethodReferenceUtils.mainMethod(Main.class)) | |
| .build(), | |
| ExternalStartupClass.builder().setClassReference(Reference.classFromClass(A.class)).build(), | |
| ExternalStartupMethod.builder() | |
| .setMethodReference(Reference.methodFromMethod(A.class.getDeclaredMethod("a"))) | |
| .build(), | |
| ExternalStartupClass.builder().setClassReference(Reference.classFromClass(B.class)).build(), | |
| ExternalStartupMethod.builder() | |
| .setMethodReference( | |
| Reference.methodFromMethod(B.class.getDeclaredMethod("b", boolean.class))) | |
| .build()); | |
| if (useLambda) { | |
| if (isStartupListForOriginalApp) { | |
| builder.add( | |
| ExternalSyntheticStartupMethod.builder() | |
| .setSyntheticContextReference(Reference.classFromClass(B.class)) | |
| .build()); | |
| } else { | |
| ClassSubject bClassSubject = inspector.clazz(B.class); | |
| MethodSubject syntheticLambdaAccessorMethod = | |
| bClassSubject.uniqueMethodThatMatches( | |
| method -> | |
| method | |
| .getOriginalName() | |
| .startsWith(LambdaClass.R8_LAMBDA_ACCESSOR_METHOD_PREFIX)); | |
| assertThat(syntheticLambdaAccessorMethod, isPresent()); | |
| ClassSubject externalSyntheticLambdaClassSubject = | |
| inspector.clazz(getSyntheticLambdaClassReference()); | |
| assertThat(externalSyntheticLambdaClassSubject, isPresent()); | |
| ClassReference externalSyntheticLambdaClassReference = | |
| externalSyntheticLambdaClassSubject.getFinalReference(); | |
| builder.add( | |
| ExternalStartupClass.builder() | |
| .setClassReference(externalSyntheticLambdaClassReference) | |
| .build(), | |
| ExternalStartupMethod.builder() | |
| .setMethodReference( | |
| MethodReferenceUtils.instanceConstructor(externalSyntheticLambdaClassReference)) | |
| .build(), | |
| ExternalStartupMethod.builder() | |
| .setMethodReference( | |
| Reference.method( | |
| externalSyntheticLambdaClassReference, | |
| "accept", | |
| ImmutableList.of(Reference.classFromClass(Object.class)), | |
| null)) | |
| .build(), | |
| ExternalStartupMethod.builder() | |
| .setMethodReference( | |
| Reference.method( | |
| Reference.classFromClass(B.class), | |
| syntheticLambdaAccessorMethod.getFinalName(), | |
| ImmutableList.of(Reference.classFromClass(Object.class)), | |
| null)) | |
| .build()); | |
| } | |
| builder.add( | |
| ExternalStartupMethod.builder() | |
| .setMethodReference( | |
| Reference.methodFromMethod(B.class.getDeclaredMethod("lambda$b$0", Object.class))) | |
| .build()); | |
| } | |
| builder.add( | |
| ExternalStartupClass.builder().setClassReference(Reference.classFromClass(C.class)).build(), | |
| ExternalStartupMethod.builder() | |
| .setMethodReference(Reference.methodFromMethod(C.class.getDeclaredMethod("c"))) | |
| .build()); | |
| return builder.build(); | |
| } | |
| private List<ClassReference> getExpectedClassDataLayout( | |
| CodeInspector inspector, int virtualFile) { | |
| ClassSubject syntheticLambdaClassSubject = inspector.clazz(getSyntheticLambdaClassReference()); | |
| // The synthetic lambda should only be placed alongside its synthetic context (B) if it is used. | |
| // Otherwise, it should be last, or in the second dex file if compiling with minimal startup. | |
| ImmutableList.Builder<ClassReference> layoutBuilder = ImmutableList.builder(); | |
| if (virtualFile == 0) { | |
| layoutBuilder.add( | |
| Reference.classFromClass(Main.class), | |
| Reference.classFromClass(A.class), | |
| Reference.classFromClass(B.class)); | |
| if (useLambda) { | |
| layoutBuilder.add(syntheticLambdaClassSubject.getFinalReference()); | |
| } | |
| layoutBuilder.add(Reference.classFromClass(C.class)); | |
| } | |
| if (!useLambda) { | |
| if (!enableMinimalStartupDex || virtualFile == 1) { | |
| layoutBuilder.add(syntheticLambdaClassSubject.getFinalReference()); | |
| } | |
| } | |
| return layoutBuilder.build(); | |
| } | |
| private MixedSectionLayoutInspector getMixedSectionLayoutInspector(CodeInspector inspector) { | |
| return new MixedSectionLayoutInspector() { | |
| @Override | |
| public void inspectClassDataLayout(int virtualFile, Collection<DexProgramClass> layout) { | |
| assertThat( | |
| layout, isEqualToClassDataLayout(getExpectedClassDataLayout(inspector, virtualFile))); | |
| } | |
| }; | |
| } | |
| private void inspectPrimaryDex(CodeInspector inspector) { | |
| assertThat(inspector.clazz(Main.class), isPresent()); | |
| assertThat(inspector.clazz(A.class), isPresent()); | |
| assertThat(inspector.clazz(B.class), isPresent()); | |
| assertThat(inspector.clazz(C.class), isPresent()); | |
| assertThat( | |
| inspector.clazz(getSyntheticLambdaClassReference()), | |
| notIf(isPresent(), enableMinimalStartupDex && !useLambda)); | |
| } | |
| private void inspectSecondaryDex(CodeInspector inspector) { | |
| if (enableMinimalStartupDex && !useLambda) { | |
| assertEquals(1, inspector.allClasses().size()); | |
| assertThat(inspector.clazz(getSyntheticLambdaClassReference()), isPresent()); | |
| } else { | |
| assertTrue(inspector.allClasses().isEmpty()); | |
| } | |
| } | |
| private static ClassReference getSyntheticLambdaClassReference() { | |
| return SyntheticItemsTestUtils.syntheticLambdaClass(B.class, 0); | |
| } | |
| static class Main { | |
| public static void main(String[] args) { | |
| boolean useLambda = args.length > 0 && args[0].equals("true"); | |
| A.a(); | |
| B.b(useLambda); | |
| C.c(); | |
| } | |
| } | |
| static class A { | |
| static void a() { | |
| System.out.println("A"); | |
| } | |
| } | |
| static class B { | |
| static void b(boolean useLambda) { | |
| String message = System.currentTimeMillis() > 0 ? "B" : null; | |
| if (useLambda) { | |
| Consumer<Object> consumer = obj -> {}; | |
| consumer.accept(consumer); | |
| } | |
| System.out.println(message); | |
| } | |
| } | |
| static class C { | |
| static void c() { | |
| System.out.println("C"); | |
| } | |
| } | |
| } |