blob: 1d7f782a51e38161f1ec3daa6831f6b152ec7ed0 [file] [log] [blame]
// Copyright (c) 2017, 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.ir.optimize;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import com.android.tools.r8.R8Command;
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.ToolHelper.ProcessResult;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.ir.optimize.enums.EnumUnboxingRewriter;
import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.utils.BooleanUtils;
import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.ZipUtils;
import com.android.tools.r8.utils.codeinspector.ClassSubject;
import com.android.tools.r8.utils.codeinspector.CodeInspector;
import com.android.tools.r8.utils.codeinspector.InstructionSubject;
import com.android.tools.r8.utils.codeinspector.InvokeInstructionSubject;
import com.android.tools.r8.utils.codeinspector.MethodSubject;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.class)
public class R8InliningTest extends TestBase {
private static final String DEFAULT_DEX_FILENAME = "classes.dex";
private static final String DEFAULT_MAP_FILENAME = "proguard.map";
private static final String NAME = "inlining";
private static final String KEEP_RULES_FILE = ToolHelper.EXAMPLES_DIR + NAME + "/keep-rules.txt";
@Parameters(name = "{1}, allow access modification: {0}")
public static Collection<Object[]> data() {
return buildParameters(
BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
}
private final boolean allowAccessModification;
private final TestParameters parameters;
private Path outputDir = null;
public R8InliningTest(boolean allowAccessModification, TestParameters parameters) {
this.allowAccessModification = allowAccessModification;
this.parameters = parameters;
}
private Path getInputFile() {
return Paths.get(ToolHelper.EXAMPLES_BUILD_DIR, NAME + FileUtils.JAR_EXTENSION);
}
private Path getGeneratedDexFile() {
return outputDir.resolve(DEFAULT_DEX_FILENAME);
}
private List<Path> getGeneratedFiles(Path dir) throws IOException {
if (parameters.isDexRuntime()) {
return Collections.singletonList(dir.resolve(Paths.get(DEFAULT_DEX_FILENAME)));
}
assert parameters.isCfRuntime();
return Files.walk(dir)
.filter(f -> f.toString().endsWith(".class"))
.collect(Collectors.toList());
}
private List<Path> getGeneratedFiles() throws IOException {
return getGeneratedFiles(outputDir);
}
private String getGeneratedProguardMap() {
Path mapFile = outputDir.resolve(DEFAULT_MAP_FILENAME);
if (Files.exists(mapFile)) {
return mapFile.toAbsolutePath().toString();
}
return null;
}
private void generateR8Version(Path out, Path mapFile, boolean inlining) throws Exception {
assert parameters.isDexRuntime() || parameters.isCfRuntime();
R8Command.Builder commandBuilder =
R8Command.builder()
.addProgramFiles(getInputFile())
.setOutput(out, outputMode(parameters.getBackend()))
.addProguardConfigurationFiles(Paths.get(KEEP_RULES_FILE))
.addLibraryFiles(ToolHelper.getMostRecentAndroidJar())
.setDisableMinification(true);
if (mapFile != null) {
commandBuilder.setProguardMapOutputPath(mapFile);
}
if (parameters.isDexRuntime()) {
commandBuilder.setMinApiLevel(parameters.getApiLevel().getLevel());
}
if (allowAccessModification) {
commandBuilder.addProguardConfiguration(
ImmutableList.of("-allowaccessmodification"), Origin.unknown());
}
ToolHelper.allowTestProguardOptions(commandBuilder);
ToolHelper.runR8(
commandBuilder.build(),
o -> {
// Disable class inlining to prevent that the instantiation of Nullability is removed, and
// that the class is therefore made abstract.
o.enableClassInlining = false;
o.enableInlining = inlining;
o.enableInliningOfInvokesWithNullableReceivers = false;
o.inliningInstructionLimit = 6;
// Tests depend on nullability of receiver and argument in general. Learning very accurate
// nullability from actual usage in tests bothers what we want to test.
o.callSiteOptimizationOptions().disableTypePropagationForTesting();
});
}
@Before
public void generateR8Version() throws Exception {
outputDir = temp.newFolder().toPath();
Path mapFile = outputDir.resolve(DEFAULT_MAP_FILENAME);
generateR8Version(outputDir, mapFile, true);
String output;
if (parameters.isDexRuntime()) {
output =
ToolHelper.runArtNoVerificationErrors(
Collections.singletonList(outputDir.resolve(DEFAULT_DEX_FILENAME).toString()),
"inlining.Inlining",
builder -> {},
parameters.getRuntime().asDex().getVm());
} else {
assert parameters.isCfRuntime();
output =
ToolHelper.runJava(
parameters.getRuntime().asCf(),
Collections.singletonList("-noverify"),
Collections.singletonList(outputDir),
"inlining.Inlining")
.stdout;
}
// Compare result with Java to make sure we have the same behavior.
ProcessResult javaResult = ToolHelper.runJava(getInputFile(), "inlining.Inlining");
assertEquals(0, javaResult.exitCode);
assertEquals(javaResult.stdout, output);
}
private void checkAbsentBooleanMethod(ClassSubject clazz, String name) {
checkAbsent(clazz, "boolean", name, Collections.emptyList());
}
private void checkAbsent(ClassSubject clazz, String returnType, String name, List<String> args) {
assertTrue(clazz.isPresent());
MethodSubject method = clazz.method(returnType, name, args);
assertFalse(method.isPresent());
}
private void dump(DexEncodedMethod method) {
System.out.println(method);
System.out.println(method.codeToString());
}
private void dump(Path path, String title) throws Throwable {
System.out.println(title + ":");
CodeInspector inspector = new CodeInspector(path.toAbsolutePath());
inspector.clazz("inlining.Inlining").forAllMethods(m -> dump(m.getMethod()));
System.out.println(title + " size: " + Files.size(path));
}
@Test
public void checkNoInvokes() throws Throwable {
CodeInspector inspector =
new CodeInspector(getGeneratedFiles(), getGeneratedProguardMap(), null);
ClassSubject clazz = inspector.clazz("inlining.Inlining");
// Simple constant inlining.
checkAbsentBooleanMethod(clazz, "longExpression");
checkAbsentBooleanMethod(clazz, "intExpression");
checkAbsentBooleanMethod(clazz, "doubleExpression");
checkAbsentBooleanMethod(clazz, "floatExpression");
// Simple return argument inlining.
checkAbsentBooleanMethod(clazz, "longArgumentExpression");
checkAbsentBooleanMethod(clazz, "intArgumentExpression");
checkAbsentBooleanMethod(clazz, "doubleArgumentExpression");
checkAbsentBooleanMethod(clazz, "floatArgumentExpression");
// Static method calling interface method. The interface method implementation is in
// a private class in another package.
checkAbsent(clazz, "int", "callInterfaceMethod", ImmutableList.of("inlining.IFace"));
clazz = inspector.clazz("inlining.Nullability");
checkAbsentBooleanMethod(clazz, "inlinableWithPublicField");
checkAbsentBooleanMethod(clazz, "inlinableWithControlFlow");
}
private long sumOfClassFileSizes(Path dir) throws IOException {
long size = 0;
for (Path p : getGeneratedFiles(dir)) {
if (ZipUtils.isClassFile(p.toString())) {
size += p.toFile().length();
}
}
return size;
}
@Test
public void processedFileIsSmaller() throws Throwable {
Path nonInlinedOutputDir = temp.newFolder().toPath();
generateR8Version(nonInlinedOutputDir, null, false);
long nonInlinedSize, inlinedSize;
if (parameters.isDexRuntime()) {
Path nonInlinedDexFile = nonInlinedOutputDir.resolve(DEFAULT_DEX_FILENAME);
nonInlinedSize = Files.size(nonInlinedDexFile);
inlinedSize = Files.size(getGeneratedDexFile());
final boolean ALWAYS_DUMP = false; // Used for debugging.
if (ALWAYS_DUMP || inlinedSize > nonInlinedSize) {
dump(nonInlinedDexFile, "No inlining");
dump(getGeneratedDexFile(), "Inlining enabled");
}
} else {
assert parameters.isCfRuntime();
nonInlinedSize = sumOfClassFileSizes(nonInlinedOutputDir);
inlinedSize = sumOfClassFileSizes(outputDir);
}
assertTrue("Inlining failed to reduce size", nonInlinedSize > inlinedSize);
}
// Count invokes of callee in two code sections in Inlining.main(). The section boundaries are
// marked with a bunch of calls to the marker0(), marker1() and marker2() methods.
private static int[] countInvokes(CodeInspector inspector, MethodSubject callee) {
// 'counters' counts the number of calls (invoke) to callee between marker0/1 and between
// marker1/2.
int[] counters = {0, 0};
if (!callee.isPresent()) {
// Method is not present, no invokes, only inlined uses possible.
return counters;
}
ClassSubject clazz = inspector.clazz("inlining.Inlining");
MethodSubject m = clazz.method("void", "main", ImmutableList.of("java.lang.String[]"));
assertTrue(m.isPresent());
// Find DexMethods for the marker0, marker1, marker2 methods.
DexMethod[] markers = new DexMethod[3];
for (int i = 0; i < 3; ++i) {
MethodSubject markerSubject = clazz.method("void", "marker" + i, Collections.emptyList());
assertTrue(markerSubject.isPresent());
markers[i] = markerSubject.getMethod().method;
}
// Count invokes to callee between markers.
Iterator<InstructionSubject> iterator = m.iterateInstructions();
int phase = -1;
while (iterator.hasNext()) {
InstructionSubject instruction = iterator.next();
if (!instruction.isInvoke()) {
continue;
}
DexMethod target = ((InvokeInstructionSubject) instruction).invokedMethod();
if (target == callee.getMethod().method) {
assertTrue(phase == 0 || phase == 1);
++counters[phase];
continue;
}
for (int i = 0; i <= 2; ++i) {
if (target == markers[i]) {
assertTrue(phase == i - 1 || phase == i); // Make sure markers found in order.
phase = i;
break;
}
}
}
assertEquals(2, phase);
return counters;
}
private static void assertCounters(int expected0, int expected1, int[] counters) {
assert counters.length == 2;
assertEquals(expected0, counters[0]);
assertEquals(expected1, counters[1]);
}
@Test
public void invokeOnNullableReceiver() throws Exception {
CodeInspector inspector =
new CodeInspector(getGeneratedFiles(), getGeneratedProguardMap(), null);
// These constants describe the expected number of invoke instructions calling a possibly
// inlined method.
final int ALWAYS_INLINABLE = 0;
final int INLINABLE = allowAccessModification ? 0 : 1;
final int NEVER_INLINABLE = 1;
ClassSubject clazz = inspector.clazz("inlining.Nullability");
MethodSubject m;
m = clazz.method("int", "inlinable", ImmutableList.of("inlining.A"));
assertCounters(INLINABLE, INLINABLE, countInvokes(inspector, m));
m = clazz.method("int", "notInlinable", ImmutableList.of("inlining.A"));
assertCounters(INLINABLE, NEVER_INLINABLE, countInvokes(inspector, m));
m = clazz.method("int", "notInlinableDueToMissingNpe", ImmutableList.of("inlining.A"));
assertCounters(INLINABLE, NEVER_INLINABLE, countInvokes(inspector, m));
m = clazz.method("int", "notInlinableDueToSideEffect", ImmutableList.of("inlining.A"));
assertCounters(INLINABLE, NEVER_INLINABLE, countInvokes(inspector, m));
m = clazz.method("int", "notInlinableOnThrow", ImmutableList.of("java.lang.Throwable"));
assertCounters(ALWAYS_INLINABLE, NEVER_INLINABLE, countInvokes(inspector, m));
m =
clazz.method(
"int",
"notInlinableDueToMissingNpeBeforeThrow",
ImmutableList.of("java.lang.Throwable"));
assertCounters(ALWAYS_INLINABLE, NEVER_INLINABLE * 2, countInvokes(inspector, m));
}
@Test
public void invokeOnNonNullReceiver() throws Exception {
CodeInspector inspector =
new CodeInspector(getGeneratedFiles(), getGeneratedProguardMap(), null);
ClassSubject clazz = inspector.clazz("inlining.Nullability");
MethodSubject m = clazz.method("int", "conditionalOperator", ImmutableList.of("inlining.A"));
assertTrue(m.isPresent());
// Verify that a.b() is resolved to an inline instance-get.
Iterator<InstructionSubject> iterator = m.iterateInstructions();
int instanceGetCount = 0;
int invokeCount = 0;
while (iterator.hasNext()) {
InstructionSubject instruction = iterator.next();
if (instruction.isInstanceGet()) {
++instanceGetCount;
} else if (instruction.isInvoke()) {
++invokeCount;
}
}
assertEquals(1, instanceGetCount);
assertEquals(0, invokeCount);
// The enum parameter may get unboxed.
m = clazz.uniqueMethodWithName("moreControlFlows");
assertTrue(m.isPresent());
// Verify that a.b() is resolved to an inline instance-get.
iterator = m.iterateInstructions();
instanceGetCount = 0;
invokeCount = 0;
while (iterator.hasNext()) {
InstructionSubject instruction = iterator.next();
if (instruction.isInstanceGet()) {
++instanceGetCount;
} else if (instruction.isInvoke() && !isEnumInvoke(instruction)) {
++invokeCount;
}
}
assertEquals(1, instanceGetCount);
assertEquals(0, invokeCount);
}
private boolean isEnumInvoke(InstructionSubject instruction) {
InternalOptions defaults = new InternalOptions();
if (parameters.isDexRuntime() && defaults.enableEnumUnboxing) {
return instruction
.getMethod()
.name
.toString()
.startsWith(EnumUnboxingRewriter.ENUM_UNBOXING_UTILITY_METHOD_PREFIX);
} else {
return ((InvokeInstructionSubject) instruction)
.holder()
.toString()
.contains("java.lang.Enum");
}
}
}