Emit a pc-to-pc line mapping in debug info tables.

The use of pc-to-pc line mappings allows most methods to share the the
same debug_info_item for the encoding of their debug information. The
shared info items reduces the size of the DEX files and unifies the line
mappings such that they are the same on legacy devices as well as on
newer devices that support emitting the PC directly.

Bug: 205910335
Change-Id: I557f8c47f481536c4a8470aafc18bd9ec7082e54
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 8c16e46..72c6f81 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCode.java
@@ -27,6 +27,7 @@
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.ArrayUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.structural.Equatable;
 import com.android.tools.r8.utils.structural.HashCodeVisitor;
@@ -276,7 +277,7 @@
           new DexString[callee.getArity()],
           new DexDebugEvent[] {
             new DexDebugEvent.SetInlineFrame(callee, callerPosition),
-            DexDebugEvent.ZERO_CHANGE_DEFAULT_EVENT
+            DexDebugEvent.Default.ZERO_CHANGE_DEFAULT_EVENT
           });
     }
     DexDebugEvent[] oldEvents = debugInfo.events;
@@ -438,13 +439,15 @@
 
     DexDebugEntry debugInfo = null;
     Iterator<DexDebugEntry> debugInfoIterator = Collections.emptyIterator();
-    if (getDebugInfo() != null && method != null) {
+    boolean isPc2PcMapping = getDebugInfo() != null && getDebugInfo().isPcToPcMapping();
+    if (!isPc2PcMapping && getDebugInfo() != null && method != null) {
       debugInfoIterator = new DexDebugEntryBuilder(method, new DexItemFactory()).build().iterator();
       debugInfo = debugInfoIterator.hasNext() ? debugInfoIterator.next() : null;
     }
     int instructionNumber = 0;
     Map<Integer, DebugLocalInfo> locals = Collections.emptyMap();
     for (Instruction insn : instructions) {
+      debugInfo = advanceToOffset(insn.getOffset() - 1, debugInfo, debugInfoIterator);
       while (debugInfo != null && debugInfo.address == insn.getOffset()) {
         if (debugInfo.lineEntry || !locals.equals(debugInfo.locals)) {
           builder.append("         ").append(debugInfo.toString(false)).append("\n");
@@ -462,8 +465,16 @@
       }
       builder.append('\n');
     }
-    if (debugInfoIterator.hasNext()) {
-      throw new Unreachable("Could not print all debug information.");
+    if (isPc2PcMapping) {
+      builder.append("(has pc2pc debug info mapping)\n");
+    } else if (debugInfoIterator.hasNext()) {
+      Instruction lastInstruction = ArrayUtils.last(instructions);
+      debugInfo = advanceToOffset(lastInstruction.getOffset(), debugInfo, debugInfoIterator);
+      if (debugInfo != null) {
+        throw new Unreachable("Could not print all debug information.");
+      } else {
+        builder.append("(has debug events past last pc)\n");
+      }
     }
     if (tries.length > 0) {
       builder.append("Tries (numbers are offsets)\n");
@@ -483,6 +494,14 @@
     return builder.toString();
   }
 
+  DexDebugEntry advanceToOffset(
+      int offset, DexDebugEntry current, Iterator<DexDebugEntry> iterator) {
+    while (current != null && current.address <= offset) {
+      current = iterator.hasNext() ? iterator.next() : null;
+    }
+    return current;
+  }
+
   public String toSmaliString(ClassNameMapper naming) {
     StringBuilder builder = new StringBuilder();
     // Find labeled targets.
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 bb93147..b1194bb 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugEvent.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugEvent.java
@@ -27,8 +27,6 @@
 
   public static final DexDebugEvent[] EMPTY_ARRAY = {};
 
-  public static final DexDebugEvent.Default ZERO_CHANGE_DEFAULT_EVENT = Default.create(0, 0);
-
   public void collectIndexedItems(IndexedItemCollection collection, GraphLens graphLens) {
     // Empty by default.
   }
@@ -711,6 +709,9 @@
 
   public static class Default extends DexDebugEvent {
 
+    public static final DexDebugEvent.Default ZERO_CHANGE_DEFAULT_EVENT = Default.create(0, 0);
+    public static final DexDebugEvent.Default ONE_CHANGE_DEFAULT_EVENT = Default.create(1, 1);
+
     final int value;
 
     Default(int value) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugInfo.java b/src/main/java/com/android/tools/r8/graph/DexDebugInfo.java
index 75588f6..7abeeca 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugInfo.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.dex.MixedSectionCollection;
+import com.android.tools.r8.graph.DexDebugEvent.Default;
 import com.android.tools.r8.utils.structural.Equatable;
 import com.android.tools.r8.utils.structural.StructuralItem;
 import com.android.tools.r8.utils.structural.StructuralMapping;
@@ -91,4 +92,24 @@
     builder.append("]\n");
     return builder.toString();
   }
+
+  public boolean isPcToPcMapping() {
+    if (startLine != 0) {
+      return false;
+    }
+    for (DexString parameter : parameters) {
+      if (parameter != null) {
+        return false;
+      }
+    }
+    if (events.length < 1 || !Default.ZERO_CHANGE_DEFAULT_EVENT.equals(events[0])) {
+      return false;
+    }
+    for (int i = 1; i < events.length; i++) {
+      if (!Default.ONE_CHANGE_DEFAULT_EVENT.equals(events[i])) {
+        return false;
+      }
+    }
+    return true;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugInfoForSingleLineMethod.java b/src/main/java/com/android/tools/r8/graph/DexDebugInfoForSingleLineMethod.java
index 32d3126..9818a86 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugInfoForSingleLineMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugInfoForSingleLineMethod.java
@@ -4,10 +4,13 @@
 
 package com.android.tools.r8.graph;
 
+import com.android.tools.r8.graph.DexDebugEvent.Default;
+
 public class DexDebugInfoForSingleLineMethod extends DexDebugInfo {
 
   private static final DexDebugInfoForSingleLineMethod INSTANCE =
-      new DexDebugInfoForSingleLineMethod(0, DexString.EMPTY_ARRAY, DexDebugEvent.EMPTY_ARRAY);
+      new DexDebugInfoForSingleLineMethod(
+          0, DexString.EMPTY_ARRAY, new DexDebugEvent[] {Default.ZERO_CHANGE_DEFAULT_EVENT});
 
   private DexDebugInfoForSingleLineMethod(
       int startLine, DexString[] parameters, DexDebugEvent[] events) {
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 1cbbc21..0a13ce5 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -1862,8 +1862,21 @@
     return !isDesugaring() || hasMinApi(AndroidApiLevel.N);
   }
 
-  public boolean canUseDexPcAsDebugInformation() {
-    return lineNumberOptimization == LineNumberOptimization.ON && hasMinApi(AndroidApiLevel.O);
+  // Debug entries may be dropped only if the source file content allows being omitted from
+  // stack traces, or if the VM will report the source file even with a null valued debug info.
+  public boolean allowDiscardingResidualDebugInfo() {
+    // TODO(b/146565491): We can drop debug info once fixed at a known min-api.
+    return sourceFileProvider != null && sourceFileProvider.allowDiscardingSourceFile();
+  }
+
+  public boolean canUseDexPc2PcAsDebugInformation() {
+    return lineNumberOptimization == LineNumberOptimization.ON;
+  }
+
+  public boolean canUseNativeDexPcInsteadOfDebugInfo() {
+    return canUseDexPc2PcAsDebugInformation()
+        && hasMinApi(AndroidApiLevel.O)
+        && allowDiscardingResidualDebugInfo();
   }
 
   public boolean isInterfaceMethodDesugaringEnabled() {
diff --git a/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java b/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
index 2e9c933..685e202 100644
--- a/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
+++ b/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
@@ -323,6 +323,94 @@
     }
   }
 
+  private interface PcBasedDebugInfo {
+    /** Callback to record a code object with a given max instruction PC and parameter count. */
+    void recordPcMappingFor(DexCode code, int lastInstructionPc, int parameterCount);
+
+    /** Callback to record a code object with only a single "line". */
+    void recordSingleLineFor(DexCode code);
+
+    /**
+     * Install the correct debug info objects.
+     *
+     * <p>Must be called after all recordings have been given to allow computing the debug info
+     * items to be installed.
+     */
+    void updateDebugInfoInCodeObjects();
+  }
+
+  private static class Pc2PcMappingSupport implements PcBasedDebugInfo {
+
+    private final boolean allowDiscardingSourceFile;
+
+    int maxInstructionPc = 0;
+    int maxParameterCount = 0;
+    List<DexCode> codesToUpdate = new ArrayList<>();
+    List<DexCode> singleLineCodes = new ArrayList<>();
+
+    public Pc2PcMappingSupport(boolean allowDiscardingSourceFile) {
+      this.allowDiscardingSourceFile = allowDiscardingSourceFile;
+    }
+
+    @Override
+    public void recordPcMappingFor(DexCode code, int lastInstructionPc, int parameterCount) {
+      codesToUpdate.add(code);
+      maxInstructionPc = Math.max(maxInstructionPc, lastInstructionPc);
+      maxParameterCount = Math.max(maxParameterCount, parameterCount);
+    }
+
+    @Override
+    public void recordSingleLineFor(DexCode code) {
+      singleLineCodes.add(code);
+    }
+
+    @Override
+    public void updateDebugInfoInCodeObjects() {
+      DexDebugInfo debugInfo = buildPc2PcDebugInfo(maxInstructionPc, maxParameterCount);
+      codesToUpdate.forEach(c -> c.setDebugInfo(debugInfo));
+      // After updating to PC mappings, ensure single-line info is correct.
+      if (allowDiscardingSourceFile) {
+        // If the source file content may be omitted it is safe to just null out the info.
+        singleLineCodes.forEach(c -> c.setDebugInfo(null));
+      } else {
+        // Otherwise, we need to keep the debug info to ensure that VMs with PC support won't
+        // omit printing the source file. We reset the singleton single-line debug info item.
+        // This has a small constant size increase equal to size of the single-line debug item,
+        // but avoids linking the smallest items to a large PC table.
+        singleLineCodes.forEach(c -> c.setDebugInfo(DexDebugInfoForSingleLineMethod.getInstance()));
+      }
+    }
+
+    private static DexDebugInfo buildPc2PcDebugInfo(int lastInstructionPc, int parameterCount) {
+      DexDebugEvent[] events = new DexDebugEvent[lastInstructionPc + 1];
+      events[0] = Default.ZERO_CHANGE_DEFAULT_EVENT;
+      for (int i = 1; i < lastInstructionPc + 1; i++) {
+        events[i] = Default.ONE_CHANGE_DEFAULT_EVENT;
+      }
+      return new DexDebugInfo(0, new DexString[parameterCount], events);
+    }
+  }
+
+  private static class NativePcSupport implements PcBasedDebugInfo {
+
+    @Override
+    public void recordPcMappingFor(DexCode code, int lastInstructionPc, int length) {
+      // Strip the info in full as the runtime will emit the PC directly.
+      code.setDebugInfo(null);
+    }
+
+    @Override
+    public void recordSingleLineFor(DexCode code) {
+      // Strip the info at once as it does not conflict with any PC mapping update.
+      code.setDebugInfo(null);
+    }
+
+    @Override
+    public void updateDebugInfoInCodeObjects() {
+      // Already null out the info so nothing to do.
+    }
+  }
+
   public static ClassNameMapper run(
       AppView<?> appView, DexApplication application, AndroidApp inputApp, NamingLens namingLens) {
     // For finding methods in kotlin files based on SourceDebugExtensions, we use a line method map.
@@ -332,6 +420,11 @@
 
     Map<DexMethod, OutlineFixupBuilder> outlinesToFix = new IdentityHashMap<>();
 
+    PcBasedDebugInfo pcBasedDebugInfo =
+        appView.options().canUseNativeDexPcInsteadOfDebugInfo()
+            ? new NativePcSupport()
+            : new Pc2PcMappingSupport(appView.options().allowDiscardingResidualDebugInfo());
+
     // Collect which files contain which classes that need to have their line numbers optimized.
     for (DexProgramClass clazz : application.classes()) {
       boolean isSyntheticClass = appView.getSyntheticItems().isSyntheticClass(clazz);
@@ -362,10 +455,6 @@
         }
       }
 
-      boolean canStripDebugInfo =
-          appView.options().sourceFileProvider != null
-              && appView.options().sourceFileProvider.allowDiscardingSourceFile();
-
       if (isSyntheticClass) {
         onDemandClassNamingBuilder
             .get()
@@ -417,13 +506,13 @@
           List<MappedPosition> mappedPositions;
           Code code = method.getCode();
           boolean canUseDexPc =
-              canStripDebugInfo
-                  && appView.options().canUseDexPcAsDebugInformation()
-                  && methods.size() == 1;
+              appView.options().canUseDexPc2PcAsDebugInformation() && methods.size() == 1;
           if (code != null) {
             if (code.isDexCode() && doesContainPositions(code.asDexCode())) {
               if (canUseDexPc) {
-                mappedPositions = optimizeDexCodePositionsForPc(method, appView, kotlinRemapper);
+                mappedPositions =
+                    optimizeDexCodePositionsForPc(
+                        method, appView, kotlinRemapper, pcBasedDebugInfo);
               } else {
                 mappedPositions =
                     optimizeDexCodePositions(
@@ -458,6 +547,9 @@
               && methodMappingInfo.isEmpty()
               && obfuscatedNameDexString == originalMethod.name
               && originalMethod.holder == originalType) {
+            assert appView.options().lineNumberOptimization == LineNumberOptimization.OFF
+                || !doesContainPositions(method)
+                || appView.isCfByteCodePassThrough(method);
             continue;
           }
 
@@ -588,11 +680,10 @@
             }
             i = j;
           }
-          if (canStripDebugInfo
-              && method.getCode().isDexCode()
+          if (method.getCode().isDexCode()
               && method.getCode().asDexCode().getDebugInfo()
                   == DexDebugInfoForSingleLineMethod.getInstance()) {
-            method.getCode().asDexCode().setDebugInfo(null);
+            pcBasedDebugInfo.recordSingleLineFor(method.getCode().asDexCode());
           }
         } // for each method of the group
       } // for each method group, grouped by name
@@ -601,6 +692,9 @@
     // Fixup all outline positions
     outlinesToFix.values().forEach(OutlineFixupBuilder::fixup);
 
+    // Update all the debug-info objects.
+    pcBasedDebugInfo.updateDebugInfoInCodeObjects();
+
     return classNameMapperBuilder.build();
   }
 
@@ -960,14 +1054,16 @@
   }
 
   private static List<MappedPosition> optimizeDexCodePositionsForPc(
-      DexEncodedMethod method, AppView<?> appView, PositionRemapper positionRemapper) {
+      DexEncodedMethod method,
+      AppView<?> appView,
+      PositionRemapper positionRemapper,
+      PcBasedDebugInfo debugInfoProvider) {
     List<MappedPosition> mappedPositions = new ArrayList<>();
     // Do the actual processing for each method.
     DexCode dexCode = method.getCode().asDexCode();
     DexDebugInfo debugInfo = dexCode.getDebugInfo();
-
+    BooleanBox singleOriginalLine = new BooleanBox(true);
     Pair<Integer, Position> lastPosition = new Pair<>();
-
     DexDebugEventVisitor visitor =
         new DexDebugPositionState(
             debugInfo.startLine,
@@ -976,7 +1072,12 @@
           public void visit(Default defaultEvent) {
             super.visit(defaultEvent);
             assert getCurrentLine() >= 0;
+            Position currentPosition = getPositionFromPositionState(this);
             if (lastPosition.getSecond() != null) {
+              if (singleOriginalLine.isTrue()
+                  && !currentPosition.equals(lastPosition.getSecond())) {
+                singleOriginalLine.set(false);
+              }
               remapAndAddForPc(
                   lastPosition.getFirst(),
                   getCurrentPc(),
@@ -985,7 +1086,7 @@
                   mappedPositions);
             }
             lastPosition.setFirst(getCurrentPc());
-            lastPosition.setSecond(getPositionFromPositionState(this));
+            lastPosition.setSecond(currentPosition);
             resetOutlineInformation();
           }
         };
@@ -994,17 +1095,26 @@
       event.accept(visitor);
     }
 
+    int lastInstructionPc = ArrayUtils.last(dexCode.instructions).getOffset();
     if (lastPosition.getSecond() != null) {
-      int lastPc = dexCode.instructions[dexCode.instructions.length - 1].getOffset();
       remapAndAddForPc(
           lastPosition.getFirst(),
-          lastPc + 1,
+          lastInstructionPc + 1,
           lastPosition.getSecond(),
           positionRemapper,
           mappedPositions);
     }
 
-    dexCode.setDebugInfo(null);
+    // If we only have one original line we can always retrace back uniquely.
+    assert !mappedPositions.isEmpty();
+    if (singleOriginalLine.isTrue()
+        && lastPosition.getSecond() != null
+        && !mappedPositions.get(0).isOutlineCaller()) {
+      dexCode.setDebugInfo(DexDebugInfoForSingleLineMethod.getInstance());
+      debugInfoProvider.recordSingleLineFor(dexCode);
+    } else {
+      debugInfoProvider.recordPcMappingFor(dexCode, lastInstructionPc, debugInfo.parameters.length);
+    }
     return mappedPositions;
   }
 
diff --git a/src/test/java/com/android/tools/r8/debuginfo/CanonicalizeWithInline.java b/src/test/java/com/android/tools/r8/debuginfo/CanonicalizeWithInline.java
index 48b9fc8..a8eb9cc 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/CanonicalizeWithInline.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/CanonicalizeWithInline.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.debuginfo;
 
+import static org.junit.Assert.assertNull;
+
 import com.android.tools.r8.AssumeMayHaveSideEffects;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.OutputMode;
@@ -13,6 +15,8 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.dex.DexParser;
 import com.android.tools.r8.dex.DexSection;
+import com.android.tools.r8.graph.DexDebugInfo;
+import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -31,10 +35,8 @@
     return getTestParameters().withNoneRuntime().build();
   }
 
-  private final TestParameters parameters;
-
   public CanonicalizeWithInline(TestParameters parameters) {
-    this.parameters = parameters;
+    parameters.assertNoneRuntime();
   }
 
   private int getNumberOfDebugInfos(Path file) throws IOException {
@@ -58,10 +60,17 @@
             .addProgramClasses(clazzA, clazzB)
             .addKeepRules(
                 "-keepattributes SourceFile,LineNumberTable",
-                "-keep class ** {\n" + "public void call(int);\n" + "}")
+                "-keep class ** { public void call(int); }")
             .enableInliningAnnotations()
             .enableSideEffectAnnotations()
             .compile();
+    result.inspect(
+        inspector -> {
+          DexEncodedMethod method =
+              inspector.clazz(ClassA.class).uniqueMethodWithName("call").getMethod();
+          DexDebugInfo debugInfo = method.getCode().asDexCode().getDebugInfo();
+          assertNull(debugInfo);
+        });
     Path classesPath = temp.getRoot().toPath();
     result.app.write(classesPath, OutputMode.DexIndexed);
     int numberOfDebugInfos =
diff --git a/src/test/java/com/android/tools/r8/debuginfo/DexPcWithDebugInfoForOverloadedMethodsTestRunner.java b/src/test/java/com/android/tools/r8/debuginfo/DexPcWithDebugInfoForOverloadedMethodsTestRunner.java
index 37d2b6e..b3c1c08 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/DexPcWithDebugInfoForOverloadedMethodsTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/DexPcWithDebugInfoForOverloadedMethodsTestRunner.java
@@ -23,7 +23,6 @@
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.retrace.RetraceFrameResult;
-import com.android.tools.r8.shaking.ProguardKeepAttributes;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -68,7 +67,6 @@
         .addKeepMainRule(MAIN)
         .addKeepMethodRules(MAIN, "void overloaded(...)")
         .addKeepAttributeLineNumberTable()
-        .addKeepAttributes(ProguardKeepAttributes.SOURCE_FILE)
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), MAIN)
         .assertFailureWithErrorThatMatches(containsString(EXPECTED))
diff --git a/src/test/java/com/android/tools/r8/debuginfo/EnsureNoDebugInfoEmittedForPcOnlyTestRunner.java b/src/test/java/com/android/tools/r8/debuginfo/EnsureNoDebugInfoEmittedForPcOnlyTestRunner.java
index 30505ab..a5e76eb 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/EnsureNoDebugInfoEmittedForPcOnlyTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/EnsureNoDebugInfoEmittedForPcOnlyTestRunner.java
@@ -7,8 +7,8 @@
 import static com.android.tools.r8.naming.retrace.StackTrace.isSameExceptForFileNameAndLineNumber;
 import static com.android.tools.r8.utils.InternalOptions.LineNumberOptimization.ON;
 import static junit.framework.TestCase.assertEquals;
-import static junit.framework.TestCase.assertNull;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
@@ -21,7 +21,6 @@
 import com.android.tools.r8.graph.DexDebugEntryBuilder;
 import com.android.tools.r8.naming.retrace.StackTrace;
 import com.android.tools.r8.naming.retrace.StackTrace.StackTraceLine;
-import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
@@ -38,7 +37,6 @@
 
   private static final String FILENAME_MAIN = "EnsureNoDebugInfoEmittedForPcOnlyTest.java";
   private static final Class<?> MAIN = EnsureNoDebugInfoEmittedForPcOnlyTest.class;
-  private static final int INLINED_DEX_PC = 32;
 
   private final TestParameters parameters;
 
@@ -51,8 +49,9 @@
     this.parameters = parameters;
   }
 
