Add infrastructure to optimize line numbers, not enabled yet.
Bug:
Change-Id: I04722643c69a7425998fd427c51501271540c748
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 76a6ed1..c1b1096 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -45,6 +45,8 @@
import com.android.tools.r8.utils.AndroidAppOutputSink;
import com.android.tools.r8.utils.CfgPrinter;
import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
+import com.android.tools.r8.utils.LineNumberOptimizer;
import com.android.tools.r8.utils.StringDiagnostic;
import com.android.tools.r8.utils.ThreadUtils;
import com.android.tools.r8.utils.Timing;
@@ -314,6 +316,15 @@
: new Minifier(appInfo.withLiveness(), rootSet, options).run(timing);
timing.end();
+ if (options.lineNumberOptimization != LineNumberOptimization.OFF) {
+ timing.begin("Line number remapping");
+ LineNumberOptimizer.run(
+ application,
+ namingLens,
+ options.lineNumberOptimization == LineNumberOptimization.IDENTITY_MAPPING);
+ timing.end();
+ }
+
// If a method filter is present don't produce output since the application is likely partial.
if (options.hasMethodsFilter()) {
System.out.println("Finished compilation with method filter: ");
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugEvent.java b/src/main/java/com/android/tools/r8/graph/DexDebugEvent.java
index 9d42c82..58d8473 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugEvent.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugEvent.java
@@ -446,7 +446,7 @@
@Override
public String toString() {
- return String.format("DEFAULT %d (dpc %d, %dline %d)", value, getPCDelta(), getLineDelta());
+ return String.format("DEFAULT %d (dpc %d, dline %d)", value, getPCDelta(), getLineDelta());
}
@Override
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java b/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
index 1c71b9b..83e01a2 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
@@ -29,7 +29,7 @@
*/
public class DexDebugEventBuilder {
- private static final int NO_PC_INFO = -1;
+ public static final int NO_PC_INFO = -1;
private static final int NO_LINE_INFO = -1;
private final DexEncodedMethod method;
@@ -234,7 +234,7 @@
}
}
- private static void emitAdvancementEvents(
+ public static void emitAdvancementEvents(
int previousPc,
Position previousPosition,
int nextPc,
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugPositionState.java b/src/main/java/com/android/tools/r8/graph/DexDebugPositionState.java
index 280b27e..a02e546 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugPositionState.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugPositionState.java
@@ -17,8 +17,6 @@
* the current state using the getters after a Default event.
*/
public class DexDebugPositionState implements DexDebugEventVisitor {
- private final DexMethod method;
-
private int currentPc = 0;
private int currentLine;
private DexString currentFile = null;
@@ -26,7 +24,6 @@
private Position currentCallerPosition = null;
public DexDebugPositionState(int startLine, DexMethod method) {
- this.method = method;
currentLine = startLine;
currentMethod = method;
}
@@ -44,9 +41,6 @@
@Override
public void visit(SetInlineFrame setInlineFrame) {
- assert (setInlineFrame.caller == null && setInlineFrame.callee == method)
- || (setInlineFrame.caller != null
- && setInlineFrame.caller.getOutermostCaller().method == method);
currentMethod = setInlineFrame.callee;
currentCallerPosition = setInlineFrame.caller;
}
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 1616a71..24df0e4 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -4,13 +4,13 @@
package com.android.tools.r8.utils;
import com.android.tools.r8.DiagnosticsHandler;
-import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.dex.Marker;
import com.android.tools.r8.errors.InvalidDebugInfoException;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.shaking.ProguardConfiguration;
import com.android.tools.r8.shaking.ProguardConfigurationRule;
import com.google.common.collect.ImmutableList;
@@ -25,6 +25,12 @@
public class InternalOptions {
+ public enum LineNumberOptimization {
+ OFF,
+ ON,
+ IDENTITY_MAPPING
+ }
+
public final DexItemFactory itemFactory;
public final ProguardConfiguration proguardConfiguration;
@@ -125,6 +131,8 @@
public ImmutableList<ProguardConfigurationRule> mainDexKeepRules = ImmutableList.of();
public boolean minimalMainDex;
+ public LineNumberOptimization lineNumberOptimization = LineNumberOptimization.OFF;
+
public static class InvalidParameterAnnotationInfo {
final DexMethod method;
diff --git a/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java b/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
new file mode 100644
index 0000000..85741e2
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
@@ -0,0 +1,305 @@
+// 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.utils;
+
+import com.android.tools.r8.graph.Code;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexCode;
+import com.android.tools.r8.graph.DexDebugEvent;
+import com.android.tools.r8.graph.DexDebugEventBuilder;
+import com.android.tools.r8.graph.DexDebugEventVisitor;
+import com.android.tools.r8.graph.DexDebugInfo;
+import com.android.tools.r8.graph.DexDebugPositionState;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.naming.NamingLens;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.List;
+
+public class LineNumberOptimizer {
+
+ // EventFilter is a visitor for DebugEvents, splits events into two sinks:
+ // - Forwards non-positional events unchanged into a BypassedEventReceiver
+ // - Forwards positional events, accumulated into DexDebugPositionStates, into
+ // positionEventReceiver.
+ private static class EventFilter implements DexDebugEventVisitor {
+ private final BypassedEventReceiver bypassedEventReceiver;
+ private final PositionEventReceiver positionEventReceiver;
+
+ private interface BypassedEventReceiver {
+ void receiveBypassedEvent(DexDebugEvent event);
+ }
+
+ private interface PositionEventReceiver {
+ void receivePositionEvent(DexDebugPositionState positionState);
+ }
+
+ private DexDebugPositionState positionState;
+
+ private EventFilter(
+ int startLine,
+ DexMethod method,
+ BypassedEventReceiver bypassedEventReceiver,
+ PositionEventReceiver positionEventReceiver) {
+ positionState = new DexDebugPositionState(startLine, method);
+ this.bypassedEventReceiver = bypassedEventReceiver;
+ this.positionEventReceiver = positionEventReceiver;
+ }
+
+ @Override
+ public void visit(DexDebugEvent.SetPrologueEnd event) {
+ bypassedEventReceiver.receiveBypassedEvent(event);
+ }
+
+ @Override
+ public void visit(DexDebugEvent.SetEpilogueBegin event) {
+ bypassedEventReceiver.receiveBypassedEvent(event);
+ }
+
+ @Override
+ public void visit(DexDebugEvent.StartLocal event) {
+ bypassedEventReceiver.receiveBypassedEvent(event);
+ }
+
+ @Override
+ public void visit(DexDebugEvent.EndLocal event) {
+ bypassedEventReceiver.receiveBypassedEvent(event);
+ }
+
+ @Override
+ public void visit(DexDebugEvent.RestartLocal event) {
+ bypassedEventReceiver.receiveBypassedEvent(event);
+ }
+
+ @Override
+ public void visit(DexDebugEvent.AdvancePC advancePC) {
+ positionState.visit(advancePC);
+ }
+
+ @Override
+ public void visit(DexDebugEvent.AdvanceLine advanceLine) {
+ positionState.visit(advanceLine);
+ }
+
+ @Override
+ public void visit(DexDebugEvent.SetInlineFrame setInlineFrame) {
+ positionState.visit(setInlineFrame);
+ }
+
+ @Override
+ public void visit(DexDebugEvent.Default defaultEvent) {
+ positionState.visit(defaultEvent);
+ positionEventReceiver.receivePositionEvent(positionState);
+ }
+
+ @Override
+ public void visit(DexDebugEvent.SetFile setFile) {
+ positionState.visit(setFile);
+ }
+ }
+
+ // PositionRemapper is a stateful function which takes a position (represented by a
+ // DexDebugPositionState) and returns a remapped Position.
+ private static class PositionRemapper {
+ private int nextLineNumber;
+
+ PositionRemapper(int nextLineNumber) {
+ this.nextLineNumber = nextLineNumber;
+ }
+
+ private Position createRemappedPosition(DexDebugPositionState positionState) {
+ // TODO(tamaskenez) Actual remapping is to be implemented here.
+ // For now this is only identity-mapping.
+ return new Position(
+ positionState.getCurrentLine(),
+ positionState.getCurrentFile(),
+ positionState.getCurrentMethod(),
+ positionState.getCurrentCallerPosition());
+ }
+
+ int getNextLineNumber() {
+ return nextLineNumber;
+ }
+ }
+
+ // PositionEventEmitter is a stateful function which converts a Position into series of
+ // position-related DexDebugEvents and puts them into a processedEvents list.
+ private static class PositionEventEmitter {
+ private final DexItemFactory dexItemFactory;
+ private int startLine = -1;
+ private DexMethod method;
+ private int previousPc = DexDebugEventBuilder.NO_PC_INFO;
+ private Position previousPosition = null;
+ private List<DexDebugEvent> processedEvents;
+
+ private PositionEventEmitter(
+ DexItemFactory dexItemFactory, DexMethod method, List<DexDebugEvent> processedEvents) {
+ this.dexItemFactory = dexItemFactory;
+ this.method = method;
+ this.processedEvents = processedEvents;
+ }
+
+ private void emitPositionEvents(int currentPc, Position currentPosition) {
+ if (previousPosition == null) {
+ startLine = currentPosition.line;
+ previousPosition = new Position(startLine, null, method, null);
+ }
+ DexDebugEventBuilder.emitAdvancementEvents(
+ previousPc,
+ previousPosition,
+ currentPc,
+ currentPosition,
+ processedEvents,
+ dexItemFactory);
+ previousPc = currentPc;
+ previousPosition = currentPosition;
+ }
+
+ private int getStartLine() {
+ assert (startLine >= 0);
+ return startLine;
+ }
+ }
+
+ public static void run(
+ DexApplication application, NamingLens namingLens, boolean identityMapping) {
+ IdentityHashMap<DexString, List<DexProgramClass>> classesOfFiles = new IdentityHashMap<>();
+ // Collect which files contain which classes that need to have their line numbers optimized.
+ for (DexProgramClass clazz : application.classes()) {
+
+ // TODO(tamaskenez) fix b/69356670 and remove the conditional skipping.
+ if (!clazz.getSynthesizedFrom().isEmpty()) {
+ continue;
+ }
+
+ // Group methods by name
+ IdentityHashMap<DexString, List<DexEncodedMethod>> methodsByName =
+ new IdentityHashMap<>(clazz.directMethods().length + clazz.virtualMethods().length);
+ clazz.forEachMethod(
+ method -> {
+ if (doesContainPositions(method)) {
+ methodsByName.compute(
+ method.method.name,
+ (name, methods) -> {
+ if (methods == null) {
+ methods = new ArrayList<>();
+ }
+ methods.add(method);
+ return methods;
+ });
+ }
+ });
+ for (List<DexEncodedMethod> methods : methodsByName.values()) {
+ if (methods.size() > 1) {
+ // If there are multiple methods with the same name (overloaded) then sort them for
+ // deterministic behaviour: the algorithm will assign new line numbers in this order.
+ // Methods with different names can share the same line numbers, that's why they don't
+ // need to be sorted.
+ methods.sort(
+ (lhs, rhs) -> {
+ int startLineDiff =
+ lhs.getCode().asDexCode().getDebugInfo().startLine
+ - rhs.getCode().asDexCode().getDebugInfo().startLine;
+ if (startLineDiff != 0) return startLineDiff;
+ return DexEncodedMethod.slowCompare(lhs, rhs);
+ });
+ }
+ int nextLineNumber = 1;
+ for (DexEncodedMethod method : methods) {
+ // Do the actual processing for each method.
+ DexCode dexCode = method.getCode().asDexCode();
+ DexDebugInfo debugInfo = dexCode.getDebugInfo();
+ List<DexDebugEvent> processedEvents = new ArrayList<>();
+
+ // Our pipeline will be:
+ // [debugInfo.events] -> eventFilter -> positionRemapper -> positionEventEmitter ->
+ // [processedEvents]
+ PositionEventEmitter positionEventEmitter =
+ new PositionEventEmitter(application.dexItemFactory, method.method, processedEvents);
+ PositionRemapper positionRemapper = new PositionRemapper(nextLineNumber);
+
+ EventFilter eventFilter =
+ new EventFilter(
+ debugInfo.startLine,
+ method.method,
+ processedEvents::add,
+ positionState -> {
+ Position position = positionRemapper.createRemappedPosition(positionState);
+ positionEventEmitter.emitPositionEvents(positionState.getCurrentPc(), position);
+ });
+ for (DexDebugEvent event : debugInfo.events) {
+ event.accept(eventFilter);
+ }
+ nextLineNumber = positionRemapper.getNextLineNumber();
+ DexDebugInfo optimizedDebugInfo =
+ new DexDebugInfo(
+ positionEventEmitter.getStartLine(),
+ debugInfo.parameters,
+ processedEvents.toArray(new DexDebugEvent[processedEvents.size()]));
+
+ // TODO(tamaskenez) Remove this as soon as we have external tests testing not only the
+ // remapping but whether the non-positional debug events remain intact.
+ if (identityMapping) {
+ assert (optimizedDebugInfo.startLine == debugInfo.startLine);
+ assert (optimizedDebugInfo.events.length == debugInfo.events.length);
+ for (int i = 0; i < debugInfo.events.length; ++i) {
+ assert optimizedDebugInfo.events[i].equals(debugInfo.events[i]);
+ }
+ }
+ dexCode.setDebugInfo(optimizedDebugInfo);
+ }
+ }
+ }
+ }
+
+ // Return true if any of the methods' debug infos describe a Position which lists the containing
+ // method as the outermost caller.
+ private static boolean checkMethodsForSelfReferenceInPositions(DexEncodedMethod[] methods) {
+ for (DexEncodedMethod method : methods) {
+ if (!doesContainPositions(method)) {
+ continue;
+ }
+ DexDebugInfo debugInfo = method.getCode().asDexCode().getDebugInfo();
+
+ DexDebugPositionState positionState =
+ new DexDebugPositionState(debugInfo.startLine, method.method);
+ for (DexDebugEvent event : debugInfo.events) {
+ event.accept(positionState);
+ if (event instanceof DexDebugEvent.Default) {
+ Position caller = positionState.getCurrentCallerPosition();
+ DexMethod outermostMethod =
+ caller == null
+ ? positionState.getCurrentMethod()
+ : caller.getOutermostCaller().method;
+ if (outermostMethod == method.method) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ private static boolean doesContainPositions(DexEncodedMethod method) {
+ Code code = method.getCode();
+ if (code == null || !code.isDexCode()) {
+ return false;
+ }
+ DexDebugInfo debugInfo = code.asDexCode().getDebugInfo();
+ if (debugInfo == null) {
+ return false;
+ }
+ for (DexDebugEvent event : debugInfo.events) {
+ if (event instanceof DexDebugEvent.Default) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/test/debugTestResources/LineNumberOptimization1.java b/src/test/debugTestResources/LineNumberOptimization1.java
new file mode 100644
index 0000000..c25ecc6
--- /dev/null
+++ b/src/test/debugTestResources/LineNumberOptimization1.java
@@ -0,0 +1,14 @@
+// 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 LineNumberOptimization1 {
+ private static void callThisFromSameFile() {
+ System.out.println("callThisFromSameFile");
+ LineNumberOptimization2.callThisFromAnotherFile();
+ }
+
+ public static void main(String[] args) {
+ callThisFromSameFile();
+ }
+}
diff --git a/src/test/debugTestResources/LineNumberOptimization2.java b/src/test/debugTestResources/LineNumberOptimization2.java
new file mode 100644
index 0000000..0f07ae9
--- /dev/null
+++ b/src/test/debugTestResources/LineNumberOptimization2.java
@@ -0,0 +1,30 @@
+// 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 LineNumberOptimization2 {
+ public static void callThisFromAnotherFile() {
+ System.out.println("callThisFromAnotherFile");
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/debug/LineNumberOptimizationTest.java b/src/test/java/com/android/tools/r8/debug/LineNumberOptimizationTest.java
new file mode 100644
index 0000000..ed6b315
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/LineNumberOptimizationTest.java
@@ -0,0 +1,89 @@
+// 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 com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
+import java.util.Collections;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/** Tests source file and line numbers on inlined methods. */
+public class LineNumberOptimizationTest extends DebugTestBase {
+
+ public static final String SOURCE_FILE = "LineNumberOptimization1.java";
+ private static DebuggeePath debuggeePathOptimized;
+ private static DebuggeePath debuggeePathNotOptimized;
+ private static DebuggeePath debuggeePathIdentityTest;
+
+ private final String class1 = "LineNumberOptimization1";
+ private final String class2 = "LineNumberOptimization2";
+ private final String file1 = class1 + ".java";
+ private final String file2 = class2 + ".java";
+ private final String mainSignature = "([Ljava/lang/String;)V";
+
+ private static DebuggeePath makeDex(LineNumberOptimization lineNumberOptimization)
+ throws Exception {
+ return DebuggeePath.makeDex(
+ compileToDexViaR8(
+ oc -> {
+ oc.lineNumberOptimization = lineNumberOptimization;
+ oc.inlineAccessors = false;
+ },
+ null,
+ DEBUGGEE_JAR,
+ Collections.<String>emptyList(),
+ true,
+ CompilationMode.RELEASE));
+ }
+
+ @BeforeClass
+ public static void initDebuggeePath() throws Exception {
+ debuggeePathNotOptimized = makeDex(LineNumberOptimization.OFF);
+ debuggeePathOptimized = makeDex(LineNumberOptimization.ON);
+ debuggeePathIdentityTest = makeDex(LineNumberOptimization.IDENTITY_MAPPING);
+ }
+
+ @Test
+ public void testNotOptimized() throws Throwable {
+ runDebugTest(
+ debuggeePathNotOptimized,
+ class1,
+ breakpoint(class1, "main", mainSignature),
+ run(),
+ checkMethod(class1, "main", mainSignature),
+ checkLine(file1, 12),
+ stepInto(),
+ checkMethod(class1, "callThisFromSameFile", "()V"),
+ checkLine(file1, 7),
+ stepOver(),
+ checkMethod(class1, "callThisFromSameFile", "()V"),
+ checkLine(file1, 8),
+ stepInto(),
+ checkMethod(class2, "callThisFromAnotherFile", "()V"),
+ checkLine(file2, 28),
+ run());
+ }
+
+ @Test
+ public void testOptimized() throws Throwable {
+ runDebugTest(
+ debuggeePathOptimized,
+ class1,
+ breakpoint(class1, "main", mainSignature),
+ run(),
+ checkMethod(class1, "main", mainSignature),
+ checkLine(file1, 12),
+ stepInto(),
+ checkMethod(class1, "callThisFromSameFile", "()V"),
+ checkLine(file1, 7),
+ stepOver(),
+ checkMethod(class1, "callThisFromSameFile", "()V"),
+ checkLine(file1, 8),
+ stepInto(),
+ checkMethod(class2, "callThisFromAnotherFile", "()V"),
+ checkLine(file2, 28),
+ run());
+ }
+}