Do not classinline when root is a static get and is used with a monitor
We can only remove monitor instructions in the class inliner, when the
root is a new instance. For static get roots, we cannot guarantee that
the instance is not shared.
Bug: 147411673
Change-Id: I4d5463ceecbde2941c3431510295551bfc2d9766
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerEligibilityInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerEligibilityInfo.java
index bf06bc1..8d178a3 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerEligibilityInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerEligibilityInfo.java
@@ -21,9 +21,14 @@
*/
final OptionalBool returnsReceiver;
+ final boolean hasMonitorOnReceiver;
+
public ClassInlinerEligibilityInfo(
- List<Pair<Invoke.Type, DexMethod>> callsReceiver, OptionalBool returnsReceiver) {
+ List<Pair<Invoke.Type, DexMethod>> callsReceiver,
+ OptionalBool returnsReceiver,
+ boolean hasMonitorOnReceiver) {
this.callsReceiver = callsReceiver;
this.returnsReceiver = returnsReceiver;
+ this.hasMonitorOnReceiver = hasMonitorOnReceiver;
}
}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
index 157e7b4..fe1b092 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
@@ -819,12 +819,16 @@
}
if (root.isStaticGet()) {
- // If we are class inlining a singleton instance from a static-get, then we don't the value of
- // the fields.
+ // If we are class inlining a singleton instance from a static-get, then we don't know the
+ // value of the fields.
ParameterUsage receiverUsage = optimizationInfo.getParameterUsages(0);
if (receiverUsage == null || receiverUsage.hasFieldRead) {
return null;
}
+ if (eligibility.hasMonitorOnReceiver) {
+ // We will not be able to remove the monitor instruction afterwards.
+ return null;
+ }
}
// If the method returns receiver and the return value is actually
@@ -977,6 +981,10 @@
}
}
+ if (parameterUsage.isUsedInMonitor) {
+ return !root.isStaticGet();
+ }
+
if (!Sets.difference(parameterUsage.ifZeroTest, ALLOWED_ZERO_TEST_TYPES).isEmpty()) {
// Used in unsupported zero-check-if kinds.
return false;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
index 1fb2e0c..8238723 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
@@ -166,12 +166,14 @@
List<Pair<Invoke.Type, DexMethod>> callsReceiver = new ArrayList<>();
boolean seenSuperInitCall = false;
+ boolean seenMonitor = false;
for (Instruction insn : receiver.aliasedUsers()) {
if (insn.isAssume()) {
continue;
}
if (insn.isMonitor()) {
+ seenMonitor = true;
continue;
}
@@ -246,11 +248,15 @@
return;
}
+ boolean synchronizedVirtualMethod =
+ method.accessFlags.isSynchronized() && method.isVirtualMethod();
+
feedback.setClassInlinerEligibility(
method,
new ClassInlinerEligibilityInfo(
callsReceiver,
- new ClassInlinerReceiverAnalysis(appView, method, code).computeReturnsReceiver()));
+ new ClassInlinerReceiverAnalysis(appView, method, code).computeReturnsReceiver(),
+ seenMonitor || synchronizedVirtualMethod));
}
private void identifyParameterUsages(
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/ParameterUsagesInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/ParameterUsagesInfo.java
index ef87dcf..e911307 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/ParameterUsagesInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/ParameterUsagesInfo.java
@@ -12,6 +12,7 @@
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.Invoke;
import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
+import com.android.tools.r8.ir.code.Monitor;
import com.android.tools.r8.ir.code.Return;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.utils.ListUtils;
@@ -86,6 +87,9 @@
// If this argument is returned: return arg.
public final boolean isReturned;
+ // If this argument is used in a monitor instruction.
+ public final boolean isUsedInMonitor;
+
ParameterUsage(
int index,
Set<Type> ifZeroTest,
@@ -93,7 +97,8 @@
boolean hasFieldAssignment,
boolean hasFieldRead,
boolean isAssignedToField,
- boolean isReturned) {
+ boolean isReturned,
+ boolean isUsedInMonitor) {
this.index = index;
this.ifZeroTest =
ifZeroTest.isEmpty() ? Collections.emptySet() : ImmutableSet.copyOf(ifZeroTest);
@@ -103,6 +108,7 @@
this.hasFieldRead = hasFieldRead;
this.isAssignedToField = isAssignedToField;
this.isReturned = isReturned;
+ this.isUsedInMonitor = isUsedInMonitor;
}
static ParameterUsage copyAndShift(ParameterUsage original, int shift) {
@@ -114,7 +120,8 @@
original.hasFieldAssignment,
original.hasFieldRead,
original.isAssignedToField,
- original.isReturned);
+ original.isReturned,
+ original.isUsedInMonitor);
}
public boolean notUsed() {
@@ -123,7 +130,8 @@
&& !hasFieldAssignment
&& !hasFieldRead
&& !isAssignedToField
- && !isReturned;
+ && !isReturned
+ && !isUsedInMonitor;
}
}
@@ -138,6 +146,7 @@
private boolean hasFieldRead = false;
private boolean isAssignedToField = false;
private boolean isReturned = false;
+ private boolean isUsedInMonitor = false;
ParameterUsageBuilder(Value arg, int index) {
this.arg = arg;
@@ -166,6 +175,9 @@
if (instruction.isReturn()) {
return note(instruction.asReturn());
}
+ if (instruction.isMonitor()) {
+ return note(instruction.asMonitor());
+ }
return false;
}
@@ -177,7 +189,8 @@
hasFieldAssignment,
hasFieldRead,
isAssignedToField,
- isReturned);
+ isReturned,
+ isUsedInMonitor);
}
private boolean note(If ifInstruction) {
@@ -231,5 +244,12 @@
isReturned = true;
return true;
}
+
+ private boolean note(Monitor monitorInstruction) {
+ assert monitorInstruction.inValues().size() == 1;
+ assert monitorInstruction.inValues().get(0).getAliasedValue() == arg;
+ isUsedInMonitor = true;
+ return true;
+ }
}
}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerStaticGetDirectMonitorTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerStaticGetDirectMonitorTest.java
new file mode 100644
index 0000000..f6252ea
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerStaticGetDirectMonitorTest.java
@@ -0,0 +1,114 @@
+// Copyright (c) 2020, 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.classinliner;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Currently, the class-inliner will not inline if the root is used in a monitor. This test just
+ * ensures, that if that ever changes, the monitor instructions will not be removed.
+ */
+@RunWith(Parameterized.class)
+public class ClassInlinerStaticGetDirectMonitorTest extends TestBase {
+
+ private final TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ public ClassInlinerStaticGetDirectMonitorTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void test() throws Exception {
+ testForR8(parameters.getBackend())
+ .addInnerClasses(ClassInlinerStaticGetDirectMonitorTest.class)
+ .addKeepMainRule(TestClass.class)
+ .enableInliningAnnotations()
+ .setMinApi(parameters.getApiLevel())
+ .noMinification()
+ .compile()
+ .inspect(this::inspect)
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutputLines("20000");
+ }
+
+ private void inspect(CodeInspector inspector) {
+ assertTrue(
+ inspector
+ .clazz(TestClass.class)
+ .uniqueMethodWithName("produce1")
+ .streamInstructions()
+ .anyMatch(InstructionSubject::isMonitorEnter));
+ assertTrue(
+ inspector
+ .clazz(TestClass.class)
+ .uniqueMethodWithName("produce2")
+ .streamInstructions()
+ .anyMatch(InstructionSubject::isMonitorExit));
+ }
+
+ static class TestClass {
+
+ private static volatile Thread t1 = new Thread(TestClass::produce1);
+ private static volatile Thread t2 = new Thread(TestClass::produce2);
+
+ @NeverInline
+ static void produce1() {
+ Container instance = Container.getInstance();
+ for (int i = 0; i < 10000; i++) {
+ synchronized (instance) {
+ instance.increment();
+ }
+ }
+ }
+
+ @NeverInline
+ static void produce2() {
+ Container instance = Container.getInstance();
+ for (int i = 0; i < 10000; i++) {
+ synchronized (instance) {
+ instance.increment();
+ }
+ }
+ }
+
+ public static void main(String[] args) {
+ t1.start();
+ t2.start();
+ while (t1.isAlive() || t2.isAlive()) {}
+ System.out.println(Container.counter);
+ }
+ }
+
+ static class Container {
+
+ static Container INSTANCE = new Container();
+ public static int counter = 0;
+
+ static Container getInstance() {
+ return INSTANCE;
+ }
+
+ @NeverInline
+ final void increment() {
+ counter += 1;
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerStaticGetExtraMethodMonitorTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerStaticGetExtraMethodMonitorTest.java
new file mode 100644
index 0000000..0e0b260
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerStaticGetExtraMethodMonitorTest.java
@@ -0,0 +1,105 @@
+// Copyright (c) 2020, 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.classinliner;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * This is a reproduction of b/147411673 where we inline classes and remove monitor instructions.
+ */
+@RunWith(Parameterized.class)
+public class ClassInlinerStaticGetExtraMethodMonitorTest extends TestBase {
+
+ private final TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ public ClassInlinerStaticGetExtraMethodMonitorTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void test() throws Exception {
+ testForR8(parameters.getBackend())
+ .addInnerClasses(ClassInlinerStaticGetExtraMethodMonitorTest.class)
+ .addKeepMainRule(TestClass.class)
+ .enableInliningAnnotations()
+ .setMinApi(parameters.getApiLevel())
+ .noMinification()
+ .compile()
+ .inspect(this::inspect)
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutputLines("20000");
+ }
+
+ private void inspect(CodeInspector inspector) {
+ assertThat(inspector.clazz(Container.class).uniqueMethodWithName("increment"), isPresent());
+ }
+
+ static class TestClass {
+
+ private static volatile Thread t1 = new Thread(TestClass::produce1);
+ private static volatile Thread t2 = new Thread(TestClass::produce2);
+
+ @NeverInline
+ static void produce1() {
+ Container instance = Container.getInstance();
+ for (int i = 0; i < 10000; i++) {
+ synchronizeOnExtraMethod(instance);
+ }
+ }
+
+ @NeverInline
+ static void produce2() {
+ Container instance = Container.getInstance();
+ for (int i = 0; i < 10000; i++) {
+ synchronizeOnExtraMethod(instance);
+ }
+ }
+
+ @NeverInline
+ static void synchronizeOnExtraMethod(Container container) {
+ synchronized (container) {
+ container.increment();
+ }
+ }
+
+ public static void main(String[] args) {
+ t1.start();
+ t2.start();
+ while (t1.isAlive() || t2.isAlive()) {}
+ System.out.println(Container.counter);
+ }
+ }
+
+ static class Container {
+
+ static Container INSTANCE = new Container();
+ public static int counter = 0;
+
+ static Container getInstance() {
+ return INSTANCE;
+ }
+
+ @NeverInline
+ final void increment() {
+ counter += 1;
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerStaticGetMonitorTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerStaticGetMonitorTest.java
new file mode 100644
index 0000000..216f34e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerStaticGetMonitorTest.java
@@ -0,0 +1,98 @@
+// Copyright (c) 2019, 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.classinliner;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * This is a reproduction of b/147411673 where we inline classes and remove monitor instructions.
+ */
+@RunWith(Parameterized.class)
+public class ClassInlinerStaticGetMonitorTest extends TestBase {
+
+ private final TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ public ClassInlinerStaticGetMonitorTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void test() throws Exception {
+ testForR8(parameters.getBackend())
+ .addInnerClasses(ClassInlinerStaticGetMonitorTest.class)
+ .addKeepMainRule(TestClass.class)
+ .enableInliningAnnotations()
+ .setMinApi(parameters.getApiLevel())
+ .noMinification()
+ .compile()
+ .inspect(this::inspect)
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutputLines("20000");
+ }
+
+ private void inspect(CodeInspector inspector) {
+ assertThat(inspector.clazz(Container.class).uniqueMethodWithName("increment"), isPresent());
+ }
+
+ static class TestClass {
+
+ private static volatile Thread t1 = new Thread(TestClass::produce1);
+ private static volatile Thread t2 = new Thread(TestClass::produce2);
+
+ @NeverInline
+ static void produce1() {
+ for (int i = 0; i < 10000; i++) {
+ Container.getInstance().increment();
+ }
+ }
+
+ @NeverInline
+ static void produce2() {
+ for (int i = 0; i < 10000; i++) {
+ Container.getInstance().increment();
+ }
+ }
+
+ public static void main(String[] args) {
+ t1.start();
+ t2.start();
+ while (t1.isAlive() || t2.isAlive()) {}
+ System.out.println(Container.counter);
+ }
+ }
+
+ static class Container {
+
+ static Container INSTANCE = new Container();
+ public static int counter = 0;
+
+ static Container getInstance() {
+ return INSTANCE;
+ }
+
+ @NeverInline
+ final void increment() {
+ synchronized (this) {
+ counter += 1;
+ }
+ }
+ }
+}