Reland "Prune single caller inlined direct methods when wave ends"

This reverts commit f38f03822379a0cb6e5e5d6e38e34cd1dff26d71.

Change-Id: I5954e3ed22435542c7b20237232732b44c4ea6c1
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index 4d3d50a..cfb13ae 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -514,6 +514,10 @@
     return options().testing;
   }
 
+  public boolean hasRootSet() {
+    return rootSet != null;
+  }
+
   public RootSet rootSet() {
     return rootSet;
   }
@@ -711,7 +715,10 @@
       setProguardCompatibilityActions(
           getProguardCompatibilityActions().withoutPrunedItems(prunedItems));
     }
-    if (mainDexRootSet != null) {
+    if (hasRootSet()) {
+      rootSet.pruneItems(prunedItems);
+    }
+    if (hasMainDexRootSet()) {
       setMainDexRootSet(mainDexRootSet.withoutPrunedItems(prunedItems));
     }
   }
diff --git a/src/main/java/com/android/tools/r8/graph/GraphLens.java b/src/main/java/com/android/tools/r8/graph/GraphLens.java
index f0675ba..9236d1f 100644
--- a/src/main/java/com/android/tools/r8/graph/GraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/GraphLens.java
@@ -284,6 +284,10 @@
 
   public abstract DexField getOriginalFieldSignature(DexField field);
 
+  public final DexMember<?, ?> getOriginalMemberSignature(DexMember<?, ?> member) {
+    return member.apply(this::getOriginalFieldSignature, this::getOriginalMethodSignature);
+  }
+
   public final DexMethod getOriginalMethodSignature(DexMethod method) {
     return getOriginalMethodSignature(method, null);
   }
