Test debug stepping in <clinit>
This disables most of the <clinit> optimization in debug mode, as static
initialization through asignemnt has debug position in the <clinit>
code generated by javac.
For now the release mode still performs all optimizations.
Bug: 65133411
Change-Id: I11813a4c1d687f0a3eabaef2e67563bd6f902994
diff --git a/src/main/java/com/android/tools/r8/graph/DexCode.java b/src/main/java/com/android/tools/r8/graph/DexCode.java
index 1a8602c..1893949 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCode.java
@@ -82,6 +82,17 @@
this.debugInfo = debugInfo;
}
+ public boolean hasDebugPositions() {
+ if (debugInfo != null) {
+ for (DexDebugEvent event : debugInfo.events) {
+ if (event instanceof DexDebugEvent.Default) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
public DexDebugInfo debugInfoWithAdditionalFirstParameter(DexString name) {
if (debugInfo == null) {
return null;
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index 8c071c8..b76177b 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -209,6 +209,11 @@
code = null;
}
+ public boolean hasDebugPositions() {
+ assert code != null && code.isDexCode();
+ return code.asDexCode().hasDebugPositions();
+ }
+
public String qualifiedName() {
return method.qualifiedName();
}
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 24e0396..94a88b5 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
@@ -343,7 +343,8 @@
}
private void removeEmptyClassInitializer(DexProgramClass clazz) {
- if (clazz.hasTrivialClassInitializer()) {
+ if (clazz.hasTrivialClassInitializer()
+ && !clazz.getClassInitializer().hasDebugPositions()) {
clazz.removeStaticMethod(clazz.getClassInitializer());
}
}
@@ -493,7 +494,9 @@
codeRewriter.foldConstants(code);
codeRewriter.rewriteSwitch(code);
codeRewriter.simplifyIf(code);
- codeRewriter.collectClassInitializerDefaults(method, code);
+ if (!options.debug) {
+ codeRewriter.collectClassInitializerDefaults(method, code);
+ }
if (Log.ENABLED) {
Log.debug(getClass(), "Intermediate (SSA) flow graph for %s:\n%s",
method.toSourceString(), code);
diff --git a/src/test/debugTestResources/ClassInitializerAssignmentInitialization.java b/src/test/debugTestResources/ClassInitializerAssignmentInitialization.java
new file mode 100644
index 0000000..fa6078a
--- /dev/null
+++ b/src/test/debugTestResources/ClassInitializerAssignmentInitialization.java
@@ -0,0 +1,16 @@
+// 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.
+
+public class ClassInitializerAssignmentInitialization {
+
+ static int x = 1;
+ static int y;
+
+ static int z = 2;
+
+ public static void main(String[] args) {
+ System.out.println("x=" + x);
+ System.out.println("y=" + y);
+ }
+}
diff --git a/src/test/debugTestResources/ClassInitializerEmpty.java b/src/test/debugTestResources/ClassInitializerEmpty.java
new file mode 100644
index 0000000..9062d62
--- /dev/null
+++ b/src/test/debugTestResources/ClassInitializerEmpty.java
@@ -0,0 +1,12 @@
+// 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.
+
+public class ClassInitializerEmpty {
+
+ static {
+ }
+
+ public static void main(String[] args) {
+ }
+}
diff --git a/src/test/debugTestResources/ClassInitializerMixedInitialization.java b/src/test/debugTestResources/ClassInitializerMixedInitialization.java
new file mode 100644
index 0000000..504db6c
--- /dev/null
+++ b/src/test/debugTestResources/ClassInitializerMixedInitialization.java
@@ -0,0 +1,25 @@
+// 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.
+
+public class ClassInitializerMixedInitialization {
+
+ static boolean b;
+ static int x = 1;
+ static int y;
+
+ static {
+ x = 2;
+ if (b) {
+ y = 1;
+ } else {
+ y = 2;
+ }
+ x = 3;
+ }
+
+ public static void main(String[] args) {
+ System.out.println("x=" + x);
+ System.out.println("y=" + y);
+ }
+}
diff --git a/src/test/debugTestResources/ClassInitializerStaticBlockInitialization.java b/src/test/debugTestResources/ClassInitializerStaticBlockInitialization.java
new file mode 100644
index 0000000..78fe9a0
--- /dev/null
+++ b/src/test/debugTestResources/ClassInitializerStaticBlockInitialization.java
@@ -0,0 +1,26 @@
+// 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.
+
+public class ClassInitializerStaticBlockInitialization {
+
+ static boolean b;
+ static int x;
+ static int y;
+
+ static {
+ x = 1;
+ x = 2;
+ if (b) {
+ y = 1;
+ } else {
+ y = 2;
+ }
+ x = 3;
+ }
+
+ public static void main(String[] args) {
+ System.out.println("x=" + x);
+ System.out.println("y=" + y);
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index cb8e307..fcbc1ef 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -14,15 +14,11 @@
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.shaking.ProguardConfiguration;
import com.android.tools.r8.shaking.ProguardConfigurationParser;
-import com.android.tools.r8.shaking.ProguardConfigurationRule;
import com.android.tools.r8.shaking.ProguardRuleParserException;
-import com.android.tools.r8.shaking.RootSetBuilder;
-import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.ListUtils;
import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.ThreadUtils;
import com.android.tools.r8.utils.Timing;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -93,6 +89,10 @@
return compareTo(other) > 0;
}
+ public boolean isOlderThanOrEqual(DexVm other) {
+ return compareTo(other) <= 0;
+ }
+
private DexVm(String shortName) {
this.shortName = shortName;
}
diff --git a/src/test/java/com/android/tools/r8/debug/ClassInitializationTest.java b/src/test/java/com/android/tools/r8/debug/ClassInitializationTest.java
new file mode 100644
index 0000000..e90bca7
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/ClassInitializationTest.java
@@ -0,0 +1,110 @@
+// 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.debug;
+
+import org.apache.harmony.jpda.tests.framework.jdwp.Value;
+import org.junit.Test;
+
+public class ClassInitializationTest extends DebugTestBase {
+
+ @Test
+ public void testStaticAssingmentInitialization() throws Throwable {
+ final String SOURCE_FILE = "ClassInitializerAssignmentInitialization.java";
+ final String CLASS = "ClassInitializerAssignmentInitialization";
+
+ runDebugTest(CLASS,
+ breakpoint(CLASS, "<clinit>"),
+ run(),
+ checkLine(SOURCE_FILE, 7),
+ checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(0)),
+ checkStaticFieldClinitSafe(CLASS, "y", null, Value.createInt(0)),
+ checkStaticFieldClinitSafe(CLASS, "z", null, Value.createInt(0)),
+ stepOver(),
+ checkLine(SOURCE_FILE, 10),
+ checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(1)),
+ checkStaticFieldClinitSafe(CLASS, "y", null, Value.createInt(0)),
+ checkStaticFieldClinitSafe(CLASS, "z", null, Value.createInt(0)),
+ breakpoint(CLASS, "main"),
+ run(),
+ checkStaticField(CLASS, "x", null, Value.createInt(1)),
+ checkStaticField(CLASS, "y", null, Value.createInt(0)),
+ checkStaticField(CLASS, "z", null, Value.createInt(2)),
+ run());
+ }
+
+ @Test
+ public void testBreakpointInEmptyClassInitializer() throws Throwable {
+ final String SOURCE_FILE = "ClassInitializerEmpty.java";
+ final String CLASS = "ClassInitializerEmpty";
+
+ runDebugTest(CLASS,
+ breakpoint(CLASS, "<clinit>"),
+ run(),
+ checkLine(SOURCE_FILE, 8),
+ run());
+ }
+
+ @Test
+ public void testStaticBlockInitialization() throws Throwable {
+ final String SOURCE_FILE = "ClassInitializerStaticBlockInitialization.java";
+ final String CLASS = "ClassInitializerStaticBlockInitialization";
+
+ runDebugTest(CLASS,
+ breakpoint(CLASS, "<clinit>"),
+ run(),
+ checkLine(SOURCE_FILE, 12),
+ checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(0)),
+ stepOver(),
+ checkLine(SOURCE_FILE, 13),
+ checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(1)),
+ stepOver(),
+ checkLine(SOURCE_FILE, 14),
+ checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(2)),
+ stepOver(),
+ checkLine(SOURCE_FILE, 17),
+ stepOver(),
+ checkLine(SOURCE_FILE, 19),
+ breakpoint(CLASS, "main"),
+ run(),
+ checkLine(SOURCE_FILE, 23),
+ checkStaticField(CLASS, "x", null, Value.createInt(3)),
+ run());
+ }
+
+ @Test
+ public void testStaticMixedInitialization() throws Throwable {
+ final String SOURCE_FILE = "ClassInitializerMixedInitialization.java";
+ final String CLASS = "ClassInitializerMixedInitialization";
+
+ runDebugTest(CLASS,
+ breakpoint(CLASS, "<clinit>"),
+ run(),
+ checkLine(SOURCE_FILE, 8),
+ checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(0)),
+ checkStaticFieldClinitSafe(CLASS, "y", null, Value.createInt(0)),
+ stepOver(),
+ checkLine(SOURCE_FILE, 12),
+ checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(1)),
+ checkStaticFieldClinitSafe(CLASS, "y", null, Value.createInt(0)),
+ stepOver(),
+ checkLine(SOURCE_FILE, 13),
+ checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(2)),
+ checkStaticFieldClinitSafe(CLASS, "y", null, Value.createInt(0)),
+ stepOver(),
+ checkLine(SOURCE_FILE, 16),
+ checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(2)),
+ checkStaticFieldClinitSafe(CLASS, "y", null, Value.createInt(0)),
+ stepOver(),
+ checkLine(SOURCE_FILE, 18),
+ checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(2)),
+ checkStaticFieldClinitSafe(CLASS, "y", null, Value.createInt(2)),
+ breakpoint(CLASS, "main"),
+ run(),
+ checkLine(SOURCE_FILE, 22),
+ checkStaticField(CLASS, "x", null, Value.createInt(3)),
+ checkStaticField(CLASS, "y", null, Value.createInt(2)),
+ run());
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
index 7635e52..fa78fd0 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -10,9 +10,12 @@
import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
import com.android.tools.r8.ToolHelper.DexVm;
import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.OffOrAuto;
import com.google.common.collect.ImmutableList;
+import it.unimi.dsi.fastutil.longs.LongArrayList;
+import it.unimi.dsi.fastutil.longs.LongList;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
@@ -62,8 +65,10 @@
import org.junit.rules.TestName;
/**
+ * Base class for debugging tests.
*
- * Base class for debugging tests
+ * The protocol messages are described here:
+ * https://docs.oracle.com/javase/8/docs/platform/jpda/jdwp/jdwp-protocol.html
*/
public abstract class DebugTestBase {
@@ -347,6 +352,27 @@
});
}
+ protected final JUnit3Wrapper.Command checkStaticFieldClinitSafe(
+ String className, String fieldName, String fieldSignature, Value expectedValue) {
+ return inspect(t -> {
+ // TODO(65148874): The current Art from AOSP master hangs when requesting static fields
+ // when breaking in <clinit>. Last known good version is 7.0.0.
+ Assume.assumeTrue(
+ "Skipping test " + testName.getMethodName() + " because ART version is not supported",
+ isRunningJava() || ToolHelper.getDexVm().isOlderThanOrEqual(DexVm.ART_7_0_0));
+ checkStaticField(className, fieldName, fieldSignature, expectedValue);
+ });
+ }
+
+ protected final JUnit3Wrapper.Command checkStaticField(
+ String className, String fieldName, String fieldSignature, Value expectedValue) {
+ return inspect(t -> {
+ Value value = t.getStaticField(className, fieldName, fieldSignature);
+ Assert.assertEquals("Incorrect value for static '" + className + "." + fieldName + "'",
+ expectedValue, value);
+ });
+ }
+
protected final JUnit3Wrapper.Command inspect(Consumer<JUnit3Wrapper.DebuggeeState> inspector) {
return t -> inspector.accept(t.debuggeeState);
}
@@ -561,6 +587,7 @@
public static class DebuggeeState implements FrameInspector {
private class DebuggeeFrame implements FrameInspector {
+
private final long frameId;
private final Location location;
@@ -819,6 +846,77 @@
public String getMethodSignature() {
return getTopFrame().getMethodSignature();
}
+
+ public Value getStaticField(String className, String fieldName, String fieldSignature) {
+ String classSignature = DescriptorUtils.javaTypeToDescriptor(className);
+ byte typeTag = TypeTag.CLASS;
+ long classId = getMirror().getClassID(classSignature);
+ Assert.assertFalse("No class named " + className + " found", classId == -1);
+
+ // The class is available, lookup and read the field.
+ long fieldId = findField(getMirror(), classId, fieldName, fieldSignature);
+ return getField(getMirror(), classId, fieldId);
+ }
+
+ private long findField(VmMirror mirror, long classId, String fieldName,
+ String fieldSignature) {
+
+ boolean withGenericSignature = true;
+ CommandPacket commandPacket = new CommandPacket(ReferenceTypeCommandSet.CommandSetID,
+ ReferenceTypeCommandSet.FieldsWithGenericCommand);
+ commandPacket.setNextValueAsReferenceTypeID(classId);
+ ReplyPacket replyPacket = mirror.performCommand(commandPacket);
+ if (replyPacket.getErrorCode() != Error.NONE) {
+ // Retry with older command ReferenceType.Fields.
+ withGenericSignature = false;
+ commandPacket.setCommand(ReferenceTypeCommandSet.FieldsCommand);
+ replyPacket = mirror.performCommand(commandPacket);
+ assert replyPacket.getErrorCode() == Error.NONE;
+ }
+
+ int fieldsCount = replyPacket.getNextValueAsInt();
+ LongList matchingFieldIds = new LongArrayList();
+ for (int i = 0; i < fieldsCount; ++i) {
+ long currentFieldId = replyPacket.getNextValueAsFieldID();
+ String currentFieldName = replyPacket.getNextValueAsString();
+ String currentFieldSignature = replyPacket.getNextValueAsString();
+ if (withGenericSignature) {
+ replyPacket.getNextValueAsString(); // Skip generic signature.
+ }
+ replyPacket.getNextValueAsInt(); // Skip modifiers.
+
+ // Filter fields based on name (and signature if there is).
+ if (fieldName.equals(currentFieldName)) {
+ if (fieldSignature == null || fieldSignature.equals(currentFieldSignature)) {
+ matchingFieldIds.add(currentFieldId);
+ }
+ }
+ }
+ Assert.assertTrue(replyPacket.isAllDataRead());
+
+ Assert.assertFalse("No field named " + fieldName + " found", matchingFieldIds.isEmpty());
+ // There must be only one matching field.
+ Assert.assertEquals("More than 1 field found: please specify a signature", 1,
+ matchingFieldIds.size());
+ return matchingFieldIds.getLong(0);
+ }
+
+ private Value getField(VmMirror mirror, long classId, long fieldId) {
+
+ CommandPacket commandPacket = new CommandPacket(ReferenceTypeCommandSet.CommandSetID,
+ ReferenceTypeCommandSet.GetValuesCommand);
+ commandPacket.setNextValueAsReferenceTypeID(classId);
+ commandPacket.setNextValueAsInt(1);
+ commandPacket.setNextValueAsFieldID(fieldId);
+ ReplyPacket replyPacket = mirror.performCommand(commandPacket);
+ assert replyPacket.getErrorCode() == Error.NONE;
+
+ int fieldsCount = replyPacket.getNextValueAsInt();
+ assert fieldsCount == 1;
+ Value result = replyPacket.getNextValueAsValue();
+ Assert.assertTrue(replyPacket.isAllDataRead());
+ return result;
+ }
}
private static boolean inScope(long index, Variable var) {
@@ -1017,7 +1115,8 @@
}
Assert.assertTrue(replyPacket.isAllDataRead());
- Assert.assertFalse("No method named " + methodName + " found", matchingMethodIds.isEmpty());
+ Assert
+ .assertFalse("No method named " + methodName + " found", matchingMethodIds.isEmpty());
// There must be only one matching method
Assert.assertEquals("More than 1 method found: please specify a signature", 1,
matchingMethodIds.size());