Refactor LineNumberOptimizer.

Change-Id: I6df7be38e3fc89919cecd53dd5b18e0a638755a4
diff --git a/src/main/java/com/android/tools/r8/debuginfo/DebugRepresentation.java b/src/main/java/com/android/tools/r8/debuginfo/DebugRepresentation.java
index 2abeeba..6d6a53a 100644
--- a/src/main/java/com/android/tools/r8/debuginfo/DebugRepresentation.java
+++ b/src/main/java/com/android/tools/r8/debuginfo/DebugRepresentation.java
@@ -17,7 +17,7 @@
 import com.android.tools.r8.utils.internal.CollectionUtils;
 import com.android.tools.r8.utils.internal.IterableUtils;
 import com.android.tools.r8.utils.internal.StringUtils;
-import com.android.tools.r8.utils.positions.LineNumberOptimizer;
+import com.android.tools.r8.utils.positions.OverloadedMethodOrdering;
 import com.android.tools.r8.utils.positions.PositionUtils;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
@@ -98,14 +98,11 @@
     Int2ReferenceMap<CostSummary> paramCountToCosts = new Int2ReferenceOpenHashMap<>();
     for (DexProgramClass clazz : file.classes()) {
       IdentityHashMap<DexString, List<ProgramMethod>> overloads =
-          LineNumberOptimizer.groupMethodsByRenamedName(appView, clazz);
+          OverloadedMethodOrdering.groupMethodsByRenamedName(appView, clazz);
       for (List<ProgramMethod> methods : overloads.values()) {
-        if (methods.size() != 1) {
-          // Only use PC info for the first method in the set of overloaded methods.
-          // They need distinct lines to disambiguate.
-          LineNumberOptimizer.sortMethods(methods);
-        }
-        ProgramMethod method = methods.get(0);
+        // Only use PC info for the first method in the set of overloaded methods.
+        // They need distinct lines to disambiguate.
+        ProgramMethod method = OverloadedMethodOrdering.getFirstOverload(methods);
         DexEncodedMethod definition = method.getDefinition();
         if (!isPcCandidate(definition, options)) {
           continue;
diff --git a/src/main/java/com/android/tools/r8/utils/positions/ClassFilePositionToMappedRangeMapper.java b/src/main/java/com/android/tools/r8/utils/positions/ClassFilePositionToMappedRangeMapper.java
index 2347368..2ca30a9 100644
--- a/src/main/java/com/android/tools/r8/utils/positions/ClassFilePositionToMappedRangeMapper.java
+++ b/src/main/java/com/android/tools/r8/utils/positions/ClassFilePositionToMappedRangeMapper.java
@@ -42,7 +42,7 @@
   }
 
   @Override
-  public void updateDebugInfoInCodeObjects() {
+  public void updateDebugInfoInCodeObjects(Timing timing) {
     // Intentionally empty.
   }
 
diff --git a/src/main/java/com/android/tools/r8/utils/positions/LineNumberOptimizer.java b/src/main/java/com/android/tools/r8/utils/positions/LineNumberOptimizer.java
index 080ae12..79f1433 100644
--- a/src/main/java/com/android/tools/r8/utils/positions/LineNumberOptimizer.java
+++ b/src/main/java/com/android/tools/r8/utils/positions/LineNumberOptimizer.java
@@ -5,17 +5,13 @@
 
 import static com.android.tools.r8.utils.positions.PositionUtils.mustHaveResidualDebugInfo;
 
-import com.android.tools.r8.cf.code.CfInstruction;
-import com.android.tools.r8.cf.code.CfPosition;
 import com.android.tools.r8.debuginfo.DebugRepresentation.DebugRepresentationPredicate;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.Code;
 import com.android.tools.r8.graph.DexClassAndMethod;
-import com.android.tools.r8.graph.DexDebugInfo;
 import com.android.tools.r8.graph.DexEncodedMethod;
-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.graph.ProgramMethod;
@@ -30,9 +26,10 @@
 import com.android.tools.r8.shaking.KeepInfoCollection;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.internal.ObjectUtils;
 import com.android.tools.r8.utils.OriginalSourceFiles;
 import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.internal.ObjectUtils;
+import com.android.tools.r8.utils.internal.StringUtils;
 import com.android.tools.r8.utils.positions.MappedPositionToClassNameMapperBuilder.MappedPositionToClassNamingBuilder;
 import com.android.tools.r8.utils.timing.Timing;
 import java.io.IOException;
@@ -55,37 +52,54 @@
       OriginalSourceFiles originalSourceFiles,
       DebugRepresentationPredicate representation)
       throws ExecutionException {
+    return new LineNumberOptimizer(
+            inputApp, appView, executorService, originalSourceFiles, representation)
+        .runAndWriteMap(timing);
+  }
+
+  private final AndroidApp inputApp;
+  private final AppView<?> appView;
+  private final ExecutorService executorService;
+  private final OriginalSourceFiles originalSourceFiles;
+  private final DebugRepresentationPredicate representation;
+
+  private LineNumberOptimizer(
+      AndroidApp inputApp,
+      AppView<?> appView,
+      ExecutorService executorService,
+      OriginalSourceFiles originalSourceFiles,
+      DebugRepresentationPredicate representation) {
+    this.inputApp = inputApp;
+    this.appView = appView;
+    this.executorService = executorService;
+    this.originalSourceFiles = originalSourceFiles;
+    this.representation = representation;
+  }
+
+  @SuppressWarnings("InconsistentOverloads")
+  private ProguardMapSupplierResult runAndWriteMap(Timing timing) throws ExecutionException {
     assert appView.options().hasMappingFileSupport();
-    if (shouldEmitOriginalMappingFile(appView)) {
-      appView.options().reporter.warning(new NotSupportedMapVersionForMappingComposeDiagnostic());
-      timing.begin("Spawn write proguard map - emitting original mapping file");
-      ProguardMapSupplierResult result =
-          ProguardMapSupplier.create(appView.appInfo().app().getProguardMap(), appView.options())
-              .writeProguardMap(appView, executorService, timing);
-      timing.end();
-      return result;
+
+    if (shouldWriteOriginalMappingFile()) {
+      return writeOriginalMappingFile(timing);
     }
-    // When line number optimization is turned off the identity mapping for line numbers is
-    // used. We still run the line number optimizer to collect line numbers and inline frame
-    // information for the mapping file.
-    timing.begin("Line number remapping");
-    ClassNameMapper mapper =
-        run(appView, inputApp, originalSourceFiles, representation, executorService, timing);
-    timing.end();
-    if (appView.options().mappingComposeOptions().generatedClassNameMapperConsumer != null) {
-      appView.options().mappingComposeOptions().generatedClassNameMapperConsumer.accept(mapper);
+    ClassNameMapper mapper = run(timing);
+    notifyMappingConsumer(mapper);
+    if (shouldComposeOriginalMappingFile()) {
+      mapper = composeOriginalMappingFile(mapper, timing);
     }
-    if (appView.options().mappingComposeOptions().enableExperimentalMappingComposition
-        && appView.appInfo().app().getProguardMap() != null) {
-      try (Timing t0 = timing.begin("Compose proguard map")) {
-        mapper =
-            ClassNameMapper.mapperFromStringWithPreamble(
-                MappingComposer.compose(
-                    appView.options(), appView.appInfo().app().getProguardMap(), mapper));
-      } catch (IOException | MappingComposeException e) {
-        throw new CompilationError(e.getMessage(), e);
-      }
+    return writeMappingFile(mapper, timing);
+  }
+
+  private void notifyMappingConsumer(ClassNameMapper mapper) {
+    var consumer = appView.options().mappingComposeOptions().generatedClassNameMapperConsumer;
+    if (consumer != null) {
+      consumer.accept(mapper);
     }
+  }
+
+  private ProguardMapSupplierResult writeMappingFile(ClassNameMapper mapper, Timing timing)
+      throws ExecutionException {
     timing.begin("Spawn write proguard map");
     ProguardMapSupplierResult result =
         ProguardMapSupplier.create(mapper, appView.options())
@@ -94,7 +108,24 @@
     return result;
   }
 
-  private static boolean shouldEmitOriginalMappingFile(AppView<?> appView) {
+  private ClassNameMapper composeOriginalMappingFile(ClassNameMapper mapper, Timing timing) {
+    try (Timing ignored = timing.begin("Compose proguard map")) {
+      String composed =
+          MappingComposer.compose(
+              appView.options(), appView.appInfo().app().getProguardMap(), mapper);
+      mapper = ClassNameMapper.mapperFromStringWithPreamble(composed);
+    } catch (IOException | MappingComposeException e) {
+      throw new CompilationError(e.getMessage(), e);
+    }
+    return mapper;
+  }
+
+  private boolean shouldComposeOriginalMappingFile() {
+    return appView.options().mappingComposeOptions().enableExperimentalMappingComposition
+        && appView.appInfo().app().getProguardMap() != null;
+  }
+
+  private boolean shouldWriteOriginalMappingFile() {
     if (!appView.options().mappingComposeOptions().enableExperimentalMappingComposition
         || appView.appInfo().app().getProguardMap() == null) {
       return false;
@@ -109,35 +140,40 @@
         || newMapVersion.isUnknown();
   }
 
-  public static ClassNameMapper run(
-      AppView<?> appView,
-      AndroidApp inputApp,
-      OriginalSourceFiles originalSourceFiles,
-      DebugRepresentationPredicate representation,
-      ExecutorService executorService,
-      Timing timing)
+  private ProguardMapSupplierResult writeOriginalMappingFile(Timing timing)
       throws ExecutionException {
+    appView.options().reporter.warning(new NotSupportedMapVersionForMappingComposeDiagnostic());
+    return writeMappingFile(appView.appInfo().app().getProguardMap(), timing);
+  }
+
+  /** Optimizes line numbers and returns a corresponding mapping. */
+  private ClassNameMapper run(Timing timing) throws ExecutionException {
+    timing.begin("Line number remapping");
     PositionToMappedRangeMapper positionToMappedRangeMapper =
         PositionToMappedRangeMapper.create(appView);
-    MappedPositionToClassNameMapperBuilder builder =
-        MappedPositionToClassNameMapperBuilder.builder(appView, originalSourceFiles);
 
-    // Collect which files contain which classes that need to have their line numbers optimized.
+    var optimizedClasses = optimizePositions(positionToMappedRangeMapper, timing);
+    ClassNameMapper mapper = buildMapper(optimizedClasses, timing);
+    positionToMappedRangeMapper.updateDebugInfoInCodeObjects(timing);
+
+    timing.end();
+    return mapper;
+  }
+
+  /** Optimize line numbers and return the resulting mapping. */
+  private Iterable<ClassPositionMapping> optimizePositions(
+      PositionToMappedRangeMapper positionToMappedRangeMapper, Timing timing)
+      throws ExecutionException {
     timing.begin("Process classes");
     AppPositionRemapper positionRemapper = AppPositionRemapper.create(appView, inputApp, timing);
-    Deque<MappedPositionsForClassResult> worklist = new ConcurrentLinkedDeque<>();
+    Deque<ClassPositionMapping> worklist = new ConcurrentLinkedDeque<>();
     ThreadUtils.processItemsThatMatches(
         appView.appInfo().classes(),
-        clazz -> shouldRun(clazz, appView),
+        this::shouldRunForClass,
         (clazz, threadTiming) -> {
-          MappedPositionsForClassResult classResult =
-              runForClass(
-                  clazz,
-                  appView,
-                  representation,
-                  positionRemapper,
-                  positionToMappedRangeMapper,
-                  threadTiming);
+          ClassPositionMapping classResult =
+              optimizePositionsForClass(
+                  clazz, positionRemapper, positionToMappedRangeMapper, threadTiming);
           worklist.addLast(classResult);
         },
         appView.options(),
@@ -145,32 +181,31 @@
         timing,
         timing.beginMerger("Map positions concurrently", executorService));
     timing.end();
-
-    timing.begin("Add class naming");
-    while (!worklist.isEmpty()) {
-      MappedPositionsForClassResult classResult = worklist.removeFirst();
-      MappedPositionToClassNamingBuilder classNamingBuilder =
-          builder.addClassNaming(classResult.getClazz());
-      for (MappedPositionsForMethodResult methodResult :
-          classResult.getMappedPositionsForMethods()) {
-        classNamingBuilder.addMappedPositions(
-            methodResult.method,
-            methodResult.mappedPositions,
-            methodResult.positionRemapper,
-            methodResult.canUsePc);
-      }
-    }
-    timing.end();
-
-    // Update all the debug-info objects.
-    timing.begin("Update debug info in code objects");
-    positionToMappedRangeMapper.updateDebugInfoInCodeObjects();
-    timing.end();
-
-    return builder.build();
+    return worklist;
   }
 
-  private static boolean shouldRun(DexProgramClass clazz, AppView<?> appView) {
+  private ClassNameMapper buildMapper(Iterable<ClassPositionMapping> results, Timing timing) {
+    try (Timing ignored = timing.begin("Add class naming")) {
+      // TODO(b/552916515): Do this concurrently.
+      MappedPositionToClassNameMapperBuilder builder =
+          MappedPositionToClassNameMapperBuilder.builder(appView, originalSourceFiles);
+      for (ClassPositionMapping classResult : results) {
+        MappedPositionToClassNamingBuilder classNamingBuilder =
+            builder.addClassNaming(classResult.clazz);
+        for (MethodPositionMapping methodResult : classResult.methodMappings) {
+          classNamingBuilder.addMappedPositions(
+              methodResult.method,
+              methodResult.mappedPositions,
+              methodResult.positionRemapper,
+              methodResult.canUsePc);
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  /** In R8 partial compilation, skip classes that are compiled by D8. */
+  private boolean shouldRunForClass(DexProgramClass clazz) {
     InternalOptions options = appView.options();
     if (options.partialSubCompilationConfiguration == null) {
       return true;
@@ -179,16 +214,14 @@
     }
   }
 
-  private static MappedPositionsForClassResult runForClass(
+  private ClassPositionMapping optimizePositionsForClass(
       DexProgramClass clazz,
-      AppView<?> appView,
-      DebugRepresentationPredicate representation,
       AppPositionRemapper positionRemapper,
       PositionToMappedRangeMapper positionToMappedRangeMapper,
       Timing timing) {
     timing.begin("Prelude");
     IdentityHashMap<DexString, List<ProgramMethod>> methodsByRenamedName =
-        groupMethodsByRenamedName(appView, clazz);
+        OverloadedMethodOrdering.groupMethodsByRenamedName(appView, clazz);
 
     // Process methods ordered by renamed name.
     List<DexString> renamedMethodNames = new ArrayList<>(methodsByRenamedName.keySet());
@@ -197,70 +230,68 @@
 
     ClassPositionRemapper classPositionRemapper =
         positionRemapper.createClassPositionRemapper(clazz);
-    List<MappedPositionsForMethodResult> mappedPositionsForMethodResults = new ArrayList<>();
-    for (DexString methodName : renamedMethodNames) {
-      List<ProgramMethod> methods = methodsByRenamedName.get(methodName);
-      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.
-        // If we are compiling to DEX we will try to not generate overloaded names. This saves
-        // space by allowing more debug-information to be canonicalized. If we have overloaded
-        // methods, we either did not rename them, we renamed them according to a supplied map or
-        // they may be bridges for interface methods with covariant return types.
-        sortMethods(methods);
-        assert verifyMethodsAreKeptDirectlyOrIndirectly(appView, methods);
-      }
-
-      timing.begin("Process methods");
-      // We must reuse the same MethodPositionRemapper for methods with the same name.
-      MethodPositionRemapper methodPositionRemapper =
-          classPositionRemapper.createMethodPositionRemapper();
-      for (ProgramMethod method : methods) {
-        if (shouldRunForMethod(method, appView, methodName, methods)) {
-          MappedPositionsForMethodResult mappedPositionsForMethodResult =
-              runForMethod(
-                  method,
-                  appView,
-                  methods,
-                  methodPositionRemapper,
-                  positionToMappedRangeMapper,
-                  representation,
-                  timing);
-          mappedPositionsForMethodResults.add(mappedPositionsForMethodResult);
-        }
-      }
-      timing.end();
+    List<MethodPositionMapping> methodPositionMappings = new ArrayList<>();
+    for (DexString newMethodName : renamedMethodNames) {
+      List<ProgramMethod> methods = methodsByRenamedName.get(newMethodName);
+      var results =
+          optimizePositionsForOverloads(
+              newMethodName, methods, classPositionRemapper, positionToMappedRangeMapper, timing);
+      methodPositionMappings.addAll(results);
     }
-    return new MappedPositionsForClassResult(clazz, mappedPositionsForMethodResults);
+    return new ClassPositionMapping(clazz, methodPositionMappings);
   }
 
-  private static boolean shouldRunForMethod(
-      ProgramMethod method, AppView<?> appView, DexString methodName, List<ProgramMethod> methods) {
+  private List<MethodPositionMapping> optimizePositionsForOverloads(
+      DexString newMethodName,
+      List<ProgramMethod> methods,
+      ClassPositionRemapper classPositionRemapper,
+      PositionToMappedRangeMapper positionToMappedRangeMapper,
+      Timing timing) {
+    // Sort the methods for deterministic numbering.
+    OverloadedMethodOrdering.sortOverloadedMethods(methods);
+    assert methods.size() <= 1 || verifyMethodsAreKeptDirectlyOrIndirectly(methods)
+        : "Overloads are only allowed with good reason";
+
+    timing.begin("Process methods");
+    MethodPositionRemapper methodPositionRemapper =
+        classPositionRemapper.createMethodPositionRemapper();
+    List<MethodPositionMapping> results = new ArrayList<>(methods.size());
+    for (ProgramMethod method : methods) {
+      if (shouldOptimizeMethod(newMethodName, method, methods)) {
+        results.add(
+            optimizePositionsForMethod(
+                method, methods, methodPositionRemapper, positionToMappedRangeMapper, timing));
+      }
+    }
+    timing.end();
+    return results;
+  }
+
+  private boolean shouldOptimizeMethod(
+      DexString newMethodName, ProgramMethod method, List<ProgramMethod> methods) {
+    assert method.getDefinition() != null : "Method has no definition " + method;
     DexEncodedMethod definition = method.getDefinition();
-    return !method.getName().isIdenticalTo(methodName)
+    return !method.getName().isIdenticalTo(newMethodName)
         || mustHaveResidualDebugInfo(appView.options(), definition)
         || definition.isD8R8Synthesized()
         || methods.size() > 1;
   }
 
-  private static MappedPositionsForMethodResult runForMethod(
+  private MethodPositionMapping optimizePositionsForMethod(
       ProgramMethod method,
-      AppView<?> appView,
       List<ProgramMethod> methods,
       MethodPositionRemapper positionRemapper,
       PositionToMappedRangeMapper positionToMappedRangeMapper,
-      DebugRepresentationPredicate representation,
       Timing timing) {
+    assert method.getDefinition() != null : "Method has no definition " + method;
     Code code = method.getDefinition().getCode();
     if (code == null
         || !(code.isCfCode() || code.isDexCode())
         || appView.isCfByteCodePassThrough(method)) {
-      return new MappedPositionsForMethodResult(
+      return new MethodPositionMapping(
           method, Collections.emptyList(), positionRemapper, representation.canUseDexPc(methods));
     }
-    try (Timing t0 = timing.begin("Get mapped positions")) {
+    try (Timing ignored = timing.begin("Get mapped positions")) {
       int pcEncodingCutoff =
           ObjectUtils.identical(method, methods.get(0))
               ? representation.getDexPcEncodingCutoff(method)
@@ -269,23 +300,23 @@
       List<MappedPosition> mappedPositions =
           positionToMappedRangeMapper.getMappedPositions(
               method, positionRemapper, methods.size() > 1, canUseDexPc, pcEncodingCutoff, timing);
-      return new MappedPositionsForMethodResult(
-          method, mappedPositions, positionRemapper, canUseDexPc);
+      return new MethodPositionMapping(method, mappedPositions, positionRemapper, canUseDexPc);
     }
   }
 
-  @SuppressWarnings("ComplexBooleanConstant")
-  private static boolean verifyMethodsAreKeptDirectlyOrIndirectly(
-      AppView<?> appView, List<ProgramMethod> methods) {
+  @SuppressWarnings("SameReturnValue")
+  private boolean verifyMethodsAreKeptDirectlyOrIndirectly(List<ProgramMethod> methods) {
+    assert !methods.isEmpty();
     if (appView.options().isGeneratingClassFiles() || !appView.appInfo().hasClassHierarchy()) {
       return true;
     }
     AppInfoWithClassHierarchy appInfo = appView.appInfo().withClassHierarchy();
     KeepInfoCollection keepInfo = appView.getKeepInfo();
     boolean allSeenAreInstanceInitializers = true;
-    DexString originalName;
+    List<DexString> originalNames = new ArrayList<>(methods.size());
     for (ProgramMethod method : methods) {
       // We cannot rename instance initializers.
+      assert method.getDefinition() != null;
       if (method.getDefinition().isInstanceInitializer()) {
         assert allSeenAreInstanceInitializers;
         continue;
@@ -295,7 +326,7 @@
       if (!keepInfo.isMinificationAllowed(method, appView.options())) {
         continue;
       }
-      // With desugared library, call-backs names are reserved here.
+      // With desugared library, call-back names are reserved here.
       if (method.getDefinition().isLibraryMethodOverride().isTrue()) {
         continue;
       }
@@ -306,107 +337,37 @@
         // We cannot rename methods we cannot look up.
         continue;
       }
-      String errorString = method.getReference().qualifiedName() + " is not kept but is overloaded";
-      assert lookupResult.getHolder().isInterface() : errorString;
-      // TODO(b/159113601): Reenable assert.
-      assert true || originalName == null || originalName.equals(method.getReference().name)
-          : errorString;
-      originalName = method.getReference().name;
+      String fullMethodName = method.getReference().qualifiedName();
+      assert lookupResult.getHolder().isInterface()
+          : "Expected " + fullMethodName + " to be kept or an interface method";
+      originalNames.add(method.getReference().name);
     }
+    assert originalNames.stream().allMatch(name -> originalNames.get(0).isIdenticalTo(name))
+        : "Non-overloaded methods should not become overloaded "
+            + StringUtils.join(", ", originalNames);
     return true;
   }
 
-  private static int getMethodStartLine(ProgramMethod method) {
-    Code code = method.getDefinition().getCode();
-    if (code == null) {
-      return 0;
-    }
-    if (code.isDexCode()) {
-      DexDebugInfo dexDebugInfo = code.asDexCode().getDebugInfo();
-      return dexDebugInfo == null ? 0 : dexDebugInfo.getStartLine();
-    } else if (code.isCfCode()) {
-      List<CfInstruction> instructions = code.asCfCode().getInstructions();
-      for (CfInstruction instruction : instructions) {
-        if (!(instruction instanceof CfPosition)) {
-          continue;
-        }
-        return ((CfPosition) instruction).getPosition().getLine();
-      }
-    }
-    return 0;
-  }
-
-  public static void sortMethods(List<ProgramMethod> methods) {
-    // Sort by startline, then DexEncodedMethod.slowCompare. Use startLine = 0 if no debuginfo.
-    methods.sort(
-        (lhs, rhs) -> {
-          int lhsStartLine = getMethodStartLine(lhs);
-          int rhsStartLine = getMethodStartLine(rhs);
-          int startLineDiff = lhsStartLine - rhsStartLine;
-          if (startLineDiff != 0) return startLineDiff;
-          return DexEncodedMethod.slowCompare(lhs.getDefinition(), rhs.getDefinition());
-        });
-    // Insert the largest method first since we can use pc encoding for this method.
-    int largestIndex = -1;
-    int largestCode = -1;
-    for (int i = 0; i < methods.size(); i++) {
-      ProgramMethod method = methods.get(i);
-      if (method.getDefinition().hasCode() && method.getDefinition().getCode().isDexCode()) {
-        int codeSizeInBytes = method.getDefinition().getCode().asDexCode().codeSizeInBytes();
-        if (codeSizeInBytes > largestCode) {
-          largestIndex = i;
-          largestCode = codeSizeInBytes;
-        }
-      }
-    }
-    if (largestIndex > 0) {
-      Collections.swap(methods, 0, largestIndex);
-    }
-  }
-
-  public static IdentityHashMap<DexString, List<ProgramMethod>> groupMethodsByRenamedName(
-      AppView<?> appView, DexProgramClass clazz) {
-    IdentityHashMap<DexString, List<ProgramMethod>> methodsByRenamedName =
-        new IdentityHashMap<>(clazz.getMethodCollection().size());
-    for (ProgramMethod programMethod : clazz.programMethods()) {
-      // Add method only if renamed, moved, or if it has debug info to map.
-      DexMethod method = programMethod.getReference();
-      DexString renamedName = appView.getNamingLens().lookupName(method);
-      methodsByRenamedName
-          .computeIfAbsent(renamedName, key -> new ArrayList<>())
-          .add(programMethod);
-    }
-    return methodsByRenamedName;
-  }
-
-  private static class MappedPositionsForClassResult {
+  private static class ClassPositionMapping {
 
     private final DexProgramClass clazz;
-    private final List<MappedPositionsForMethodResult> mappedPositionsForMethods;
+    private final List<MethodPositionMapping> methodMappings;
 
-    private MappedPositionsForClassResult(
-        DexProgramClass clazz, List<MappedPositionsForMethodResult> mappedPositionsForMethods) {
+    private ClassPositionMapping(
+        DexProgramClass clazz, List<MethodPositionMapping> methodMappings) {
       this.clazz = clazz;
-      this.mappedPositionsForMethods = mappedPositionsForMethods;
-    }
-
-    DexProgramClass getClazz() {
-      return clazz;
-    }
-
-    List<MappedPositionsForMethodResult> getMappedPositionsForMethods() {
-      return mappedPositionsForMethods;
+      this.methodMappings = methodMappings;
     }
   }
 
-  private static class MappedPositionsForMethodResult {
+  private static class MethodPositionMapping {
 
     private final ProgramMethod method;
     private final List<MappedPosition> mappedPositions;
     private final MethodPositionRemapper positionRemapper;
     private final boolean canUsePc;
 
-    private MappedPositionsForMethodResult(
+    private MethodPositionMapping(
         ProgramMethod method,
         List<MappedPosition> mappedPositions,
         MethodPositionRemapper positionRemapper,
diff --git a/src/main/java/com/android/tools/r8/utils/positions/MappedPosition.java b/src/main/java/com/android/tools/r8/utils/positions/MappedPosition.java
index 3b29392..d8fe871 100644
--- a/src/main/java/com/android/tools/r8/utils/positions/MappedPosition.java
+++ b/src/main/java/com/android/tools/r8/utils/positions/MappedPosition.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.ir.code.Position;
 
+/** Represents a mapping between an original {@link Position} and its new obfuscated line number. */
 public class MappedPosition {
 
   private final int obfuscatedLine;
diff --git a/src/main/java/com/android/tools/r8/utils/positions/MethodPositionRemapper.java b/src/main/java/com/android/tools/r8/utils/positions/MethodPositionRemapper.java
index 2910c82..cf63ed7 100644
--- a/src/main/java/com/android/tools/r8/utils/positions/MethodPositionRemapper.java
+++ b/src/main/java/com/android/tools/r8/utils/positions/MethodPositionRemapper.java
@@ -6,9 +6,13 @@
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.utils.internal.collections.Pair;
 
+/** Stateful remapper that maps positions for a single method. */
 public interface MethodPositionRemapper {
 
+  /** Returns {@code (original, mapped)} positions. */
   Pair<Position, Position> createRemappedPosition(Position position);
 
+  /** Sets the next line number to allocate when assigning remapped positions. */
   void setNextOptimizedLineNumber(int nextOptimizedLineNumber);
 }
+
diff --git a/src/main/java/com/android/tools/r8/utils/positions/OverloadedMethodOrdering.java b/src/main/java/com/android/tools/r8/utils/positions/OverloadedMethodOrdering.java
new file mode 100644
index 0000000..3ce1ee7
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/positions/OverloadedMethodOrdering.java
@@ -0,0 +1,152 @@
+// Copyright (c) 2026, 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.positions;
+
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfPosition;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.Code;
+import com.android.tools.r8.graph.DexDebugInfo;
+import com.android.tools.r8.graph.DexEncodedMethod;
+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.graph.ProgramMethod;
+import com.android.tools.r8.utils.internal.AssertionUtils;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
+
+public class OverloadedMethodOrdering {
+
+  private static int compareMethods(ProgramMethod m1, ProgramMethod m2) {
+    int m1StartLine = getMethodStartLine(m1, 0);
+    int m2StartLine = getMethodStartLine(m2, 0);
+    int startLineDiff = m1StartLine - m2StartLine;
+    if (startLineDiff != 0) return startLineDiff;
+    return DexEncodedMethod.slowCompare(m1.getDefinition(), m2.getDefinition());
+  }
+
+  public static void sortOverloadedMethods(List<ProgramMethod> methods) {
+    if (methods.size() <= 1) {
+      return;
+    }
+
+    methods.sort(OverloadedMethodOrdering::compareMethods);
+    // Only the first method can use pc-encoding, bias the choice by code size.
+    moveLargestCodeToFront(methods);
+  }
+
+  public static ProgramMethod getFirstOverload(List<ProgramMethod> methods) {
+    if (methods.size() <= 1) {
+      return methods.get(0);
+    }
+    int largestCode = -1;
+    ProgramMethod minMethod = null;
+    for (ProgramMethod method : methods) {
+      var codeSize = getCodeSize(method);
+      if (codeSize == -1) {
+        // Do nothing.
+      } else if (codeSize > largestCode) {
+        largestCode = codeSize;
+        minMethod = method;
+      } else if (codeSize == largestCode) {
+        if (compareMethods(method, minMethod) < 0) {
+          minMethod = method;
+        }
+      }
+      if (minMethod == null) {
+        minMethod = method;
+      } else if (largestCode == -1) {
+        // We are still based on comparator.
+        if (compareMethods(method, minMethod) < 0) {
+          minMethod = method;
+        }
+      }
+    }
+    if (AssertionUtils.assertionsEnabled()) {
+      var dup = new ArrayList<>(methods);
+      sortOverloadedMethods(dup);
+      assert dup.get(0) == minMethod;
+    }
+    return minMethod;
+  }
+
+  @SuppressWarnings("SameParameterValue")
+  private static int getMethodStartLine(ProgramMethod method, int defaultValue) {
+    Code code = method.getDefinition().getCode();
+    if (code == null) {
+      return defaultValue;
+    }
+    if (code.isDexCode()) {
+      DexDebugInfo dexDebugInfo = code.asDexCode().getDebugInfo();
+      if (dexDebugInfo != null) {
+        return dexDebugInfo.getStartLine();
+      }
+    } else if (code.isCfCode()) {
+      for (CfInstruction instruction : code.asCfCode().getInstructions()) {
+        if (instruction instanceof CfPosition) {
+          return ((CfPosition) instruction).getPosition().getLine();
+        }
+      }
+    }
+    return defaultValue;
+  }
+
+  private static void moveLargestCodeToFront(List<ProgramMethod> methods) {
+    int largestIndex = getIndexOfLargest(methods);
+    if (largestIndex > 0) {
+      Collections.swap(methods, 0, largestIndex);
+    }
+  }
+
+  /** If multiple are tied, returns the first largest method. */
+  private static int getIndexOfLargest(List<ProgramMethod> methods) {
+    int largestIndex = -1;
+    int largestCode = -1;
+    for (int i = 0; i < methods.size(); i++) {
+      ProgramMethod method = methods.get(i);
+      int codeSize = getCodeSize(method);
+      if (codeSize > largestCode) {
+        largestIndex = i;
+        largestCode = codeSize;
+      }
+    }
+    return largestIndex;
+  }
+
+  private static int getCodeSize(ProgramMethod method) {
+    Code code = method.getDefinition().getCode();
+    if (code == null) {
+      return -1;
+    } else if (code.isDexCode()) {
+      return code.asDexCode().codeSizeInBytes();
+    } else if (code.isCfCode()) {
+      // CF code is ignored for size.
+      return -1;
+    } else {
+      return -1;
+    }
+  }
+
+  /**
+   * Returns a map from renamed names, to the methods that were renamed to it (or stayed with that
+   * name).
+   */
+  public static IdentityHashMap<DexString, List<ProgramMethod>> groupMethodsByRenamedName(
+      AppView<?> appView, DexProgramClass clazz) {
+    IdentityHashMap<DexString, List<ProgramMethod>> methodsByRenamedName =
+        new IdentityHashMap<>(clazz.getMethodCollection().size());
+    for (ProgramMethod programMethod : clazz.programMethods()) {
+      DexMethod method = programMethod.getReference();
+      DexString renamedName = appView.getNamingLens().lookupName(method);
+      methodsByRenamedName
+          .computeIfAbsent(renamedName, key -> new ArrayList<>())
+          .add(programMethod);
+    }
+    return methodsByRenamedName;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/positions/PositionToMappedRangeMapper.java b/src/main/java/com/android/tools/r8/utils/positions/PositionToMappedRangeMapper.java
index c154154..5c08421 100644
--- a/src/main/java/com/android/tools/r8/utils/positions/PositionToMappedRangeMapper.java
+++ b/src/main/java/com/android/tools/r8/utils/positions/PositionToMappedRangeMapper.java
@@ -26,7 +26,7 @@
       int pcEncodingCutoff,
       Timing timing);
 
-  void updateDebugInfoInCodeObjects();
+  void updateDebugInfoInCodeObjects(Timing timing);
 
   static PositionToMappedRangeMapper create(AppView<?> appView) {
     return appView.options().isGeneratingClassFiles()
@@ -65,8 +65,8 @@
     }
 
     @Override
-    public void updateDebugInfoInCodeObjects() {
-      pcBasedDebugInfoRecorder.updateDebugInfoInCodeObjects();
+    public void updateDebugInfoInCodeObjects(Timing timing) {
+      pcBasedDebugInfoRecorder.updateDebugInfoInCodeObjects(timing);
     }
   }
 
@@ -80,7 +80,7 @@
      * <p>Must be called after all recordings have been given to allow computing the debug info
      * items to be installed.
      */
-    void updateDebugInfoInCodeObjects();
+    void updateDebugInfoInCodeObjects(Timing timing);
 
     int getPcEncoding(int pc);
   }
@@ -135,7 +135,8 @@
     }
 
     @Override
-    public void updateDebugInfoInCodeObjects() {
+    public void updateDebugInfoInCodeObjects(Timing timing) {
+      timing.begin("Update debug info in code objects");
       Map<UpdateInfo, DexDebugInfo> debugInfos = new HashMap<>();
       codesToUpdate.forEach(
           entry -> {
@@ -146,6 +147,7 @@
             assert debugInfo.asPcBasedInfo().getMaxPc() == entry.maxEncodingPc;
             entry.code.setDebugInfo(debugInfo);
           });
+      timing.end();
     }
 
     private static DexDebugInfo buildPc2PcDebugInfo(UpdateInfo info) {
@@ -172,7 +174,7 @@
     }
 
     @Override
-    public void updateDebugInfoInCodeObjects() {
+    public void updateDebugInfoInCodeObjects(Timing timing) {
       // Already null out the info so nothing to do.
     }
   }