Process inlining of methods in waves instead of ruling out breakers.

We used to never inline across an edge in the call-graph that was
removed during cycle removal to avoid non-determinism. With this
change, we only disallow inlining of methods that are being processed
concurrently. This is equivalent, as a method that is being inlined
concurrently must have had an edge in the call-graph that was removed if
it is called from another method. However, we do not rule out inlining
of such breakers across unrelated edges.

Bug:
Change-Id: I463fe850e677686624104060f48c2b2eab401c98
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index 8d28546..8c071c8 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -26,6 +26,7 @@
 import com.android.tools.r8.ir.code.ValueNumberGenerator;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.ir.regalloc.RegisterAllocator;
 import com.android.tools.r8.ir.synthetic.ForwardMethodSourceCode;
 import com.android.tools.r8.ir.synthetic.SynthesizedCode;
@@ -107,13 +108,13 @@
     return accessFlags.isConstructor() && accessFlags.isStatic();
   }
 
-  public boolean isInliningCandidate(DexEncodedMethod container, boolean alwaysInline,
+  public boolean isInliningCandidate(DexEncodedMethod container, Reason inliningReason,
       AppInfoWithSubtyping appInfo) {
     if (isClassInitializer()) {
       // This will probably never happen but never inline a class initializer.
       return false;
     }
-    if (alwaysInline) {
+    if (inliningReason == Reason.FORCE) {
       return true;
     }
     switch (compilationState) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java b/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
index 585260b..b32f34d 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
@@ -18,12 +18,11 @@
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ThreadUtils;
-import com.android.tools.r8.utils.ThrowingConsumer;
+import com.android.tools.r8.utils.ThrowingBiConsumer;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
@@ -32,8 +31,8 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
-import java.util.function.Consumer;
 import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 /**
@@ -51,7 +50,7 @@
  * <p>
  * Recursive calls are not present.
  */
-public class CallGraph {
+public class CallGraph extends CallSiteInformation {
 
   private CallGraph(InternalOptions options) {
     this.shuffle = options.testing.irOrdering;
@@ -132,15 +131,7 @@
   }
 
   private final Map<DexEncodedMethod, Node> nodes = new LinkedHashMap<>();
-  private final Map<DexEncodedMethod, Set<DexEncodedMethod>> breakers = new HashMap<>();
-  private final Function<List<DexEncodedMethod>, List<DexEncodedMethod>> shuffle;
-
-  // Returns whether the method->callee edge has been removed from the call graph
-  // to break a cycle in the call graph.
-  public boolean isBreaker(DexEncodedMethod method, DexEncodedMethod callee) {
-    Set<DexEncodedMethod> value = breakers.get(method);
-    return (value != null) && value.contains(callee);
-  }
+  private final Function<Set<DexEncodedMethod>, Set<DexEncodedMethod>> shuffle;
 
   private Set<DexEncodedMethod> singleCallSite = Sets.newIdentityHashSet();
   private Set<DexEncodedMethod> doubleCallSite = Sets.newIdentityHashSet();
@@ -170,10 +161,12 @@
    * For pinned methods (methods kept through Proguard keep rules) this will always answer
    * <code>false</code>.
    */
+  @Override
   public boolean hasSingleCallSite(DexEncodedMethod method) {
     return singleCallSite.contains(method);
   }
 
+  @Override
   public boolean hasDoubleCallSite(DexEncodedMethod method) {
     return doubleCallSite.contains(method);
   }
@@ -211,12 +204,10 @@
    * All nodes in the graph are extracted if called repeatedly until null is returned.
    * Please note that there are no cycles in this graph (see {@link #breakCycles}).
    * <p>
-   *
-   * @return List of {@link DexEncodedMethod}.
    */
-  private List<DexEncodedMethod> extractLeaves() {
+  private Set<DexEncodedMethod> extractLeaves() {
     if (isEmpty()) {
-      return Collections.emptyList();
+      return Collections.emptySet();
     }
     // First identify all leaves before removing them from the graph.
     List<Node> leaves = nodes.values().stream().filter(Node::isLeaf).collect(Collectors.toList());
@@ -224,7 +215,8 @@
       leaf.callers.forEach(caller -> caller.callees.remove(leaf));
       nodes.remove(leaf.method);
     });
-    return shuffle.apply(leaves.stream().map(leaf -> leaf.method).collect(Collectors.toList()));
+    return shuffle.apply(leaves.stream().map(leaf -> leaf.method)
+        .collect(Collectors.toCollection(LinkedHashSet::new)));
   }
 
   private int traverse(Node node, Set<Node> stack, Set<Node> marked) {
@@ -246,8 +238,6 @@
           // We have a cycle; break it by removing node->callee.
           toBeRemoved.add(callee);
           callee.callers.remove(node);
-          breakers.computeIfAbsent(node.method,
-              ignore -> Sets.newIdentityHashSet()).add(callee.method);
         } else {
           numberOfCycles += traverse(callee, stack, marked);
         }
@@ -264,7 +254,6 @@
 
   private int breakCycles() {
     // Break cycles in this call graph by removing edges causing cycles.
-    // The remove edges are stored in @breakers.
     int numberOfCycles = 0;
     Set<Node> stack = Sets.newIdentityHashSet();
     Set<Node> marked = Sets.newIdentityHashSet();
@@ -294,16 +283,23 @@
     return nodes.size() == 0;
   }
 
+  /**
+   * Applies the given method to all leaf nodes of the graph.
+   * <p>
+   * As second parameter, a predicate that can be used to decide whether another method is
+   * processed at the same time is passed. This can be used to avoid races in concurrent processing.
+   */
   public <E extends Exception> void forEachMethod(
-      ThrowingConsumer<DexEncodedMethod, E> consumer, ExecutorService executorService)
+      ThrowingBiConsumer<DexEncodedMethod, Predicate<DexEncodedMethod>, E> consumer,
+      ExecutorService executorService)
       throws ExecutionException {
     while (!isEmpty()) {
-      List<DexEncodedMethod> methods = extractLeaves();
+      Set<DexEncodedMethod> methods = extractLeaves();
       assert methods.size() > 0;
       List<Future<?>> futures = new ArrayList<>();
       for (DexEncodedMethod method : methods) {
         futures.add(executorService.submit(() -> {
-          consumer.accept(method);
+          consumer.accept(method, methods::contains);
           return null; // we want a Callable not a Runnable to be able to throw
         }));
       }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CallSiteInformation.java b/src/main/java/com/android/tools/r8/ir/conversion/CallSiteInformation.java
new file mode 100644
index 0000000..651fb6f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CallSiteInformation.java
@@ -0,0 +1,38 @@
+// Copyright (c) 2017, 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.ir.conversion;
+
+import com.android.tools.r8.graph.DexEncodedMethod;
+
+public abstract class CallSiteInformation {
+
+  /**
+   * Check if the <code>method</code> is guaranteed to only have a single call site.
+   * <p>
+   * For pinned methods (methods kept through Proguard keep rules) this will always answer
+   * <code>false</code>.
+   */
+  public abstract boolean hasSingleCallSite(DexEncodedMethod method);
+
+  public abstract boolean hasDoubleCallSite(DexEncodedMethod method);
+
+  public static CallSiteInformation empty() {
+    return EmptyCallSiteInformation.EMPTY_INFO;
+  }
+
+  private static class EmptyCallSiteInformation extends CallSiteInformation {
+
+    private static EmptyCallSiteInformation EMPTY_INFO = new EmptyCallSiteInformation();
+
+    @Override
+    public boolean hasSingleCallSite(DexEncodedMethod method) {
+      return false;
+    }
+
+    @Override
+    public boolean hasDoubleCallSite(DexEncodedMethod method) {
+      return false;
+    }
+  }
+}
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 82cc92f..b1e68f7 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
@@ -49,6 +49,7 @@
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.function.BiConsumer;
+import java.util.function.Predicate;
 
 public class IRConverter {
 
@@ -68,7 +69,6 @@
   private final LensCodeRewriter lensCodeRewriter;
   private final Inliner inliner;
   private final ProtoLitePruner protoLiteRewriter;
-  private CallGraph callGraph;
 
   private OptimizationFeedback ignoreOptimizationFeedback = new OptimizationFeedbackIgnore();
   private DexString highestSortingString;
@@ -251,7 +251,9 @@
       boolean matchesMethodFilter = options.methodMatchesFilter(method);
       if (matchesMethodFilter) {
         if (method.getCode().isJarCode()) {
-          rewriteCode(method, ignoreOptimizationFeedback, Outliner::noProcessing);
+          // We do not process in call graph order, so anything could be a leaf.
+          rewriteCode(method, ignoreOptimizationFeedback, x -> true, CallSiteInformation.empty(),
+              Outliner::noProcessing);
         }
         updateHighestSortingStrings(method);
       }
@@ -271,10 +273,6 @@
       throws ExecutionException, ApiLevelException {
     removeLambdaDeserializationMethods();
 
-    timing.begin("Build call graph");
-    callGraph = CallGraph.build(application, appInfo.withSubtyping(), graphLense, options);
-    timing.end();
-
     // The process is in two phases.
     // 1) Subject all DexEncodedMethods to optimization (except outlining).
     //    - a side effect is candidates for outlining are identified.
@@ -282,13 +280,19 @@
     // Ideally, we should outline eagerly when threshold for a template has been reached.
 
     // Process the application identifying outlining candidates.
-    timing.begin("IR conversion phase 1");
     OptimizationFeedback directFeedback = new OptimizationFeedbackDirect();
-    callGraph.forEachMethod(method -> {
-          processMethod(method, directFeedback,
-              outliner == null ? Outliner::noProcessing : outliner::identifyCandidates);
-    }, executorService);
-    timing.end();
+    {
+      timing.begin("Build call graph");
+      CallGraph callGraph = CallGraph
+          .build(application, appInfo.withSubtyping(), graphLense, options);
+      timing.end();
+      timing.begin("IR conversion phase 1");
+      callGraph.forEachMethod((method, isProcessedConcurrently) -> {
+        processMethod(method, directFeedback, isProcessedConcurrently, callGraph,
+            outliner == null ? Outliner::noProcessing : outliner::identifyCandidates);
+      }, executorService);
+      timing.end();
+    }
 
     // Get rid of <clinit> methods with no code.
     removeEmptyClassInitializers();
@@ -313,16 +317,17 @@
       if (outlineClass != null) {
         // We need a new call graph to ensure deterministic order and also processing inside out
         // to get maximal inlining. Use a identity lense, as the code has been rewritten.
-        callGraph = CallGraph
+        CallGraph callGraph = CallGraph
             .build(application, appInfo.withSubtyping(), GraphLense.getIdentityLense(), options);
         Set<DexEncodedMethod> outlineMethods = outliner.getMethodsSelectedForOutlining();
-        callGraph.forEachMethod(method -> {
+        callGraph.forEachMethod((method, isProcessedConcurrently) -> {
           if (!outlineMethods.contains(method)) {
             return;
           }
           // This is the second time we compile this method first mark it not processed.
           assert !method.getCode().isOutlineCode();
-          processMethod(method, ignoreOptimizationFeedback, outliner::applyOutliningCandidate);
+          processMethod(method, ignoreOptimizationFeedback, isProcessedConcurrently, callGraph,
+              outliner::applyOutliningCandidate);
           assert method.isProcessed();
         }, executorService);
         builder.addSynthesizedClass(outlineClass, true);
@@ -405,7 +410,8 @@
 
   public void optimizeSynthesizedMethod(DexEncodedMethod method) throws ApiLevelException {
     // Process the generated method, but don't apply any outlining.
-    processMethod(method, ignoreOptimizationFeedback, Outliner::noProcessing);
+    processMethod(method, ignoreOptimizationFeedback, x -> false, CallSiteInformation.empty(),
+        Outliner::noProcessing);
   }
 
   private String logCode(InternalOptions options, DexEncodedMethod method) {
@@ -414,12 +420,14 @@
 
   public void processMethod(DexEncodedMethod method,
       OptimizationFeedback feedback,
+      Predicate<DexEncodedMethod> isProcessedConcurrently,
+      CallSiteInformation callSiteInformation,
       BiConsumer<IRCode, DexEncodedMethod> outlineHandler)
           throws ApiLevelException {
     Code code = method.getCode();
     boolean matchesMethodFilter = options.methodMatchesFilter(method);
     if (code != null && matchesMethodFilter) {
-      rewriteCode(method, feedback, outlineHandler);
+      rewriteCode(method, feedback, isProcessedConcurrently, callSiteInformation, outlineHandler);
     } else {
       // Mark abstract methods as processed as well.
       method.markProcessed(Constraint.NEVER);
@@ -428,6 +436,8 @@
 
   private void rewriteCode(DexEncodedMethod method,
       OptimizationFeedback feedback,
+      Predicate<DexEncodedMethod> isProcessedConcurrently,
+      CallSiteInformation callSiteInformation,
       BiConsumer<IRCode, DexEncodedMethod> outlineHandler)
       throws ApiLevelException {
     if (options.verbose) {
@@ -473,7 +483,7 @@
     if (options.inlineAccessors && inliner != null) {
       // TODO(zerny): Should we support inlining in debug mode? b/62937285
       assert !options.debug;
-      inliner.performInlining(method, code, callGraph);
+      inliner.performInlining(method, code, isProcessedConcurrently, callSiteInformation);
     }
     codeRewriter.removeCastChains(code);
     codeRewriter.rewriteLongCompareAndRequireNonNull(code, options);
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 b90d2ad..a11d924 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
@@ -23,7 +23,7 @@
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.code.ValueNumberGenerator;
-import com.android.tools.r8.ir.conversion.CallGraph;
+import com.android.tools.r8.ir.conversion.CallSiteInformation;
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.conversion.LensCodeRewriter;
 import com.android.tools.r8.ir.conversion.OptimizationFeedback;
@@ -36,6 +36,7 @@
 import java.util.ListIterator;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 public class Inliner {
@@ -136,7 +137,8 @@
           .sorted(DexEncodedMethod::slowCompare)
           .collect(Collectors.toList());
       for (DexEncodedMethod method : methods) {
-        converter.processMethod(method, feedback, Outliner::noProcessing);
+        converter.processMethod(method, feedback, x -> false, CallSiteInformation.empty(),
+            Outliner::noProcessing);
         assert method.isProcessed();
       }
     }
@@ -222,7 +224,7 @@
       this.reason = reason;
     }
 
-    boolean forceInline() {
+    boolean ignoreInstructionBudget() {
       return reason != Reason.SIMPLE;
     }
 
@@ -315,7 +317,9 @@
     return true;
   }
 
-  public void performInlining(DexEncodedMethod method, IRCode code, CallGraph callGraph)
+  public void performInlining(DexEncodedMethod method, IRCode code,
+      Predicate<DexEncodedMethod> isProcessedConcurrently,
+      CallSiteInformation callSiteInformation)
       throws ApiLevelException {
     int instruction_allowance = 1500;
     instruction_allowance -= numberOfInstructions(code);
@@ -323,7 +327,8 @@
       return;
     }
     computeReceiverMustBeNonNull(code);
-    InliningOracle oracle = new InliningOracle(this, method, callGraph);
+    InliningOracle oracle = new InliningOracle(this, method, callSiteInformation,
+        isProcessedConcurrently);
 
     List<BasicBlock> blocksToRemove = new ArrayList<>();
     ListIterator<BasicBlock> blockIterator = code.listIterator();
@@ -339,15 +344,7 @@
           InvokeMethod invoke = current.asInvokeMethod();
           InlineAction result = invoke.computeInlining(oracle);
           if (result != null) {
-            DexEncodedMethod target = appInfo.lookup(invoke.getType(), invoke.getInvokedMethod());
-            if (target == null) {
-              // The declared target cannot be found so skip inlining.
-              continue;
-            }
-            if (!(target.isProcessed() || result.reason == Reason.FORCE)) {
-              // Do not inline code that was not processed unless we have to force inline.
-              continue;
-            }
+            DexEncodedMethod target = result.target;
             IRCode inlinee = result
                 .buildIR(code.valueNumberGenerator, appInfo, graphLense, options);
             if (inlinee != null) {
@@ -355,10 +352,6 @@
               if (block.hasCatchHandlers() && inlinee.getNormalExitBlock() == null) {
                 continue;
               }
-              if (callGraph.isBreaker(method, target)) {
-                // Make sure we don't inline a call graph breaker.
-                continue;
-              }
               // If this code did not go through the full pipeline, apply inlining to make sure
               // that force inline targets get processed.
               if (!target.isProcessed()) {
@@ -366,7 +359,8 @@
                 if (Log.ENABLED) {
                   Log.verbose(getClass(), "Forcing extra inline on " + target.toSourceString());
                 }
-                performInlining(target, inlinee, callGraph);
+                performInlining(target, inlinee, isProcessedConcurrently,
+                    callSiteInformation);
               }
               // Make sure constructor inlining is legal.
               assert !target.isClassInitializer();
@@ -378,7 +372,7 @@
               if (invoke.isInvokeMethodWithReceiver()) {
                 // If the invoke has a receiver but the declared method holder is different
                 // from the computed target holder, inlining requires a downcast of the receiver.
-                if (result.target.method.getHolder() != target.method.getHolder()) {
+                if (target.method.getHolder() != invoke.getInvokedMethod().getHolder()) {
                   downcast = result.target.method.getHolder();
                 }
               }
@@ -386,7 +380,7 @@
               // Back up before the invoke instruction.
               iterator.previous();
               instruction_allowance -= numberOfInstructions(inlinee);
-              if (instruction_allowance >= 0 || result.forceInline()) {
+              if (instruction_allowance >= 0 || result.ignoreInstructionBudget()) {
                 iterator.inlineInvoke(code, inlinee, blockIterator, blocksToRemove, downcast);
               }
               // If we inlined the invoke from a bridge method, it is no longer a bridge method.
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java
index 91f0a61..8b7a05e 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java
@@ -11,10 +11,11 @@
 import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
 import com.android.tools.r8.ir.code.InvokePolymorphic;
 import com.android.tools.r8.ir.code.InvokeStatic;
-import com.android.tools.r8.ir.conversion.CallGraph;
+import com.android.tools.r8.ir.conversion.CallSiteInformation;
 import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
 import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.logging.Log;
+import java.util.function.Predicate;
 
 /**
  * The InliningOracle contains information needed for when inlining
@@ -26,16 +27,19 @@
 
   private final Inliner inliner;
   private final DexEncodedMethod method;
-  private final CallGraph callGraph;
+  private final CallSiteInformation callSiteInformation;
+  private final Predicate<DexEncodedMethod> isProcessedConcurrently;
   private final InliningInfo info;
 
   InliningOracle(
       Inliner inliner,
       DexEncodedMethod method,
-      CallGraph callGraph) {
+      CallSiteInformation callSiteInformation,
+      Predicate<DexEncodedMethod> isProcessedConcurrently) {
     this.inliner = inliner;
     this.method = method;
-    this.callGraph = callGraph;
+    this.callSiteInformation = callSiteInformation;
+    this.isProcessedConcurrently = isProcessedConcurrently;
     info = Log.ENABLED ? new InliningInfo(method) : null;
   }
 
@@ -66,7 +70,7 @@
         && inliner.appInfo.withLiveness().alwaysInline.contains(target)) {
       return Reason.ALWAYS;
     }
-    if (callGraph.hasSingleCallSite(target)) {
+    if (callSiteInformation.hasSingleCallSite(target)) {
       return Reason.SINGLE_CALLER;
     }
     if (isDoubleInliningTarget(target)) {
@@ -91,18 +95,13 @@
 
   private synchronized boolean isDoubleInliningTarget(DexEncodedMethod candidate) {
     // 10 is found from measuring.
-    return callGraph.hasDoubleCallSite(candidate)
+    return callSiteInformation.hasDoubleCallSite(candidate)
         && candidate.getCode().isDexCode()
         && (candidate.getCode().asDexCode().instructions.length <= 10);
   }
 
   private boolean passesInliningConstraints(InvokeMethod invoke, DexEncodedMethod candidate,
       Reason reason) {
-    if (callGraph.isBreaker(method, candidate)) {
-      // Cycle breaker so abort to preserve compilation order.
-      return false;
-    }
-
     if (method == candidate) {
       // Cannot handle recursive inlining at this point.
       // Force inlined method should never be recursive.
@@ -113,6 +112,13 @@
       return false;
     }
 
+    if (reason != Reason.FORCE && isProcessedConcurrently.test(candidate)) {
+      if (info != null) {
+        info.exclude(invoke, "is processed in parallel");
+      }
+      return false;
+    }
+
     // Abort inlining attempt if method -> target access is not right.
     if (!inliner.hasInliningAccess(method, candidate)) {
       if (info != null) {
@@ -176,7 +182,7 @@
     }
 
     Reason reason = computeInliningReason(candidate);
-    if (!candidate.isInliningCandidate(method, reason == Reason.FORCE, inliner.appInfo)) {
+    if (!candidate.isInliningCandidate(method, reason, inliner.appInfo)) {
       // Abort inlining attempt if the single target is not an inlining candidate.
       if (info != null) {
         info.exclude(invoke, "target is not identified for inlining");
@@ -202,7 +208,7 @@
 
     Reason reason = computeInliningReason(candidate);
     // Determine if this should be inlined no matter how big it is.
-    if (!candidate.isInliningCandidate(method, reason == Reason.FORCE, inliner.appInfo)) {
+    if (!candidate.isInliningCandidate(method, reason, inliner.appInfo)) {
       // Abort inlining attempt if the single target is not an inlining candidate.
       if (info != null) {
         info.exclude(invoke, "target is not identified for inlining");
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 9f3488c..9a87320 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -14,6 +14,7 @@
 import com.google.common.collect.ImmutableList;
 import java.nio.file.Path;
 import java.util.List;
+import java.util.Set;
 import java.util.function.Function;
 
 public class InternalOptions {
@@ -197,7 +198,7 @@
 
   public static class TestingOptions {
 
-    public Function<List<DexEncodedMethod>, List<DexEncodedMethod>> irOrdering =
+    public Function<Set<DexEncodedMethod>, Set<DexEncodedMethod>> irOrdering =
         Function.identity();
   }
 
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
index bc3d758..75a0275 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
@@ -11,19 +11,23 @@
 import com.android.tools.r8.shaking.ProguardRuleParserException;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.OutputMode;
+import com.beust.jcommander.internal.Lists;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import org.junit.Test;
 
 public class R8GMSCoreDeterministicTest extends GMSCoreCompilationTestBase {
 
-  public List<DexEncodedMethod> shuffle(List<DexEncodedMethod> methods) {
-    Collections.shuffle(methods);
-    return methods;
+  public Set<DexEncodedMethod> shuffle(Set<DexEncodedMethod> methods) {
+    List<DexEncodedMethod> toShuffle = Lists.newArrayList(methods);
+    Collections.shuffle(toShuffle);
+    return new LinkedHashSet<>(toShuffle);
   }
 
   private AndroidApp doRun()