@@ -572,7 +576,7 @@
   }
 
   public <R extends DexReference, T> Map<R, T> rewriteReferenceKeys(
-      Map<R, T> map, Function<List<T>, T> merge) {
+      Map<R, T> map, BiFunction<R, List<T>, T> merge) {
     Map<R, T> result = new IdentityHashMap<>();
     Map<R, List<T>> needsMerge = new IdentityHashMap<>();
     map.forEach(
@@ -593,7 +597,7 @@
         });
     needsMerge.forEach(
         (rewrittenReference, unmergedValues) -> {
-          T mergedValue = merge.apply(unmergedValues);
+          T mergedValue = merge.apply(rewrittenReference, unmergedValues);
           if (mergedValue != null) {
             result.put(rewrittenReference, mergedValue);
           }
diff --git a/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java b/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
index 459c008..aa5e4c8 100644
--- a/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
+++ b/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
@@ -19,8 +19,10 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.IdentityHashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
@@ -144,6 +146,25 @@
     return builder(true, null).rewrittenWithLens(this, definitions, lens).build(definitions);
   }
 
+  public ObjectAllocationInfoCollectionImpl withoutPrunedItems(PrunedItems prunedItems) {
+    if (prunedItems.hasRemovedMethods()) {
+      Iterator<Entry<DexProgramClass, Set<DexEncodedMethod>>> iterator =
+          classesWithAllocationSiteTracking.entrySet().iterator();
+      while (iterator.hasNext()) {
+        Entry<DexProgramClass, Set<DexEncodedMethod>> entry = iterator.next();
+        Set<DexEncodedMethod> allocationSites = entry.getValue();
+        allocationSites.removeIf(
+            allocationSite ->
+                prunedItems.getRemovedMethods().contains(allocationSite.getReference()));
+        if (allocationSites.isEmpty()) {
+          classesWithoutAllocationSiteTracking.add(entry.getKey());
+          iterator.remove();
+        }
+      }
+    }
+    return this;
+  }
+
   public void forEachInstantiatedSubType(
       DexType type,
       Consumer<DexProgramClass> onClass,
diff --git a/src/main/java/com/android/tools/r8/graph/PrunedItems.java b/src/main/java/com/android/tools/r8/graph/PrunedItems.java
index 4489421..4f901fd 100644
--- a/src/main/java/com/android/tools/r8/graph/PrunedItems.java
+++ b/src/main/java/com/android/tools/r8/graph/PrunedItems.java
@@ -55,6 +55,10 @@
     return removedMethods.contains(method) || removedClasses.contains(method.getHolderType());
   }
 
+  public boolean isRemoved(DexReference reference) {
+    return reference.apply(this::isRemoved, this::isRemoved, this::isRemoved);
+  }
+
   public boolean isRemoved(DexType type) {
     return removedClasses.contains(type);
   }
@@ -107,7 +111,7 @@
     private final Set<DexType> noLongerSyntheticItems = Sets.newIdentityHashSet();
     private Set<DexType> removedClasses = Sets.newIdentityHashSet();
     private final Set<DexField> removedFields = Sets.newIdentityHashSet();
-    private final Set<DexMethod> removedMethods = Sets.newIdentityHashSet();
+    private Set<DexMethod> removedMethods = Sets.newIdentityHashSet();
 
     public Builder setPrunedApp(DexApplication prunedApp) {
       this.prunedApp = prunedApp;
@@ -146,6 +150,11 @@
       return this;
     }
 
+    public Builder setRemovedMethods(Set<DexMethod> removedMethods) {
+      this.removedMethods = removedMethods;
+      return this;
+    }
+
     public PrunedItems build() {
       return new PrunedItems(
           prunedApp,
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
index 2b12d6d..03a044d 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
@@ -250,13 +250,7 @@
             MutableBidirectionalManyToOneRepresentativeMap<R, R> newMemberSignatures,
             MutableBidirectionalManyToOneRepresentativeMap<R, R> pendingNewMemberSignatureUpdates) {
       newMemberSignatures.removeAll(pendingNewMemberSignatureUpdates.keySet());
-      pendingNewMemberSignatureUpdates.forEachManyToOneMapping(
-          (keys, value, representative) -> {
-            newMemberSignatures.put(keys, value);
-            if (keys.size() > 1) {
-              newMemberSignatures.setRepresentative(value, representative);
-            }
-          });
+      newMemberSignatures.putAll(pendingNewMemberSignatureUpdates);
       pendingNewMemberSignatureUpdates.clear();
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCode.java b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
index e5c792f..f4a2cdc 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCode.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
@@ -595,7 +595,11 @@
     for (Instruction instruction : instructions()) {
       if (instruction.outValue != null && instruction.outValue.getType().isClassType()) {
         ClassTypeElement classTypeLattice = instruction.outValue.getType().asClassType();
-        assert !mergedClasses.hasBeenMergedIntoDifferentType(classTypeLattice.getClassType());
+        assert !mergedClasses.hasBeenMergedIntoDifferentType(classTypeLattice.getClassType())
+            : "Expected reference to "
+                + classTypeLattice.getClassType().getTypeName()
+                + " to be rewritten at instruction "
+                + instruction.toString();
         assert !classTypeLattice
             .getInterfaces()
             .anyMatch(
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index 07ba0ce..6ffe166 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -24,6 +24,7 @@
 import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.PrunedItems;
 import com.android.tools.r8.ir.analysis.TypeChecker;
 import com.android.tools.r8.ir.analysis.VerifyTypesHelper;
 import com.android.tools.r8.ir.analysis.constant.SparseConditionalConstantPropagation;
@@ -112,10 +113,12 @@
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import com.android.tools.r8.utils.collections.SortedProgramMethodSet;
 import com.google.common.base.Suppliers;
+import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -171,6 +174,7 @@
   private DexString highestSortingString;
 
   private List<Action> onWaveDoneActions = null;
+  private final Set<DexMethod> prunedMethodsInWave = Sets.newIdentityHashSet();
 
   private final List<DexString> neverMergePrefixes;
   // Use AtomicBoolean to satisfy TSAN checking (see b/153714743).
@@ -326,6 +330,10 @@
     this(AppView.createForD8(appInfo), timing, printer);
   }
 
+  public Inliner getInliner() {
+    return inliner;
+  }
+
   private void synthesizeBridgesForNestBasedAccessesOnClasspath(
       D8MethodProcessor methodProcessor, ExecutorService executorService)
       throws ExecutionException {
@@ -710,7 +718,7 @@
     appView.withArgumentPropagator(
         argumentPropagator ->
             argumentPropagator.tearDownCodeScanner(
-                postMethodProcessorBuilder, executorService, timing));
+                this, postMethodProcessorBuilder, executorService, timing));
     appView.withCallSiteOptimizationInfoPropagator(
         callSiteOptimizationInfoPropagator ->
             callSiteOptimizationInfoPropagator.enqueueMethodsForReprocessing(
@@ -844,7 +852,8 @@
     onWaveDoneActions = Collections.synchronizedList(new ArrayList<>());
   }
 
-  private void waveDone(ProgramMethodSet wave) {
+  private void waveDone(ProgramMethodSet wave, ExecutorService executorService)
+      throws ExecutionException {
     delayedOptimizationFeedback.refineAppInfoWithLiveness(appView.appInfo().withLiveness());
     delayedOptimizationFeedback.updateVisibleOptimizationInfo();
     if (options.enableFieldAssignmentTracker) {
@@ -858,6 +867,15 @@
     assert delayedOptimizationFeedback.noUpdatesLeft();
     onWaveDoneActions.forEach(com.android.tools.r8.utils.Action::execute);
     onWaveDoneActions = null;
+    if (!prunedMethodsInWave.isEmpty()) {
+      appView.pruneItems(
+          PrunedItems.builder()
+              .setRemovedMethods(prunedMethodsInWave)
+              .setPrunedApp(appView.appInfo().app())
+              .build(),
+          executorService);
+      prunedMethodsInWave.clear();
+    }
   }
 
   public void addWaveDoneAction(com.android.tools.r8.utils.Action action) {
@@ -1961,9 +1979,13 @@
     appView.withArgumentPropagator(argumentPropagator -> argumentPropagator.onMethodPruned(method));
     enumUnboxer.onMethodPruned(method);
     outliner.onMethodPruned(method);
+    if (classStaticizer != null) {
+      classStaticizer.onMethodPruned(method);
+    }
     if (inliner != null) {
       inliner.onMethodPruned(method);
     }
+    prunedMethodsInWave.add(method.getReference());
   }
 
   /**
@@ -1977,6 +1999,9 @@
         argumentPropagator -> argumentPropagator.onMethodCodePruned(method));
     enumUnboxer.onMethodCodePruned(method);
     outliner.onMethodCodePruned(method);
+    if (classStaticizer != null) {
+      classStaticizer.onMethodCodePruned(method);
+    }
     if (inliner != null) {
       inliner.onMethodCodePruned(method);
     }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryMethodProcessor.java b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryMethodProcessor.java
index bea221a..9998d53 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryMethodProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryMethodProcessor.java
@@ -23,7 +23,6 @@
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
-import java.util.function.Consumer;
 
 /**
  * A {@link MethodProcessor} that processes methods in the whole program in a bottom-up manner,
@@ -36,6 +35,12 @@
     void notifyWaveStart(ProgramMethodSet wave);
   }
 
+  interface WaveDoneAction {
+
+    void notifyWaveDone(ProgramMethodSet wave, ExecutorService executorService)
+        throws ExecutionException;
+  }
+
   private final AppView<?> appView;
   private final CallSiteInformation callSiteInformation;
   private final Deque<SortedProgramMethodSet> waves;
@@ -110,7 +115,7 @@
   <E extends Exception> void forEachMethod(
       MethodAction<E> consumer,
       WaveStartAction waveStartAction,
-      Consumer<ProgramMethodSet> waveDone,
+      WaveDoneAction waveDoneAction,
       Timing timing,
       ExecutorService executorService)
       throws ExecutionException {
@@ -133,7 +138,7 @@
                 },
                 executorService);
         merger.add(timings);
-        waveDone.accept(wave);
+        waveDoneAction.notifyWaveDone(wave, executorService);
         prepareForWaveExtensionProcessing();
       } while (!wave.isEmpty());
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index 32d2228..f0de604 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -98,6 +98,8 @@
   // pruned when the wave ends.
   private final Map<DexProgramClass, ProgramMethodSet> singleCallerInlinedMethodsInWave =
       new ConcurrentHashMap<>();
+  private final Set<DexMethod> singleCallerInlinedPrunedMethodsForTesting =
+      Sets.newIdentityHashSet();
 
   private final AvailableApiExceptions availableApiExceptions;
 
@@ -1268,10 +1270,13 @@
         (clazz, singleCallerInlinedMethodsForClass) -> {
           // Convert and remove virtual single caller inlined methods to abstract or throw null.
           singleCallerInlinedMethodsForClass.removeIf(
-              singleCallerInlinedMethod -> {
-                if (singleCallerInlinedMethod.getDefinition().belongsToVirtualPool() || true) {
-                  singleCallerInlinedMethod.convertToAbstractOrThrowNullMethod(appView);
-                  converter.onMethodCodePruned(singleCallerInlinedMethod);
+              method -> {
+                // TODO(b/203188583): Enable pruning of methods with generic signatures. For this to
+                //  work we need to pass in a seed to GenericSignatureContextBuilder.create in R8.
+                if (method.getDefinition().belongsToVirtualPool()
+                    || method.getDefinition().getGenericSignature().hasSignature()) {
+                  method.convertToAbstractOrThrowNullMethod(appView);
+                  converter.onMethodCodePruned(method);
                   return true;
                 }
                 return false;
@@ -1284,7 +1289,10 @@
                 .removeMethods(
                     singleCallerInlinedMethodsForClass.toDefinitionSet(
                         SetUtils::newIdentityHashSet));
-            singleCallerInlinedMethodsForClass.forEach(converter::onMethodPruned);
+            for (ProgramMethod method : singleCallerInlinedMethodsForClass) {
+              converter.onMethodPruned(method);
+              singleCallerInlinedPrunedMethodsForTesting.add(method.getReference());
+            }
           }
         });
     singleCallerInlinedMethodsInWave.clear();
@@ -1302,4 +1310,9 @@
     }
     return true;
   }
+
+  public boolean verifyIsPrunedDueToSingleCallerInlining(DexMethod method) {
+    assert singleCallerInlinedPrunedMethodsForTesting.contains(method);
+    return true;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizer.java b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizer.java
index 748253f..30b0da1 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizer.java
@@ -105,6 +105,8 @@
   final Map<CandidateInfo, LongLivedProgramMethodSetBuilder<?>> referencedFrom =
       new ConcurrentHashMap<>();
 
+  private final Set<DexMethod> prunedMethods = Sets.newIdentityHashSet();
+
   // The map storing all the potential candidates for staticizing.
   final ConcurrentHashMap<DexType, CandidateInfo> candidates = new ConcurrentHashMap<>();
 
@@ -114,6 +116,14 @@
     this.converter = converter;
   }
 
+  public void onMethodPruned(ProgramMethod method) {
+    onMethodCodePruned(method);
+  }
+
+  public void onMethodCodePruned(ProgramMethod method) {
+    prunedMethods.add(method.getReference());
+  }
+
   public void prepareForPrimaryOptimizationPass(GraphLens graphLensForPrimaryOptimizationPass) {
     collectCandidates();
     this.graphLensForOptimizationPass = graphLensForPrimaryOptimizationPass;
@@ -129,8 +139,11 @@
         .values()
         .forEach(
             referencedFromBuilder ->
-                referencedFromBuilder.rewrittenWithLens(graphLensForSecondaryOptimizationPass));
+                referencedFromBuilder
+                    .removeAll(prunedMethods)
+                    .rewrittenWithLens(graphLensForSecondaryOptimizationPass));
     this.graphLensForOptimizationPass = graphLensForSecondaryOptimizationPass;
+    prunedMethods.clear();
   }
 
   // Before doing any usage-based analysis we collect a set of classes that can be
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
index c73132f..806b057 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
@@ -237,9 +237,6 @@
         referencedFrom =
             referencedFromBuilder
                 .rewrittenWithLens(appView)
-                .removeIf(
-                    appView,
-                    method -> method.getOptimizationInfo().hasBeenInlinedIntoSingleCallSite())
                 .build(appView);
         materializedReferencedFromCollections.put(info, referencedFrom);
       } else {
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java
index 158a8de..45f4984 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java
@@ -130,6 +130,7 @@
   }
 
   public void tearDownCodeScanner(
+      IRConverter converter,
       PostMethodProcessor.Builder postMethodProcessorBuilder,
       ExecutorService executorService,
       Timing timing)
@@ -152,6 +153,7 @@
     Map<Set<DexProgramClass>, DexMethodSignatureSet> interfaceDispatchOutsideProgram =
         new IdentityHashMap<>();
     populateParameterOptimizationInfo(
+        converter,
         immediateSubtypingInfo,
         stronglyConnectedProgramComponents,
         (stronglyConnectedProgramComponent, signature) -> {
@@ -189,6 +191,7 @@
    * optimization info.
    */
   private void populateParameterOptimizationInfo(
+      IRConverter converter,
       ImmediateProgramSubtypingInfo immediateSubtypingInfo,
       List<Set<DexProgramClass>> stronglyConnectedProgramComponents,
       BiConsumer<Set<DexProgramClass>, DexMethodSignature> interfaceDispatchOutsideProgram,
@@ -209,7 +212,7 @@
             reprocessingCriteriaCollection,
             stronglyConnectedProgramComponents,
             interfaceDispatchOutsideProgram)
-        .populateOptimizationInfo(executorService, timing);
+        .populateOptimizationInfo(converter, executorService, timing);
     reprocessingCriteriaCollection = null;
     timing.end();
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java
index 5470ed5..e050de8 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.type.Nullability;
+import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.optimize.info.ConcreteCallSiteOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
@@ -82,7 +83,8 @@
    * Computes an over-approximation of each parameter's value and type and stores the result in
    * {@link com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo}.
    */
-  void populateOptimizationInfo(ExecutorService executorService, Timing timing)
+  void populateOptimizationInfo(
+      IRConverter converter, ExecutorService executorService, Timing timing)
       throws ExecutionException {
     // TODO(b/190154391): Propagate argument information to handle virtual dispatch.
     // TODO(b/190154391): To deal with arguments that are themselves passed as arguments to invoke
@@ -98,7 +100,7 @@
 
     // Solve the parameter flow constraints.
     timing.begin("Solve flow constraints");
-    new InParameterFlowPropagator(appView, methodStates).run(executorService);
+    new InParameterFlowPropagator(appView, converter, methodStates).run(executorService);
     timing.end();
 
     // The information stored on each method is now sound, and can be used as optimization info.
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InParameterFlowPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InParameterFlowPropagator.java
index 9b583c7..9b10015 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InParameterFlowPropagator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InParameterFlowPropagator.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMonomorphicMethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteParameterState;
@@ -42,11 +43,15 @@
 public class InParameterFlowPropagator {
 
   final AppView<AppInfoWithLiveness> appView;
+  final IRConverter converter;
   final MethodStateCollectionByReference methodStates;
 
   public InParameterFlowPropagator(
-      AppView<AppInfoWithLiveness> appView, MethodStateCollectionByReference methodStates) {
+      AppView<AppInfoWithLiveness> appView,
+      IRConverter converter,
+      MethodStateCollectionByReference methodStates) {
     this.appView = appView;
+    this.converter = converter;
     this.methodStates = methodStates;
   }
 
@@ -206,6 +211,16 @@
       ParameterNode node = getOrCreateParameterNode(method, parameterIndex, methodState);
       for (MethodParameter inParameter : concreteParameterState.getInParameters()) {
         ProgramMethod enclosingMethod = getEnclosingMethod(inParameter);
+        if (enclosingMethod == null) {
+          // This is a parameter of a single caller inlined method. Since this method has been
+          // pruned, the call from inside the method no longer exists, and we can therefore safely
+          // skip it.
+          assert converter
+              .getInliner()
+              .verifyIsPrunedDueToSingleCallerInlining(inParameter.getMethod());
+          continue;
+        }
+
         MethodState enclosingMethodState = getMethodState(enclosingMethod);
         if (enclosingMethodState.isBottom()) {
           // The current method is called from a dead method; no need to propagate any information
diff --git a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
index b0f3ea9..04728fb 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -355,7 +355,7 @@
         pruneMethods(previous.liveMethods, prunedItems, executorService, futures),
         previous.fieldAccessInfoCollection,
         previous.methodAccessInfoCollection,
-        previous.objectAllocationInfoCollection,
+        previous.objectAllocationInfoCollection.withoutPrunedItems(prunedItems),
         previous.callSites,
         extendPinnedItems(previous, prunedItems.getAdditionalPinnedItems()),
         previous.mayHaveSideEffects,
@@ -439,6 +439,7 @@
   private static <T> Set<T> pruneItems(
       Set<T> items, Set<T> removedItems, ExecutorService executorService, List<Future<?>> futures) {
     if (!removedItems.isEmpty()) {
+
       futures.add(
           ThreadUtils.processAsynchronously(
               () -> {
@@ -1271,10 +1272,14 @@
         lens.rewriteCallSites(callSites, definitionSupplier),
         keepInfo.rewrite(definitionSupplier, lens, application.options),
         // Take any rule in case of collisions.
-        lens.rewriteReferenceKeys(mayHaveSideEffects, ListUtils::first),
-        // Drop assume rules in case of collisions.
-        lens.rewriteReferenceKeys(noSideEffects, rules -> null),
-        lens.rewriteReferenceKeys(assumedValues, rules -> null),
+        lens.rewriteReferenceKeys(mayHaveSideEffects, (reference, rules) -> ListUtils.first(rules)),
+        // Take the assume rule from the representative in case of collisions.
+        lens.rewriteReferenceKeys(
+            noSideEffects,
+            (reference, rules) -> noSideEffects.get(lens.getOriginalMemberSignature(reference))),
+        lens.rewriteReferenceKeys(
+            assumedValues,
+            (reference, rules) -> assumedValues.get(lens.getOriginalMemberSignature(reference))),
         lens.rewriteReferences(alwaysInline),
         lens.rewriteReferences(neverInline),
         lens.rewriteReferences(neverInlineDueToSingleCaller),
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 5b0ff74..75998bb 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -129,7 +129,6 @@
 import com.android.tools.r8.shaking.ScopedDexMethodSet.AddMethodIfMoreVisibleResult;
 import com.android.tools.r8.synthesis.SyntheticItems.SynthesizingContextOracle;
 import com.android.tools.r8.utils.Action;
-import com.android.tools.r8.utils.BooleanBox;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.IteratorUtils;
 import com.android.tools.r8.utils.OptionalBool;
@@ -3726,11 +3725,11 @@
                 : missingClassesBuilder.assertNoMissingClasses(appView),
             SetUtils.mapIdentityHashSet(liveTypes.getItems(), DexProgramClass::getType),
             Enqueuer.toDescriptorSet(targetedMethods.getItems()),
-            Collections.unmodifiableSet(failedMethodResolutionTargets),
-            Collections.unmodifiableSet(failedFieldResolutionTargets),
-            Collections.unmodifiableSet(bootstrapMethods),
-            Collections.unmodifiableSet(methodsTargetedByInvokeDynamic),
-            Collections.unmodifiableSet(virtualMethodsTargetedByInvokeDirect),
+            failedMethodResolutionTargets,
+            failedFieldResolutionTargets,
+            bootstrapMethods,
+            methodsTargetedByInvokeDynamic,
+            virtualMethodsTargetedByInvokeDirect,
             toDescriptorSet(liveMethods.getItems()),
             // Filter out library fields and pinned fields, because these are read by default.
             fieldAccessInfoCollection,
@@ -3772,16 +3771,15 @@
     if (methods.isEmpty() || interfaceProcessor == null) {
       return methods;
     }
-    BooleanBox changed = new BooleanBox(false);
-    ImmutableSet.Builder<DexMethod> builder = ImmutableSet.builder();
+    Set<DexMethod> companionMethods = Sets.newIdentityHashSet();
     interfaceProcessor.forEachMethodToMove(
         (method, companion) -> {
           if (methods.contains(method)) {
-            changed.set(true);
-            builder.add(companion);
+            companionMethods.add(companion);
           }
         });
-    return changed.isTrue() ? builder.addAll(methods).build() : methods;
+    methods.addAll(companionMethods);
+    return methods;
   }
 
   private boolean verifyReferences(DexApplication app) {
@@ -3850,11 +3848,11 @@
 
   private static <D extends DexEncodedMember<D, R>, R extends DexMember<D, R>>
       Set<R> toDescriptorSet(Set<D> set) {
-    ImmutableSet.Builder<R> builder = new ImmutableSet.Builder<>();
+    Set<R> result = Sets.newIdentityHashSet();
     for (D item : set) {
-      builder.add(item.getReference());
+      result.add(item.getReference());
     }
-    return builder.build();
+    return result;
   }
 
   private static Object2BooleanMap<DexMember<?, ?>> joinIdentifierNameStrings(
diff --git a/src/main/java/com/android/tools/r8/shaking/MinimumKeepInfoCollection.java b/src/main/java/com/android/tools/r8/shaking/MinimumKeepInfoCollection.java
index ec1594a..8062beb 100644
--- a/src/main/java/com/android/tools/r8/shaking/MinimumKeepInfoCollection.java
+++ b/src/main/java/com/android/tools/r8/shaking/MinimumKeepInfoCollection.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.graph.ProgramDefinition;
 import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.PrunedItems;
 import com.android.tools.r8.shaking.KeepInfo.Joiner;
 import com.android.tools.r8.utils.MapUtils;
 import java.util.Collections;
@@ -131,6 +132,10 @@
         });
   }
 
+  public void pruneItems(PrunedItems prunedItems) {
+    minimumKeepInfo.keySet().removeIf(prunedItems::isRemoved);
+  }
+
   public KeepClassInfo.Joiner remove(DexType clazz) {
     return (KeepClassInfo.Joiner) minimumKeepInfo.remove(clazz);
   }
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java b/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
index b105a80..03cff10 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
@@ -1740,6 +1740,17 @@
           });
     }
 
+    public void pruneItems(PrunedItems prunedItems) {
+      MinimumKeepInfoCollection unconditionalMinimumKeepInfo =
+          getDependentMinimumKeepInfo().getUnconditionalMinimumKeepInfoOrDefault(null);
+      if (unconditionalMinimumKeepInfo != null) {
+        unconditionalMinimumKeepInfo.pruneItems(prunedItems);
+        if (unconditionalMinimumKeepInfo.isEmpty()) {
+          getDependentMinimumKeepInfo().remove(UnconditionalKeepInfoEvent.get());
+        }
+      }
+    }
+
     void shouldNotBeMinified(ProgramDefinition definition) {
       getDependentMinimumKeepInfo()
           .getOrCreateUnconditionalMinimumKeepInfoFor(definition.getReference())
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
index e157ff8..c44bdc9 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -1012,8 +1012,13 @@
         DexEncodedMethod shadowedBy = findMethodInTarget(virtualMethod);
         if (shadowedBy != null) {
           if (virtualMethod.isAbstract()) {
-            // Remove abstract/interface methods that are shadowed.
-            deferredRenamings.map(virtualMethod.getReference(), shadowedBy.getReference());
+            // Remove abstract/interface methods that are shadowed. The identity mapping below is
+            // needed to ensure we correctly fixup the mapping in case the signature refers to
+            // merged classes.
+            deferredRenamings
+                .map(virtualMethod.getReference(), shadowedBy.getReference())
+                .map(shadowedBy.getReference(), shadowedBy.getReference())
+                .recordMerge(virtualMethod.getReference(), shadowedBy.getReference());
 
             // The override now corresponds to the method in the parent, so unset its synthetic flag
             // if the method in the parent is not synthetic.
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java
index ea9f971..0464966 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java
@@ -16,9 +16,10 @@
 import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
 import com.android.tools.r8.ir.code.Invoke.Type;
 import com.android.tools.r8.utils.IterableUtils;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeHashMap;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
 import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
-import com.android.tools.r8.utils.collections.BidirectionalOneToOneMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneRepresentativeMap;
 import com.android.tools.r8.utils.collections.MutableBidirectionalOneToOneMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
@@ -74,7 +75,7 @@
       Set<DexMethod> mergedMethods,
       Map<DexType, Map<DexMethod, GraphLensLookupResultProvider>>
           contextualVirtualToDirectMethodMaps,
-      BidirectionalOneToOneMap<DexMethod, DexMethod> newMethodSignatures,
+      BidirectionalManyToOneRepresentativeMap<DexMethod, DexMethod> newMethodSignatures,
       Map<DexMethod, DexMethod> originalMethodSignaturesForBridges) {
     super(appView, fieldMap, methodMap, mergedClasses.getForwardMap(), newMethodSignatures);
     this.appView = appView;
@@ -164,8 +165,8 @@
     private final Map<DexType, Map<DexMethod, GraphLensLookupResultProvider>>
         contextualVirtualToDirectMethodMaps = new IdentityHashMap<>();
 
-    private final MutableBidirectionalOneToOneMap<DexMethod, DexMethod> newMethodSignatures =
-        new BidirectionalOneToOneHashMap<>();
+    private final MutableBidirectionalManyToOneRepresentativeMap<DexMethod, DexMethod>
+        newMethodSignatures = BidirectionalManyToOneRepresentativeHashMap.newIdentityHashMap();
     private final Map<DexMethod, DexMethod> originalMethodSignaturesForBridges =
         new IdentityHashMap<>();
 
@@ -208,12 +209,17 @@
               context);
         }
       }
-      builder.newMethodSignatures.forEach(
-          (originalMethodSignature, renamedMethodSignature) ->
-              newBuilder.recordMove(
-                  originalMethodSignature,
-                  builder.getMethodSignatureAfterClassMerging(
-                      renamedMethodSignature, mergedClasses)));
+      builder.newMethodSignatures.forEachManyToOneMapping(
+          (originalMethodSignatures, renamedMethodSignature, representative) -> {
+            DexMethod methodSignatureAfterClassMerging =
+                builder.getMethodSignatureAfterClassMerging(renamedMethodSignature, mergedClasses);
+            newBuilder.newMethodSignatures.put(
+                originalMethodSignatures, methodSignatureAfterClassMerging);
+            if (originalMethodSignatures.size() > 1) {
+              newBuilder.newMethodSignatures.setRepresentative(
+                  methodSignatureAfterClassMerging, representative);
+            }
+          });
       for (Map.Entry<DexMethod, DexMethod> entry :
           builder.originalMethodSignaturesForBridges.entrySet()) {
         newBuilder.recordCreationOfBridgeMethod(
@@ -317,6 +323,12 @@
       return this;
     }
 
+    public void recordMerge(DexMethod from, DexMethod to) {
+      newMethodSignatures.put(from, to);
+      newMethodSignatures.put(to, to);
+      newMethodSignatures.setRepresentative(to, to);
+    }
+
     public void recordMove(DexMethod from, DexMethod to) {
       newMethodSignatures.put(from, to);
     }
@@ -336,7 +348,18 @@
       fieldMap.putAll(builder.fieldMap);
       methodMap.putAll(builder.methodMap);
       mergedMethodsBuilder.addAll(builder.mergedMethodsBuilder.build());
-      newMethodSignatures.putAll(builder.newMethodSignatures);
+      builder.newMethodSignatures.forEachManyToOneMapping(
+          (keys, value, representative) -> {
+            if (newMethodSignatures.containsValue(value)
+                && !newMethodSignatures.hasExplicitRepresentativeKey(value)) {
+              newMethodSignatures.setRepresentative(
+                  value, newMethodSignatures.getRepresentativeKey(value));
+            }
+            newMethodSignatures.put(keys, value);
+            if (keys.size() > 1 && !newMethodSignatures.hasExplicitRepresentativeKey(value)) {
+              newMethodSignatures.setRepresentative(value, representative);
+            }
+          });
       originalMethodSignaturesForBridges.putAll(builder.originalMethodSignaturesForBridges);
       for (DexType context : builder.contextualVirtualToDirectMethodMaps.keySet()) {
         Map<DexMethod, GraphLensLookupResultProvider> current =
diff --git a/src/main/java/com/android/tools/r8/utils/SetUtils.java b/src/main/java/com/android/tools/r8/utils/SetUtils.java
index a7305a2..f363c3da 100644
--- a/src/main/java/com/android/tools/r8/utils/SetUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/SetUtils.java
@@ -34,15 +34,16 @@
     return result;
   }
 
-  public static <T> Set<T> newIdentityHashSet(T element) {
-    Set<T> result = Sets.newIdentityHashSet();
-    result.add(element);
+  @SafeVarargs
+  public static <T> HashSet<T> newHashSet(T... elements) {
+    HashSet<T> result = new HashSet<>(elements.length);
+    Collections.addAll(result, elements);
     return result;
   }
 
-  public static <T> Set<T> newIdentityHashSet(T[] elements) {
+  public static <T> Set<T> newIdentityHashSet(T element) {
     Set<T> result = Sets.newIdentityHashSet();
-    Collections.addAll(result, elements);
+    result.add(element);
     return result;
   }
 
diff --git a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneRepresentativeHashMap.java b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneRepresentativeHashMap.java
index bdd70c8..0887329 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneRepresentativeHashMap.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneRepresentativeHashMap.java
@@ -81,6 +81,17 @@
   }
 
   @Override
+  public void putAll(BidirectionalManyToOneRepresentativeMap<K, V> map) {
+    map.forEachManyToOneMapping(
+        (keys, value, representative) -> {
+          put(keys, value);
+          if (keys.size() > 1) {
+            setRepresentative(value, representative);
+          }
+        });
+  }
+
+  @Override
   public V remove(K key) {
     V value = super.remove(key);
     if (hasExplicitRepresentativeKey(value)) {
diff --git a/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalManyToOneRepresentativeMap.java b/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalManyToOneRepresentativeMap.java
index 24f91ac..b80cc5b 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalManyToOneRepresentativeMap.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalManyToOneRepresentativeMap.java
@@ -8,6 +8,8 @@
 public interface MutableBidirectionalManyToOneRepresentativeMap<K, V>
     extends MutableBidirectionalManyToOneMap<K, V>, BidirectionalManyToOneRepresentativeMap<K, V> {
 
+  void putAll(BidirectionalManyToOneRepresentativeMap<K, V> map);
+
   K removeRepresentativeFor(V value);
 
   void setRepresentative(V value, K representative);
diff --git a/src/test/java/com/android/tools/r8/cf/bootstrap/BootstrapCurrentEqualityTest.java b/src/test/java/com/android/tools/r8/cf/bootstrap/BootstrapCurrentEqualityTest.java
index d1dc197..cdecb76 100644
--- a/src/test/java/com/android/tools/r8/cf/bootstrap/BootstrapCurrentEqualityTest.java
+++ b/src/test/java/com/android/tools/r8/cf/bootstrap/BootstrapCurrentEqualityTest.java
@@ -64,8 +64,8 @@
   @BeforeClass
   public static void beforeAll() throws Exception {
     if (data().stream().count() > 0) {
-      r8R8Debug = compileR8(CompilationMode.DEBUG);
       r8R8Release = compileR8(CompilationMode.RELEASE);
+      r8R8Debug = compileR8(CompilationMode.DEBUG);
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
index 699bb50..7e98cdd 100644
--- a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
@@ -35,6 +35,7 @@
 import com.android.tools.r8.utils.AndroidApp.Builder;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -1065,15 +1066,17 @@
         };
     // SimpleInterface cannot be merged into SimpleInterfaceImpl because SimpleInterfaceImpl
     // is in a different package and is not public.
-    ImmutableSet<String> preservedClassNames =
-        ImmutableSet.of(
+    Set<String> preservedClassNames =
+        SetUtils.newHashSet(
             "classmerging.SimpleInterfaceAccessTest",
-            "classmerging.SimpleInterfaceAccessTest$1",
             "classmerging.SimpleInterfaceAccessTest$SimpleInterface",
             "classmerging.SimpleInterfaceAccessTest$OtherSimpleInterface",
             "classmerging.SimpleInterfaceAccessTest$OtherSimpleInterfaceImpl",
             "classmerging.pkg.SimpleInterfaceImplRetriever",
             "classmerging.pkg.SimpleInterfaceImplRetriever$SimpleInterfaceImpl");
+    if (parameters.isCfRuntime()) {
+      preservedClassNames.add("classmerging.SimpleInterfaceAccessTest$1");
+    }
     runTest(
         testForR8(parameters.getBackend())
             .addKeepRules(getProguardConfig(EXAMPLE_KEEP))
diff --git a/src/test/java/com/android/tools/r8/regress/b69825683/Regress69825683Test.java b/src/test/java/com/android/tools/r8/regress/b69825683/Regress69825683Test.java
index 7138094..01d1c30 100644
--- a/src/test/java/com/android/tools/r8/regress/b69825683/Regress69825683Test.java
+++ b/src/test/java/com/android/tools/r8/regress/b69825683/Regress69825683Test.java
@@ -62,14 +62,13 @@
 
     List<FoundClassSubject> classes = inspector.allClasses();
 
-    // Check that the synthetic class is still present.
-    assertEquals(3, classes.size());
+    // Check that the synthetic class is still present when generating class files.
+    assertEquals(parameters.isCfRuntime() ? 3 : 2, classes.size());
     assertEquals(
-        1,
+        parameters.isCfRuntime(),
         classes.stream()
             .map(FoundClassSubject::getOriginalName)
-            .filter(name -> name.endsWith("$1"))
-            .count());
+            .anyMatch(name -> name.endsWith("$1")));
   }
 
   @Test
@@ -94,13 +93,12 @@
 
     List<FoundClassSubject> classes = inspector.allClasses();
 
-    // Check that the synthetic class is still present.
-    assertEquals(3, classes.size());
+    // The synthetic class is still present when generating class files.
+    assertEquals(parameters.isCfRuntime() ? 3 : 2, classes.size());
     assertEquals(
-        1,
+        parameters.isCfRuntime(),
         classes.stream()
             .map(FoundClassSubject::getOriginalName)
-            .filter(name -> name.endsWith("$1"))
-            .count());
+            .anyMatch(name -> name.endsWith("$1")));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/StringBuildersAfterAssumenosideeffectsTest.java b/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/StringBuildersAfterAssumenosideeffectsTest.java
index 2787932..b80febc 100644
--- a/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/StringBuildersAfterAssumenosideeffectsTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/StringBuildersAfterAssumenosideeffectsTest.java
@@ -28,7 +28,7 @@
 
   @Parameterized.Parameters(name = "{0}")
   public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().build();
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
   private final TestParameters parameters;
@@ -51,7 +51,7 @@
             "  void info(...);",
             "}")
         .noMinification()
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), MAIN)
         .assertSuccessWithOutput(EXPECTED)
         .inspect(this::inspect);