Refactor position remapper to better facilitate concurrency

When the line number optimization runs, a single stateful  ClassPositionRemapper is used, which tracks the following state:

  DexMethod previousMethod = null;
  int previousSourceLine = -1;
  int nextOptimizedLineNumber = 1;

This CL splits the position remapper into an app position remapper, a class position remapper, and a method position remapper. A stateful class position remapper can be created from the app position remapper, and a stateful method position remapper can be created from a class position remapper.

This makes it possible for different threads to create their own state for the classes and methods they process. This is outside the scope of the current CL.

Bug: b/422947619
Change-Id: I3273a235c6a237a8f1fb7d9667815ce1f9665bd1
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 ed04432..e5db358 100644
--- a/src/main/java/com/android/tools/r8/debuginfo/DebugRepresentation.java
+++ b/src/main/java/com/android/tools/r8/debuginfo/DebugRepresentation.java
@@ -475,13 +475,13 @@
   }
 
   public static DexInstruction getLastExecutableInstruction(DexInstruction[] instructions) {
-    DexInstruction lastInstruction = null;
-    for (DexInstruction instruction : instructions) {
+    for (int i = instructions.length - 1; i >= 0; i--) {
+      DexInstruction instruction = instructions[i];
       if (!instruction.isPayload()) {
-        lastInstruction = instruction;
+        return instruction;
       }
     }
-    return lastInstruction;
+    return null;
   }
 
   private static int estimatedDebugInfoSize(DexDebugInfo info) {
diff --git a/src/main/java/com/android/tools/r8/utils/positions/AppPositionRemapper.java b/src/main/java/com/android/tools/r8/utils/positions/AppPositionRemapper.java
new file mode 100644
index 0000000..221ed96
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/positions/AppPositionRemapper.java
@@ -0,0 +1,30 @@
+// Copyright (c) 2025, 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.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.utils.CfLineToMethodMapper;
+import com.android.tools.r8.utils.positions.ClassPositionRemapper.IdentityPositionRemapper;
+import com.android.tools.r8.utils.positions.ClassPositionRemapper.KotlinInlineFunctionAppPositionRemapper;
+import com.android.tools.r8.utils.positions.ClassPositionRemapper.OptimizingPositionRemapper;
+
+public interface AppPositionRemapper {
+
+  ClassPositionRemapper createClassPositionRemapper(DexProgramClass clazz);
+
+  static AppPositionRemapper create(AppView<?> appView, CfLineToMethodMapper cfLineToMethodMapper) {
+    boolean identityMapping = appView.options().lineNumberOptimization.isOff();
+    AppPositionRemapper positionRemapper =
+        identityMapping
+            ? new IdentityPositionRemapper()
+            : new OptimizingPositionRemapper(appView.options());
+
+    // Kotlin inline functions and arguments have their inlining information stored in the
+    // source debug extension annotation. Instantiate the kotlin remapper on top of the original
+    // remapper to allow for remapping original positions to kotlin inline positions.
+    return new KotlinInlineFunctionAppPositionRemapper(
+        appView, positionRemapper, cfLineToMethodMapper);
+  }
+}
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 99c3df9..ef6d075 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
@@ -16,6 +16,7 @@
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Position.SyntheticPosition;
 import com.android.tools.r8.utils.Pair;
+import com.android.tools.r8.utils.timing.Timing;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -30,10 +31,11 @@
   @Override
   public List<MappedPosition> getMappedPositions(
       ProgramMethod method,
-      ClassPositionRemapper positionRemapper,
+      MethodPositionRemapper positionRemapper,
       boolean hasOverloads,
       boolean canUseDexPc,
-      int pcEncodingCutoff) {
+      int pcEncodingCutoff,
+      Timing timing) {
     return appView.options().getTestingOptions().usePcEncodingInCfForTesting
         ? getPcEncodedPositions(method, positionRemapper)
         : getMappedPositionsRemapped(method, positionRemapper, hasOverloads);
@@ -45,7 +47,7 @@
   }
 
   private List<MappedPosition> getMappedPositionsRemapped(
-      ProgramMethod method, ClassPositionRemapper positionRemapper, boolean hasOverloads) {
+      ProgramMethod method, MethodPositionRemapper positionRemapper, boolean hasOverloads) {
     List<MappedPosition> mappedPositions = new ArrayList<>();
     // Do the actual processing for each method.
     CfCode oldCode = method.getDefinition().getCode().asCfCode();
@@ -99,21 +101,18 @@
     return mappedPositions;
   }
 
-  @SuppressWarnings("UnusedVariable")
   private List<MappedPosition> getPcEncodedPositions(
-      ProgramMethod method, ClassPositionRemapper positionRemapper) {
+      ProgramMethod method, MethodPositionRemapper positionRemapper) {
     List<MappedPosition> mappedPositions = new ArrayList<>();
     // Do the actual processing for each method.
     CfCode oldCode = method.getDefinition().getCode().asCfCode();
     List<CfInstruction> oldInstructions = oldCode.getInstructions();
     List<CfInstruction> newInstructions = new ArrayList<>(oldInstructions.size() * 3);
     Position currentPosition = null;
-    boolean isFirstEntry = false;
     for (CfInstruction oldInstruction : oldInstructions) {
       if (oldInstruction.isPosition()) {
         CfPosition cfPosition = oldInstruction.asPosition();
         currentPosition = cfPosition.getPosition();
-        isFirstEntry = true;
       } else {
         if (currentPosition != null) {
           Pair<Position, Position> remappedPosition =
@@ -125,7 +124,6 @@
           newInstructions.add(position);
           newInstructions.add(position.getLabel());
         }
-        isFirstEntry = false;
         newInstructions.add(oldInstruction);
       }
     }
diff --git a/src/main/java/com/android/tools/r8/utils/positions/ClassPositionRemapper.java b/src/main/java/com/android/tools/r8/utils/positions/ClassPositionRemapper.java
index 5444395..f53d3f9 100644
--- a/src/main/java/com/android/tools/r8/utils/positions/ClassPositionRemapper.java
+++ b/src/main/java/com/android/tools/r8/utils/positions/ClassPositionRemapper.java
@@ -5,13 +5,13 @@
 
 import com.android.tools.r8.ResourceException;
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexClass;
-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.graph.DexType;
 import com.android.tools.r8.graph.DexValue.DexValueString;
+import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Position.SourcePosition;
 import com.android.tools.r8.kotlin.KotlinSourceDebugExtensionParser;
@@ -21,6 +21,7 @@
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.Pair;
 import java.util.IdentityHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 
@@ -28,26 +29,20 @@
 // DexDebugPositionState) and returns a remapped Position.
 public interface ClassPositionRemapper {
 
-  Pair<Position, Position> createRemappedPosition(Position position);
+  MethodPositionRemapper createMethodPositionRemapper(List<ProgramMethod> methods);
 
-  static ClassPositionRemapper getPositionRemapper(
-      AppView<?> appView, CfLineToMethodMapper cfLineToMethodMapper) {
-    boolean identityMapping = appView.options().lineNumberOptimization.isOff();
-    ClassPositionRemapper positionRemapper =
-        identityMapping
-            ? new IdentityPositionRemapper()
-            : new OptimizingPositionRemapper(appView.options());
+  class IdentityPositionRemapper
+      implements AppPositionRemapper, ClassPositionRemapper, MethodPositionRemapper {
 
-    // Kotlin inline functions and arguments have their inlining information stored in the
-    // source debug extension annotation. Instantiate the kotlin remapper on top of the original
-    // remapper to allow for remapping original positions to kotlin inline positions.
-    return new KotlinInlineFunctionPositionRemapper(
-        appView, positionRemapper, cfLineToMethodMapper);
-  }
+    @Override
+    public ClassPositionRemapper createClassPositionRemapper(DexProgramClass clazz) {
+      return this;
+    }
 
-  void setCurrentMethod(DexEncodedMethod definition);
-
-  class IdentityPositionRemapper implements ClassPositionRemapper {
+    @Override
+    public MethodPositionRemapper createMethodPositionRemapper(List<ProgramMethod> methods) {
+      return this;
+    }
 
     @Override
     public Pair<Position, Position> createRemappedPosition(Position position) {
@@ -55,157 +50,193 @@
       assert position.getOutlineCallee() == null;
       return new Pair<>(position, position);
     }
-
-    @Override
-    public void setCurrentMethod(DexEncodedMethod definition) {
-      // This has no effect.
-    }
   }
 
-  class OptimizingPositionRemapper implements ClassPositionRemapper {
+  class OptimizingPositionRemapper implements AppPositionRemapper, ClassPositionRemapper {
+
     private final int maxLineDelta;
-    private DexMethod previousMethod = null;
-    private int previousSourceLine = -1;
-    private int nextOptimizedLineNumber = 1;
 
     OptimizingPositionRemapper(InternalOptions options) {
-      // TODO(113198295): For dex using "Constants.DBG_LINE_RANGE + Constants.DBG_LINE_BASE"
-      // instead of 1 creates a ~30% smaller map file but the dex files gets larger due to reduced
-      // debug info canonicalization.
+      // TODO(b/113198295): For dex using "Constants.DBG_LINE_RANGE + Constants.DBG_LINE_BASE"
+      //  instead of 1 creates a ~30% smaller map file but the dex files gets larger due to reduced
+      //  debug info canonicalization.
       maxLineDelta = options.isGeneratingClassFiles() ? Integer.MAX_VALUE : 1;
     }
 
     @Override
-    public Pair<Position, Position> createRemappedPosition(Position position) {
-      assert position.getMethod() != null;
-      if (position.getMethod().isIdenticalTo(previousMethod)) {
-        assert previousSourceLine >= 0;
-        if (position.getLine() > previousSourceLine
-            && position.getLine() - previousSourceLine <= maxLineDelta) {
-          nextOptimizedLineNumber += (position.getLine() - previousSourceLine) - 1;
-        }
-      }
-
-      Position newPosition =
-          position
-              .builderWithCopy()
-              .setLine(nextOptimizedLineNumber++)
-              .setCallerPosition(null)
-              .build();
-      previousSourceLine = position.getLine();
-      previousMethod = position.getMethod();
-      return new Pair<>(position, newPosition);
+    public ClassPositionRemapper createClassPositionRemapper(DexProgramClass clazz) {
+      return this;
     }
 
     @Override
-    public void setCurrentMethod(DexEncodedMethod definition) {
-      // This has no effect.
+    public MethodPositionRemapper createMethodPositionRemapper(List<ProgramMethod> methods) {
+      return new OptimizingMethodPositionRemapper();
+    }
+
+    class OptimizingMethodPositionRemapper implements MethodPositionRemapper {
+
+      private DexMethod previousMethod = null;
+      private int previousSourceLine = -1;
+      private int nextOptimizedLineNumber = 1;
+
+      @Override
+      public Pair<Position, Position> createRemappedPosition(Position position) {
+        assert position.getMethod() != null;
+        if (position.getMethod().isIdenticalTo(previousMethod)) {
+          assert previousSourceLine >= 0;
+          if (position.getLine() > previousSourceLine
+              && position.getLine() - previousSourceLine <= maxLineDelta) {
+            nextOptimizedLineNumber += (position.getLine() - previousSourceLine) - 1;
+          }
+        }
+
+        Position newPosition =
+            position
+                .builderWithCopy()
+                .setLine(nextOptimizedLineNumber++)
+                .setCallerPosition(null)
+                .build();
+        previousSourceLine = position.getLine();
+        previousMethod = position.getMethod();
+        return new Pair<>(position, newPosition);
+      }
     }
   }
 
-  class KotlinInlineFunctionPositionRemapper implements ClassPositionRemapper {
+  class KotlinInlineFunctionAppPositionRemapper implements AppPositionRemapper {
 
     private final AppView<?> appView;
+    private final AppPositionRemapper baseRemapper;
     private final DexItemFactory factory;
-    private final Map<DexType, Result> parsedKotlinSourceDebugExtensions = new IdentityHashMap<>();
     private final CfLineToMethodMapper lineToMethodMapper;
-    private final ClassPositionRemapper baseRemapper;
 
-    // Fields for the current context.
-    private DexEncodedMethod currentMethod;
-    private Result parsedData = null;
+    private final Map<DexType, Result> parsedKotlinSourceDebugExtensions = new IdentityHashMap<>();
 
-    private KotlinInlineFunctionPositionRemapper(
+    KotlinInlineFunctionAppPositionRemapper(
         AppView<?> appView,
-        ClassPositionRemapper baseRemapper,
+        AppPositionRemapper baseRemapper,
         CfLineToMethodMapper lineToMethodMapper) {
       this.appView = appView;
-      this.factory = appView.dexItemFactory();
       this.baseRemapper = baseRemapper;
+      this.factory = appView.dexItemFactory();
       this.lineToMethodMapper = lineToMethodMapper;
     }
 
     @Override
-    public Pair<Position, Position> createRemappedPosition(Position position) {
-      assert currentMethod != null;
-      int line = position.getLine();
-      Result parsedData = getAndParseSourceDebugExtension(position.getMethod().holder);
-      if (parsedData == null) {
-        return baseRemapper.createRemappedPosition(position);
+    public ClassPositionRemapper createClassPositionRemapper(DexProgramClass clazz) {
+      ClassPositionRemapper baseClassRemapper = baseRemapper.createClassPositionRemapper(clazz);
+      assert baseClassRemapper == baseRemapper;
+      return new KotlinInlineFunctionClassPositionRemapper(baseClassRemapper);
+    }
+
+    class KotlinInlineFunctionClassPositionRemapper implements ClassPositionRemapper {
+
+      private final ClassPositionRemapper baseRemapper;
+
+      private KotlinInlineFunctionClassPositionRemapper(ClassPositionRemapper baseRemapper) {
+        this.baseRemapper = baseRemapper;
       }
-      Map.Entry<Integer, KotlinSourceDebugExtensionParser.Position> inlinedPosition =
-          parsedData.lookupInlinedPosition(line);
-      if (inlinedPosition == null) {
-        return baseRemapper.createRemappedPosition(position);
+
+      @Override
+      public MethodPositionRemapper createMethodPositionRemapper(List<ProgramMethod> methods) {
+        MethodPositionRemapper baseMethodRemapper =
+            baseRemapper.createMethodPositionRemapper(methods);
+        return new KotlinInlineFunctionMethodPositionRemapper(baseMethodRemapper, methods);
       }
-      int inlineeLineDelta = line - inlinedPosition.getKey();
-      int originalInlineeLine = inlinedPosition.getValue().getRange().from + inlineeLineDelta;
-      try {
-        String binaryName = inlinedPosition.getValue().getSource().getPath();
-        String nameAndDescriptor =
-            lineToMethodMapper.lookupNameAndDescriptor(binaryName, originalInlineeLine);
-        if (nameAndDescriptor == null) {
+
+      class KotlinInlineFunctionMethodPositionRemapper implements MethodPositionRemapper {
+
+        private final MethodPositionRemapper baseRemapper;
+        private final DexProgramClass currentHolder;
+
+        private Result parsedData;
+
+        private KotlinInlineFunctionMethodPositionRemapper(
+            MethodPositionRemapper baseRemapper, List<ProgramMethod> methods) {
+          assert !methods.isEmpty();
+          this.baseRemapper = baseRemapper;
+          this.currentHolder = methods.iterator().next().getHolder();
+        }
+
+        @Override
+        public Pair<Position, Position> createRemappedPosition(Position position) {
+          int line = position.getLine();
+          Result parsedData = getAndParseSourceDebugExtension(position.getMethod().holder);
+          if (parsedData == null) {
+            return baseRemapper.createRemappedPosition(position);
+          }
+          Map.Entry<Integer, KotlinSourceDebugExtensionParser.Position> inlinedPosition =
+              parsedData.lookupInlinedPosition(line);
+          if (inlinedPosition == null) {
+            return baseRemapper.createRemappedPosition(position);
+          }
+          int inlineeLineDelta = line - inlinedPosition.getKey();
+          int originalInlineeLine = inlinedPosition.getValue().getRange().from + inlineeLineDelta;
+          try {
+            String binaryName = inlinedPosition.getValue().getSource().getPath();
+            String nameAndDescriptor =
+                lineToMethodMapper.lookupNameAndDescriptor(binaryName, originalInlineeLine);
+            if (nameAndDescriptor == null) {
+              return baseRemapper.createRemappedPosition(position);
+            }
+            String clazzDescriptor = DescriptorUtils.getDescriptorFromClassBinaryName(binaryName);
+            String methodName = CfLineToMethodMapper.getName(nameAndDescriptor);
+            String methodDescriptor = CfLineToMethodMapper.getDescriptor(nameAndDescriptor);
+            String returnTypeDescriptor = DescriptorUtils.getReturnTypeDescriptor(methodDescriptor);
+            String[] argumentDescriptors =
+                DescriptorUtils.getArgumentTypeDescriptors(methodDescriptor);
+            DexString[] argumentDexStringDescriptors = new DexString[argumentDescriptors.length];
+            for (int i = 0; i < argumentDescriptors.length; i++) {
+              argumentDexStringDescriptors[i] = factory.createString(argumentDescriptors[i]);
+            }
+            DexMethod inlinee =
+                factory.createMethod(
+                    factory.createString(clazzDescriptor),
+                    factory.createString(methodName),
+                    factory.createString(returnTypeDescriptor),
+                    argumentDexStringDescriptors);
+            if (!inlinee.equals(position.getMethod())) {
+              // We have an inline from a different method than the current position.
+              Entry<Integer, KotlinSourceDebugExtensionParser.Position> calleePosition =
+                  parsedData.lookupCalleePosition(line);
+              if (calleePosition != null) {
+                // Take the first line as the callee position
+                int calleeLine = Math.max(0, calleePosition.getValue().getRange().from);
+                position = position.builderWithCopy().setLine(calleeLine).build();
+              }
+              return baseRemapper.createRemappedPosition(
+                  SourcePosition.builder()
+                      .setLine(originalInlineeLine)
+                      .setMethod(inlinee)
+                      .setCallerPosition(position)
+                      .build());
+            }
+            // This is the same position, so we should really not mark this as an inline position.
+            // Fall through to the default case.
+          } catch (ResourceException ignored) {
+            // Intentionally left empty. Remapping of kotlin functions utility is a best effort
+            // mapping.
+          }
           return baseRemapper.createRemappedPosition(position);
         }
-        String clazzDescriptor = DescriptorUtils.getDescriptorFromClassBinaryName(binaryName);
-        String methodName = CfLineToMethodMapper.getName(nameAndDescriptor);
-        String methodDescriptor = CfLineToMethodMapper.getDescriptor(nameAndDescriptor);
-        String returnTypeDescriptor = DescriptorUtils.getReturnTypeDescriptor(methodDescriptor);
-        String[] argumentDescriptors = DescriptorUtils.getArgumentTypeDescriptors(methodDescriptor);
-        DexString[] argumentDexStringDescriptors = new DexString[argumentDescriptors.length];
-        for (int i = 0; i < argumentDescriptors.length; i++) {
-          argumentDexStringDescriptors[i] = factory.createString(argumentDescriptors[i]);
-        }
-        DexMethod inlinee =
-            factory.createMethod(
-                factory.createString(clazzDescriptor),
-                factory.createString(methodName),
-                factory.createString(returnTypeDescriptor),
-                argumentDexStringDescriptors);
-        if (!inlinee.equals(position.getMethod())) {
-          // We have an inline from a different method than the current position.
-          Entry<Integer, KotlinSourceDebugExtensionParser.Position> calleePosition =
-              parsedData.lookupCalleePosition(line);
-          if (calleePosition != null) {
-            // Take the first line as the callee position
-            int calleeLine = Math.max(0, calleePosition.getValue().getRange().from);
-            position = position.builderWithCopy().setLine(calleeLine).build();
+
+        private Result getAndParseSourceDebugExtension(DexType holder) {
+          if (parsedData == null) {
+            parsedData = parsedKotlinSourceDebugExtensions.get(holder);
           }
-          return baseRemapper.createRemappedPosition(
-              SourcePosition.builder()
-                  .setLine(originalInlineeLine)
-                  .setMethod(inlinee)
-                  .setCallerPosition(position)
-                  .build());
+          if (parsedData != null || parsedKotlinSourceDebugExtensions.containsKey(holder)) {
+            return parsedData;
+          }
+          DexValueString sourceDebugExtension =
+              appView.getSourceDebugExtensionForType(currentHolder);
+          if (sourceDebugExtension != null) {
+            parsedData =
+                KotlinSourceDebugExtensionParser.parse(sourceDebugExtension.getValue().toString());
+          }
+          parsedKotlinSourceDebugExtensions.put(holder, parsedData);
+          return parsedData;
         }
-        // This is the same position, so we should really not mark this as an inline position. Fall
-        // through to the default case.
-      } catch (ResourceException ignored) {
-        // Intentionally left empty. Remapping of kotlin functions utility is a best effort mapping.
       }
-      return baseRemapper.createRemappedPosition(position);
-    }
-
-    private Result getAndParseSourceDebugExtension(DexType holder) {
-      if (parsedData == null) {
-        parsedData = parsedKotlinSourceDebugExtensions.get(holder);
-      }
-      if (parsedData != null || parsedKotlinSourceDebugExtensions.containsKey(holder)) {
-        return parsedData;
-      }
-      DexClass clazz = appView.definitionFor(currentMethod.getHolderType());
-      DexValueString dexValueString = appView.getSourceDebugExtensionForType(clazz);
-      if (dexValueString != null) {
-        parsedData = KotlinSourceDebugExtensionParser.parse(dexValueString.value.toString());
-      }
-      parsedKotlinSourceDebugExtensions.put(holder, parsedData);
-      return parsedData;
-    }
-
-    @Override
-    public void setCurrentMethod(DexEncodedMethod method) {
-      this.currentMethod = method;
-      this.parsedData = null;
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/positions/DexPositionToNoPcMappedRangeMapper.java b/src/main/java/com/android/tools/r8/utils/positions/DexPositionToNoPcMappedRangeMapper.java
index 0b28e8e..9430e76 100644
--- a/src/main/java/com/android/tools/r8/utils/positions/DexPositionToNoPcMappedRangeMapper.java
+++ b/src/main/java/com/android/tools/r8/utils/positions/DexPositionToNoPcMappedRangeMapper.java
@@ -26,6 +26,7 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Position.SourcePosition;
+import com.android.tools.r8.utils.timing.Timing;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -84,7 +85,7 @@
 
     private final PositionEventEmitter positionEventEmitter;
     private final List<MappedPosition> mappedPositions;
-    private final ClassPositionRemapper positionRemapper;
+    private final MethodPositionRemapper positionRemapper;
     private final List<DexDebugEvent> processedEvents;
 
     // Keep track of what PC has been emitted.
@@ -95,7 +96,7 @@
     public DexDebugPositionStateVisitor(
         PositionEventEmitter positionEventEmitter,
         List<MappedPosition> mappedPositions,
-        ClassPositionRemapper positionRemapper,
+        MethodPositionRemapper positionRemapper,
         List<DexDebugEvent> processedEvents,
         DexItemFactory factory,
         int startLine,
@@ -181,7 +182,8 @@
   }
 
   public List<MappedPosition> optimizeDexCodePositions(
-      ProgramMethod method, ClassPositionRemapper positionRemapper) {
+      ProgramMethod method, MethodPositionRemapper positionRemapper, Timing timing) {
+    timing.begin("No pc mapper");
     List<MappedPosition> mappedPositions = new ArrayList<>();
     // Do the actual processing for each method.
     DexApplication application = appView.appInfo().app();
@@ -221,6 +223,7 @@
         || verifyIdentityMapping(method, debugInfo, optimizedDebugInfo);
 
     dexCode.setDebugInfo(optimizedDebugInfo);
+    timing.end();
     return mappedPositions;
   }
 
diff --git a/src/main/java/com/android/tools/r8/utils/positions/DexPositionToPcMappedRangeMapper.java b/src/main/java/com/android/tools/r8/utils/positions/DexPositionToPcMappedRangeMapper.java
index 7b67169..760f493 100644
--- a/src/main/java/com/android/tools/r8/utils/positions/DexPositionToPcMappedRangeMapper.java
+++ b/src/main/java/com/android/tools/r8/utils/positions/DexPositionToPcMappedRangeMapper.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.utils.IntBox;
 import com.android.tools.r8.utils.Pair;
 import com.android.tools.r8.utils.positions.PositionToMappedRangeMapper.PcBasedDebugInfoRecorder;
+import com.android.tools.r8.utils.timing.Timing;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -34,12 +35,18 @@
   }
 
   public List<MappedPosition> optimizeDexCodePositionsForPc(
-      ProgramMethod method, ClassPositionRemapper positionRemapper, int pcEncodingCutoff) {
+      ProgramMethod method,
+      MethodPositionRemapper positionRemapper,
+      int pcEncodingCutoff,
+      Timing timing) {
+    timing.begin("Pc mapper");
     List<MappedPosition> mappedPositions = new ArrayList<>();
     // Do the actual processing for each method.
     DexCode dexCode = method.getDefinition().getCode().asDexCode();
+    timing.begin("Convert to event based debug info");
     EventBasedDebugInfo debugInfo =
         getEventBasedDebugInfo(method.getDefinition(), dexCode, appView);
+    timing.end();
     IntBox firstDefaultEventPc = new IntBox(-1);
     Pair<Integer, Position> lastPosition = new Pair<>();
     DexDebugEventVisitor visitor =
@@ -62,17 +69,21 @@
                   getCurrentPc(),
                   lastPosition.getSecond(),
                   positionRemapper,
-                  mappedPositions);
+                  mappedPositions,
+                  timing);
             }
             lastPosition.setFirst(getCurrentPc());
             lastPosition.setSecond(currentPosition);
           }
         };
 
+    timing.begin("Visit events");
     for (DexDebugEvent event : debugInfo.events) {
       event.accept(visitor);
     }
+    timing.end();
 
+    timing.begin("Flush");
     int lastInstructionPc = DebugRepresentation.getLastExecutableInstruction(dexCode).getOffset();
     if (lastPosition.getSecond() != null) {
       remapAndAddForPc(
@@ -81,13 +92,18 @@
           lastInstructionPc + 1,
           lastPosition.getSecond(),
           positionRemapper,
-          mappedPositions);
+          mappedPositions,
+          timing);
     }
+    timing.end();
 
     assert !mappedPositions.isEmpty()
         || dexCode.instructions.length == 1
         || !dexCode.hasThrowingInstructions();
+    timing.begin("Record pc mapping");
     pcBasedDebugInfo.recordPcMappingFor(method, pcEncodingCutoff);
+    timing.end();
+    timing.end();
     return mappedPositions;
   }
 
@@ -96,14 +112,19 @@
       int startPc,
       int endPc,
       Position position,
-      ClassPositionRemapper remapper,
-      List<MappedPosition> mappedPositions) {
+      MethodPositionRemapper remapper,
+      List<MappedPosition> mappedPositions,
+      Timing timing) {
+    timing.begin("Remap position");
     Pair<Position, Position> remappedPosition = remapper.createRemappedPosition(position);
+    timing.end();
+    timing.begin("Update mapped positions");
     Position oldPosition = remappedPosition.getFirst();
     for (int currentPc = startPc; currentPc < endPc; currentPc++) {
       mappedPositions.add(
           new MappedPosition(oldPosition, debugInfoProvider.getPcEncoding(currentPc)));
     }
+    timing.end();
   }
 
   // This conversion *always* creates an event based debug info encoding as any non-info will
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 aa55443..02a46e9 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
@@ -36,6 +36,7 @@
 import com.android.tools.r8.utils.timing.Timing;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.IdentityHashMap;
 import java.util.List;
 
@@ -120,6 +121,8 @@
 
     // Collect which files contain which classes that need to have their line numbers optimized.
     timing.begin("Process classes");
+    AppPositionRemapper positionRemapper =
+        AppPositionRemapper.create(appView, cfLineToMethodMapper);
     for (DexProgramClass clazz : appView.appInfo().classes()) {
       if (shouldRun(clazz, appView)) {
         runForClass(
@@ -127,7 +130,7 @@
             appView,
             representation,
             builder,
-            cfLineToMethodMapper,
+            positionRemapper,
             positionToMappedRangeMapper,
             timing);
       }
@@ -156,7 +159,7 @@
       AppView<?> appView,
       DebugRepresentationPredicate representation,
       MappedPositionToClassNameMapperBuilder builder,
-      CfLineToMethodMapper cfLineToMethodMapper,
+      AppPositionRemapper positionRemapper,
       PositionToMappedRangeMapper positionToMappedRangeMapper,
       Timing timing) {
     timing.begin("Prelude");
@@ -170,6 +173,8 @@
     renamedMethodNames.sort(DexString::compareTo);
     timing.end();
 
+    ClassPositionRemapper classPositionRemapper =
+        positionRemapper.createClassPositionRemapper(clazz);
     for (DexString methodName : renamedMethodNames) {
       List<ProgramMethod> methods = methodsByRenamedName.get(methodName);
       if (methods.size() > 1) {
@@ -185,62 +190,66 @@
         assert verifyMethodsAreKeptDirectlyOrIndirectly(appView, methods);
       }
 
-      ClassPositionRemapper positionRemapper =
-          ClassPositionRemapper.getPositionRemapper(appView, cfLineToMethodMapper);
-
       timing.begin("Process methods");
+      // We must reuse the same MethodPositionRemapper for methods with the same name.
+      MethodPositionRemapper methodPositionRemapper =
+          classPositionRemapper.createMethodPositionRemapper(methods);
       for (ProgramMethod method : methods) {
-        runForMethod(
-            method,
-            appView,
-            classNamingBuilder,
-            methodName,
-            methods,
-            positionRemapper,
-            positionToMappedRangeMapper,
-            representation,
-            timing);
+        if (shouldRunForMethod(method, appView, methodName, methods)) {
+          List<MappedPosition> mappedPositions =
+              runForMethod(
+                  method,
+                  appView,
+                  methods,
+                  methodPositionRemapper,
+                  positionToMappedRangeMapper,
+                  representation,
+                  timing);
+          timing.begin("Add mapped positions");
+          boolean canUseDexPc =
+              methods.size() == 1 && representation.getDexPcEncodingCutoff(method) > 0;
+          classNamingBuilder.addMappedPositions(
+              method, mappedPositions, methodPositionRemapper, canUseDexPc);
+          timing.end();
+        }
       }
       timing.end();
     } // for each method group, grouped by name
   }
 
-  private static void runForMethod(
+  private static boolean shouldRunForMethod(
+      ProgramMethod method, AppView<?> appView, DexString methodName, List<ProgramMethod> methods) {
+    DexEncodedMethod definition = method.getDefinition();
+    return !method.getName().isIdenticalTo(methodName)
+        || mustHaveResidualDebugInfo(appView.options(), definition)
+        || definition.isD8R8Synthesized()
+        || methods.size() > 1;
+  }
+
+  private static List<MappedPosition> runForMethod(
       ProgramMethod method,
       AppView<?> appView,
-      MappedPositionToClassNamingBuilder classNamingBuilder,
-      DexString methodName,
       List<ProgramMethod> methods,
-      ClassPositionRemapper positionRemapper,
+      MethodPositionRemapper positionRemapper,
       PositionToMappedRangeMapper positionToMappedRangeMapper,
       DebugRepresentationPredicate representation,
       Timing timing) {
-    DexEncodedMethod definition = method.getDefinition();
-    if (method.getName().isIdenticalTo(methodName)
-        && !mustHaveResidualDebugInfo(appView.options(), definition)
-        && !definition.isD8R8Synthesized()
-        && methods.size() <= 1) {
-      return;
-    }
-    positionRemapper.setCurrentMethod(definition);
     List<MappedPosition> mappedPositions;
     int pcEncodingCutoff = methods.size() == 1 ? representation.getDexPcEncodingCutoff(method) : -1;
     boolean canUseDexPc = pcEncodingCutoff > 0;
-    if (definition.getCode() != null
-        && (definition.getCode().isCfCode() || definition.getCode().isDexCode())
+    Code code = method.getDefinition().getCode();
+    if (code != null
+        && (code.isCfCode() || code.isDexCode())
         && !appView.isCfByteCodePassThrough(method)) {
       timing.begin("Get mapped positions");
       mappedPositions =
           positionToMappedRangeMapper.getMappedPositions(
-              method, positionRemapper, methods.size() > 1, canUseDexPc, pcEncodingCutoff);
+              method, positionRemapper, methods.size() > 1, canUseDexPc, pcEncodingCutoff, timing);
       timing.end();
     } else {
-      mappedPositions = new ArrayList<>();
+      mappedPositions = Collections.emptyList();
     }
-
-    timing.begin("Add mapped positions");
-    classNamingBuilder.addMappedPositions(method, mappedPositions, positionRemapper, canUseDexPc);
-    timing.end();
+    return mappedPositions;
   }
 
   @SuppressWarnings("ComplexBooleanConstant")
diff --git a/src/main/java/com/android/tools/r8/utils/positions/MappedPositionToClassNameMapperBuilder.java b/src/main/java/com/android/tools/r8/utils/positions/MappedPositionToClassNameMapperBuilder.java
index b854592..3530795 100644
--- a/src/main/java/com/android/tools/r8/utils/positions/MappedPositionToClassNameMapperBuilder.java
+++ b/src/main/java/com/android/tools/r8/utils/positions/MappedPositionToClassNameMapperBuilder.java
@@ -220,7 +220,7 @@
     public MappedPositionToClassNamingBuilder addMappedPositions(
         ProgramMethod method,
         List<MappedPosition> mappedPositions,
-        ClassPositionRemapper positionRemapper,
+        MethodPositionRemapper positionRemapper,
         boolean canUseDexPc) {
       DexEncodedMethod definition = method.getDefinition();
       DexMethod residualMethod =
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
new file mode 100644
index 0000000..98e1b50
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/positions/MethodPositionRemapper.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2025, 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.ir.code.Position;
+import com.android.tools.r8.utils.Pair;
+
+public interface MethodPositionRemapper {
+
+  Pair<Position, Position> createRemappedPosition(Position position);
+}
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 10a79ac..ba0e2ea 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
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.graph.DexDebugInfo;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.utils.timing.Timing;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -19,10 +20,11 @@
 
   List<MappedPosition> getMappedPositions(
       ProgramMethod method,
-      ClassPositionRemapper positionRemapper,
+      MethodPositionRemapper positionRemapper,
       boolean hasOverloads,
       boolean canUseDexPc,
-      int pcEncodingCutoff);
+      int pcEncodingCutoff,
+      Timing timing);
 
   void updateDebugInfoInCodeObjects();
 
@@ -51,13 +53,15 @@
     @Override
     public List<MappedPosition> getMappedPositions(
         ProgramMethod method,
-        ClassPositionRemapper positionRemapper,
+        MethodPositionRemapper positionRemapper,
         boolean hasOverloads,
         boolean canUseDexPc,
-        int pcEncodingCutoff) {
+        int pcEncodingCutoff,
+        Timing timing) {
       return canUseDexPc
-          ? pcMapper.optimizeDexCodePositionsForPc(method, positionRemapper, pcEncodingCutoff)
-          : noPcMapper.optimizeDexCodePositions(method, positionRemapper);
+          ? pcMapper.optimizeDexCodePositionsForPc(
+              method, positionRemapper, pcEncodingCutoff, timing)
+          : noPcMapper.optimizeDexCodePositions(method, positionRemapper, timing);
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/utils/positions/PositionUtils.java b/src/main/java/com/android/tools/r8/utils/positions/PositionUtils.java
index 9852eeb..3ee5a32 100644
--- a/src/main/java/com/android/tools/r8/utils/positions/PositionUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/positions/PositionUtils.java
@@ -20,7 +20,7 @@
 public class PositionUtils {
 
   public static Position remapAndAdd(
-      Position position, ClassPositionRemapper remapper, List<MappedPosition> mappedPositions) {
+      Position position, MethodPositionRemapper remapper, List<MappedPosition> mappedPositions) {
     Pair<Position, Position> remappedPosition = remapper.createRemappedPosition(position);
     Position oldPosition = remappedPosition.getFirst();
     Position newPosition = remappedPosition.getSecond();
diff --git a/src/test/java/com/android/tools/r8/benchmarks/appdumps/AppDumpBenchmarkBuilder.java b/src/test/java/com/android/tools/r8/benchmarks/appdumps/AppDumpBenchmarkBuilder.java
index dc7a609..5a1910e 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/appdumps/AppDumpBenchmarkBuilder.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/appdumps/AppDumpBenchmarkBuilder.java
@@ -329,7 +329,7 @@
       ThrowableConsumer<? super R8FullTestBuilder> configuration) {
     return environment ->
         BenchmarkBase.runner(environment)
-            .setWarmupIterations(builder.warmupIterations)
+            .setWarmupIterations(0)
             .run(
                 results -> {
                   CompilerDump dump = builder.getExtractedDump(environment);