-  private boolean apiLevelSupportsPcOutput() {
-    return parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.O);
+  private boolean apiLevelSupportsPcAndSourceFileOutput() {
+    // TODO(b/146565491): Update with API level once fixed.
+    return false;
   }
 
   @Test
@@ -77,7 +76,7 @@
         .internalEnableMappingOutput()
         // TODO(b/191038746): Enable LineNumberOptimization for release builds for DEX PC Output.
         .applyIf(
-            apiLevelSupportsPcOutput(),
+            apiLevelSupportsPcAndSourceFileOutput(),
             builder ->
                 builder.addOptionsModification(
                     options -> {
@@ -98,7 +97,7 @@
         .run(parameters.getRuntime(), MAIN)
         .inspectFailure(
             inspector -> {
-              if (apiLevelSupportsPcOutput()) {
+              if (apiLevelSupportsPcAndSourceFileOutput()) {
                 checkNoDebugInfo(inspector, 5);
               } else {
                 checkHasLineNumberInfo(inspector);
@@ -122,7 +121,7 @@
 
   @Test
   public void testNoEmittedDebugInfoR8() throws Exception {
-    assumeTrue(apiLevelSupportsPcOutput());
+    assumeTrue(apiLevelSupportsPcAndSourceFileOutput());
     testForR8(parameters.getBackend())
         .addProgramClasses(MAIN)
         .addKeepMainRule(MAIN)
diff --git a/src/test/java/com/android/tools/r8/debuginfo/SingleLineInfoRemoveTest.java b/src/test/java/com/android/tools/r8/debuginfo/SingleLineInfoRemoveTest.java
index 652f401..b881f55 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/SingleLineInfoRemoveTest.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/SingleLineInfoRemoveTest.java
@@ -9,14 +9,18 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
 
 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.TestRuntime.CfRuntime;
 import com.android.tools.r8.naming.retrace.StackTrace;
+import com.android.tools.r8.naming.retrace.StackTrace.StackTraceLine;
+import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import java.util.List;
+import org.hamcrest.CoreMatchers;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -27,14 +31,17 @@
 public class SingleLineInfoRemoveTest extends TestBase {
 
   private final TestParameters parameters;
+  private final boolean customSourceFile;
 
-  @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  @Parameters(name = "{0}, custom-source-file:{1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimesAndApiLevels().build(), BooleanUtils.values());
   }
 
-  public SingleLineInfoRemoveTest(TestParameters parameters) {
+  public SingleLineInfoRemoveTest(TestParameters parameters, boolean customSourceFile) {
     this.parameters = parameters;
+    this.customSourceFile = customSourceFile;
   }
 
   public StackTrace expectedStackTrace;
@@ -58,9 +65,27 @@
         .addKeepMainRule(Main.class)
         .addKeepAttributeSourceFile()
         .addKeepAttributeLineNumberTable()
+        .applyIf(
+            customSourceFile,
+            b -> b.getBuilder().setSourceFileProvider(env -> "MyCustomSourceFile"))
         .enableInliningAnnotations()
         .run(parameters.getRuntime(), Main.class)
         .assertFailureWithErrorThatThrows(NullPointerException.class)
+        .inspectOriginalStackTrace(
+            stackTrace -> {
+              for (StackTraceLine line : stackTrace.getStackTraceLines()) {
+                if (customSourceFile) {
+                  assertEquals("MyCustomSourceFile", line.fileName);
+                } else if (parameters.isCfRuntime()) {
+                  assertEquals("SourceFile", line.fileName);
+                } else {
+                  assertThat(
+                      line.fileName,
+                      CoreMatchers.anyOf(
+                          CoreMatchers.is("SourceFile"), CoreMatchers.is("Unknown Source")));
+                }
+              }
+            })
         .inspectStackTrace(
             (stackTrace, inspector) -> {
               assertThat(stackTrace, isSame(expectedStackTrace));
@@ -68,10 +93,14 @@
               assertThat(mainSubject, isPresent());
               assertThat(
                   mainSubject.uniqueMethodWithName("shouldRemoveLineNumber"),
-                  notIf(hasLineNumberTable(), parameters.isDexRuntime()));
+                  notIf(hasLineNumberTable(), canSingleLineDebugInfoBeDiscarded()));
             });
   }
 
+  private boolean canSingleLineDebugInfoBeDiscarded() {
+    return parameters.isDexRuntime() && !customSourceFile;
+  }
+
   public static class Main {
 
     @NeverInline
diff --git a/src/test/java/com/android/tools/r8/regress/b150400371/DebuginfoForInlineFrameRegressionTest.java b/src/test/java/com/android/tools/r8/regress/b150400371/DebuginfoForInlineFrameRegressionTest.java
index cc7900e..5405ecb 100644
--- a/src/test/java/com/android/tools/r8/regress/b150400371/DebuginfoForInlineFrameRegressionTest.java
+++ b/src/test/java/com/android/tools/r8/regress/b150400371/DebuginfoForInlineFrameRegressionTest.java
@@ -4,8 +4,8 @@
 
 package com.android.tools.r8.regress.b150400371;
 
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -45,8 +45,9 @@
               MethodSubject main =
                   inspector.method(InlineInto.class.getDeclaredMethod("main", String[].class));
               if (parameters.getApiLevel().isLessThan(apiLevelWithPcAsLineNumberSupport())) {
+                // Method has 14 actual lines, the PC mapping table will have about 50.
                 IntSet lines = new IntArraySet(main.getLineNumberTable().getLines());
-                assertEquals(2, lines.size());
+                assertTrue(lines.size() > 20);
               } else {
                 assertNull(main.getLineNumberTable());
               }
diff --git a/tools/run_on_app_dump.py b/tools/run_on_app_dump.py
index f71a314..6c25a4d 100755
--- a/tools/run_on_app_dump.py
+++ b/tools/run_on_app_dump.py
@@ -945,6 +945,9 @@
   result.add_argument('--shrinker',
                       help='The shrinkers to use (by default, all are run)',
                       action='append')
+  result.add_argument('--temp',
+                      help='A directory to use for temporaries and outputs.',
+                      default=None)
   result.add_argument('--version',
                       default='main',
                       help='The version of R8 to use (e.g., 1.4.51)')
@@ -1087,6 +1090,8 @@
     return 0
 
   with utils.TempDir() as temp_dir:
+    if options.temp:
+      temp_dir = options.temp
     if options.hash:
       # Download r8-<hash>.jar from
       # https://storage.googleapis.com/r8-releases/raw/.