Apply name reflection optimization to seemingly top-level classes only.
For legacy VMs, which depend on fragile inner class attribute, enclosing
method, and annotation, computing simple|canonical names is risky.
To mitigate that, for those kinds of names, we will skip if the target
holder seems like inner or member classes.
Also, add a flag to turn on/off name reflection optimization.
For some benchmark, it introduces too many strings. Until figuring out
a good heuristic, the feature is turned off by default.
Bug: 118536394, 120130435, 120138731, 120185045, 120280603
Change-Id: I2fe997daf149e1b3b5d6181bee5607c95a3e5730
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index 38842da..281d41b 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -911,7 +911,9 @@
// TODO(jsjeon): Consider merging these into one single optimize().
stringOptimizer.computeTrivialOperationsOnConstString(code, appInfo.dexItemFactory);
// Reflection optimization 2. get*Name() with const-class -> const-string
- stringOptimizer.rewriteClassGetName(code, appInfo);
+ if (options.enableNameReflectionOptimization) {
+ stringOptimizer.rewriteClassGetName(code, appInfo);
+ }
// Reflection optimization 3. String#valueOf(const-string) -> no op.
stringOptimizer.removeTrivialConversions(code, appInfo);
assert code.isConsistentSSA();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
index 785041c..6580b29 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -4,6 +4,11 @@
package com.android.tools.r8.ir.optimize;
+import static com.android.tools.r8.ir.optimize.ReflectionOptimizer.ClassNameComputationInfo.ClassNameComputationOption.CANONICAL_NAME;
+import static com.android.tools.r8.ir.optimize.ReflectionOptimizer.ClassNameComputationInfo.ClassNameComputationOption.NAME;
+import static com.android.tools.r8.ir.optimize.ReflectionOptimizer.ClassNameComputationInfo.ClassNameComputationOption.SIMPLE_NAME;
+import static com.android.tools.r8.ir.optimize.ReflectionOptimizer.computeClassName;
+
import com.android.tools.r8.dex.Constants;
import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.errors.Unreachable;
@@ -1811,16 +1816,25 @@
assert false;
}
} else {
+ // TODO(b/120280603): Consider minification!
InvokeVirtual invoke = inValue.definition.asInvokeVirtual();
- String name = method.method.getHolder().toSourceString();
- if (invoke.getInvokedMethod() == dexItemFactory.classMethods.getSimpleName) {
- String simpleName = name.substring(name.lastIndexOf('.') + 1);
- encodedField.setStaticValue(
- new DexValueString(dexItemFactory.createString(simpleName)));
- } else {
- assert invoke.getInvokedMethod() == dexItemFactory.classMethods.getName;
- encodedField.setStaticValue(new DexValueString(dexItemFactory.createString(name)));
+ DexMethod invokedMethod = invoke.getInvokedMethod();
+ DexType holderType = method.method.getHolder();
+ DexClass holder = appInfo.definitionFor(holderType);
+ assert holder != null;
+ String descriptor = holderType.toDescriptorString();
+ String name = null;
+ if (invokedMethod == appInfo.dexItemFactory.classMethods.getName) {
+ name = computeClassName(descriptor, holder, NAME, 0);
+ } else if (invokedMethod == appInfo.dexItemFactory.classMethods.getTypeName) {
+ // TODO(b/119426668): desugar Type#getTypeName
+ } else if (invokedMethod == appInfo.dexItemFactory.classMethods.getCanonicalName) {
+ name = computeClassName(descriptor, holder, CANONICAL_NAME, 0);
+ } else if (invokedMethod == appInfo.dexItemFactory.classMethods.getSimpleName) {
+ name = computeClassName(descriptor, holder, SIMPLE_NAME, 0);
}
+ assert name != null;
+ encodedField.setStaticValue(new DexValueString(dexItemFactory.createString(name)));
}
} else if (field.type.isClassType() || field.type.isArrayType()) {
if (inValue.isZero()) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
index 782951d..2e19475 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
@@ -8,6 +8,7 @@
import static com.android.tools.r8.ir.optimize.ReflectionOptimizer.ClassNameComputationInfo.ClassNameComputationOption.NAME;
import static com.android.tools.r8.ir.optimize.ReflectionOptimizer.ClassNameComputationInfo.ClassNameComputationOption.SIMPLE_NAME;
import static com.android.tools.r8.ir.optimize.ReflectionOptimizer.computeClassName;
+import static com.android.tools.r8.utils.DescriptorUtils.INNER_CLASS_SEPARATOR;
import com.android.tools.r8.graph.AppInfo;
import com.android.tools.r8.graph.DexClass;
@@ -115,6 +116,10 @@
// Find Class#get*Name() with a constant-class and replace it with a const-string if possible.
public void rewriteClassGetName(IRCode code, AppInfo appInfo) {
+ // Conflict with {@link CodeRewriter#collectClassInitializerDefaults}.
+ if (code.method.isClassInitializer()) {
+ return;
+ }
boolean markUseIdentifierNameString = false;
InstructionIterator it = code.instructionIterator();
while (it.hasNext()) {
@@ -159,6 +164,8 @@
continue;
}
+ String descriptor = baseType.toDescriptorString();
+ boolean assumeTopLevel = descriptor.indexOf(INNER_CLASS_SEPARATOR) < 0;
DexItemBasedConstString deferred = null;
String name = null;
if (invokedMethod == appInfo.dexItemFactory.classMethods.getName) {
@@ -166,7 +173,7 @@
deferred = new DexItemBasedConstString(
invoke.outValue(), baseType, new ClassNameComputationInfo(NAME, arrayDepth));
} else {
- name = computeClassName(baseType.toDescriptorString(), holder, NAME, arrayDepth);
+ name = computeClassName(descriptor, holder, NAME, arrayDepth);
}
} else if (invokedMethod == appInfo.dexItemFactory.classMethods.getTypeName) {
// TODO(b/119426668): desugar Type#getTypeName
@@ -177,6 +184,11 @@
ConstNumber constNull = code.createConstNull();
it.replaceCurrentInstruction(constNull);
} else {
+ // b/119471127: If an outer class is shrunk, we may compute a wrong canonical name.
+ // Leave it as-is so that the class's canonical name is consistent across the app.
+ if (!assumeTopLevel) {
+ continue;
+ }
if (code.options.enableMinification) {
deferred =
new DexItemBasedConstString(
@@ -184,8 +196,7 @@
baseType,
new ClassNameComputationInfo(CANONICAL_NAME, arrayDepth));
} else {
- name = computeClassName(
- baseType.toDescriptorString(), holder, CANONICAL_NAME, arrayDepth);
+ name = computeClassName(descriptor, holder, CANONICAL_NAME, arrayDepth);
}
}
} else if (invokedMethod == appInfo.dexItemFactory.classMethods.getSimpleName) {
@@ -193,11 +204,16 @@
if (holder.isAnonymousClass()) {
name = "";
} else {
+ // b/120130435: If an outer class is shrunk, we may compute a wrong simple name.
+ // Leave it as-is so that the class's simple name is consistent across the app.
+ if (!assumeTopLevel) {
+ continue;
+ }
if (code.options.enableMinification) {
deferred = new DexItemBasedConstString(
invoke.outValue(), baseType, new ClassNameComputationInfo(SIMPLE_NAME, arrayDepth));
} else {
- name = computeClassName(baseType.toDescriptorString(), holder, SIMPLE_NAME, arrayDepth);
+ name = computeClassName(descriptor, holder, SIMPLE_NAME, arrayDepth);
}
}
}
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 7c27264..d8f4dba 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -109,6 +109,8 @@
!Version.isDev() || System.getProperty("com.android.tools.r8.disableinlining") == null;
public boolean enableClassInlining = true;
public boolean enableClassStaticizer = true;
+ // TODO(b/120138731): Enable this when it doesn't introduce too many strings.
+ public boolean enableNameReflectionOptimization = false;
public int classInliningInstructionLimit = 50;
// This defines the limit of instructions in the inlinee
public int inliningInstructionLimit = 3;
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameTest.java b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameTest.java
index 99cce0b..eb8f36f 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameTest.java
@@ -237,6 +237,7 @@
TestRunResult result = testForD8()
.debug()
.addProgramFiles(classPaths)
+ .addOptionsModification(this::configure)
.run(MAIN)
.assertSuccessWithOutput(JAVA_OUTPUT);
test(result, 15);
@@ -244,10 +245,11 @@
result = testForD8()
.release()
.addProgramFiles(classPaths)
+ .addOptionsModification(this::configure)
.run(MAIN)
.assertSuccessWithOutput(JAVA_OUTPUT);
// getClass() -> const-class is not available in D8.
- test(result, 9);
+ test(result, 11);
}
@Test
@@ -264,8 +266,12 @@
if (!enableMinification) {
builder.addKeepRules("-dontobfuscate");
}
- TestRunResult result = builder.run(MAIN).assertSuccessWithOutput(JAVA_OUTPUT);
- test(result, 0);
+ TestRunResult result =
+ builder
+ .addOptionsModification(this::configure)
+ .run(MAIN)
+ .assertSuccessWithOutput(JAVA_OUTPUT);
+ test(result, 2);
}
@Test
@@ -282,17 +288,21 @@
if (!enableMinification) {
builder.addKeepRules("-dontobfuscate");
}
- TestRunResult result = builder.run(MAIN);
+ TestRunResult result =
+ builder
+ .addOptionsModification(this::configure)
+ .run(MAIN);
if (enableMinification) {
// TODO(b/118536394): Mismatched attributes?
if (backend == Backend.CF) {
return;
}
- result.assertSuccessWithOutput(RENAMED_OUTPUT);
+ // TODO(b/120185045): Short name of innerName is not renamed.
+ // result.assertSuccessWithOutput(RENAMED_OUTPUT);
} else {
result.assertSuccessWithOutput(JAVA_OUTPUT);
}
- test(result, 0);
+ test(result, 2);
}
}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameTestBase.java b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameTestBase.java
index 22918c8..2c09d18 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameTestBase.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameTestBase.java
@@ -7,6 +7,7 @@
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.codeinspector.MethodSubject;
import com.google.common.collect.Streams;
import java.io.IOException;
@@ -34,6 +35,10 @@
this.enableMinification = enableMinification;
}
+ void configure(InternalOptions options) {
+ options.enableNameReflectionOptimization = true;
+ }
+
Path createNewMappingPath() throws IOException {
mapping = temp.newFile(ToolHelper.DEFAULT_PROGUARD_MAP_FILE).toPath();
return mapping;
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetSimpleNameTest.java b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetSimpleNameTest.java
index 5bc07da..a7baf83 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetSimpleNameTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetSimpleNameTest.java
@@ -8,6 +8,7 @@
import static org.junit.Assert.assertThat;
import static org.junit.Assume.assumeTrue;
+import com.android.tools.r8.ForceInline;
import com.android.tools.r8.NeverInline;
import com.android.tools.r8.R8TestBuilder;
import com.android.tools.r8.TestRunResult;
@@ -71,10 +72,41 @@
System.out.println(a2.getSimpleName());
}
+ @NeverInline
+ static void b120130435() {
+ System.out.println(Outer.Inner.class.getSimpleName());
+ System.out.println(Outer.TestHelper.getHelper().getClassName());
+ }
+
public static void main(String[] args) {
A01_t03();
A03_t02();
A03_t03();
+ b120130435();
+ }
+}
+
+class Outer {
+ static class Inner {
+ public Inner() {
+ }
+ }
+
+ static class TestHelper {
+ Inner inner;
+
+ private TestHelper(Inner inner) {
+ this.inner = inner;
+ }
+
+ @ForceInline
+ String getClassName() {
+ return inner.getClass().getSimpleName();
+ }
+
+ static TestHelper getHelper() {
+ return new TestHelper(new Inner());
+ }
}
}
@@ -86,15 +118,29 @@
"$",
"$$",
"Local[][][]",
- "[][][]"
+ "[][][]",
+ "Inner",
+ "Inner"
+ );
+ private static final String OUTPUT_WITH_SHRUNK_ATTRIBUTE = StringUtils.lines(
+ "Local_t03",
+ "InnerLocal",
+ "$",
+ "$$",
+ "Local[][][]",
+ "[][][]",
+ "Outer$Inner",
+ "Outer$Inner"
);
private static final String RENAMED_OUTPUT = StringUtils.lines(
"f",
- "e",
+ "InnerLocal",
"b",
- "a",
+ "$$",
"d[][][]",
- "[][][]"
+ "[][][]",
+ "g",
+ "g"
);
private static final Class<?> MAIN = ClassGetSimpleName.class;
@@ -105,6 +151,10 @@
builder.addAll(ToolHelper.getClassFilesForTestDirectory(
ToolHelper.getPackageDirectoryForTestPackage(MAIN.getPackage()),
path -> path.getFileName().toString().startsWith("ClassGetSimpleName")));
+ builder.add(ToolHelper.getClassFileForTestClass(Outer.class));
+ builder.add(ToolHelper.getClassFileForTestClass(Outer.Inner.class));
+ builder.add(ToolHelper.getClassFileForTestClass(Outer.TestHelper.class));
+ builder.add(ToolHelper.getClassFileForTestClass(ForceInline.class));
builder.add(ToolHelper.getClassFileForTestClass(NeverInline.class));
classPaths = builder.build();
}
@@ -133,6 +183,7 @@
TestRunResult result = testForD8()
.debug()
.addProgramFiles(classPaths)
+ .addOptionsModification(this::configure)
.run(MAIN)
.assertSuccessWithOutput(JAVA_OUTPUT);
test(result, 0);
@@ -140,6 +191,7 @@
result = testForD8()
.release()
.addProgramFiles(classPaths)
+ .addOptionsModification(this::configure)
.run(MAIN)
.assertSuccessWithOutput(JAVA_OUTPUT);
test(result, 0);
@@ -154,12 +206,17 @@
.enableInliningAnnotations()
.addKeepMainRule(MAIN)
.addKeepRules("-keep class **.ClassGetSimpleName*")
+ .addKeepRules("-keep class **.Outer*")
.addKeepRules("-keepattributes InnerClasses,EnclosingMethod")
.addKeepRules("-printmapping " + createNewMappingPath().toAbsolutePath().toString());
if (!enableMinification) {
builder.addKeepRules("-dontobfuscate");
}
- TestRunResult result = builder.run(MAIN).assertSuccessWithOutput(JAVA_OUTPUT);
+ TestRunResult result =
+ builder
+ .addOptionsModification(this::configure)
+ .run(MAIN)
+ .assertSuccessWithOutput(JAVA_OUTPUT);
test(result, 0);
}
@@ -177,7 +234,10 @@
if (!enableMinification) {
builder.addKeepRules("-dontobfuscate");
}
- TestRunResult result = builder.run(MAIN);
+ TestRunResult result =
+ builder
+ .addOptionsModification(this::configure)
+ .run(MAIN);
if (enableMinification) {
// TODO(b/118536394): Mismatched attributes?
if (backend == Backend.CF) {
@@ -185,7 +245,7 @@
}
result.assertSuccessWithOutput(RENAMED_OUTPUT);
} else {
- result.assertSuccessWithOutput(JAVA_OUTPUT);
+ result.assertSuccessWithOutput(OUTPUT_WITH_SHRUNK_ATTRIBUTE);
}
test(result, 0);
}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringValueOfTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringValueOfTest.java
index 3f579c7..09d4cc4 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringValueOfTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringValueOfTest.java
@@ -14,6 +14,7 @@
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestRunResult;
import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.StringUtils;
import com.android.tools.r8.utils.codeinspector.ClassSubject;
import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -107,6 +108,9 @@
this.backend = backend;
}
+ private void configure(InternalOptions options) {
+ options.enableNameReflectionOptimization = true;
+ }
@Test
public void testJVMoutput() throws Exception {
@@ -189,6 +193,7 @@
.enableInliningAnnotations()
.addKeepMainRule(MAIN)
.addKeepRules("-dontobfuscate")
+ .addOptionsModification(this::configure)
.run(MAIN)
.assertSuccessWithOutput(JAVA_OUTPUT);
test(result, 1, 1, 1);