Reapply "Extend single caller inliner to all invoke types"

This reverts commit e81aa3800fa5ddcf963ee3661982eb9b37ead80f.

Change-Id: I0a5ebdf8d3091541aa6572c4c62d3b8c288e8bea
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
index aa82269..1f95169 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
@@ -66,6 +66,7 @@
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
@@ -128,7 +129,7 @@
     this.reprocessingCriteriaCollection = reprocessingCriteriaCollection;
   }
 
-  public synchronized void addMonomorphicVirtualMethods(Set<DexMethod> extension) {
+  public synchronized void addMonomorphicVirtualMethods(Collection<DexMethod> extension) {
     monomorphicVirtualMethods.addAll(extension);
   }
 
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysis.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysis.java
index dd7eac12..3a9e9c6 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysis.java
@@ -4,7 +4,6 @@
 
 package com.android.tools.r8.optimize.argumentpropagation.codescanner;
 
-import static com.android.tools.r8.utils.MapUtils.ignoreKey;
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexMethod;
@@ -12,16 +11,11 @@
 import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagatorCodeScanner;
-import com.android.tools.r8.optimize.argumentpropagation.utils.DepthFirstTopDownClassHierarchyTraversal;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.collections.DexMethodSignatureMap;
-import com.android.tools.r8.utils.collections.ProgramMethodSet;
-import com.google.common.collect.Sets;
+import com.google.common.collect.Iterables;
+import java.util.ArrayList;
 import java.util.Collection;
-import java.util.IdentityHashMap;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
 import java.util.function.Consumer;
 
 /**
@@ -31,83 +25,7 @@
  * <p>The analysis can be used to easily mark effectively final classes and methods as final, and
  * therefore does this as a side effect.
  */
-public class VirtualRootMethodsAnalysis extends DepthFirstTopDownClassHierarchyTraversal {
-
-  static class VirtualRootMethod {
-
-    private final VirtualRootMethod parent;
-    private final ProgramMethod root;
-    private final ProgramMethodSet overrides = ProgramMethodSet.create();
-
-    VirtualRootMethod(ProgramMethod root) {
-      this(root, null);
-    }
-
-    VirtualRootMethod(ProgramMethod root, VirtualRootMethod parent) {
-      assert root != null;
-      this.parent = parent;
-      this.root = root;
-    }
-
-    void addOverride(ProgramMethod override) {
-      assert override.getDefinition() != root.getDefinition();
-      assert override.getMethodSignature().equals(root.getMethodSignature());
-      overrides.add(override);
-      if (hasParent()) {
-        getParent().addOverride(override);
-      }
-    }
-
-    boolean hasParent() {
-      return parent != null;
-    }
-
-    VirtualRootMethod getParent() {
-      return parent;
-    }
-
-    ProgramMethod getRoot() {
-      return root;
-    }
-
-    ProgramMethod getSingleNonAbstractMethod() {
-      ProgramMethod singleNonAbstractMethod = root.getAccessFlags().isAbstract() ? null : root;
-      for (ProgramMethod override : overrides) {
-        if (!override.getAccessFlags().isAbstract()) {
-          if (singleNonAbstractMethod != null) {
-            // Not a single non-abstract method.
-            return null;
-          }
-          singleNonAbstractMethod = override;
-        }
-      }
-      assert singleNonAbstractMethod == null
-          || !singleNonAbstractMethod.getAccessFlags().isAbstract();
-      return singleNonAbstractMethod;
-    }
-
-    void forEach(Consumer<ProgramMethod> consumer) {
-      consumer.accept(root);
-      overrides.forEach(consumer);
-    }
-
-    boolean hasOverrides() {
-      return !overrides.isEmpty();
-    }
-
-    boolean isInterfaceMethodWithSiblings() {
-      // TODO(b/190154391): Conservatively returns true for all interface methods, but should only
-      //  return true for those with siblings.
-      return root.getHolder().isInterface();
-    }
-  }
-
-  private final Map<DexProgramClass, DexMethodSignatureMap<VirtualRootMethod>>
-      virtualRootMethodsPerClass = new IdentityHashMap<>();
-
-  private final Set<DexMethod> monomorphicVirtualMethods = Sets.newIdentityHashSet();
-
-  private final Map<DexMethod, DexMethod> virtualRootMethods = new IdentityHashMap<>();
+public class VirtualRootMethodsAnalysis extends VirtualRootMethodsAnalysisBase {
 
   public VirtualRootMethodsAnalysis(
       AppView<AppInfoWithLiveness> appView, ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
@@ -121,11 +39,23 @@
     run(stronglyConnectedComponent);
 
     // Commit the result to the code scanner.
-    codeScanner.addMonomorphicVirtualMethods(monomorphicVirtualMethods);
+    List<DexMethod> monomorphicVirtualMethodReferences =
+        new ArrayList<>(
+            monomorphicVirtualRootMethods.size() + monomorphicVirtualNonRootMethods.size());
+    for (ProgramMethod method :
+        Iterables.concat(monomorphicVirtualRootMethods, monomorphicVirtualNonRootMethods)) {
+      monomorphicVirtualMethodReferences.add(method.getReference());
+    }
+    codeScanner.addMonomorphicVirtualMethods(monomorphicVirtualMethodReferences);
     codeScanner.addVirtualRootMethods(virtualRootMethods);
   }
 
   @Override
+  protected void acceptVirtualMethod(ProgramMethod method, VirtualRootMethod virtualRootMethod) {
+    promoteToFinalIfPossible(method, virtualRootMethod);
+  }
+
+  @Override
   public void forEachSubClass(DexProgramClass clazz, Consumer<DexProgramClass> consumer) {
     List<DexProgramClass> subclasses = immediateSubtypingInfo.getSubclasses(clazz);
     if (subclasses.isEmpty()) {
@@ -135,78 +65,6 @@
     }
   }
 
-  @Override
-  public void visit(DexProgramClass clazz) {
-    DexMethodSignatureMap<VirtualRootMethod> state = computeVirtualRootMethodsState(clazz);
-    virtualRootMethodsPerClass.put(clazz, state);
-  }
-
-  private DexMethodSignatureMap<VirtualRootMethod> computeVirtualRootMethodsState(
-      DexProgramClass clazz) {
-    DexMethodSignatureMap<VirtualRootMethod> virtualRootMethodsForClass =
-        DexMethodSignatureMap.create();
-    immediateSubtypingInfo.forEachImmediateProgramSuperClass(
-        clazz,
-        superclass -> {
-          DexMethodSignatureMap<VirtualRootMethod> virtualRootMethodsForSuperclass =
-              virtualRootMethodsPerClass.get(superclass);
-          virtualRootMethodsForSuperclass.forEach(
-              (signature, info) ->
-                  virtualRootMethodsForClass.computeIfAbsent(
-                      signature, ignoreKey(() -> new VirtualRootMethod(info.getRoot(), info))));
-        });
-    clazz.forEachProgramVirtualMethod(
-        method -> {
-          if (virtualRootMethodsForClass.containsKey(method)) {
-            virtualRootMethodsForClass.get(method).getParent().addOverride(method);
-          } else {
-            virtualRootMethodsForClass.put(method, new VirtualRootMethod(method));
-          }
-        });
-    return virtualRootMethodsForClass;
-  }
-
-  @Override
-  public void prune(DexProgramClass clazz) {
-    // Record the overrides for each virtual method that is rooted at this class.
-    DexMethodSignatureMap<VirtualRootMethod> virtualRootMethodsForClass =
-        virtualRootMethodsPerClass.remove(clazz);
-    clazz.forEachProgramVirtualMethod(
-        rootCandidate -> {
-          VirtualRootMethod virtualRootMethod =
-              virtualRootMethodsForClass.remove(rootCandidate.getMethodSignature());
-          promoteToFinalIfPossible(rootCandidate, virtualRootMethod);
-          if (!rootCandidate.isStructurallyEqualTo(virtualRootMethod.getRoot())) {
-            return;
-          }
-          boolean isMonomorphicVirtualMethod =
-              !clazz.isInterface() && !virtualRootMethod.hasOverrides();
-          if (isMonomorphicVirtualMethod) {
-            monomorphicVirtualMethods.add(rootCandidate.getReference());
-          } else {
-            ProgramMethod singleNonAbstractMethod = virtualRootMethod.getSingleNonAbstractMethod();
-            if (singleNonAbstractMethod != null
-                && !virtualRootMethod.isInterfaceMethodWithSiblings()) {
-              virtualRootMethod.forEach(
-                  method -> {
-                    // Interface methods can have siblings and can therefore not be mapped to their
-                    // unique non-abstract implementation, unless the interface method does not have
-                    // any siblings.
-                    virtualRootMethods.put(
-                        method.getReference(), singleNonAbstractMethod.getReference());
-                  });
-              if (!singleNonAbstractMethod.getHolder().isInterface()) {
-                monomorphicVirtualMethods.add(singleNonAbstractMethod.getReference());
-              }
-            } else {
-              virtualRootMethod.forEach(
-                  method ->
-                      virtualRootMethods.put(method.getReference(), rootCandidate.getReference()));
-            }
-          }
-        });
-  }
-
   private void promoteToFinalIfPossible(DexProgramClass clazz) {
     if (!appView.testing().disableMarkingClassesFinal
         && !clazz.isAbstract()
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysisBase.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysisBase.java
new file mode 100644
index 0000000..b5f1374
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysisBase.java
@@ -0,0 +1,184 @@
+// Copyright (c) 2024, 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.optimize.argumentpropagation.codescanner;
+
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.optimize.argumentpropagation.utils.DepthFirstTopDownClassHierarchyTraversal;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.collections.DexMethodSignatureMap;
+import com.android.tools.r8.utils.collections.ProgramMethodSet;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+
+/**
+ * Computes the set of virtual methods for which we can use a monomorphic method state as well as
+ * the mapping from virtual methods to their representative root methods.
+ */
+public class VirtualRootMethodsAnalysisBase extends DepthFirstTopDownClassHierarchyTraversal {
+
+  protected static class VirtualRootMethod {
+
+    private final VirtualRootMethod parent;
+    private final ProgramMethod root;
+    private final ProgramMethodSet overrides = ProgramMethodSet.create();
+
+    VirtualRootMethod(ProgramMethod root) {
+      this(root, null);
+    }
+
+    VirtualRootMethod(ProgramMethod root, VirtualRootMethod parent) {
+      assert root != null;
+      this.parent = parent;
+      this.root = root;
+    }
+
+    void addOverride(ProgramMethod override) {
+      assert override.getDefinition() != root.getDefinition();
+      assert override.getMethodSignature().equals(root.getMethodSignature());
+      overrides.add(override);
+      if (hasParent()) {
+        getParent().addOverride(override);
+      }
+    }
+
+    boolean hasParent() {
+      return parent != null;
+    }
+
+    VirtualRootMethod getParent() {
+      return parent;
+    }
+
+    ProgramMethod getRoot() {
+      return root;
+    }
+
+    ProgramMethod getSingleNonAbstractMethod() {
+      ProgramMethod singleNonAbstractMethod = root.getAccessFlags().isAbstract() ? null : root;
+      for (ProgramMethod override : overrides) {
+        if (!override.getAccessFlags().isAbstract()) {
+          if (singleNonAbstractMethod != null) {
+            // Not a single non-abstract method.
+            return null;
+          }
+          singleNonAbstractMethod = override;
+        }
+      }
+      assert singleNonAbstractMethod == null
+          || !singleNonAbstractMethod.getAccessFlags().isAbstract();
+      return singleNonAbstractMethod;
+    }
+
+    void forEach(Consumer<ProgramMethod> consumer) {
+      consumer.accept(root);
+      overrides.forEach(consumer);
+    }
+
+    boolean hasOverrides() {
+      return !overrides.isEmpty();
+    }
+
+    boolean isInterfaceMethodWithSiblings() {
+      // TODO(b/190154391): Conservatively returns true for all interface methods, but should only
+      //  return true for those with siblings.
+      return root.getHolder().isInterface();
+    }
+  }
+
+  private final Map<DexProgramClass, DexMethodSignatureMap<VirtualRootMethod>>
+      virtualRootMethodsPerClass = new IdentityHashMap<>();
+
+  protected final ProgramMethodSet monomorphicVirtualRootMethods = ProgramMethodSet.create();
+  protected final ProgramMethodSet monomorphicVirtualNonRootMethods = ProgramMethodSet.create();
+
+  protected final Map<DexMethod, DexMethod> virtualRootMethods = new IdentityHashMap<>();
+
+  protected VirtualRootMethodsAnalysisBase(
+      AppView<AppInfoWithLiveness> appView, ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
+    super(appView, immediateSubtypingInfo);
+  }
+
+  @Override
+  public void visit(DexProgramClass clazz) {
+    DexMethodSignatureMap<VirtualRootMethod> state = computeVirtualRootMethodsState(clazz);
+    virtualRootMethodsPerClass.put(clazz, state);
+  }
+
+  private DexMethodSignatureMap<VirtualRootMethod> computeVirtualRootMethodsState(
+      DexProgramClass clazz) {
+    DexMethodSignatureMap<VirtualRootMethod> virtualRootMethodsForClass =
+        DexMethodSignatureMap.create();
+    immediateSubtypingInfo.forEachImmediateProgramSuperClass(
+        clazz,
+        superclass -> {
+          DexMethodSignatureMap<VirtualRootMethod> virtualRootMethodsForSuperclass =
+              virtualRootMethodsPerClass.get(superclass);
+          virtualRootMethodsForSuperclass.forEach(
+              (signature, info) ->
+                  virtualRootMethodsForClass.computeIfAbsent(
+                      signature, ignoreKey(() -> new VirtualRootMethod(info.getRoot(), info))));
+        });
+    clazz.forEachProgramVirtualMethod(
+        method -> {
+          if (virtualRootMethodsForClass.containsKey(method)) {
+            virtualRootMethodsForClass.get(method).getParent().addOverride(method);
+          } else {
+            virtualRootMethodsForClass.put(method, new VirtualRootMethod(method));
+          }
+        });
+    return virtualRootMethodsForClass;
+  }
+
+  @Override
+  public void prune(DexProgramClass clazz) {
+    // Record the overrides for each virtual method that is rooted at this class.
+    DexMethodSignatureMap<VirtualRootMethod> virtualRootMethodsForClass =
+        virtualRootMethodsPerClass.remove(clazz);
+    clazz.forEachProgramVirtualMethod(
+        rootCandidate -> {
+          VirtualRootMethod virtualRootMethod =
+              virtualRootMethodsForClass.remove(rootCandidate.getMethodSignature());
+          acceptVirtualMethod(rootCandidate, virtualRootMethod);
+          if (!rootCandidate.isStructurallyEqualTo(virtualRootMethod.getRoot())) {
+            return;
+          }
+          boolean isMonomorphicVirtualMethod =
+              !clazz.isInterface() && !virtualRootMethod.hasOverrides();
+          if (isMonomorphicVirtualMethod) {
+            monomorphicVirtualRootMethods.add(rootCandidate);
+          } else {
+            ProgramMethod singleNonAbstractMethod = virtualRootMethod.getSingleNonAbstractMethod();
+            if (singleNonAbstractMethod != null
+                && !virtualRootMethod.isInterfaceMethodWithSiblings()) {
+              virtualRootMethod.forEach(
+                  method -> {
+                    // Interface methods can have siblings and can therefore not be mapped to their
+                    // unique non-abstract implementation, unless the interface method does not have
+                    // any siblings.
+                    virtualRootMethods.put(
+                        method.getReference(), singleNonAbstractMethod.getReference());
+                  });
+              if (!singleNonAbstractMethod.getHolder().isInterface()) {
+                monomorphicVirtualNonRootMethods.add(singleNonAbstractMethod);
+              }
+            } else {
+              virtualRootMethod.forEach(
+                  method ->
+                      virtualRootMethods.put(method.getReference(), rootCandidate.getReference()));
+            }
+          }
+        });
+  }
+
+  protected void acceptVirtualMethod(ProgramMethod method, VirtualRootMethod virtualRootMethod) {
+    // Intentionally empty.
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/singlecaller/MonomorphicVirtualMethodsAnalysis.java b/src/main/java/com/android/tools/r8/optimize/singlecaller/MonomorphicVirtualMethodsAnalysis.java
new file mode 100644
index 0000000..8d589a8
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/singlecaller/MonomorphicVirtualMethodsAnalysis.java
@@ -0,0 +1,54 @@
+// Copyright (c) 2024, 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.optimize.singlecaller;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.VirtualRootMethodsAnalysisBase;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.collections.ProgramMethodSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+public class MonomorphicVirtualMethodsAnalysis extends VirtualRootMethodsAnalysisBase {
+
+  public MonomorphicVirtualMethodsAnalysis(
+      AppView<AppInfoWithLiveness> appView, ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
+    super(appView, immediateSubtypingInfo);
+  }
+
+  public static ProgramMethodSet computeMonomorphicVirtualRootMethods(
+      AppView<AppInfoWithLiveness> appView,
+      ImmediateProgramSubtypingInfo immediateSubtypingInfo,
+      List<Set<DexProgramClass>> stronglyConnectedComponents,
+      ExecutorService executorService)
+      throws ExecutionException {
+    ProgramMethodSet monomorphicVirtualMethods = ProgramMethodSet.createConcurrent();
+    ThreadUtils.processItems(
+        stronglyConnectedComponents,
+        stronglyConnectedComponent -> {
+          ProgramMethodSet monomorphicVirtualMethodsInComponent =
+              computeMonomorphicVirtualRootMethodsInComponent(
+                  appView, immediateSubtypingInfo, stronglyConnectedComponent);
+          monomorphicVirtualMethods.addAll(monomorphicVirtualMethodsInComponent);
+        },
+        appView.options().getThreadingModule(),
+        executorService);
+    return monomorphicVirtualMethods;
+  }
+
+  private static ProgramMethodSet computeMonomorphicVirtualRootMethodsInComponent(
+      AppView<AppInfoWithLiveness> appView,
+      ImmediateProgramSubtypingInfo immediateSubtypingInfo,
+      Set<DexProgramClass> stronglyConnectedComponent) {
+    MonomorphicVirtualMethodsAnalysis analysis =
+        new MonomorphicVirtualMethodsAnalysis(appView, immediateSubtypingInfo);
+    analysis.run(stronglyConnectedComponent);
+    return analysis.monomorphicVirtualRootMethods;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/singlecaller/SingleCallerInliner.java b/src/main/java/com/android/tools/r8/optimize/singlecaller/SingleCallerInliner.java
index 780ae8a..02b5231 100644
--- a/src/main/java/com/android/tools/r8/optimize/singlecaller/SingleCallerInliner.java
+++ b/src/main/java/com/android/tools/r8/optimize/singlecaller/SingleCallerInliner.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.EnclosingMethodAttribute;
+import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
 import com.android.tools.r8.graph.InnerClassAttribute;
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
@@ -33,13 +34,15 @@
 import com.android.tools.r8.ir.optimize.inliner.NopWhyAreYouNotInliningReporter;
 import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter;
 import com.android.tools.r8.lightir.LirCode;
+import com.android.tools.r8.optimize.argumentpropagation.utils.ProgramClassesBidirectedGraph;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.collections.ProgramMethodMap;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import java.util.Deque;
+import java.util.List;
+import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 
@@ -61,16 +64,15 @@
   }
 
   private boolean shouldRun() {
-    InternalOptions options = appView.options();
-    return !options.debug
-        && !options.intermediate
-        && options.isOptimizing()
-        && options.isShrinking();
+    return appView.options().getSingleCallerInlinerOptions().isEnabled();
   }
 
   public void run(ExecutorService executorService) throws ExecutionException {
+    ProgramMethodSet monomorphicVirtualMethods =
+        computeMonomorphicVirtualRootMethods(executorService);
     ProgramMethodMap<ProgramMethod> singleCallerMethods =
-        new SingleCallerScanner(appView).getSingleCallerMethods(executorService);
+        new SingleCallerScanner(appView, monomorphicVirtualMethods)
+            .getSingleCallerMethods(executorService);
     if (singleCallerMethods.isEmpty()) {
       return;
     }
@@ -81,6 +83,21 @@
     pruneItems(singleCallerMethods, executorService);
   }
 
+  // We only allow single caller inlining of "direct dispatch virtual methods". We currently only
+  // deal with (rooted) virtual methods that do not override abstract/interface methods. In order to
+  // also deal with virtual methods that override abstract/interface methods we would need to record
+  // calls to the abstract/interface methods as calls to the non-abstract virtual method.
+  private ProgramMethodSet computeMonomorphicVirtualRootMethods(ExecutorService executorService)
+      throws ExecutionException {
+    ImmediateProgramSubtypingInfo immediateSubtypingInfo =
+        ImmediateProgramSubtypingInfo.create(appView);
+    List<Set<DexProgramClass>> stronglyConnectedComponents =
+        new ProgramClassesBidirectedGraph(appView, immediateSubtypingInfo)
+            .computeStronglyConnectedComponents();
+    return MonomorphicVirtualMethodsAnalysis.computeMonomorphicVirtualRootMethods(
+        appView, immediateSubtypingInfo, stronglyConnectedComponents, executorService);
+  }
+
   private void processCallees(
       Inliner inliner,
       ProgramMethodMap<ProgramMethod> singleCallerMethods,
diff --git a/src/main/java/com/android/tools/r8/optimize/singlecaller/SingleCallerInlinerOptions.java b/src/main/java/com/android/tools/r8/optimize/singlecaller/SingleCallerInlinerOptions.java
new file mode 100644
index 0000000..fa8d1f0
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/singlecaller/SingleCallerInlinerOptions.java
@@ -0,0 +1,29 @@
+// Copyright (c) 2024, 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.optimize.singlecaller;
+
+import com.android.tools.r8.utils.InternalOptions;
+
+public class SingleCallerInlinerOptions {
+
+  private final InternalOptions options;
+
+  private boolean enable = true;
+
+  public SingleCallerInlinerOptions(InternalOptions options) {
+    this.options = options;
+  }
+
+  public boolean isEnabled() {
+    return enable
+        && !options.debug
+        && !options.intermediate
+        && options.isOptimizing()
+        && options.isShrinking();
+  }
+
+  public void setEnable(boolean enable) {
+    this.enable = enable;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/singlecaller/SingleCallerScanner.java b/src/main/java/com/android/tools/r8/optimize/singlecaller/SingleCallerScanner.java
index 6ac2b54..fa0bdfd 100644
--- a/src/main/java/com/android/tools/r8/optimize/singlecaller/SingleCallerScanner.java
+++ b/src/main/java/com/android/tools/r8/optimize/singlecaller/SingleCallerScanner.java
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.optimize.singlecaller;
 
-import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 import static com.android.tools.r8.utils.MapUtils.ignoreKey;
 
 import com.android.tools.r8.graph.AppView;
@@ -17,7 +16,7 @@
 import com.android.tools.r8.lightir.LirCode;
 import com.android.tools.r8.lightir.LirConstant;
 import com.android.tools.r8.lightir.LirInstructionView;
-import com.android.tools.r8.lightir.LirOpcodes;
+import com.android.tools.r8.lightir.LirOpcodeUtils;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ObjectUtils;
@@ -32,9 +31,12 @@
   private static final ProgramMethod MULTIPLE_CALLERS = ProgramMethod.createSentinel();
 
   private final AppView<AppInfoWithLiveness> appView;
+  private final ProgramMethodSet monomorphicVirtualMethods;
 
-  SingleCallerScanner(AppView<AppInfoWithLiveness> appView) {
+  SingleCallerScanner(
+      AppView<AppInfoWithLiveness> appView, ProgramMethodSet monomorphicVirtualMethods) {
     this.appView = appView;
+    this.monomorphicVirtualMethods = monomorphicVirtualMethods;
   }
 
   public ProgramMethodMap<ProgramMethod> getSingleCallerMethods(ExecutorService executorService)
@@ -139,28 +141,13 @@
     if (referencedMethod.getHolderType().isArrayType()) {
       return;
     }
-    if (referencedMethod.isInstanceInitializer(appView.dexItemFactory())) {
-      ProgramMethod referencedProgramMethod =
-          appView
-              .appInfo()
-              .unsafeResolveMethodDueToDexFormat(referencedMethod)
-              .getResolvedProgramMethod();
-      if (referencedProgramMethod != null) {
-        recordCallEdge(method, referencedProgramMethod, threadLocalSingleCallerMethods);
-      }
-    } else {
-      DexProgramClass referencedProgramMethodHolder =
-          asProgramClassOrNull(
-              appView
-                  .appInfo()
-                  .definitionForWithoutExistenceAssert(referencedMethod.getHolderType()));
-      ProgramMethod referencedProgramMethod =
-          referencedMethod.lookupOnProgramClass(referencedProgramMethodHolder);
-      if (referencedProgramMethod != null
-          && referencedProgramMethod.getAccessFlags().isPrivate()
-          && !referencedProgramMethod.getAccessFlags().isStatic()) {
-        recordCallEdge(method, referencedProgramMethod, threadLocalSingleCallerMethods);
-      }
+    ProgramMethod resolvedMethod =
+        appView
+            .appInfo()
+            .unsafeResolveMethodDueToDexFormat(referencedMethod)
+            .getResolvedProgramMethod();
+    if (resolvedMethod != null) {
+      recordCallEdge(method, resolvedMethod, threadLocalSingleCallerMethods);
     }
   }
 
@@ -187,10 +174,7 @@
           ProgramMethodMap<Integer> counters = ProgramMethodMap.create();
           for (LirInstructionView view : code) {
             int opcode = view.getOpcode();
-            if (opcode != LirOpcodes.INVOKEDIRECT
-                && opcode != LirOpcodes.INVOKEDIRECT_ITF
-                // JDK 17 generates invokevirtual to private methods.
-                && opcode != LirOpcodes.INVOKEVIRTUAL) {
+            if (!LirOpcodeUtils.isInvokeMethod(opcode)) {
               continue;
             }
             DexMethod invokedMethod =
@@ -198,11 +182,17 @@
             ProgramMethod resolvedMethod =
                 appView
                     .appInfo()
-                    .resolveMethod(invokedMethod, opcode == LirOpcodes.INVOKEDIRECT_ITF)
+                    .resolveMethod(
+                        invokedMethod, LirOpcodeUtils.getInterfaceBitFromInvokeOpcode(opcode))
                     .getResolvedProgramMethod();
-            if (resolvedMethod != null && callees.contains(resolvedMethod)) {
-              counters.put(resolvedMethod, counters.getOrDefault(resolvedMethod, 0) + 1);
+            if (resolvedMethod == null || !callees.contains(resolvedMethod)) {
+              continue;
             }
+            if (resolvedMethod.getAccessFlags().belongsToVirtualPool()
+                && !monomorphicVirtualMethods.contains(resolvedMethod)) {
+              continue;
+            }
+            counters.put(resolvedMethod, counters.getOrDefault(resolvedMethod, 0) + 1);
           }
           callees.forEach(
               (callee) -> {
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index a021521..629d003 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -519,6 +519,7 @@
       appView.getResourceShrinkerState().setEnqueuerCallback(this::recordReferenceFromResources);
     }
     if (mode.isTreeShaking()) {
+      InitializedClassesInInstanceMethodsAnalysis.register(appView, this);
       GetArrayOfMissingTypeVerifyErrorWorkaround.register(appView, this);
       InitializedClassesInInstanceMethodsAnalysis.register(appView, this);
       InvokeVirtualToInterfaceVerifyErrorWorkaround.register(appView, this);
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergingWithMissingTypeArgsSubstitutionTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergingWithMissingTypeArgsSubstitutionTest.java
index 966addb..cac41ff 100644
--- a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergingWithMissingTypeArgsSubstitutionTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergingWithMissingTypeArgsSubstitutionTest.java
@@ -48,10 +48,10 @@
         .addKeepClassRules(A.class)
         .addKeepAttributeSignature()
         .addOptionsModification(
-            options ->
-                options
-                    .getVerticalClassMergerOptions()
-                    .setEnableBridgeAnalysis(enableBridgeAnalysis))
+            options -> {
+              options.getSingleCallerInlinerOptions().setEnable(false);
+              options.getVerticalClassMergerOptions().setEnableBridgeAnalysis(enableBridgeAnalysis);
+            })
         .addVerticallyMergedClassesInspector(
             inspector -> inspector.assertMergedIntoSubtype(B.class).assertNoOtherClassesMerged())
         .enableInliningAnnotations()
diff --git a/src/test/java/com/android/tools/r8/internal/proto/Proto2ShrinkingTest.java b/src/test/java/com/android/tools/r8/internal/proto/Proto2ShrinkingTest.java
index 55a961e..64e4c42 100644
--- a/src/test/java/com/android/tools/r8/internal/proto/Proto2ShrinkingTest.java
+++ b/src/test/java/com/android/tools/r8/internal/proto/Proto2ShrinkingTest.java
@@ -4,8 +4,8 @@
 
 package com.android.tools.r8.internal.proto;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsentIf;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
-import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
@@ -249,9 +249,8 @@
 
     // Verify that the registry methods are still present in the output.
     //
-    // We expect findLiteExtensionByNumber2() to be inlined into findLiteExtensionByNumber1(). The
-    // method findLiteExtensionByNumber1() has two call sites from findLiteExtensionByNumber(),
-    // which prevents it from being single-caller inlined.
+    // We expect findLiteExtensionByNumber2() to be inlined into findLiteExtensionByNumber1() and
+    // findLiteExtensionByNumber1() to be inlined into findLiteExtensionByNumber().
     {
       ClassSubject generatedExtensionRegistryLoader = outputInspector.clazz(extensionRegistryName);
       assertThat(generatedExtensionRegistryLoader, isPresent());
@@ -262,11 +261,11 @@
       assertThat(
           generatedExtensionRegistryLoader.uniqueMethodWithOriginalName(
               "findLiteExtensionByNumber1"),
-          isPresent());
+          isAbsentIf(enableMinification));
       assertThat(
           generatedExtensionRegistryLoader.uniqueMethodWithOriginalName(
               "findLiteExtensionByNumber2"),
-          notIf(isPresent(), enableMinification));
+          isAbsentIf(enableMinification));
     }
 
     // Verify that unused extensions have been removed with -allowaccessmodification.
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/BridgeWithCastsInliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/BridgeWithCastsInliningTest.java
index cf1b311..f5e7361 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/BridgeWithCastsInliningTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/BridgeWithCastsInliningTest.java
@@ -10,6 +10,7 @@
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverSingleCallerInline;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.utils.BooleanUtils;
@@ -55,6 +56,7 @@
             })
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
+        .enableNeverSingleCallerInlineAnnotations()
         .setMinApi(parameters)
         .compile()
         .inspect(this::inspect)
@@ -106,6 +108,7 @@
       foo(o, o, o, o, o);
     }
 
+    @NeverSingleCallerInline
     static void foo(Object o1, Object o2, Object o3, Object o4, Object o5) {
       A a1 = (A) o1;
       A a2 = (A) o2;
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/BridgeWithUnboxingInliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/BridgeWithUnboxingInliningTest.java
index 2623bc0..2dea095 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/BridgeWithUnboxingInliningTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/BridgeWithUnboxingInliningTest.java
@@ -10,6 +10,7 @@
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverSingleCallerInline;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.utils.BooleanUtils;
@@ -55,6 +56,7 @@
             })
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
+        .enableNeverSingleCallerInlineAnnotations()
         .setMinApi(parameters)
         .compile()
         .inspect(this::inspect)
@@ -114,6 +116,7 @@
       foo(o1, o2, o3, o4, o5);
     }
 
+    @NeverSingleCallerInline
     static void foo(Integer o1, Integer o2, Integer o3, Integer o4, Integer o5) {
       int i1 = o1;
       int i2 = o2;
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/InliningOfVirtualMethodOnKeptClassTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/InliningOfVirtualMethodOnKeptClassTest.java
index 2b4c4bb..946d711 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/InliningOfVirtualMethodOnKeptClassTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/InliningOfVirtualMethodOnKeptClassTest.java
@@ -4,8 +4,8 @@
 
 package com.android.tools.r8.ir.optimize.inliner;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
-import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.NeverClassInline;
@@ -50,7 +50,7 @@
   private void verifyOutput(CodeInspector inspector) {
     ClassSubject classSubject = inspector.clazz(TestClass.class);
     assertThat(classSubject, isPresent());
-    assertThat(classSubject.uniqueMethodWithOriginalName("foo"), not(isPresent()));
+    assertThat(classSubject.uniqueMethodWithOriginalName("foo"), isAbsent());
     assertThat(classSubject.uniqueMethodWithOriginalName("bar"), isPresent());
     assertThat(classSubject.uniqueMethodWithOriginalName("baz"), isPresent());
   }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java
index 95cf23e..59e609d 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java
@@ -50,9 +50,11 @@
         .addInnerClasses(SingleTargetAfterInliningTest.class)
         .addKeepMainRule(TestClass.class)
         .addOptionsModification(
-            options ->
-                options.inlinerOptions().applyInliningToInlineePredicateForTesting =
-                    (appView, inlinee, inliningDepth) -> inliningDepth <= maxInliningDepth)
+            options -> {
+              options.inlinerOptions().applyInliningToInlineePredicateForTesting =
+                  (appView, inlinee, inliningDepth) -> inliningDepth <= maxInliningDepth;
+              options.getSingleCallerInlinerOptions().setEnable(false);
+            })
         .enableAlwaysInliningAnnotations()
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepEmptyClassTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepEmptyClassTest.java
index a80be3c..3e9b7ee 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepEmptyClassTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepEmptyClassTest.java
@@ -64,10 +64,14 @@
       assertTrue(parameters.isExtractRules());
       // PG and R8 with keep rules will keep the residual class.
       assertThat(classA, isPresentAndRenamed());
-      // R8 using keep rules will soft-pin the precondition method too.
+      // R8 using keep rules will soft-pin the precondition method too. The soft pinning is only
+      // applied in the first round of tree shaking, however, so R8 can still single caller inline
+      // the method after the final round of tree shaking.
       assertThat(
           classA.uniqueMethodWithOriginalName("foo"),
-          parameters.isPG() ? isAbsent() : isPresentAndRenamed());
+          parameters.isPG() || (parameters.isCurrentR8() && parameters.isExtractRules())
+              ? isAbsent()
+              : isPresentAndRenamed());
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/keepanno/compatissues/BackReferenceIssuesTest.java b/src/test/java/com/android/tools/r8/keepanno/compatissues/BackReferenceIssuesTest.java
index 63692c5..85464e5 100644
--- a/src/test/java/com/android/tools/r8/keepanno/compatissues/BackReferenceIssuesTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/compatissues/BackReferenceIssuesTest.java
@@ -8,7 +8,6 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
-import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assume.assumeTrue;
 
@@ -82,7 +81,7 @@
                 assertThat(
                     inspector.clazz(A.class).uniqueMethodWithOriginalName("foo"),
                     // The rule is not valid and does not keep the method in R8.
-                    shrinker.isPG() ? isPresentAndNotRenamed() : isPresentAndRenamed()));
+                    shrinker.isPG() ? isPresentAndNotRenamed() : isAbsent()));
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/VerticalClassMergingRetraceTest.java b/src/test/java/com/android/tools/r8/naming/retrace/VerticalClassMergingRetraceTest.java
index cb94b5d..34b7502 100644
--- a/src/test/java/com/android/tools/r8/naming/retrace/VerticalClassMergingRetraceTest.java
+++ b/src/test/java/com/android/tools/r8/naming/retrace/VerticalClassMergingRetraceTest.java
@@ -73,7 +73,8 @@
   private int expectedActualStackTraceHeight() {
     // In RELEASE mode a synthetic bridge is added by the vertical class merger if the method is
     // targeted by the invoke-super (which is modeled by setting enableBridgeAnalysis to false).
-    return mode == CompilationMode.DEBUG || enableBridgeAnalysis ? 2 : 3;
+    // Due to single caller inlining we still end up with a stack trace height of 2.
+    return 2;
   }
 
   private boolean filterSynthesizedMethodWhenLineNumberAvailable(
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/IfMemberRuleWithUnusedParameterTest.java b/src/test/java/com/android/tools/r8/shaking/ifrule/IfMemberRuleWithUnusedParameterTest.java
index 2d871fc..4490fff 100644
--- a/src/test/java/com/android/tools/r8/shaking/ifrule/IfMemberRuleWithUnusedParameterTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/IfMemberRuleWithUnusedParameterTest.java
@@ -7,6 +7,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -38,6 +39,7 @@
             "  public static void print(" + Object.class.getTypeName() + ");",
             "}",
             "-keep class " + KeptByIf.class.getTypeName())
+        .enableInliningAnnotations()
         .setMinApi(parameters)
         .compile()
         .inspect(
@@ -61,6 +63,7 @@
       print(args);
     }
 
+    @NeverInline
     public static void print(Object unused) {
       System.out.println("Hello, world!");
     }
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/inlining/IfRuleWithInlining.java b/src/test/java/com/android/tools/r8/shaking/ifrule/inlining/IfRuleWithInlining.java
index 07eb940..4f0a320 100644
--- a/src/test/java/com/android/tools/r8/shaking/ifrule/inlining/IfRuleWithInlining.java
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/inlining/IfRuleWithInlining.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.shaking.ifrule.inlining;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsentIf;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
@@ -70,8 +71,10 @@
     CodeInspector inspector = new CodeInspector(app);
     ClassSubject clazzA = inspector.clazz(A.class);
     assertThat(clazzA, isPresent());
-    // A.a should not be inlined.
-    assertThat(clazzA.method("int", "a", ImmutableList.of()), isPresent());
+    // A.a may be inlined when neverInlineMethod is false.
+    assertThat(
+        clazzA.uniqueMethodWithOriginalName("a"),
+        isAbsentIf(shrinker.isR8() && !neverInlineMethod));
     assertThat(inspector.clazz(D.class), isPresent());
     ProcessResult result;
     if (shrinker == Shrinker.R8) {
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedParameterTypeTest.java b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedParameterTypeTest.java
index eb770fe..67ba97e 100644
--- a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedParameterTypeTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedParameterTypeTest.java
@@ -4,11 +4,11 @@
 
 package com.android.tools.r8.shaking.ifrule.verticalclassmerging;
 
-import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NoAccessModification;
 import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.R8FullTestBuilder;
@@ -58,6 +58,7 @@
     @NoHorizontalClassMerging
     static class SuperTestClass {
 
+      @NeverInline
       public static void method(A obj) {
         System.out.print(obj.getClass().getName());
       }
@@ -92,6 +93,7 @@
                   inspector
                       .applyIf(enableVerticalClassMerging, i -> i.assertMergedIntoSubtype(A.class))
                       .assertNoOtherClassesMerged())
+          .enableInliningAnnotations()
           .enableNoHorizontalClassMergingAnnotations();
     }
 
@@ -118,11 +120,6 @@
       assertThat(testClassSubject, isPresent());
 
       if (enableVerticalClassMerging) {
-        // Verify that SuperTestClass has been merged into TestClass.
-        assertThat(inspector.clazz(SuperTestClass.class), isAbsent());
-        assertEquals(
-            "java.lang.Object", testClassSubject.getDexProgramClass().superType.toSourceString());
-
         // Verify that TestClass.method has been removed.
         List<FoundMethodSubject> methods =
             testClassSubject.allMethods().stream()
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedReturnTypeTest.java b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedReturnTypeTest.java
index 7bb0f91..57f5d05 100644
--- a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedReturnTypeTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedReturnTypeTest.java
@@ -6,14 +6,15 @@
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.core.IsNot.not;
 import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.AssumeMayHaveSideEffects;
+import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NoAccessModification;
 import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.shaking.ifrule.verticalclassmerging.MergedParameterTypeTest.MergedParameterTypeWithCollisionTest.SuperTestClass;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
@@ -67,6 +68,7 @@
     static class SuperTestClass {
 
       @AssumeMayHaveSideEffects
+      @NeverInline
       public static A method() {
         return new B();
       }
@@ -101,6 +103,7 @@
                   inspector
                       .applyIf(enableVerticalClassMerging, i -> i.assertMergedIntoSubtype(A.class))
                       .assertNoOtherClassesMerged())
+          .enableInliningAnnotations()
           .enableNoHorizontalClassMergingAnnotations()
           .enableSideEffectAnnotations();
     }
@@ -128,11 +131,6 @@
       assertThat(testClassSubject, isPresent());
 
       if (enableVerticalClassMerging) {
-        // Verify that SuperTestClass has been merged into TestClass.
-        assertThat(inspector.clazz(SuperTestClass.class), not(isPresent()));
-        assertEquals(
-            "java.lang.Object", testClassSubject.getDexProgramClass().superType.toSourceString());
-
         // Verify that TestClass.method has been removed.
         List<FoundMethodSubject> methods =
             testClassSubject.allMethods().stream()
diff --git a/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptByConditionalOnMethodTest.java b/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptByConditionalOnMethodTest.java
index 6a9341c..235de24 100644
--- a/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptByConditionalOnMethodTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptByConditionalOnMethodTest.java
@@ -7,6 +7,7 @@
 import static com.android.tools.r8.references.Reference.methodFromMethod;
 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;
@@ -24,6 +25,7 @@
 public class KeptByConditionalOnMethodTest extends TestBase {
 
   public static class IfClass {
+    @NeverInline
     public void foo(String name) throws Exception {
       Class<?> clazz = Class.forName(name);
       Object object = clazz.getDeclaredConstructor().newInstance();
@@ -77,6 +79,7 @@
             .addProgramClasses(Main.class, IfClass.class, ThenClass.class)
             .addKeepMainRule(Main.class)
             .addKeepRules(ifRuleContent)
+            .enableInliningAnnotations()
             .setMinApi(parameters)
             .run(parameters.getRuntime(), Main.class, ThenClass.class.getTypeName())
             .assertSuccessWithOutput(EXPECTED)
diff --git a/src/test/java/com/android/tools/r8/startup/StartupSyntheticPlacementTest.java b/src/test/java/com/android/tools/r8/startup/StartupSyntheticPlacementTest.java
index 2f41828..f391aff 100644
--- a/src/test/java/com/android/tools/r8/startup/StartupSyntheticPlacementTest.java
+++ b/src/test/java/com/android/tools/r8/startup/StartupSyntheticPlacementTest.java
@@ -162,6 +162,7 @@
             testBuilder ->
                 configureStartupOptions(
                     testBuilder, instrumentationCompileResult.inspector(), startupList))
+        .noInliningOfSynthetics()
         .setMinApi(parameters)
         .compile()
         .inspectDiagnosticMessages(