Merge commit 'fd460d2100e349d16a6a95dc5d1b11db54177751' into dev-release

Change-Id: I516d2653ef1b33df8ff42a229f58a12422879fb7
diff --git a/src/main/java/com/android/tools/r8/AndroidResourceInput.java b/src/main/java/com/android/tools/r8/AndroidResourceInput.java
index 9403c75..dc2602e 100644
--- a/src/main/java/com/android/tools/r8/AndroidResourceInput.java
+++ b/src/main/java/com/android/tools/r8/AndroidResourceInput.java
@@ -23,11 +23,13 @@
     MANIFEST,
     // The resource table, in proto format.
     RESOURCE_TABLE,
-    // An xml file within the res folder, in proto format if not inside res/raw, otherwise
-    // in UTF-8 format.
+    // An xml file within the res folder, in proto format if not inside res/raw.
     XML_FILE,
     // Any other binary file withing the res folder.
     RES_FOLDER_FILE,
+    // A xml file in text format which should be parsed for keep/discard rules. The files is not
+    // required to include rules. The files will NOT be copied to the output provider.
+    KEEP_RULE_FILE,
     // Other files are ignored, but copied through
     UNKNOWN
   }
diff --git a/src/main/java/com/android/tools/r8/ArchiveProtoAndroidResourceProvider.java b/src/main/java/com/android/tools/r8/ArchiveProtoAndroidResourceProvider.java
index 7af6e19..e67c735 100644
--- a/src/main/java/com/android/tools/r8/ArchiveProtoAndroidResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/ArchiveProtoAndroidResourceProvider.java
@@ -35,6 +35,7 @@
   private static final String MANIFEST_NAME = "AndroidManifest.xml";
   private static final String RESOURCE_TABLE = "resources.pb";
   private static final String RES_FOLDER = "res/";
+  private static final String RES_RAW_FOLDER = RES_FOLDER + "raw/";
   private static final String XML_SUFFIX = ".xml";
 
   /**
@@ -56,13 +57,24 @@
       while (entries.hasMoreElements()) {
         ZipEntry entry = entries.nextElement();
         String name = entry.getName();
+        Kind kind = getKindFromName(name);
         ByteAndroidResourceInput resource =
             new ByteAndroidResourceInput(
                 name,
-                getKindFromName(name),
+                kind,
                 ByteStreams.toByteArray(zipFile.getInputStream(entry)),
                 new ArchiveEntryOrigin(name, origin));
         resources.add(resource);
+        // We explicitly add any res/raw folder xml file also as a keep rule file.
+        if (kind == Kind.XML_FILE && name.startsWith(RES_RAW_FOLDER)) {
+          ByteAndroidResourceInput keepResource =
+              new ByteAndroidResourceInput(
+                  name,
+                  Kind.KEEP_RULE_FILE,
+                  ByteStreams.toByteArray(zipFile.getInputStream(entry)),
+                  new ArchiveEntryOrigin(name, origin));
+          resources.add(keepResource);
+        }
       }
       return resources;
     } catch (IOException e) {
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index f84fd22..da49f2e 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -114,6 +114,7 @@
 import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.Reporter;
+import com.android.tools.r8.utils.ResourceShrinkerUtils;
 import com.android.tools.r8.utils.SelfRetraceTest;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.StringUtils;
@@ -968,6 +969,9 @@
       if (options.androidResourceProguardMapStrings != null) {
         resourceShrinkerBuilder.setProguardMapStrings(options.androidResourceProguardMapStrings);
       }
+      resourceShrinkerBuilder.setShrinkerDebugReporter(
+          ResourceShrinkerUtils.shrinkerDebugReporterFromStringConsumer(
+              options.resourceShrinkerConfiguration.getDebugConsumer(), reporter));
       LegacyResourceShrinker shrinker = resourceShrinkerBuilder.build();
       ShrinkerResult shrinkerResult;
       if (options.resourceShrinkerConfiguration.isOptimizedShrinking()) {
@@ -1024,6 +1028,9 @@
                   shrinkerResult.getResourceTableInProtoFormat(featureSplit)),
               reporter);
           break;
+        case KEEP_RULE_FILE:
+          // Intentionally not written
+          break;
         case RES_FOLDER_FILE:
         case XML_FILE:
           if (toKeep.contains(androidResource.getPath().location())) {
@@ -1059,6 +1066,9 @@
           case XML_FILE:
             resourceShrinkerBuilder.addXmlInput(path, bytes);
             break;
+          case KEEP_RULE_FILE:
+            resourceShrinkerBuilder.addKeepRuleInput(bytes);
+            break;
           case UNKNOWN:
             break;
         }
diff --git a/src/main/java/com/android/tools/r8/ResourceShrinkerConfiguration.java b/src/main/java/com/android/tools/r8/ResourceShrinkerConfiguration.java
index 1a5b781..82137e2 100644
--- a/src/main/java/com/android/tools/r8/ResourceShrinkerConfiguration.java
+++ b/src/main/java/com/android/tools/r8/ResourceShrinkerConfiguration.java
@@ -30,14 +30,17 @@
 @KeepForApi
 public class ResourceShrinkerConfiguration {
   public static ResourceShrinkerConfiguration DEFAULT_CONFIGURATION =
-      new ResourceShrinkerConfiguration(false, true);
+      new ResourceShrinkerConfiguration(false, true, null);
 
   private final boolean optimizedShrinking;
   private final boolean preciseShrinking;
+  private final StringConsumer debugConsumer;
 
-  private ResourceShrinkerConfiguration(boolean optimizedShrinking, boolean preciseShrinking) {
+  private ResourceShrinkerConfiguration(
+      boolean optimizedShrinking, boolean preciseShrinking, StringConsumer debugConsumer) {
     this.optimizedShrinking = optimizedShrinking;
     this.preciseShrinking = preciseShrinking;
+    this.debugConsumer = debugConsumer;
   }
 
   public static Builder builder(DiagnosticsHandler handler) {
@@ -52,6 +55,10 @@
     return preciseShrinking;
   }
 
+  public StringConsumer getDebugConsumer() {
+    return debugConsumer;
+  }
+
   /**
    * Builder for constructing a ResourceShrinkerConfiguration.
    *
@@ -63,6 +70,7 @@
 
     private boolean optimizedShrinking = false;
     private boolean preciseShrinking = true;
+    private StringConsumer debugConsumer;
 
     private Builder() {}
 
@@ -82,6 +90,11 @@
       return this;
     }
 
+    public Builder setDebugConsumer(StringConsumer consumer) {
+      this.debugConsumer = consumer;
+      return this;
+    }
+
     /**
      * Disable precise shrinking.
      *
@@ -97,7 +110,7 @@
 
     /** Build and return the {@link ResourceShrinkerConfiguration} */
     public ResourceShrinkerConfiguration build() {
-      return new ResourceShrinkerConfiguration(optimizedShrinking, preciseShrinking);
+      return new ResourceShrinkerConfiguration(optimizedShrinking, preciseShrinking, debugConsumer);
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/inlining/SimpleInliningConstraint.java b/src/main/java/com/android/tools/r8/ir/analysis/inlining/SimpleInliningConstraint.java
index cbeb329..e32cc71 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/inlining/SimpleInliningConstraint.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/inlining/SimpleInliningConstraint.java
@@ -61,7 +61,7 @@
     }
     assert isArgumentConstraint() || isDisjunction();
     assert other.isArgumentConstraint() || other.isDisjunction();
-    return new SimpleInliningConstraintConjunction(ImmutableList.of(this, other));
+    return SimpleInliningConstraintConjunction.create(ImmutableList.of(this, other));
   }
 
   public final SimpleInliningConstraintWithDepth lazyMeet(
@@ -90,7 +90,7 @@
     }
     assert isArgumentConstraint() || isConjunction();
     assert other.isArgumentConstraint() || other.isConjunction();
-    return new SimpleInliningConstraintDisjunction(ImmutableList.of(this, other));
+    return SimpleInliningConstraintDisjunction.create(ImmutableList.of(this, other));
   }
 
   public abstract SimpleInliningConstraint fixupAfterParametersChanged(
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/inlining/SimpleInliningConstraintAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/inlining/SimpleInliningConstraintAnalysis.java
index 41e9bb0..de956ca 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/inlining/SimpleInliningConstraintAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/inlining/SimpleInliningConstraintAnalysis.java
@@ -46,6 +46,8 @@
  */
 public class SimpleInliningConstraintAnalysis {
 
+  private static final int MAX_BRANCH_DEPTH = 3;
+
   private final SimpleInliningConstraintFactory constraintFactory;
   private final DexItemFactory dexItemFactory;
   private final ProgramMethod method;
@@ -78,18 +80,20 @@
     // returns.
     InstructionIterator instructionIterator =
         code.entryBlock().iterator(code.getNumberOfArguments());
-    return analyzeInstructionsInBlock(code.entryBlock(), 0, instructionIterator);
+    return analyzeInstructionsInBlock(code.entryBlock(), 0, 0, instructionIterator);
   }
 
   private SimpleInliningConstraintWithDepth analyzeInstructionsInBlock(
-      BasicBlock block, int depth) {
-    return analyzeInstructionsInBlock(block, depth, block.iterator());
+      BasicBlock block, int branchDepth, int instructionDepth) {
+    return analyzeInstructionsInBlock(block, branchDepth, instructionDepth, block.iterator());
   }
 
   private SimpleInliningConstraintWithDepth analyzeInstructionsInBlock(
-      BasicBlock block, int instructionDepth, InstructionIterator instructionIterator) {
-    // If we reach a block that has already been seen, or one that has catch handlers, then give up.
-    if (!seen.add(block) || block.hasCatchHandlers()) {
+      BasicBlock block,
+      int branchDepth,
+      int instructionDepth,
+      InstructionIterator instructionIterator) {
+    if (!seen.add(block) || block.hasCatchHandlers() || branchDepth > MAX_BRANCH_DEPTH) {
       return SimpleInliningConstraintWithDepth.getNever();
     }
 
@@ -119,7 +123,8 @@
     }
 
     SimpleInliningConstraintWithDepth jumpConstraint =
-        computeConstraintForJumpInstruction(instruction.asJumpInstruction(), instructionDepth);
+        computeConstraintForJumpInstruction(
+            instruction.asJumpInstruction(), branchDepth, instructionDepth);
     return jumpConstraint.meet(blockConstraint);
   }
 
@@ -154,79 +159,94 @@
   }
 
   private SimpleInliningConstraintWithDepth computeConstraintForJumpInstruction(
-      JumpInstruction instruction, int instructionDepth) {
+      JumpInstruction instruction, int branchDepth, int instructionDepth) {
     switch (instruction.opcode()) {
       case IF:
-        If ifInstruction = instruction.asIf();
-        Value singleArgumentOperand = getSingleArgumentOperand(ifInstruction);
-        if (singleArgumentOperand == null || singleArgumentOperand.isThis()) {
-          break;
+        {
+          If ifInstruction = instruction.asIf();
+          Value singleArgumentOperand = getSingleArgumentOperand(ifInstruction);
+          if (singleArgumentOperand == null || singleArgumentOperand.isThis()) {
+            break;
+          }
+
+          Value otherOperand =
+              ifInstruction.isZeroTest()
+                  ? null
+                  : ifInstruction.getOperand(
+                      1 - ifInstruction.inValues().indexOf(singleArgumentOperand));
+
+          int argumentIndex =
+              singleArgumentOperand.getAliasedValue().getDefinition().asArgument().getIndex();
+          DexType argumentType = method.getDefinition().getArgumentType(argumentIndex);
+          int currentBranchDepth = branchDepth;
+          int currentInstructionDepth = instructionDepth;
+
+          // Compute the constraint for which paths through the true target are guaranteed to exit
+          // early.
+          int newBranchDepth = currentBranchDepth + 1;
+          SimpleInliningConstraintWithDepth trueTargetConstraint =
+              computeConstraintFromIfTest(
+                      argumentIndex, argumentType, otherOperand, ifInstruction.getType())
+                  // Only recurse into the true target if the constraint from the if-instruction
+                  // is not 'never'.
+                  .lazyMeet(
+                      () ->
+                          analyzeInstructionsInBlock(
+                              ifInstruction.getTrueTarget(),
+                              newBranchDepth,
+                              currentInstructionDepth));
+
+          // Compute the constraint for which paths through the false target are guaranteed to
+          // exit early.
+          SimpleInliningConstraintWithDepth fallthroughTargetConstraint =
+              computeConstraintFromIfTest(
+                      argumentIndex, argumentType, otherOperand, ifInstruction.getType().inverted())
+                  // Only recurse into the false target if the constraint from the if-instruction
+                  // is not 'never'.
+                  .lazyMeet(
+                      () ->
+                          analyzeInstructionsInBlock(
+                              ifInstruction.fallthroughBlock(),
+                              newBranchDepth,
+                              currentInstructionDepth));
+
+          // Paths going through this basic block are guaranteed to exit early if the true target
+          // is guaranteed to exit early or the false target is.
+          return trueTargetConstraint.join(fallthroughTargetConstraint);
         }
 
-        Value otherOperand =
-            ifInstruction.isZeroTest()
-                ? null
-                : ifInstruction.getOperand(
-                    1 - ifInstruction.inValues().indexOf(singleArgumentOperand));
-
-        int argumentIndex =
-            singleArgumentOperand.getAliasedValue().getDefinition().asArgument().getIndex();
-        DexType argumentType = method.getDefinition().getArgumentType(argumentIndex);
-        int currentDepth = instructionDepth;
-
-        // Compute the constraint for which paths through the true target are guaranteed to exit
-        // early.
-        SimpleInliningConstraintWithDepth trueTargetConstraint =
-            computeConstraintFromIfTest(
-                    argumentIndex, argumentType, otherOperand, ifInstruction.getType())
-                // Only recurse into the true target if the constraint from the if-instruction
-                // is not 'never'.
-                .lazyMeet(
-                    () -> analyzeInstructionsInBlock(ifInstruction.getTrueTarget(), currentDepth));
-
-        // Compute the constraint for which paths through the false target are guaranteed to
-        // exit early.
-        SimpleInliningConstraintWithDepth fallthroughTargetConstraint =
-            computeConstraintFromIfTest(
-                    argumentIndex, argumentType, otherOperand, ifInstruction.getType().inverted())
-                // Only recurse into the false target if the constraint from the if-instruction
-                // is not 'never'.
-                .lazyMeet(
-                    () ->
-                        analyzeInstructionsInBlock(ifInstruction.fallthroughBlock(), currentDepth));
-
-        // Paths going through this basic block are guaranteed to exit early if the true target
-        // is guaranteed to exit early or the false target is.
-        return trueTargetConstraint.join(fallthroughTargetConstraint);
-
       case GOTO:
-        return analyzeInstructionsInBlock(instruction.asGoto().getTarget(), instructionDepth);
+        return analyzeInstructionsInBlock(
+            instruction.asGoto().getTarget(), branchDepth, instructionDepth);
 
       case RETURN:
         return AlwaysSimpleInliningConstraint.getInstance().withDepth(instructionDepth);
 
       case STRING_SWITCH:
-        // Require that all cases including the default case are simple. In that case we can
-        // guarantee simpleness by requiring that the switch value is constant.
-        StringSwitch stringSwitch = instruction.asStringSwitch();
-        Value valueRoot = stringSwitch.value().getAliasedValue();
-        if (!valueRoot.isDefinedByInstructionSatisfying(Instruction::isArgument)) {
-          return SimpleInliningConstraintWithDepth.getNever();
-        }
-        int maxInstructionDepth = instructionDepth;
-        for (BasicBlock successor : stringSwitch.getBlock().getNormalSuccessors()) {
-          SimpleInliningConstraintWithDepth successorConstraintWithDepth =
-              analyzeInstructionsInBlock(successor, instructionDepth);
-          if (!successorConstraintWithDepth.getConstraint().isAlways()) {
+        {
+          // Require that all cases including the default case are simple. In that case we can
+          // guarantee simpleness by requiring that the switch value is constant.
+          StringSwitch stringSwitch = instruction.asStringSwitch();
+          Value valueRoot = stringSwitch.value().getAliasedValue();
+          if (!valueRoot.isDefinedByInstructionSatisfying(Instruction::isArgument)) {
             return SimpleInliningConstraintWithDepth.getNever();
           }
-          maxInstructionDepth =
-              Math.max(maxInstructionDepth, successorConstraintWithDepth.getInstructionDepth());
+          int newBranchDepth = branchDepth + 1;
+          int maxInstructionDepth = instructionDepth;
+          for (BasicBlock successor : stringSwitch.getBlock().getNormalSuccessors()) {
+            SimpleInliningConstraintWithDepth successorConstraintWithDepth =
+                analyzeInstructionsInBlock(successor, newBranchDepth, instructionDepth);
+            if (!successorConstraintWithDepth.getConstraint().isAlways()) {
+              return SimpleInliningConstraintWithDepth.getNever();
+            }
+            maxInstructionDepth =
+                Math.max(maxInstructionDepth, successorConstraintWithDepth.getInstructionDepth());
+          }
+          Argument argument = valueRoot.getDefinition().asArgument();
+          ConstSimpleInliningConstraint simpleConstraint =
+              constraintFactory.createConstConstraint(argument.getIndex());
+          return simpleConstraint.withDepth(maxInstructionDepth);
         }
-        Argument argument = valueRoot.getDefinition().asArgument();
-        ConstSimpleInliningConstraint simpleConstraint =
-            constraintFactory.createConstConstraint(argument.getIndex());
-        return simpleConstraint.withDepth(maxInstructionDepth);
 
       case THROW:
         return instruction.getBlock().hasCatchHandlers()
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/inlining/SimpleInliningConstraintConjunction.java b/src/main/java/com/android/tools/r8/ir/analysis/inlining/SimpleInliningConstraintConjunction.java
index 6147cd8..a282039 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/inlining/SimpleInliningConstraintConjunction.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/inlining/SimpleInliningConstraintConjunction.java
@@ -15,9 +15,11 @@
 
 public class SimpleInliningConstraintConjunction extends SimpleInliningConstraint {
 
+  private static final int MAX_SIZE = 3;
+
   private final List<SimpleInliningConstraint> constraints;
 
-  public SimpleInliningConstraintConjunction(List<SimpleInliningConstraint> constraints) {
+  private SimpleInliningConstraintConjunction(List<SimpleInliningConstraint> constraints) {
     assert constraints.size() > 1;
     assert constraints.stream().noneMatch(SimpleInliningConstraint::isAlways);
     assert constraints.stream().noneMatch(SimpleInliningConstraint::isConjunction);
@@ -25,6 +27,12 @@
     this.constraints = constraints;
   }
 
+  public static SimpleInliningConstraint create(List<SimpleInliningConstraint> constraints) {
+    return constraints.size() <= MAX_SIZE
+        ? new SimpleInliningConstraintConjunction(constraints)
+        : NeverSimpleInliningConstraint.getInstance();
+  }
+
   SimpleInliningConstraint add(SimpleInliningConstraint constraint) {
     assert !constraint.isAlways();
     assert !constraint.isNever();
@@ -32,16 +40,15 @@
       return addAll(constraint.asConjunction());
     }
     assert constraint.isArgumentConstraint() || constraint.isDisjunction();
-    return new SimpleInliningConstraintConjunction(
+    return create(
         ImmutableList.<SimpleInliningConstraint>builder()
             .addAll(constraints)
             .add(constraint)
             .build());
   }
 
-  public SimpleInliningConstraintConjunction addAll(
-      SimpleInliningConstraintConjunction conjunction) {
-    return new SimpleInliningConstraintConjunction(
+  public SimpleInliningConstraint addAll(SimpleInliningConstraintConjunction conjunction) {
+    return create(
         ImmutableList.<SimpleInliningConstraint>builder()
             .addAll(constraints)
             .addAll(conjunction.constraints)
@@ -103,6 +110,6 @@
       return NeverSimpleInliningConstraint.getInstance();
     }
 
-    return new SimpleInliningConstraintConjunction(rewrittenConstraints);
+    return create(rewrittenConstraints);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/inlining/SimpleInliningConstraintDisjunction.java b/src/main/java/com/android/tools/r8/ir/analysis/inlining/SimpleInliningConstraintDisjunction.java
index 02c0b01..660aa3b 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/inlining/SimpleInliningConstraintDisjunction.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/inlining/SimpleInliningConstraintDisjunction.java
@@ -15,9 +15,11 @@
 
 public class SimpleInliningConstraintDisjunction extends SimpleInliningConstraint {
 
+  private static final int MAX_SIZE = 3;
+
   private final List<SimpleInliningConstraint> constraints;
 
-  public SimpleInliningConstraintDisjunction(List<SimpleInliningConstraint> constraints) {
+  private SimpleInliningConstraintDisjunction(List<SimpleInliningConstraint> constraints) {
     assert constraints.size() > 1;
     assert constraints.stream().noneMatch(SimpleInliningConstraint::isAlways);
     assert constraints.stream().noneMatch(SimpleInliningConstraint::isDisjunction);
@@ -25,6 +27,12 @@
     this.constraints = constraints;
   }
 
+  public static SimpleInliningConstraint create(List<SimpleInliningConstraint> constraints) {
+    return constraints.size() <= MAX_SIZE
+        ? new SimpleInliningConstraintDisjunction(constraints)
+        : NeverSimpleInliningConstraint.getInstance();
+  }
+
   SimpleInliningConstraint add(SimpleInliningConstraint constraint) {
     assert !constraint.isAlways();
     assert !constraint.isNever();
@@ -32,16 +40,15 @@
       return addAll(constraint.asDisjunction());
     }
     assert constraint.isArgumentConstraint() || constraint.isConjunction();
-    return new SimpleInliningConstraintDisjunction(
+    return create(
         ImmutableList.<SimpleInliningConstraint>builder()
             .addAll(constraints)
             .add(constraint)
             .build());
   }
 
-  public SimpleInliningConstraintDisjunction addAll(
-      SimpleInliningConstraintDisjunction disjunction) {
-    return new SimpleInliningConstraintDisjunction(
+  public SimpleInliningConstraint addAll(SimpleInliningConstraintDisjunction disjunction) {
+    return create(
         ImmutableList.<SimpleInliningConstraint>builder()
             .addAll(constraints)
             .addAll(disjunction.constraints)
@@ -103,6 +110,6 @@
       return AlwaysSimpleInliningConstraint.getInstance();
     }
 
-    return new SimpleInliningConstraintDisjunction(rewrittenConstraints);
+    return create(rewrittenConstraints);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/path/PathConstraintAnalysisTransferFunction.java b/src/main/java/com/android/tools/r8/ir/analysis/path/PathConstraintAnalysisTransferFunction.java
index 11fe070..c61df2a 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/path/PathConstraintAnalysisTransferFunction.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/path/PathConstraintAnalysisTransferFunction.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.framework.intraprocedural.AbstractTransferFunction;
 import com.android.tools.r8.ir.analysis.framework.intraprocedural.TransferFunctionResult;
+import com.android.tools.r8.ir.analysis.path.state.ConcretePathConstraintAnalysisState;
 import com.android.tools.r8.ir.analysis.path.state.PathConstraintAnalysisState;
 import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
 import com.android.tools.r8.ir.code.BasicBlock;
@@ -41,6 +42,14 @@
   }
 
   @Override
+  public PathConstraintAnalysisState computeInitialState(
+      BasicBlock entryBlock, PathConstraintAnalysisState bottom) {
+    // Intentionally returns an empty state instead of BOTTOM, as BOTTOM is used to represent the
+    // path constraint for unreachable program points.
+    return new ConcretePathConstraintAnalysisState();
+  }
+
+  @Override
   public PathConstraintAnalysisState computeBlockEntryState(
       BasicBlock block, BasicBlock predecessor, PathConstraintAnalysisState predecessorExitState) {
     if (predecessorExitState.isUnknown()) {
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/path/PathConstraintSupplier.java b/src/main/java/com/android/tools/r8/ir/analysis/path/PathConstraintSupplier.java
new file mode 100644
index 0000000..cb81605
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/path/PathConstraintSupplier.java
@@ -0,0 +1,41 @@
+// 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.ir.analysis.path;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.ir.analysis.framework.intraprocedural.DataflowAnalysisResult.SuccessfulDataflowAnalysisResult;
+import com.android.tools.r8.ir.analysis.path.state.PathConstraintAnalysisState;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodParameterFactory;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+
+public class PathConstraintSupplier {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final IRCode code;
+  private final MethodParameterFactory methodParameterFactory;
+
+  private SuccessfulDataflowAnalysisResult<BasicBlock, PathConstraintAnalysisState>
+      pathConstraintAnalysisResult;
+
+  public PathConstraintSupplier(
+      AppView<AppInfoWithLiveness> appView,
+      IRCode code,
+      MethodParameterFactory methodParameterFactory) {
+    this.appView = appView;
+    this.code = code;
+    this.methodParameterFactory = methodParameterFactory;
+  }
+
+  public PathConstraintAnalysisState getPathConstraint(BasicBlock block) {
+    if (pathConstraintAnalysisResult == null) {
+      PathConstraintAnalysis analysis =
+          new PathConstraintAnalysis(appView, code, methodParameterFactory);
+      pathConstraintAnalysisResult = analysis.run(code.entryBlock()).asSuccessfulAnalysisResult();
+      assert pathConstraintAnalysisResult != null;
+    }
+    return pathConstraintAnalysisResult.getBlockExitState(block);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/path/state/ConcretePathConstraintAnalysisState.java b/src/main/java/com/android/tools/r8/ir/analysis/path/state/ConcretePathConstraintAnalysisState.java
index fca5f48..7bcb650 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/path/state/ConcretePathConstraintAnalysisState.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/path/state/ConcretePathConstraintAnalysisState.java
@@ -3,11 +3,12 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.analysis.path.state;
 
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.optimize.argumentpropagation.computation.ComputationTreeNode;
 import java.util.Collections;
-import java.util.HashSet;
-import java.util.Objects;
-import java.util.Set;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
 
 /**
  * Represents a non-trivial (neither bottom nor top) path constraint that must be satisfied to reach
@@ -16,19 +17,18 @@
  * <p>The state should be interpreted as follows:
  *
  * <ol>
- *   <li>If a path constraint is BOTH in {@link #pathConstraints} AND {@link
- *       #negatedPathConstraints} then the path constraint should be IGNORED.
- *   <li>If a path constraint is ONLY in {@link #pathConstraints} then the current program point can
- *       only be reached if the path constraint is satisfied.
- *   <li>If a path constraint is ONLY in {@link #negatedPathConstraints} then the current program
- *       point can only be reached if the path constraint is NOT satisfied.
+ *   <li>If a path constraint is DISABLED then the path constraint should be IGNORED.
+ *   <li>If a path constraint is POSITIVE then the current program point can only be reached if the
+ *       path constraint is satisfied.
+ *   <li>If a path constraint is NEGATIVE then the current program point can only be reached if the
+ *       path constraint is NOT satisfied.
  * </ol>
  *
- * <p>Example: In the below example, when entering the IF-THEN branch, we add the path constraint
- * [(flags & 1) != 0] to {@link #pathConstraints}. When entering the (empty) IF-ELSE branch, we add
- * the same path constraint to {@link #negatedPathConstraints}. When reaching the return block, the
- * two path constraint states are joined, resulting in the state where {@link #pathConstraints} and
- * {@link #negatedPathConstraints} both contains the constraint [(flags & 1) != 0].
+ * <p>Example: In the below example, when entering the IF-THEN branch, we add [(flags & 1) != 0] as
+ * a POSITIVE path constraint to {@link #pathConstraints}. When entering the (empty) IF-ELSE branch,
+ * we add the same expression as a NEGATIVE path constraint to {@link #pathConstraints}. When
+ * reaching the return block, the two path constraint states are joined, resulting in the state
+ * where the constraint [(flags & 1) != 0] is DISABLED.
  *
  * <pre>
  *   static Object Foo(Object o, int flags) {
@@ -41,61 +41,54 @@
  */
 public class ConcretePathConstraintAnalysisState extends PathConstraintAnalysisState {
 
-  private final Set<ComputationTreeNode> pathConstraints;
-  private final Set<ComputationTreeNode> negatedPathConstraints;
+  // TODO(b/302281503): Consider changing this to an ImmutableMap.
+  private final Map<ComputationTreeNode, PathConstraintKind> pathConstraints;
+
+  public ConcretePathConstraintAnalysisState() {
+    this.pathConstraints = Collections.emptyMap();
+  }
 
   ConcretePathConstraintAnalysisState(
-      Set<ComputationTreeNode> pathConstraints, Set<ComputationTreeNode> negatedPathConstraints) {
+      Map<ComputationTreeNode, PathConstraintKind> pathConstraints) {
     this.pathConstraints = pathConstraints;
-    this.negatedPathConstraints = negatedPathConstraints;
   }
 
   static ConcretePathConstraintAnalysisState create(
       ComputationTreeNode pathConstraint, boolean negate) {
-    Set<ComputationTreeNode> pathConstraints = Collections.singleton(pathConstraint);
-    if (negate) {
-      return new ConcretePathConstraintAnalysisState(Collections.emptySet(), pathConstraints);
-    } else {
-      return new ConcretePathConstraintAnalysisState(pathConstraints, Collections.emptySet());
-    }
+    return new ConcretePathConstraintAnalysisState(
+        Collections.singletonMap(pathConstraint, PathConstraintKind.get(negate)));
   }
 
   @Override
   public PathConstraintAnalysisState add(ComputationTreeNode pathConstraint, boolean negate) {
-    if (negate) {
-      if (negatedPathConstraints.contains(pathConstraint)) {
+    PathConstraintKind previousKind = pathConstraints.get(pathConstraint);
+    if (previousKind != null) {
+      if (previousKind == PathConstraintKind.DISABLED) {
+        // There is a loop.
         return this;
       }
-      return new ConcretePathConstraintAnalysisState(
-          pathConstraints, add(pathConstraint, negatedPathConstraints));
-    } else {
-      if (pathConstraints.contains(pathConstraint)) {
+      if (previousKind == PathConstraintKind.get(negate)) {
+        // This branch is dominated by a previous if-condition that has the same branch condition,
+        // e.g., if (x) { if (x) { ...
         return this;
       }
-      return new ConcretePathConstraintAnalysisState(
-          add(pathConstraint, pathConstraints), negatedPathConstraints);
+      // This branch is dominated by a previous if-condition that has the negated branch condition,
+      // e.g., if (x) { if (!x) { ...
+      return bottom();
     }
+    // No jumps can dominate the entry of their own block, so when adding the condition of a jump
+    // this cannot currently be active.
+    Map<ComputationTreeNode, PathConstraintKind> newPathConstraints =
+        new HashMap<>(pathConstraints.size() + 1);
+    newPathConstraints.putAll(pathConstraints);
+    newPathConstraints.put(pathConstraint, PathConstraintKind.get(negate));
+    return new ConcretePathConstraintAnalysisState(newPathConstraints);
   }
 
-  private static Set<ComputationTreeNode> add(
-      ComputationTreeNode pathConstraint, Set<ComputationTreeNode> pathConstraints) {
-    if (pathConstraints.isEmpty()) {
-      return Collections.singleton(pathConstraint);
-    }
-    assert !pathConstraints.contains(pathConstraint);
-    Set<ComputationTreeNode> newPathConstraints = new HashSet<>(pathConstraints);
-    newPathConstraints.add(pathConstraint);
-    return newPathConstraints;
-  }
-
-  public Set<ComputationTreeNode> getPathConstraints() {
+  public Map<ComputationTreeNode, PathConstraintKind> getPathConstraintsForTesting() {
     return pathConstraints;
   }
 
-  public Set<ComputationTreeNode> getNegatedPathConstraints() {
-    return negatedPathConstraints;
-  }
-
   @Override
   public boolean isConcrete() {
     return true;
@@ -106,55 +99,87 @@
     return this;
   }
 
+  @Override
+  public boolean isGreaterThanOrEquals(AppView<?> appView, PathConstraintAnalysisState state) {
+    if (state.isConcrete()) {
+      return isGreaterThanOrEquals(state.asConcreteState());
+    }
+    return super.isGreaterThanOrEquals(appView, state);
+  }
+
+  private boolean isGreaterThanOrEquals(ConcretePathConstraintAnalysisState other) {
+    // The current state must contain all keys of the other state.
+    if (pathConstraints.size() < other.pathConstraints.size()) {
+      return false;
+    }
+    for (ComputationTreeNode otherPathConstraint : other.pathConstraints.keySet()) {
+      if (!pathConstraints.containsKey(otherPathConstraint)) {
+        return false;
+      }
+    }
+    // The current path constraint kinds must be the join of the kinds.
+    for (Entry<ComputationTreeNode, PathConstraintKind> entry : pathConstraints.entrySet()) {
+      ComputationTreeNode pathConstraint = entry.getKey();
+      PathConstraintKind kind = entry.getValue();
+      PathConstraintKind otherKind = other.pathConstraints.get(pathConstraint);
+      PathConstraintKind joinKind = kind.join(otherKind);
+      if (kind != joinKind) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  public boolean isNegated(ComputationTreeNode pathConstraint) {
+    PathConstraintKind kind = pathConstraints.get(pathConstraint);
+    assert kind != null;
+    assert kind != PathConstraintKind.DISABLED;
+    return kind == PathConstraintKind.NEGATIVE;
+  }
+
   // TODO(b/302281503): Consider returning the list of differentiating path constraints.
   //  For example, if we have the condition X & Y, then we may not know anything about X but we
   //  could know something about Y. Add a test showing this.
-  // TODO(b/302281503): Add a field `inactivePathConstraints` and ensure that pathConstraints and
-  //  negatedPathConstraints are disjoint. This way (1) we reduce the size of the sets and (2)
-  //  this does not need to check if each path constraint is not in the negated set.
   public ComputationTreeNode getDifferentiatingPathConstraint(
       ConcretePathConstraintAnalysisState other) {
-    for (ComputationTreeNode pathConstraint : pathConstraints) {
-      if (!negatedPathConstraints.contains(pathConstraint)
-          && other.negatedPathConstraints.contains(pathConstraint)) {
-        return pathConstraint;
+    for (Entry<ComputationTreeNode, PathConstraintKind> entry : pathConstraints.entrySet()) {
+      ComputationTreeNode pathConstraint = entry.getKey();
+      PathConstraintKind kind = entry.getValue();
+      if (kind == PathConstraintKind.DISABLED) {
+        continue;
       }
-    }
-    for (ComputationTreeNode negatedPathConstraint : negatedPathConstraints) {
-      if (!pathConstraints.contains(negatedPathConstraint)
-          && other.pathConstraints.contains(negatedPathConstraint)) {
-        return negatedPathConstraint;
+      PathConstraintKind otherKind = other.pathConstraints.get(pathConstraint);
+      if (otherKind != null && kind.isNegation(otherKind)) {
+        return pathConstraint;
       }
     }
     return null;
   }
 
   public ConcretePathConstraintAnalysisState join(ConcretePathConstraintAnalysisState other) {
-    Set<ComputationTreeNode> newPathConstraints = join(pathConstraints, other.pathConstraints);
-    Set<ComputationTreeNode> newNegatedPathConstraints =
-        join(negatedPathConstraints, other.negatedPathConstraints);
-    if (identical(newPathConstraints, newNegatedPathConstraints)) {
+    if (isGreaterThanOrEquals(other)) {
       return this;
     }
-    if (other.identical(newPathConstraints, newNegatedPathConstraints)) {
+    if (other.isGreaterThanOrEquals(this)) {
       return other;
     }
-    return new ConcretePathConstraintAnalysisState(newPathConstraints, newNegatedPathConstraints);
+    Map<ComputationTreeNode, PathConstraintKind> newPathConstraints =
+        new HashMap<>(pathConstraints.size() + other.pathConstraints.size());
+    join(other, newPathConstraints);
+    other.join(this, newPathConstraints);
+    return new ConcretePathConstraintAnalysisState(newPathConstraints);
   }
 
-  private static Set<ComputationTreeNode> join(
-      Set<ComputationTreeNode> pathConstraints, Set<ComputationTreeNode> otherPathConstraints) {
-    if (pathConstraints.isEmpty()) {
-      return otherPathConstraints;
+  private void join(
+      ConcretePathConstraintAnalysisState other,
+      Map<ComputationTreeNode, PathConstraintKind> newPathConstraints) {
+    for (Entry<ComputationTreeNode, PathConstraintKind> entry : pathConstraints.entrySet()) {
+      ComputationTreeNode pathConstraint = entry.getKey();
+      PathConstraintKind kind = entry.getValue();
+      PathConstraintKind otherKind = other.pathConstraints.get(pathConstraint);
+      PathConstraintKind joinKind = kind.join(otherKind);
+      newPathConstraints.put(pathConstraint, joinKind);
     }
-    if (otherPathConstraints.isEmpty()) {
-      return pathConstraints;
-    }
-    Set<ComputationTreeNode> newPathConstraints =
-        new HashSet<>(pathConstraints.size() + otherPathConstraints.size());
-    newPathConstraints.addAll(pathConstraints);
-    newPathConstraints.addAll(otherPathConstraints);
-    return newPathConstraints;
   }
 
   @Override
@@ -166,23 +191,11 @@
       return false;
     }
     ConcretePathConstraintAnalysisState state = (ConcretePathConstraintAnalysisState) obj;
-    return equals(state.pathConstraints, state.negatedPathConstraints);
-  }
-
-  public boolean equals(
-      Set<ComputationTreeNode> pathConstraints, Set<ComputationTreeNode> negatedPathConstraints) {
-    return this.pathConstraints.equals(pathConstraints)
-        && this.negatedPathConstraints.equals(negatedPathConstraints);
-  }
-
-  public boolean identical(
-      Set<ComputationTreeNode> pathConstraints, Set<ComputationTreeNode> negatedPathConstraints) {
-    return this.pathConstraints == pathConstraints
-        && this.negatedPathConstraints == negatedPathConstraints;
+    return pathConstraints.equals(state.pathConstraints);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(pathConstraints, negatedPathConstraints);
+    return pathConstraints.hashCode();
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/path/state/PathConstraintKind.java b/src/main/java/com/android/tools/r8/ir/analysis/path/state/PathConstraintKind.java
new file mode 100644
index 0000000..4e97d27
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/path/state/PathConstraintKind.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.ir.analysis.path.state;
+
+public enum PathConstraintKind {
+  POSITIVE,
+  NEGATIVE,
+  DISABLED;
+
+  static PathConstraintKind get(boolean negate) {
+    return negate ? NEGATIVE : POSITIVE;
+  }
+
+  boolean isNegation(PathConstraintKind other) {
+    switch (this) {
+      case POSITIVE:
+        return other == NEGATIVE;
+      case NEGATIVE:
+        return other == POSITIVE;
+      default:
+        return false;
+    }
+  }
+
+  PathConstraintKind join(PathConstraintKind other) {
+    return this == other ? this : DISABLED;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/NumberFromIntervalValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/NumberFromIntervalValue.java
index 161f1a1..450c351 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/NumberFromIntervalValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/NumberFromIntervalValue.java
@@ -105,7 +105,7 @@
 
   @Override
   public int hashCode() {
-    int hash = 31 * (31 * (31 + Long.hashCode(minInclusive)) + Long.hashCode(maxInclusive));
+    int hash = 31 * (31 + Long.hashCode(minInclusive)) + Long.hashCode(maxInclusive);
     assert hash == Objects.hash(minInclusive, maxInclusive);
     return hash;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
index 9ab29f3..2d4b2ee 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
@@ -204,13 +204,10 @@
 
     TrivialGotosCollapser trivialGotosCollapser = new TrivialGotosCollapser(appView);
     timing.begin("BasicBlock peephole optimizations");
-    if (code.getConversionOptions().isPeepholeOptimizationsEnabled()) {
-      for (int i = 0; i < PEEPHOLE_OPTIMIZATION_PASSES; i++) {
-        trivialGotosCollapser.run(code, timing);
-        PeepholeOptimizer.removeIdenticalPredecessorBlocks(code, registerAllocator);
-        PeepholeOptimizer.shareIdenticalBlockSuffix(
-            code, registerAllocator, SUFFIX_SHARING_OVERHEAD);
-      }
+    for (int i = 0; i < PEEPHOLE_OPTIMIZATION_PASSES; i++) {
+      trivialGotosCollapser.run(code, timing);
+      PeepholeOptimizer.removeIdenticalPredecessorBlocks(code, registerAllocator);
+      PeepholeOptimizer.shareIdenticalBlockSuffix(code, registerAllocator, SUFFIX_SHARING_OVERHEAD);
     }
     timing.end();
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRToDexFinalizer.java b/src/main/java/com/android/tools/r8/ir/conversion/IRToDexFinalizer.java
index 205d2bc..edac43f 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRToDexFinalizer.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRToDexFinalizer.java
@@ -76,14 +76,12 @@
     registerAllocator.allocateRegisters();
     timing.end();
     TrivialGotosCollapser trivialGotosCollapser = new TrivialGotosCollapser(appView);
-    if (code.getConversionOptions().isPeepholeOptimizationsEnabled()) {
-      timing.begin("Peephole optimize");
-      for (int i = 0; i < PEEPHOLE_OPTIMIZATION_PASSES; i++) {
-        trivialGotosCollapser.run(code, timing);
-        PeepholeOptimizer.optimize(appView, code, registerAllocator);
-      }
-      timing.end();
+    timing.begin("Peephole optimize");
+    for (int i = 0; i < PEEPHOLE_OPTIMIZATION_PASSES; i++) {
+      trivialGotosCollapser.run(code, timing);
+      PeepholeOptimizer.optimize(appView, code, registerAllocator);
     }
+    timing.end();
     timing.begin("Clean up");
     CodeRewriter.removeUnneededMovesOnExitingPaths(code, registerAllocator);
     trivialGotosCollapser.run(code, timing);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
index 679a884..72e0051 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
@@ -107,7 +107,6 @@
 import com.android.tools.r8.ir.code.UnusedArgument;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.code.ValueType;
-import com.android.tools.r8.ir.conversion.passes.TrivialPhiSimplifier;
 import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.CustomLensCodeRewriter;
 import com.android.tools.r8.optimize.MemberRebindingAnalysis;
@@ -233,15 +232,6 @@
       affectedPhis.addAll(
           customLensCodeRewriter.rewriteCode(code, methodProcessor, prototypeChanges, graphLens));
     }
-    if (!unusedArguments.isEmpty()) {
-      for (UnusedArgument unusedArgument : unusedArguments) {
-        if (unusedArgument.outValue().hasPhiUsers()) {
-          // See b/240282988: We can end up in situations where the second round of IR processing
-          // introduce phis for irreducible control flow, we need to resolve them.
-          TrivialPhiSimplifier.replaceUnusedArgumentTrivialPhis(unusedArgument);
-        }
-      }
-    }
     rewritePartialDefault(
         code,
         method,
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/MethodConversionOptions.java b/src/main/java/com/android/tools/r8/ir/conversion/MethodConversionOptions.java
index fd3c580..970479e 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/MethodConversionOptions.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/MethodConversionOptions.java
@@ -78,25 +78,17 @@
 
   public abstract boolean isGeneratingDex();
 
-  public abstract boolean isPeepholeOptimizationsEnabled();
-
   public abstract boolean shouldFinalizeAfterLensCodeRewriter();
 
   public static class MutableMethodConversionOptions extends MethodConversionOptions {
 
     private final Target target;
-    private boolean enablePeepholeOptimizations = true;
     private boolean finalizeAfterLensCodeRewriter;
 
     private MutableMethodConversionOptions(Target target) {
       this.target = target;
     }
 
-    public void disablePeepholeOptimizations(MethodProcessor methodProcessor) {
-      assert methodProcessor.isPrimaryMethodProcessor();
-      enablePeepholeOptimizations = false;
-    }
-
     public MutableMethodConversionOptions setFinalizeAfterLensCodeRewriter() {
       finalizeAfterLensCodeRewriter = true;
       return this;
@@ -118,11 +110,6 @@
     }
 
     @Override
-    public boolean isPeepholeOptimizationsEnabled() {
-      return enablePeepholeOptimizations;
-    }
-
-    @Override
     public boolean shouldFinalizeAfterLensCodeRewriter() {
       return finalizeAfterLensCodeRewriter;
     }
@@ -148,10 +135,5 @@
     public boolean isGeneratingDex() {
       throw new Unreachable();
     }
-
-    @Override
-    public boolean isPeepholeOptimizationsEnabled() {
-      throw new Unreachable();
-    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java
index 59a9a0e..3b3ba7a 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryR8IRConverter.java
@@ -135,7 +135,6 @@
     // Analyze the data collected by the argument propagator, use the analysis result to update
     // the parameter optimization infos, and rewrite the application.
     // TODO(b/199237357): Automatically rewrite state when lens changes.
-    enumUnboxer.rewriteWithLens();
     numberUnboxer.rewriteWithLens();
     outliner.rewriteWithLens();
     appView.withArgumentPropagator(
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialPhiSimplifier.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialPhiSimplifier.java
index 27a3cba..eacb3aa 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialPhiSimplifier.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialPhiSimplifier.java
@@ -14,7 +14,6 @@
 import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.code.NewInstance;
 import com.android.tools.r8.ir.code.Phi;
-import com.android.tools.r8.ir.code.UnusedArgument;
 import com.android.tools.r8.ir.code.Value;
 import com.google.common.collect.Sets;
 import java.util.List;
@@ -22,34 +21,22 @@
 
 public class TrivialPhiSimplifier {
 
-  public static void replaceUnusedArgumentTrivialPhis(UnusedArgument unusedArgument) {
-    replaceTrivialPhis(unusedArgument.outValue());
-    for (Phi phiUser : unusedArgument.outValue().uniquePhiUsers()) {
-      phiUser.removeTrivialPhi();
-    }
-    assert !unusedArgument.outValue().hasPhiUsers();
-  }
-
-  @SuppressWarnings("ReferenceEquality")
   public static void ensureDirectStringNewToInit(AppView<?> appView, IRCode code) {
     boolean changed = false;
     DexItemFactory dexItemFactory = appView.dexItemFactory();
-    for (Instruction instruction : code.instructions()) {
-      if (instruction.isInvokeDirect()) {
-        InvokeDirect invoke = instruction.asInvokeDirect();
-        DexMethod method = invoke.getInvokedMethod();
-        if (dexItemFactory.isConstructor(method)
-            && method.holder == dexItemFactory.stringType
-            && invoke.getReceiver().isPhi()) {
-          NewInstance newInstance = findNewInstance(invoke.getReceiver().asPhi());
-          replaceTrivialPhis(newInstance.outValue());
-          if (invoke.getReceiver().isPhi()) {
-            throw new CompilationError(
-                "Failed to remove trivial phis between new-instance and <init>");
-          }
-          newInstance.markNoSpilling();
-          changed = true;
+    for (InvokeDirect invoke : code.<InvokeDirect>instructions(Instruction::isInvokeDirect)) {
+      DexMethod method = invoke.getInvokedMethod();
+      if (dexItemFactory.isConstructor(method)
+          && method.getHolderType().isIdenticalTo(dexItemFactory.stringType)
+          && invoke.getReceiver().isPhi()) {
+        NewInstance newInstance = findNewInstance(invoke.getReceiver().asPhi());
+        replaceTrivialPhis(newInstance.outValue());
+        if (invoke.getReceiver().isPhi()) {
+          throw new CompilationError(
+              "Failed to remove trivial phis between new-instance and <init>");
         }
+        newInstance.markNoSpilling();
+        changed = true;
       }
     }
     assert !changed || code.isConsistentSSA(appView);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
index d7bf459..520e2d8 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
@@ -38,6 +38,7 @@
 import com.android.tools.r8.ir.desugar.ServiceLoaderSourceCode;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.ConsumerUtils;
+import com.android.tools.r8.utils.DominatorChecker;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.WorkList;
 import java.util.ArrayList;
@@ -231,9 +232,9 @@
     replacements.put(loadResult.iteratorInvoke, code.createConstNull());
 
     // Iterator.hasNext() --> true / false
-    if (directRewriteResult.hasNextInstr != null) {
+    if (directRewriteResult.priorHasNextInstr != null) {
       replacements.put(
-          directRewriteResult.hasNextInstr,
+          directRewriteResult.priorHasNextInstr,
           code.createBooleanConstant(!loadResult.implClasses.isEmpty()));
     }
     if (directRewriteResult.nextInstr != null) {
@@ -279,6 +280,11 @@
         replacementExtras.put(directRewriteResult.nextInstr, List.of(initInstr));
       }
     }
+    if (directRewriteResult.subsequentHasNextInstr != null) {
+      replacements.put(
+          directRewriteResult.subsequentHasNextInstr,
+          code.createBooleanConstant(loadResult.implClasses.size() > 1));
+    }
   }
 
   private ServiceLoaderLoadResult analyzeServiceLoaderLoad(IRCode code, InvokeStatic invokeInstr) {
@@ -428,8 +434,9 @@
     if (iteratorInvoke.outValue().hasPhiUsers()) {
       return null;
     }
-    InvokeMethod hasNextInstr = null;
+    InvokeMethod priorHasNextInstr = null;
     InvokeMethod nextInstr = null;
+    InvokeMethod subsequentHasNextInstr = null;
     // We only bother to support a single call to hasNext() and next(), and they must appear within
     // the same try/catch.
     for (Instruction user : iteratorInvoke.outValue().aliasedUsers()) {
@@ -447,10 +454,14 @@
         return null;
       }
       if (curCall.getInvokedMethod().isIdenticalTo(iteratorMethods.hasNext)) {
-        if (hasNextInstr != null) {
+        if (subsequentHasNextInstr != null) {
           return null;
         }
-        hasNextInstr = curCall;
+        if (priorHasNextInstr != null) {
+          subsequentHasNextInstr = curCall;
+        } else {
+          priorHasNextInstr = curCall;
+        }
       } else if (curCall.getInvokedMethod().isIdenticalTo(iteratorMethods.next)) {
         if (nextInstr != null) {
           return null;
@@ -461,29 +472,58 @@
       }
     }
 
+    // Figure out which hasNext is the first one.
     BasicBlock iteratorBlock = iteratorInvoke.getBlock();
-    BasicBlock hasNextBlock = hasNextInstr != null ? hasNextInstr.getBlock() : null;
+    BasicBlock priorHasNextBlock = priorHasNextInstr != null ? priorHasNextInstr.getBlock() : null;
+    BasicBlock subsequentHasNextBlock =
+        subsequentHasNextInstr != null ? subsequentHasNextInstr.getBlock() : null;
     BasicBlock nextBlock = nextInstr != null ? nextInstr.getBlock() : null;
-    if (hasNextBlock != null && nextBlock != null) {
-      // See if hasNext() is reachable after next().
-      if (hasNextBlock == nextBlock) {
-        if (nextBlock.iterator(nextInstr).nextUntil(hasNextInstr) != null) {
+    if (priorHasNextBlock != null && nextBlock != null) {
+      if (subsequentHasNextBlock != null) {
+        if (priorHasNextBlock == subsequentHasNextBlock) {
+          // This would be odd, since hasNext() is usually used in conditionals.
           return null;
         }
-      } else if (hasPredecessorPathTo(iteratorBlock, hasNextBlock, nextBlock)) {
+        // Make sure subsequentHasNextBlock and priorHasNextBlock are not reversed.
+        if (hasPredecessorPathTo(iteratorBlock, priorHasNextBlock, subsequentHasNextBlock)) {
+          InvokeMethod tmp = priorHasNextInstr;
+          priorHasNextInstr = subsequentHasNextInstr;
+          subsequentHasNextInstr = tmp;
+          priorHasNextBlock = subsequentHasNextBlock;
+          subsequentHasNextBlock = subsequentHasNextInstr.getBlock();
+        }
+        // Ensure the second hasNext() comes after the call to next().
+        if (nextBlock == subsequentHasNextBlock) {
+          if (nextBlock.iterator(nextInstr).nextUntil(subsequentHasNextInstr) == null) {
+            return null;
+          }
+        } else if (!DominatorChecker.check(iteratorBlock, subsequentHasNextBlock, nextBlock)) {
+          return null;
+        }
+      }
+
+      // Ensure the first hasNext() is not after next().
+      if (priorHasNextBlock == nextBlock) {
+        if (nextBlock.iterator(nextInstr).nextUntil(priorHasNextInstr) != null) {
+          return null;
+        }
+      } else if (hasPredecessorPathTo(iteratorBlock, priorHasNextBlock, nextBlock)) {
         return null;
       }
     }
 
     // Make sure each instruction can be run at most once (no loops).
-    if (hasNextBlock != null && loopExists(iteratorBlock, hasNextBlock)) {
+    if (priorHasNextBlock != null && loopExists(iteratorBlock, priorHasNextBlock)) {
       return null;
     }
     if (nextBlock != null && loopExists(iteratorBlock, nextBlock)) {
       return null;
     }
+    if (subsequentHasNextBlock != null && loopExists(iteratorBlock, subsequentHasNextBlock)) {
+      return null;
+    }
 
-    return new DirectRewriteResult(hasNextInstr, nextInstr);
+    return new DirectRewriteResult(priorHasNextInstr, nextInstr, subsequentHasNextInstr);
   }
 
   private static boolean loopExists(BasicBlock subgraphEntryBlock, BasicBlock targetBlock) {
@@ -495,6 +535,9 @@
     if (subgraphEntryBlock == subgraphExitBlock) {
       return false;
     }
+    if (subgraphEntryBlock == targetBlock) {
+      return true;
+    }
     WorkList<BasicBlock> workList = WorkList.newIdentityWorkList();
     workList.markAsSeen(subgraphEntryBlock);
     workList.addIfNotSeen(subgraphExitBlock.getPredecessors());
@@ -636,12 +679,17 @@
   }
 
   private static class DirectRewriteResult {
+    public final InvokeMethod priorHasNextInstr;
     public final InvokeMethod nextInstr;
-    public final InvokeMethod hasNextInstr;
+    public final InvokeMethod subsequentHasNextInstr;
 
-    public DirectRewriteResult(InvokeMethod hasNextInstr, InvokeMethod nextInstr) {
-      this.hasNextInstr = hasNextInstr;
+    public DirectRewriteResult(
+        InvokeMethod priorHasNextInstr,
+        InvokeMethod nextInstr,
+        InvokeMethod subsequentHasNextInstr) {
+      this.priorHasNextInstr = priorHasNextInstr;
       this.nextInstr = nextInstr;
+      this.subsequentHasNextInstr = subsequentHasNextInstr;
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EmptyEnumUnboxer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EmptyEnumUnboxer.java
index 4e07fba..5c31e4c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EmptyEnumUnboxer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EmptyEnumUnboxer.java
@@ -12,7 +12,7 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
-import com.android.tools.r8.ir.conversion.PostMethodProcessor.Builder;
+import com.android.tools.r8.ir.conversion.PostMethodProcessor;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackDelayed;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.Timing;
@@ -54,17 +54,10 @@
   }
 
   @Override
-  @SuppressWarnings("BadImport")
-  public void rewriteWithLens() {
-    // Intentionally empty.
-  }
-
-  @Override
-  @SuppressWarnings("BadImport")
   public void unboxEnums(
       AppView<AppInfoWithLiveness> appView,
       IRConverter converter,
-      Builder postMethodProcessorBuilder,
+      PostMethodProcessor.Builder postMethodProcessorBuilder,
       ExecutorService executorService,
       OptimizationFeedbackDelayed feedback,
       Timing timing) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
index 081d608..1106670 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
@@ -12,7 +12,7 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
-import com.android.tools.r8.ir.conversion.PostMethodProcessor.Builder;
+import com.android.tools.r8.ir.conversion.PostMethodProcessor;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackDelayed;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.Timing;
@@ -41,13 +41,10 @@
 
   public abstract void recordEnumState(DexProgramClass clazz, StaticFieldValues staticFieldValues);
 
-  public abstract void rewriteWithLens();
-
-  @SuppressWarnings("BadImport")
   public abstract void unboxEnums(
       AppView<AppInfoWithLiveness> appView,
       IRConverter converter,
-      Builder postMethodProcessorBuilder,
+      PostMethodProcessor.Builder postMethodProcessorBuilder,
       ExecutorService executorService,
       OptimizationFeedbackDelayed feedback,
       Timing timing)
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
index 7a20f638..1a6404b 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
@@ -36,6 +36,7 @@
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexItemFactory.ClassMethods;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexMethodHandle;
 import com.android.tools.r8.graph.DexProgramClass;
@@ -79,7 +80,7 @@
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
-import com.android.tools.r8.ir.conversion.PostMethodProcessor.Builder;
+import com.android.tools.r8.ir.conversion.PostMethodProcessor;
 import com.android.tools.r8.ir.desugar.LambdaDescriptor;
 import com.android.tools.r8.ir.optimize.enums.EnumDataMap.EnumData;
 import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldKnownData;
@@ -156,10 +157,6 @@
   private final Map<DexType, EnumStaticFieldValues> staticFieldValuesMap =
       new ConcurrentHashMap<>();
 
-  // Methods depending on library modelisation need to be reprocessed so they are peephole
-  // optimized.
-  private LongLivedProgramMethodSetBuilder<ProgramMethodSet> methodsDependingOnLibraryModelisation;
-
   // Map from checkNotNull() methods to the enums that use the given method.
   private LongLivedProgramMethodMapBuilder<LongLivedClassSetBuilder<DexProgramClass>>
       checkNotNullMethodsBuilder;
@@ -219,10 +216,6 @@
     return false;
   }
 
-  private void markMethodDependsOnLibraryModelisation(ProgramMethod method) {
-    methodsDependingOnLibraryModelisation.add(method, appView.graphLens());
-  }
-
   private DexProgramClass getEnumUnboxingCandidateOrNull(TypeElement lattice) {
     if (lattice.isClassType()) {
       DexType classType = lattice.asClassType().getClassType();
@@ -322,13 +315,6 @@
         enumUnboxingCandidatesInfo.addMethodDependency(eligibleEnum, code.context());
       }
     }
-    // TODO(b/225838009): Remove this when always using LIR.
-    if (!appView.testing().canUseLir(appView)) {
-      if (methodsDependingOnLibraryModelisation.contains(code.context(), appView.graphLens())) {
-        code.mutateConversionOptions(
-            conversionOptions -> conversionOptions.disablePeepholeOptimizations(methodProcessor));
-      }
-    }
   }
 
   private void markEnumEligible(DexType type, Set<DexType> eligibleEnums) {
@@ -507,7 +493,6 @@
     eligibleEnums.add(enumType);
   }
 
-  @SuppressWarnings("ReferenceEquality")
   private boolean isLegitimateConstClassUser(
       Instruction user, ProgramMethod context, DexProgramClass enumClass) {
     if (user.isAssume()) {
@@ -523,11 +508,10 @@
       if (singleTarget == null) {
         return false;
       }
-      if (singleTarget.getReference() == factory.enumMembers.valueOf) {
+      if (singleTarget.getReference().isIdenticalTo(factory.enumMembers.valueOf)) {
         // The name data is required for the correct mapping from the enum name to the ordinal
         // in the valueOf utility method.
         addRequiredNameData(enumClass);
-        markMethodDependsOnLibraryModelisation(context);
         // The out-value must be cast before it is used, or an assume instruction must strengthen
         // its dynamic type, so that the out-value is analyzed by the enum unboxing analysis.
         if (invoke.hasOutValue()) {
@@ -546,7 +530,7 @@
               }
             } else if (enumUser.isCheckCast()) {
               CheckCast checkCast = enumUser.asCheckCast();
-              if (checkCast.getType() == enumClass.getType()) {
+              if (checkCast.getType().isIdenticalTo(enumClass.getType())) {
                 // OK.
                 continue;
               }
@@ -556,9 +540,9 @@
         }
         return true;
       }
-      if (singleTarget.getReference()
-          == factory.javaLangReflectArrayMembers.newInstanceMethodWithDimensions) {
-        markMethodDependsOnLibraryModelisation(context);
+      if (singleTarget
+          .getReference()
+          .isIdenticalTo(factory.javaLangReflectArrayMembers.newInstanceMethodWithDimensions)) {
         return true;
       }
     }
@@ -566,7 +550,7 @@
     if (user.isInvokeVirtual()) {
       InvokeVirtual invoke = user.asInvokeVirtual();
       DexMethod invokedMethod = invoke.getInvokedMethod();
-      if (invokedMethod == factory.classMethods.desiredAssertionStatus) {
+      if (invokedMethod.isIdenticalTo(factory.classMethods.desiredAssertionStatus)) {
         // Only valid in the enum's class initializer, since the class constant must be rewritten
         // to LocalEnumUtility.class instead of int.class.
         return context.getDefinition().isClassInitializer() && context.getHolder() == enumClass;
@@ -584,11 +568,11 @@
         enumClass, factory.enumMembers.nameField);
   }
 
-  @SuppressWarnings("ReferenceEquality")
   private boolean isUnboxableNameMethod(DexMethod method) {
-    return method == factory.classMethods.getName
-        || method == factory.classMethods.getCanonicalName
-        || method == factory.classMethods.getSimpleName;
+    ClassMethods classMethods = factory.classMethods;
+    return method.isIdenticalTo(classMethods.getName)
+        || method.isIdenticalTo(classMethods.getCanonicalName)
+        || method.isIdenticalTo(classMethods.getSimpleName);
   }
 
   private void addNullDependencies(IRCode code, Value nullValue, Set<DexType> eligibleEnums) {
@@ -676,24 +660,13 @@
     enumUnboxingCandidatesInfo =
         new EnumUnboxingCandidateAnalysis(appView, this)
             .findCandidates(graphLensForPrimaryOptimizationPass);
-    methodsDependingOnLibraryModelisation =
-        LongLivedProgramMethodSetBuilder.createConcurrentForIdentitySet(
-            graphLensForPrimaryOptimizationPass);
   }
 
   @Override
-  @SuppressWarnings("BadImport")
-  public void rewriteWithLens() {
-    methodsDependingOnLibraryModelisation =
-        methodsDependingOnLibraryModelisation.rewrittenWithLens(appView.graphLens());
-  }
-
-  @Override
-  @SuppressWarnings("BadImport")
   public void unboxEnums(
       AppView<AppInfoWithLiveness> appView,
       IRConverter converter,
-      Builder postMethodProcessorBuilder,
+      PostMethodProcessor.Builder postMethodProcessorBuilder,
       ExecutorService executorService,
       OptimizationFeedbackDelayed feedback,
       Timing timing)
@@ -762,12 +735,7 @@
             dependencies
                 .rewrittenWithLens(appView)
                 .removeAll(treeFixerResult.getPrunedItems().getRemovedMethods()))
-        .merge(
-            methodsDependingOnLibraryModelisation
-                .rewrittenWithLens(appView)
-                .removeAll(treeFixerResult.getPrunedItems().getRemovedMethods()))
         .addAll(treeFixerResult.getMethodsToProcess(), appView.graphLens());
-    methodsDependingOnLibraryModelisation.clear();
 
     updateOptimizationInfos(executorService, feedback, treeFixerResult, previousLens);
 
@@ -871,7 +839,6 @@
     return enumDataMap;
   }
 
-  @SuppressWarnings("ReferenceEquality")
   private EnumDataMap analyzeEnumInstances() {
     ImmutableMap.Builder<DexType, DexType> enumSubtypes = ImmutableMap.builder();
     ImmutableMap.Builder<DexType, EnumData> builder = ImmutableMap.builder();
@@ -888,7 +855,7 @@
             builder.put(enumClass.type, data);
             if (data.valuesTypes != null) {
               for (DexType value : data.valuesTypes.values()) {
-                if (value != enumClass.type) {
+                if (value.isNotIdenticalTo(enumClass.type)) {
                   enumSubtypes.put(value, enumClass.type);
                 }
               }
@@ -1487,20 +1454,13 @@
       return Reason.INVALID_INVOKE;
     }
 
-    Reason reason =
-        analyzeLibraryInvoke(
-            invoke,
-            context,
-            enumClass,
-            enumValue,
-            singleTarget.getReference(),
-            singleTarget.getHolder());
-
-    if (reason == Reason.ELIGIBLE) {
-      markMethodDependsOnLibraryModelisation(context);
-    }
-
-    return reason;
+    return analyzeLibraryInvoke(
+        invoke,
+        context,
+        enumClass,
+        enumValue,
+        singleTarget.getReference(),
+        singleTarget.getHolder());
   }
 
   private Reason comparableAsUnboxedValues(InvokeMethod invoke) {
@@ -1770,6 +1730,5 @@
   @Override
   public void onMethodCodePruned(ProgramMethod method) {
     enumUnboxingCandidatesInfo.addPrunedMethod(method);
-    methodsDependingOnLibraryModelisation.remove(method.getReference(), appView.graphLens());
   }
 }
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 1d0edbe..2273dbb 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
@@ -10,6 +10,7 @@
 import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.PrunedItems;
+import com.android.tools.r8.ir.analysis.path.PathConstraintSupplier;
 import com.android.tools.r8.ir.code.AbstractValueSupplier;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.conversion.IRConverter;
@@ -17,6 +18,7 @@
 import com.android.tools.r8.ir.conversion.PostMethodProcessor;
 import com.android.tools.r8.ir.conversion.PrimaryR8IRConverter;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldStateCollection;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.InFlowComparator;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.VirtualRootMethodsAnalysis;
@@ -127,7 +129,9 @@
       assert methodProcessor.isPrimaryMethodProcessor();
       AbstractValueSupplier abstractValueSupplier =
           value -> value.getAbstractValue(appView, method);
-      codeScanner.scan(method, code, abstractValueSupplier, timing);
+      PathConstraintSupplier pathConstraintSupplier =
+          new PathConstraintSupplier(appView, code, codeScanner.getMethodParameterFactory());
+      codeScanner.scan(method, code, abstractValueSupplier, pathConstraintSupplier, timing);
 
       assert effectivelyUnusedArgumentsAnalysis != null;
       effectivelyUnusedArgumentsAnalysis.scan(method, code);
@@ -249,6 +253,7 @@
     assert appView.isAllCodeProcessed();
     FieldStateCollection fieldStates = codeScanner.getFieldStates();
     MethodStateCollectionByReference methodStates = codeScanner.getMethodStates();
+    InFlowComparator inFlowComparator = codeScanner.getInFlowComparator();
     appView.testing().argumentPropagatorEventConsumer.acceptCodeScannerResult(methodStates);
     codeScanner = null;
 
@@ -261,6 +266,7 @@
             immediateSubtypingInfo,
             fieldStates,
             methodStates,
+            inFlowComparator,
             stronglyConnectedProgramComponents,
             interfaceDispatchOutsideProgram)
         .propagateOptimizationInfo(
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 b72657b..ba31341 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
@@ -4,7 +4,6 @@
 
 package com.android.tools.r8.optimize.argumentpropagation;
 
-
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClassAndMethod;
@@ -15,10 +14,8 @@
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.analysis.framework.intraprocedural.DataflowAnalysisResult.SuccessfulDataflowAnalysisResult;
-import com.android.tools.r8.ir.analysis.path.PathConstraintAnalysis;
+import com.android.tools.r8.ir.analysis.path.PathConstraintSupplier;
 import com.android.tools.r8.ir.analysis.path.state.ConcretePathConstraintAnalysisState;
-import com.android.tools.r8.ir.analysis.path.state.PathConstraintAnalysisState;
 import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.type.DynamicTypeWithUpperBound;
 import com.android.tools.r8.ir.analysis.type.Nullability;
@@ -39,6 +36,7 @@
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
 import com.android.tools.r8.ir.code.Phi;
+import com.android.tools.r8.ir.code.Position.SourcePosition;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.AbstractFunction;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.BaseInFlow;
@@ -58,6 +56,7 @@
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldValueFactory;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.IfThenElseAbstractFunction;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.InFlow;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.InFlowComparator;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.InstanceFieldReadAbstractFunction;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodParameter;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodParameterFactory;
@@ -80,6 +79,8 @@
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.structural.StructuralItem;
 import com.google.common.collect.Sets;
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -108,7 +109,7 @@
 
   private final FieldValueFactory fieldValueFactory = new FieldValueFactory();
 
-  private final MethodParameterFactory methodParameterFactory = new MethodParameterFactory();
+  final MethodParameterFactory methodParameterFactory = new MethodParameterFactory();
 
   private final Set<DexMethod> monomorphicVirtualMethods = Sets.newIdentityHashSet();
 
@@ -134,6 +135,8 @@
   private final MethodStateCollectionByReference methodStates =
       MethodStateCollectionByReference.createConcurrent();
 
+  private final InFlowComparator.Builder inFlowComparatorBuilder = InFlowComparator.builder();
+
   public ArgumentPropagatorCodeScanner(AppView<AppInfoWithLiveness> appView) {
     this(appView, new ArgumentPropagatorReprocessingCriteriaCollection(appView));
   }
@@ -158,6 +161,10 @@
     return fieldStates;
   }
 
+  public MethodParameterFactory getMethodParameterFactory() {
+    return methodParameterFactory;
+  }
+
   public MethodStateCollectionByReference getMethodStates() {
     return methodStates;
   }
@@ -166,6 +173,10 @@
     return virtualRootMethods.get(method.getReference());
   }
 
+  InFlowComparator getInFlowComparator() {
+    return inFlowComparatorBuilder.build();
+  }
+
   // TODO(b/296030319): Allow lookups in the FieldStateCollection using DexField keys to avoid the
   //  need for definitionFor here.
   private boolean isFieldValueAlreadyUnknown(DexType staticType, DexField field) {
@@ -219,8 +230,9 @@
       ProgramMethod method,
       IRCode code,
       AbstractValueSupplier abstractValueSupplier,
+      PathConstraintSupplier pathConstraintSupplier,
       Timing timing) {
-    new CodeScanner(abstractValueSupplier, code, method).scan(timing);
+    new CodeScanner(abstractValueSupplier, code, method, pathConstraintSupplier).scan(timing);
   }
 
   protected class CodeScanner {
@@ -228,15 +240,19 @@
     protected final AbstractValueSupplier abstractValueSupplier;
     protected final IRCode code;
     protected final ProgramMethod context;
+    private final PathConstraintSupplier pathConstraintSupplier;
 
-    private SuccessfulDataflowAnalysisResult<BasicBlock, PathConstraintAnalysisState>
-        pathConstraintAnalysisResult;
+    private Object2IntMap<Phi> phiNumbering = null;
 
     protected CodeScanner(
-        AbstractValueSupplier abstractValueSupplier, IRCode code, ProgramMethod method) {
+        AbstractValueSupplier abstractValueSupplier,
+        IRCode code,
+        ProgramMethod method,
+        PathConstraintSupplier pathConstraintSupplier) {
       this.abstractValueSupplier = abstractValueSupplier;
       this.code = code;
       this.context = method;
+      this.pathConstraintSupplier = pathConstraintSupplier;
     }
 
     public void scan(Timing timing) {
@@ -359,6 +375,9 @@
     // If the value is an argument of the enclosing method or defined by a field-get, then clearly
     // we have no information about its abstract value (yet). Instead of treating this as having an
     // unknown runtime value, we instead record a flow constraint.
+    // TODO(b/302281503): Cache computed in flow so that we do not compute the same in flow for the
+    //  same value multiple times.
+    // TODO(b/302281503): Canonicalize computed in flow.
     private InFlow computeInFlow(
         DexType staticType,
         Value value,
@@ -406,9 +425,13 @@
         return null;
       }
       ConcretePathConstraintAnalysisState leftPredecessorPathConstraint =
-          getPathConstraint(phi.getBlock().getPredecessors().get(0)).asConcreteState();
+          pathConstraintSupplier
+              .getPathConstraint(phi.getBlock().getPredecessors().get(0))
+              .asConcreteState();
       ConcretePathConstraintAnalysisState rightPredecessorPathConstraint =
-          getPathConstraint(phi.getBlock().getPredecessors().get(1)).asConcreteState();
+          pathConstraintSupplier
+              .getPathConstraint(phi.getBlock().getPredecessors().get(1))
+              .asConcreteState();
       if (leftPredecessorPathConstraint == null || rightPredecessorPathConstraint == null) {
         return null;
       }
@@ -420,10 +443,34 @@
       }
       NonEmptyValueState leftValue = valueStateSupplier.apply(phi.getOperand(0));
       NonEmptyValueState rightValue = valueStateSupplier.apply(phi.getOperand(1));
-      if (leftPredecessorPathConstraint.getNegatedPathConstraints().contains(condition)) {
-        return new IfThenElseAbstractFunction(condition, rightValue, leftValue);
+      IfThenElseAbstractFunction result =
+          leftPredecessorPathConstraint.isNegated(condition)
+              ? new IfThenElseAbstractFunction(condition, rightValue, leftValue)
+              : new IfThenElseAbstractFunction(condition, leftValue, rightValue);
+      recordIfThenElsePosition(result, phi);
+      return result;
+    }
+
+    private void recordIfThenElsePosition(
+        IfThenElseAbstractFunction ifThenElseAbstractFunction, Phi phi) {
+      inFlowComparatorBuilder.addIfThenElsePosition(
+          ifThenElseAbstractFunction,
+          SourcePosition.builder()
+              .setMethod(code.context().getReference())
+              .setLine(getOrCreatePhiNumbering().getInt(phi))
+              .build());
+    }
+
+    private Object2IntMap<Phi> getOrCreatePhiNumbering() {
+      if (phiNumbering == null) {
+        phiNumbering = new Object2IntOpenHashMap<>();
+        for (BasicBlock block : code.getBlocks()) {
+          for (Phi phi : block.getPhis()) {
+            phiNumbering.put(phi, phiNumbering.size());
+          }
+        }
       }
-      return new IfThenElseAbstractFunction(condition, leftValue, rightValue);
+      return phiNumbering;
     }
 
     private InFlow castBaseInFlow(InFlow inFlow, Value value) {
@@ -876,16 +923,6 @@
       }
     }
 
-    private PathConstraintAnalysisState getPathConstraint(BasicBlock block) {
-      if (pathConstraintAnalysisResult == null) {
-        PathConstraintAnalysis analysis =
-            new PathConstraintAnalysis(appView, code, methodParameterFactory);
-        pathConstraintAnalysisResult = analysis.run(code.entryBlock()).asSuccessfulAnalysisResult();
-        assert pathConstraintAnalysisResult != null;
-      }
-      return pathConstraintAnalysisResult.getBlockExitState(block);
-    }
-
     @SuppressWarnings("ReferenceEquality")
     private DexMethod getRepresentative(InvokeMethod invoke, ProgramMethod resolvedMethod) {
       if (resolvedMethod.getDefinition().belongsToDirectPool()) {
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPropagator.java
index 99b3640..800b277 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPropagator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPropagator.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
 import com.android.tools.r8.ir.conversion.PrimaryR8IRConverter;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldStateCollection;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.InFlowComparator;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
 import com.android.tools.r8.optimize.argumentpropagation.propagation.InFlowPropagator;
 import com.android.tools.r8.optimize.argumentpropagation.propagation.InterfaceMethodArgumentPropagator;
@@ -34,6 +35,7 @@
   private final PrimaryR8IRConverter converter;
   private final FieldStateCollection fieldStates;
   private final MethodStateCollectionByReference methodStates;
+  private final InFlowComparator inFlowComparator;
 
   private final ImmediateProgramSubtypingInfo immediateSubtypingInfo;
   private final List<Set<DexProgramClass>> stronglyConnectedProgramComponents;
@@ -47,6 +49,7 @@
       ImmediateProgramSubtypingInfo immediateSubtypingInfo,
       FieldStateCollection fieldStates,
       MethodStateCollectionByReference methodStates,
+      InFlowComparator inFlowComparator,
       List<Set<DexProgramClass>> stronglyConnectedProgramComponents,
       BiConsumer<Set<DexProgramClass>, DexMethodSignature> interfaceDispatchOutsideProgram) {
     this.appView = appView;
@@ -54,6 +57,7 @@
     this.immediateSubtypingInfo = immediateSubtypingInfo;
     this.fieldStates = fieldStates;
     this.methodStates = methodStates;
+    this.inFlowComparator = inFlowComparator;
     this.stronglyConnectedProgramComponents = stronglyConnectedProgramComponents;
     this.interfaceDispatchOutsideProgram = interfaceDispatchOutsideProgram;
   }
@@ -79,7 +83,8 @@
             classesWithSingleCallerInlinedInstanceInitializers,
             converter,
             fieldStates,
-            methodStates)
+            methodStates,
+            inFlowComparator)
         .run(executorService);
     timing.end();
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomPrimitiveTypeValueState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomPrimitiveTypeValueState.java
index ba50708..b429da0 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomPrimitiveTypeValueState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomPrimitiveTypeValueState.java
@@ -28,7 +28,7 @@
       StateCloner cloner,
       Action onChangedAction) {
     if (inState.isBottom()) {
-      assert inState == bottomPrimitiveTypeState();
+      assert inState.identical(bottomPrimitiveTypeState());
       return this;
     }
     if (inState.isUnknown()) {
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomValueState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomValueState.java
index 609b769..a267042 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomValueState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomValueState.java
@@ -31,4 +31,14 @@
   public ValueState mutableCopyWithoutInFlow() {
     return this;
   }
+
+  @Override
+  public final boolean equals(Object obj) {
+    return this == obj;
+  }
+
+  @Override
+  public final int hashCode() {
+    return System.identityHashCode(this);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/CastAbstractFunction.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/CastAbstractFunction.java
index a4eb77e..2586f8d 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/CastAbstractFunction.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/CastAbstractFunction.java
@@ -44,10 +44,12 @@
   }
 
   @Override
-  public int internalCompareToSameKind(InFlow other) {
+  public int internalCompareToSameKind(InFlow other, InFlowComparator comparator) {
     CastAbstractFunction fn = other.asCastAbstractFunction();
     if (inFlow != fn.inFlow) {
-      return inFlow.compareTo(fn.inFlow);
+      int result = inFlow.compareTo(fn.inFlow, comparator);
+      assert result != 0;
+      return result;
     }
     return type.compareTo(fn.type);
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteArrayTypeValueState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteArrayTypeValueState.java
index 26dc74a..ca98915 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteArrayTypeValueState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteArrayTypeValueState.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.utils.Action;
 import com.android.tools.r8.utils.SetUtils;
 import java.util.Collections;
+import java.util.Objects;
 import java.util.Set;
 import java.util.function.Supplier;
 
@@ -136,6 +137,23 @@
   }
 
   @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof ConcreteArrayTypeValueState)) {
+      return false;
+    }
+    ConcreteArrayTypeValueState state = (ConcreteArrayTypeValueState) obj;
+    return nullability.equals(state.nullability) && getInFlow().equals(state.getInFlow());
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(getClass(), nullability, getInFlow());
+  }
+
+  @Override
   public String toString() {
     assert !hasInFlow();
     return "ArrayState(" + nullability + ")";
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteClassTypeValueState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteClassTypeValueState.java
index f6a608d..96e476b 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteClassTypeValueState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteClassTypeValueState.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.utils.Action;
 import com.android.tools.r8.utils.SetUtils;
 import java.util.Collections;
+import java.util.Objects;
 import java.util.Set;
 import java.util.function.Supplier;
 
@@ -189,6 +190,25 @@
   }
 
   @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof ConcreteClassTypeValueState)) {
+      return false;
+    }
+    ConcreteClassTypeValueState state = (ConcreteClassTypeValueState) obj;
+    return abstractValue.equals(state.abstractValue)
+        && dynamicType.equals(state.dynamicType)
+        && getInFlow().equals(state.getInFlow());
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(getClass(), abstractValue, dynamicType, getInFlow());
+  }
+
+  @Override
   public String toString() {
     assert !hasInFlow();
     return "ClassState(type: " + dynamicType + ", value: " + abstractValue + ")";
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePrimitiveTypeValueState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePrimitiveTypeValueState.java
index 864c16e..118c05d 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePrimitiveTypeValueState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePrimitiveTypeValueState.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.utils.Action;
 import com.android.tools.r8.utils.SetUtils;
 import java.util.Collections;
+import java.util.Objects;
 import java.util.Set;
 import java.util.function.Supplier;
 
@@ -129,6 +130,23 @@
   }
 
   @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof ConcretePrimitiveTypeValueState)) {
+      return false;
+    }
+    ConcretePrimitiveTypeValueState state = (ConcretePrimitiveTypeValueState) obj;
+    return abstractValue.equals(state.abstractValue) && getInFlow().equals(state.getInFlow());
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(getClass(), abstractValue, getInFlow());
+  }
+
+  @Override
   public String toString() {
     assert !hasInFlow();
     return "PrimitiveState(" + abstractValue + ")";
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteReceiverValueState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteReceiverValueState.java
index 350d7f9..04711fe 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteReceiverValueState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteReceiverValueState.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.Action;
 import java.util.Collections;
+import java.util.Objects;
 import java.util.Set;
 import java.util.function.Supplier;
 
@@ -147,6 +148,23 @@
   }
 
   @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof ConcreteReceiverValueState)) {
+      return false;
+    }
+    ConcreteReceiverValueState state = (ConcreteReceiverValueState) obj;
+    return dynamicType.equals(state.dynamicType) && getInFlow().equals(state.getInFlow());
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(getClass(), dynamicType, getInFlow());
+  }
+
+  @Override
   public String toString() {
     assert !hasInFlow();
     return "ReceiverState(" + dynamicType + ")";
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FieldValue.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FieldValue.java
index 0b9e152..99fcf7b 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FieldValue.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/FieldValue.java
@@ -25,7 +25,7 @@
   }
 
   @Override
-  public int internalCompareToSameKind(InFlow other) {
+  public int internalCompareToSameKind(InFlow other, InFlowComparator comparator) {
     return field.compareTo(other.asFieldValue().getField());
   }
 
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/IdentityAbstractFunction.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/IdentityAbstractFunction.java
index 1b33495b..f31b821 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/IdentityAbstractFunction.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/IdentityAbstractFunction.java
@@ -41,7 +41,7 @@
   }
 
   @Override
-  public int internalCompareToSameKind(InFlow inFlow) {
+  public int internalCompareToSameKind(InFlow inFlow, InFlowComparator comparator) {
     assert this == inFlow;
     return 0;
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/IfThenElseAbstractFunction.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/IfThenElseAbstractFunction.java
index a6da375..4d21db9 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/IfThenElseAbstractFunction.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/IfThenElseAbstractFunction.java
@@ -6,10 +6,12 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.code.Position.SourcePosition;
 import com.android.tools.r8.optimize.argumentpropagation.computation.ComputationTreeNode;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.ListUtils;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * Represents a ternary expression (exp ? u : v). The {@link #condition} is an expression containing
@@ -133,9 +135,11 @@
   }
 
   @Override
-  public int internalCompareToSameKind(InFlow inFlow) {
-    // TODO(b/302281503): Find a way to make this comparable.
-    return hashCode() - inFlow.hashCode();
+  public int internalCompareToSameKind(InFlow inFlow, InFlowComparator comparator) {
+    SourcePosition position = comparator.getIfThenElsePosition(this);
+    SourcePosition otherPosition =
+        comparator.getIfThenElsePosition(inFlow.asIfThenElseAbstractFunction());
+    return position.compareTo(otherPosition);
   }
 
   @Override
@@ -152,4 +156,23 @@
   public InFlowKind getKind() {
     return InFlowKind.ABSTRACT_FUNCTION_IF_THEN_ELSE;
   }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof IfThenElseAbstractFunction)) {
+      return false;
+    }
+    IfThenElseAbstractFunction fn = (IfThenElseAbstractFunction) obj;
+    return condition.equals(fn.condition)
+        && thenState.equals(fn.thenState)
+        && elseState.equals(fn.elseState);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(getClass(), condition, thenState, elseState);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/InFlow.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/InFlow.java
index 4590558..3c28c20 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/InFlow.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/InFlow.java
@@ -7,17 +7,16 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.optimize.compose.UpdateChangedFlagsAbstractFunction;
 
-public interface InFlow extends Comparable<InFlow> {
+public interface InFlow {
 
-  @Override
-  default int compareTo(InFlow inFlow) {
+  default int compareTo(InFlow inFlow, InFlowComparator comparator) {
     if (getKind() == inFlow.getKind()) {
-      return internalCompareToSameKind(inFlow);
+      return internalCompareToSameKind(inFlow, comparator);
     }
     return getKind().ordinal() - inFlow.getKind().ordinal();
   }
 
-  int internalCompareToSameKind(InFlow inFlow);
+  int internalCompareToSameKind(InFlow inFlow, InFlowComparator comparator);
 
   InFlowKind getKind();
 
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/InFlowComparator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/InFlowComparator.java
new file mode 100644
index 0000000..82a1cc8
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/InFlowComparator.java
@@ -0,0 +1,53 @@
+// 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 com.android.tools.r8.ir.code.Position.SourcePosition;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+
+public class InFlowComparator implements Comparator<InFlow> {
+
+  private final Map<IfThenElseAbstractFunction, SourcePosition> ifThenElsePositions;
+
+  private InFlowComparator(Map<IfThenElseAbstractFunction, SourcePosition> ifThenElsePositions) {
+    this.ifThenElsePositions = ifThenElsePositions;
+  }
+
+  public SourcePosition getIfThenElsePosition(IfThenElseAbstractFunction fn) {
+    SourcePosition position = ifThenElsePositions.get(fn);
+    assert position != null;
+    return position;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public void clear() {
+    ifThenElsePositions.clear();
+  }
+
+  @Override
+  public int compare(InFlow inFlow, InFlow other) {
+    return inFlow.compareTo(other, this);
+  }
+
+  public static class Builder {
+
+    private final Map<IfThenElseAbstractFunction, SourcePosition> ifThenElsePositions =
+        new HashMap<>();
+
+    public void addIfThenElsePosition(IfThenElseAbstractFunction fn, SourcePosition position) {
+      synchronized (ifThenElsePositions) {
+        ifThenElsePositions.put(fn, position);
+      }
+    }
+
+    public InFlowComparator build() {
+      return new InFlowComparator(ifThenElsePositions);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/InstanceFieldReadAbstractFunction.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/InstanceFieldReadAbstractFunction.java
index d9c23c4..382706b 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/InstanceFieldReadAbstractFunction.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/InstanceFieldReadAbstractFunction.java
@@ -74,9 +74,9 @@
   }
 
   @Override
-  public int internalCompareToSameKind(InFlow other) {
+  public int internalCompareToSameKind(InFlow other, InFlowComparator comparator) {
     InstanceFieldReadAbstractFunction fn = other.asInstanceFieldReadAbstractFunction();
-    int result = receiver.compareTo(fn.receiver);
+    int result = receiver.compareTo(fn.receiver, comparator);
     if (result == 0) {
       result = field.compareTo(fn.field);
     }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodParameter.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodParameter.java
index 138725e..ed49cea 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodParameter.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodParameter.java
@@ -63,7 +63,7 @@
   }
 
   @Override
-  public int internalCompareToSameKind(InFlow other) {
+  public int internalCompareToSameKind(InFlow other, InFlowComparator comparator) {
     MethodParameter methodParameter = other.asMethodParameter();
     int result = method.compareTo(methodParameter.method);
     if (result == 0) {
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/OrAbstractFunction.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/OrAbstractFunction.java
index 8e84a76..c8294a4 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/OrAbstractFunction.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/OrAbstractFunction.java
@@ -67,9 +67,9 @@
   }
 
   @Override
-  public int internalCompareToSameKind(InFlow other) {
+  public int internalCompareToSameKind(InFlow other, InFlowComparator comparator) {
     OrAbstractFunction fn = other.asOrAbstractFunction();
-    int result = inFlow.compareTo(fn.inFlow);
+    int result = inFlow.compareTo(fn.inFlow, comparator);
     if (result == 0) {
       result = constant.getIntValue() - fn.constant.getIntValue();
     }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownAbstractFunction.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownAbstractFunction.java
index dd06a31..726ade5 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownAbstractFunction.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownAbstractFunction.java
@@ -41,7 +41,7 @@
   }
 
   @Override
-  public int internalCompareToSameKind(InFlow inFlow) {
+  public int internalCompareToSameKind(InFlow inFlow, InFlowComparator comparator) {
     assert this == inFlow;
     return 0;
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownValueState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownValueState.java
index 6fdedc4..69c1fc8 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownValueState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownValueState.java
@@ -53,6 +53,16 @@
   }
 
   @Override
+  public boolean equals(Object obj) {
+    return this == obj;
+  }
+
+  @Override
+  public int hashCode() {
+    return System.identityHashCode(this);
+  }
+
+  @Override
   public String toString() {
     return "⊤";
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ValueState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ValueState.java
index b88d240..a0ed391 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ValueState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ValueState.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.Action;
+import com.android.tools.r8.utils.ObjectUtils;
 
 public abstract class ValueState {
 
@@ -136,4 +137,14 @@
       DexType outStaticType,
       StateCloner cloner,
       Action onChangedAction);
+
+  @Override
+  public abstract boolean equals(Object obj);
+
+  @Override
+  public abstract int hashCode();
+
+  public final boolean identical(ValueState state) {
+    return ObjectUtils.identical(this, state);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraph.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraph.java
index c9649f8..98247c0 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraph.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraph.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldStateCollection;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.FlowGraphStateProvider;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.InFlowComparator;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodParameter;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ValueState;
@@ -58,8 +59,9 @@
       AppView<AppInfoWithLiveness> appView,
       IRConverter converter,
       FieldStateCollection fieldStates,
-      MethodStateCollectionByReference methodStates) {
-    return new FlowGraphBuilder(appView, converter, fieldStates, methodStates);
+      MethodStateCollectionByReference methodStates,
+      InFlowComparator inFlowComparator) {
+    return new FlowGraphBuilder(appView, converter, fieldStates, methodStates, inFlowComparator);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphBuilder.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphBuilder.java
index d564575..5830d3f 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphBuilder.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphBuilder.java
@@ -22,6 +22,7 @@
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldStateCollection;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldValue;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.InFlow;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.InFlowComparator;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodParameter;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
@@ -32,7 +33,6 @@
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMaps;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
-import java.util.Comparator;
 import java.util.LinkedHashMap;
 import java.util.List;
 
@@ -42,6 +42,7 @@
   private final IRConverter converter;
   private final FieldStateCollection fieldStates;
   private final MethodStateCollectionByReference methodStates;
+  private final InFlowComparator inFlowComparator;
 
   private final LinkedHashMap<DexField, FlowGraphFieldNode> fieldNodes = new LinkedHashMap<>();
   private final LinkedHashMap<DexMethod, Int2ReferenceMap<FlowGraphParameterNode>> parameterNodes =
@@ -51,15 +52,18 @@
       AppView<AppInfoWithLiveness> appView,
       IRConverter converter,
       FieldStateCollection fieldStates,
-      MethodStateCollectionByReference methodStates) {
+      MethodStateCollectionByReference methodStates,
+      InFlowComparator inFlowComparator) {
     this.appView = appView;
     this.converter = converter;
     this.fieldStates = fieldStates;
     this.methodStates = methodStates;
+    this.inFlowComparator = inFlowComparator;
   }
 
   public FlowGraphBuilder addClasses() {
     appView.appInfo().classesWithDeterministicOrder().forEach(this::add);
+    inFlowComparator.clear();
     return this;
   }
 
@@ -96,7 +100,7 @@
 
     FlowGraphFieldNode node = getOrCreateFieldNode(field, concreteFieldState);
     List<InFlow> inFlowWithDeterministicOrder =
-        ListUtils.sort(concreteFieldState.getInFlow(), Comparator.naturalOrder());
+        ListUtils.sort(concreteFieldState.getInFlow(), inFlowComparator);
     for (InFlow inFlow : inFlowWithDeterministicOrder) {
       if (addInFlow(inFlow, node).shouldBreak()) {
         assert node.isUnknown();
@@ -143,7 +147,9 @@
     }
 
     FlowGraphParameterNode node = getOrCreateParameterNode(method, parameterIndex, methodState);
-    for (InFlow inFlow : concreteParameterState.getInFlow()) {
+    List<InFlow> inFlowWithDeterministicOrder =
+        ListUtils.sort(concreteParameterState.getInFlow(), inFlowComparator);
+    for (InFlow inFlow : inFlowWithDeterministicOrder) {
       if (addInFlow(inFlow, node).shouldBreak()) {
         assert node.isUnknown();
         break;
@@ -259,7 +265,7 @@
       fieldStates.remove(field);
       FlowGraphFieldNode node = getFieldNode(field);
       if (node != null && !node.getState().isUnknown()) {
-        assert node.getState() == concreteFieldState;
+        assert node.getState().identical(concreteFieldState);
         node.setState(concreteFieldStateOrBottom);
       }
     }
@@ -275,7 +281,7 @@
           ValueState concreteParameterStateOrBottom = concreteParameterState.clearInFlow();
           FlowGraphParameterNode node = getParameterNode(method, i);
           if (node != null && !node.getState().isUnknown()) {
-            assert node.getState() == concreteParameterState;
+            assert node.getState().identical(concreteParameterState);
             node.setState(concreteParameterStateOrBottom);
           }
         }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphNode.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphNode.java
index 7f9f0f0..b2e3a53 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphNode.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FlowGraphNode.java
@@ -47,7 +47,7 @@
             getStaticType(),
             StateCloner.getCloner(),
             onChangedAction);
-    if (newState != oldState) {
+    if (!newState.identical(oldState)) {
       setState(newState);
       onChangedAction.execute();
     }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InFlowPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InFlowPropagator.java
index 81e3424..82f2350 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InFlowPropagator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InFlowPropagator.java
@@ -24,6 +24,7 @@
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteValueState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldStateCollection;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.FlowGraphStateProvider;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.InFlowComparator;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.NonEmptyValueState;
@@ -48,19 +49,22 @@
   final IRConverter converter;
   protected final FieldStateCollection fieldStates;
   final MethodStateCollectionByReference methodStates;
+  final InFlowComparator inFlowComparator;
 
   public InFlowPropagator(
       AppView<AppInfoWithLiveness> appView,
       Set<DexProgramClass> classesWithSingleCallerInlinedInstanceInitializers,
       IRConverter converter,
       FieldStateCollection fieldStates,
-      MethodStateCollectionByReference methodStates) {
+      MethodStateCollectionByReference methodStates,
+      InFlowComparator inFlowComparator) {
     this.appView = appView;
     this.classesWithSingleCallerInlinedInstanceInitializers =
         classesWithSingleCallerInlinedInstanceInitializers;
     this.converter = converter;
     this.fieldStates = fieldStates;
     this.methodStates = methodStates;
+    this.inFlowComparator = inFlowComparator;
   }
 
   public void run(ExecutorService executorService) throws ExecutionException {
@@ -97,7 +101,7 @@
     // Build a graph with an edge from parameter p -> parameter p' if all argument information for p
     // must be included in the argument information for p'.
     FlowGraph flowGraph =
-        FlowGraph.builder(appView, converter, fieldStates, methodStates)
+        FlowGraph.builder(appView, converter, fieldStates, methodStates, inFlowComparator)
             .addClasses()
             .clearInFlow()
             .build();
@@ -342,7 +346,7 @@
             ValueState state = node.getState();
             ValueState previousState = fieldStates.set(field, state);
             assert state.isUnknown()
-                    || state == previousState
+                    || state.identical(previousState)
                     || (state.isConcrete() && previousState.isBottom())
                 : "Expected current state to be >= previous state";
           });
diff --git a/src/main/java/com/android/tools/r8/optimize/compose/ArgumentPropagatorCodeScannerForComposableFunctions.java b/src/main/java/com/android/tools/r8/optimize/compose/ArgumentPropagatorCodeScannerForComposableFunctions.java
index 2116f3a..371af3d 100644
--- a/src/main/java/com/android/tools/r8/optimize/compose/ArgumentPropagatorCodeScannerForComposableFunctions.java
+++ b/src/main/java/com/android/tools/r8/optimize/compose/ArgumentPropagatorCodeScannerForComposableFunctions.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.analysis.path.PathConstraintSupplier;
 import com.android.tools.r8.ir.code.AbstractValueSupplier;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.InvokeMethod;
@@ -30,8 +31,9 @@
       ProgramMethod method,
       IRCode code,
       AbstractValueSupplier abstractValueSupplier,
+      PathConstraintSupplier pathConstraintSupplier,
       Timing timing) {
-    new CodeScanner(abstractValueSupplier, code, method).scan(timing);
+    new CodeScanner(abstractValueSupplier, code, method, pathConstraintSupplier).scan(timing);
   }
 
   @Override
@@ -44,8 +46,11 @@
   private class CodeScanner extends ArgumentPropagatorCodeScanner.CodeScanner {
 
     protected CodeScanner(
-        AbstractValueSupplier abstractValueSupplier, IRCode code, ProgramMethod method) {
-      super(abstractValueSupplier, code, method);
+        AbstractValueSupplier abstractValueSupplier,
+        IRCode code,
+        ProgramMethod method,
+        PathConstraintSupplier pathConstraintSupplier) {
+      super(abstractValueSupplier, code, method, pathConstraintSupplier);
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/optimize/compose/ComposeMethodProcessor.java b/src/main/java/com/android/tools/r8/optimize/compose/ComposeMethodProcessor.java
index 22ed377..3b51e7c 100644
--- a/src/main/java/com/android/tools/r8/optimize/compose/ComposeMethodProcessor.java
+++ b/src/main/java/com/android/tools/r8/optimize/compose/ComposeMethodProcessor.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.constant.SparseConditionalConstantPropagation;
+import com.android.tools.r8.ir.analysis.path.PathConstraintSupplier;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.code.AbstractValueSupplier;
 import com.android.tools.r8.ir.code.IRCode;
@@ -30,6 +31,7 @@
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcretePrimitiveTypeValueState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteValueState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.FieldStateCollection;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.InFlowComparator;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodParameter;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
@@ -95,9 +97,15 @@
       throws ExecutionException {
     prepareForInFlowPropagator();
 
+    InFlowComparator emptyComparator = InFlowComparator.builder().build();
     InFlowPropagator inFlowPropagator =
         new InFlowPropagator(
-            appView, null, converter, codeScanner.getFieldStates(), codeScanner.getMethodStates()) {
+            appView,
+            null,
+            converter,
+            codeScanner.getFieldStates(),
+            codeScanner.getMethodStates(),
+            emptyComparator) {
 
           @Override
           protected DefaultFieldValueJoiner createDefaultFieldValueJoiner(
@@ -226,7 +234,9 @@
           assert abstractValue != null;
           return abstractValue;
         };
-    codeScanner.scan(method, code, abstractValueSupplier, timing);
+    PathConstraintSupplier pathConstraintSupplier =
+        new PathConstraintSupplier(appView, code, codeScanner.getMethodParameterFactory());
+    codeScanner.scan(method, code, abstractValueSupplier, pathConstraintSupplier, timing);
   }
 
   public boolean isProcessed(ComposableCallGraphNode node) {
diff --git a/src/main/java/com/android/tools/r8/optimize/compose/UpdateChangedFlagsAbstractFunction.java b/src/main/java/com/android/tools/r8/optimize/compose/UpdateChangedFlagsAbstractFunction.java
index e57b4fb..91476d3 100644
--- a/src/main/java/com/android/tools/r8/optimize/compose/UpdateChangedFlagsAbstractFunction.java
+++ b/src/main/java/com/android/tools/r8/optimize/compose/UpdateChangedFlagsAbstractFunction.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteValueState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.FlowGraphStateProvider;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.InFlow;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.InFlowComparator;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.InFlowKind;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.OrAbstractFunction;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ValueState;
@@ -131,8 +132,8 @@
   }
 
   @Override
-  public int internalCompareToSameKind(InFlow other) {
-    return inFlow.compareTo(other.asUpdateChangedFlagsAbstractFunction().inFlow);
+  public int internalCompareToSameKind(InFlow other, InFlowComparator comparator) {
+    return inFlow.compareTo(other.asUpdateChangedFlagsAbstractFunction().inFlow, comparator);
   }
 
   @Override
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 147ce09..5ca4dbe 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -2360,20 +2360,25 @@
   private void processDeferredAnnotations(
       Map<DexType, Map<DexAnnotation, List<ProgramDefinition>>> deferredAnnotations,
       Function<ProgramDefinition, AnnotatedKind> kindProvider) {
+    // Collect annotations to process as processing the annotation can modify liveAnnotations.
+    Set<Map<DexAnnotation, List<ProgramDefinition>>> toProcess = Sets.newIdentityHashSet();
     for (DexType annotationType : liveAnnotations) {
       Map<DexAnnotation, List<ProgramDefinition>> annotations =
           deferredAnnotations.remove(annotationType);
       if (annotations != null) {
         assert annotations.keySet().stream()
             .allMatch(annotation -> annotationType.isIdenticalTo(annotation.getAnnotationType()));
-        annotations.forEach(
-            (annotation, annotatedItems) ->
-                annotatedItems.forEach(
-                    annotatedItem ->
-                        processAnnotation(
-                            annotatedItem, annotation, kindProvider.apply(annotatedItem))));
+        toProcess.add(annotations);
       }
     }
+    toProcess.forEach(
+        annotations ->
+            annotations.forEach(
+                (annotation, annotatedItems) ->
+                    annotatedItems.forEach(
+                        annotatedItem ->
+                            processAnnotation(
+                                annotatedItem, annotation, kindProvider.apply(annotatedItem)))));
   }
 
   private void ensureMethodsContinueToWidenAccess(ClassDefinition clazz) {
diff --git a/src/main/java/com/android/tools/r8/utils/ResourceShrinkerUtils.java b/src/main/java/com/android/tools/r8/utils/ResourceShrinkerUtils.java
index 41ad64f..47742f0 100644
--- a/src/main/java/com/android/tools/r8/utils/ResourceShrinkerUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ResourceShrinkerUtils.java
@@ -3,38 +3,37 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils;
 
+import com.android.build.shrinker.NoDebugReporter;
+import com.android.build.shrinker.ShrinkerDebugReporter;
 import com.android.build.shrinker.r8integration.R8ResourceShrinkerState;
 import com.android.tools.r8.AndroidResourceInput;
+import com.android.tools.r8.AndroidResourceProvider;
+import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.FeatureSplit;
 import com.android.tools.r8.ResourceException;
+import com.android.tools.r8.StringConsumer;
 import com.android.tools.r8.graph.AppView;
 import java.io.InputStream;
-import java.util.Collection;
+import java.util.function.Supplier;
 
 public class ResourceShrinkerUtils {
 
   public static R8ResourceShrinkerState createResourceShrinkerState(AppView<?> appView) {
+    InternalOptions options = appView.options();
     R8ResourceShrinkerState state =
         new R8ResourceShrinkerState(
-            exception -> appView.reporter().fatalError(new ExceptionDiagnostic(exception)));
-    InternalOptions options = appView.options();
+            exception -> appView.reporter().fatalError(new ExceptionDiagnostic(exception)),
+            shrinkerDebugReporterFromStringConsumer(
+                options.resourceShrinkerConfiguration.getDebugConsumer(), appView.reporter()));
     if (options.resourceShrinkerConfiguration.isOptimizedShrinking()
         && options.androidResourceProvider != null) {
       try {
-        addResources(
-            appView,
-            state,
-            options.androidResourceProvider.getAndroidResources(),
-            FeatureSplit.BASE);
+        addResources(appView, state, options.androidResourceProvider, FeatureSplit.BASE);
         if (options.hasFeatureSplitConfiguration()) {
           for (FeatureSplit featureSplit :
               options.getFeatureSplitConfiguration().getFeatureSplits()) {
             if (featureSplit.getAndroidResourceProvider() != null) {
-              addResources(
-                  appView,
-                  state,
-                  featureSplit.getAndroidResourceProvider().getAndroidResources(),
-                  featureSplit);
+              addResources(appView, state, featureSplit.getAndroidResourceProvider(), featureSplit);
             }
           }
         }
@@ -49,10 +48,10 @@
   private static void addResources(
       AppView<?> appView,
       R8ResourceShrinkerState state,
-      Collection<AndroidResourceInput> androidResources,
+      AndroidResourceProvider androidResourceProvider,
       FeatureSplit featureSplit)
       throws ResourceException {
-    for (AndroidResourceInput androidResource : androidResources) {
+    for (AndroidResourceInput androidResource : androidResourceProvider.getAndroidResources()) {
       switch (androidResource.getKind()) {
         case MANIFEST:
           state.addManifestProvider(
@@ -66,6 +65,10 @@
               () -> wrapThrowingInputStreamResource(appView, androidResource),
               androidResource.getPath().location());
           break;
+        case KEEP_RULE_FILE:
+          state.addKeepRuleRileProvider(
+              () -> wrapThrowingInputStreamResource(appView, androidResource));
+          break;
         case RES_FOLDER_FILE:
           state.addResFileProvider(
               () -> wrapThrowingInputStreamResource(appView, androidResource),
@@ -77,6 +80,29 @@
     }
   }
 
+  public static ShrinkerDebugReporter shrinkerDebugReporterFromStringConsumer(
+      StringConsumer consumer, DiagnosticsHandler diagnosticsHandler) {
+    if (consumer == null) {
+      return NoDebugReporter.INSTANCE;
+    }
+    return new ShrinkerDebugReporter() {
+      @Override
+      public void debug(Supplier<String> logSupplier) {
+        consumer.accept(logSupplier.get(), diagnosticsHandler);
+      }
+
+      @Override
+      public void info(Supplier<String> logProducer) {
+        consumer.accept(logProducer.get(), diagnosticsHandler);
+      }
+
+      @Override
+      public void close() throws Exception {
+        consumer.finished(diagnosticsHandler);
+      }
+    };
+  }
+
   private static InputStream wrapThrowingInputStreamResource(
       AppView<?> appView, AndroidResourceInput androidResource) {
     try {
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/ShrinkerDebugReporter.kt b/src/resourceshrinker/java/com/android/build/shrinker/ShrinkerDebugReporter.kt
index b592bb8..bcb6af3 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/ShrinkerDebugReporter.kt
+++ b/src/resourceshrinker/java/com/android/build/shrinker/ShrinkerDebugReporter.kt
@@ -18,16 +18,17 @@
 
 import java.io.File
 import java.io.PrintWriter
+import java.util.function.Supplier
 
 interface ShrinkerDebugReporter : AutoCloseable {
-    fun debug(f: () -> String)
-    fun info(f: () -> String)
+    fun debug(f: Supplier<String>)
+    fun info(f: Supplier<String>)
 }
 
 object NoDebugReporter : ShrinkerDebugReporter {
-    override fun debug(f: () -> String) = Unit
+    override fun debug(f: Supplier<String>) = Unit
 
-    override fun info(f: () -> String) = Unit
+    override fun info(f: Supplier<String>) = Unit
 
     override fun close() = Unit
 }
@@ -36,12 +37,12 @@
     reportFile: File
 ) : ShrinkerDebugReporter {
     private val writer: PrintWriter = reportFile.let { PrintWriter(it) }
-    override fun debug(f: () -> String) {
-        writer.println(f())
+    override fun debug(f: Supplier<String>) {
+        writer.println(f.get())
     }
 
-    override fun info(f: () -> String) {
-        writer.println(f())
+    override fun info(f: Supplier<String>) {
+        writer.println(f.get())
     }
 
     override fun close() {
@@ -56,14 +57,14 @@
 ) : ShrinkerDebugReporter {
     private val writer: PrintWriter? = reportFile?.let { PrintWriter(it) }
 
-    override fun debug(f: () -> String) {
-        val message = f()
+     override fun debug(f: Supplier<String>) {
+        val message = f.get()
         writer?.println(message)
         logDebug(message)
     }
 
-    override fun info(f: () -> String) {
-        val message = f()
+    override fun info(f: Supplier<String>) {
+        val message = f.get()
         writer?.println(message)
         logInfo(message)
     }
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
index 7a82e3f..019a123 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
+++ b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
@@ -10,9 +10,9 @@
 
 import com.android.aapt.Resources.ResourceTable;
 import com.android.aapt.Resources.XmlNode;
-import com.android.build.shrinker.NoDebugReporter;
 import com.android.build.shrinker.ResourceShrinkerImplKt;
 import com.android.build.shrinker.ResourceTableUtilKt;
+import com.android.build.shrinker.ShrinkerDebugReporter;
 import com.android.build.shrinker.graph.ProtoResourcesGraphBuilder;
 import com.android.build.shrinker.obfuscation.ProguardMappingsRecorder;
 import com.android.build.shrinker.r8integration.R8ResourceShrinkerState.R8ResourceShrinkerModel;
@@ -52,7 +52,9 @@
   private final Map<String, byte[]> dexInputs;
   private final Collection<PathAndBytes> resFolderInputs;
   private final Collection<PathAndBytes> xmlInputs;
+  private final List<byte[]> keepRuleInput;
   private List<String> proguardMapStrings;
+  private final ShrinkerDebugReporter debugReporter;
   private final List<PathAndBytes> manifest;
   private final Map<PathAndBytes, FeatureSplit> resourceTables;
 
@@ -61,10 +63,12 @@
     private final Map<String, byte[]> dexInputs = new HashMap<>();
     private final Map<Path, PathAndBytes> resFolderInputs = new HashMap<>();
     private final Map<Path, PathAndBytes> xmlInputs = new HashMap<>();
+    private final List<byte[]> keepRuleInput = new ArrayList<>();
 
     private final List<PathAndBytes> manifests = new ArrayList<>();
     private final Map<PathAndBytes, FeatureSplit> resourceTables = new HashMap<>();
     private List<String> proguardMapStrings;
+    private ShrinkerDebugReporter debugReporter;
 
     private Builder() {}
 
@@ -89,6 +93,11 @@
       return this;
     }
 
+    public Builder addKeepRuleInput(byte[] bytes) {
+      keepRuleInput.add(bytes);
+      return this;
+    }
+
     public Builder addResFolderInput(Path path, byte[] bytes) {
       PathAndBytes existing = resFolderInputs.get(path);
       if (existing != null) {
@@ -117,12 +126,19 @@
           manifests,
           resourceTables,
           xmlInputs.values(),
-          proguardMapStrings);
+          keepRuleInput,
+          proguardMapStrings,
+          debugReporter);
     }
 
     public void setProguardMapStrings(List<String> proguardMapStrings) {
       this.proguardMapStrings = proguardMapStrings;
     }
+
+    public Builder setShrinkerDebugReporter(ShrinkerDebugReporter debugReporter) {
+      this.debugReporter = debugReporter;
+      return this;
+    }
   }
 
   private LegacyResourceShrinker(
@@ -131,13 +147,17 @@
       List<PathAndBytes> manifests,
       Map<PathAndBytes, FeatureSplit> resourceTables,
       Collection<PathAndBytes> xmlInputs,
-      List<String> proguardMapStrings) {
+      List<byte[]> additionalRawXmlInputs,
+      List<String> proguardMapStrings,
+      ShrinkerDebugReporter debugReporter) {
     this.dexInputs = dexInputs;
     this.resFolderInputs = resFolderInputs;
     this.manifest = manifests;
     this.resourceTables = resourceTables;
     this.xmlInputs = xmlInputs;
+    this.keepRuleInput = additionalRawXmlInputs;
     this.proguardMapStrings = proguardMapStrings;
+    this.debugReporter = debugReporter;
   }
 
   public static Builder builder() {
@@ -145,7 +165,7 @@
   }
 
   public ShrinkerResult run() throws IOException, ParserConfigurationException, SAXException {
-    R8ResourceShrinkerModel model = new R8ResourceShrinkerModel(NoDebugReporter.INSTANCE, true);
+    R8ResourceShrinkerModel model = new R8ResourceShrinkerModel(debugReporter, true);
     for (PathAndBytes pathAndBytes : resourceTables.keySet()) {
       ResourceTable loadedResourceTable = ResourceTable.parseFrom(pathAndBytes.bytes);
       model.instantiateFromResourceTable(loadedResourceTable, false);
@@ -156,7 +176,7 @@
     }
     for (Entry<String, byte[]> entry : dexInputs.entrySet()) {
       // The analysis needs an origin for the dex files, synthesize an easy recognizable one.
-      Path inMemoryR8 = Paths.get("in_memory_r8_" + entry.getKey() + ".dex");
+      Path inMemoryR8 = Paths.get("in_memory_r8_" + entry.getKey());
       R8ResourceShrinker.runResourceShrinkerAnalysis(
           entry.getValue(), inMemoryR8, new DexFileAnalysisCallback(inMemoryR8, model));
     }
@@ -164,10 +184,8 @@
       ProtoAndroidManifestUsageRecorderKt.recordUsagesFromNode(
           XmlNode.parseFrom(pathAndBytes.bytes), model);
     }
-    for (PathAndBytes xmlInput : xmlInputs) {
-      if (xmlInput.path.startsWith("res/raw")) {
-        ToolsAttributeUsageRecorderKt.processRawXml(getUtfReader(xmlInput.getBytes()), model);
-      }
+    for (byte[] keepBytes : keepRuleInput) {
+      ToolsAttributeUsageRecorderKt.processRawXml(getUtfReader(keepBytes), model);
     }
 
     ImmutableMap<String, PathAndBytes> resFolderMappings =
@@ -188,11 +206,19 @@
     ResourceStore resourceStore = model.getResourceStore();
     resourceStore.processToolsAttributes();
     model.keepPossiblyReferencedResources();
+    debugReporter.debug(model.getResourceStore()::dumpResourceModel);
     // Transitively mark the reachable resources in the model.
     // Finds unused resources in provided resources collection.
     // Marks all used resources as 'reachable' in original collection.
     List<Resource> unusedResources =
-        ResourcesUtil.findUnusedResources(model.getResourceStore().getResources(), x -> {});
+        ResourcesUtil.findUnusedResources(
+            model.getResourceStore().getResources(),
+            roots -> {
+              debugReporter.debug(() -> "The root reachable resources are:");
+              roots.forEach(root -> debugReporter.debug(() -> " " + root));
+            });
+    debugReporter.debug(() -> "Unused resources are: ");
+    unusedResources.forEach(unused -> debugReporter.debug(() -> " " + unused));
     ImmutableSet.Builder<String> resEntriesToKeep = new ImmutableSet.Builder<>();
     for (PathAndBytes xmlInput : Iterables.concat(xmlInputs, resFolderInputs)) {
       if (ResourceShrinkerImplKt.isJarPathReachable(resourceStore, xmlInput.path.toString())) {
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
index 49f3546..5ac9755 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
+++ b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
@@ -16,7 +16,6 @@
 import com.android.aapt.Resources.XmlAttribute;
 import com.android.aapt.Resources.XmlElement;
 import com.android.aapt.Resources.XmlNode;
-import com.android.build.shrinker.NoDebugReporter;
 import com.android.build.shrinker.ResourceShrinkerImplKt;
 import com.android.build.shrinker.ResourceShrinkerModel;
 import com.android.build.shrinker.ResourceTableUtilKt;
@@ -54,6 +53,7 @@
   private final Function<Exception, RuntimeException> errorHandler;
   private final R8ResourceShrinkerModel r8ResourceShrinkerModel;
   private final Map<String, Supplier<InputStream>> xmlFileProviders = new HashMap<>();
+  private final List<Supplier<InputStream>> keepRuleFileProviders = new ArrayList<>();
 
   private final List<Supplier<InputStream>> manifestProviders = new ArrayList<>();
   private final Map<String, Supplier<InputStream>> resfileProviders = new HashMap<>();
@@ -69,8 +69,10 @@
     boolean tryClass(String possibleClass, Origin xmlFileOrigin);
   }
 
-  public R8ResourceShrinkerState(Function<Exception, RuntimeException> errorHandler) {
-    r8ResourceShrinkerModel = new R8ResourceShrinkerModel(NoDebugReporter.INSTANCE, true);
+  public R8ResourceShrinkerState(
+      Function<Exception, RuntimeException> errorHandler,
+      ShrinkerDebugReporter shrinkerDebugReporter) {
+    r8ResourceShrinkerModel = new R8ResourceShrinkerModel(shrinkerDebugReporter, true);
     this.errorHandler = errorHandler;
   }
 
@@ -140,6 +142,10 @@
     this.xmlFileProviders.put(location, inputStreamSupplier);
   }
 
+  public void addKeepRuleRileProvider(Supplier<InputStream> inputStreamSupplier) {
+    this.keepRuleFileProviders.add(inputStreamSupplier);
+  }
+
   public void addResFileProvider(Supplier<InputStream> inputStreamSupplier, String location) {
     this.resfileProviders.put(location, inputStreamSupplier);
   }
@@ -307,11 +313,9 @@
   }
 
   public void updateModelWithKeepXmlReferences() throws IOException {
-    for (Map.Entry<String, Supplier<InputStream>> entry : xmlFileProviders.entrySet()) {
-      if (entry.getKey().startsWith("res/raw")) {
-        ToolsAttributeUsageRecorderKt.processRawXml(
-            getUtfReader(entry.getValue().get().readAllBytes()), r8ResourceShrinkerModel);
-      }
+    for (Supplier<InputStream> keepRuleFileProvider : keepRuleFileProviders) {
+      ToolsAttributeUsageRecorderKt.processRawXml(
+          getUtfReader(keepRuleFileProvider.get().readAllBytes()), r8ResourceShrinkerModel);
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/androidresources/KeepXmlFilesTest.java b/src/test/java/com/android/tools/r8/androidresources/KeepXmlFilesTest.java
index 5d19a6a..3676fd8 100644
--- a/src/test/java/com/android/tools/r8/androidresources/KeepXmlFilesTest.java
+++ b/src/test/java/com/android/tools/r8/androidresources/KeepXmlFilesTest.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.androidresources;
 
+import com.android.tools.r8.R8TestBuilder;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
@@ -36,6 +37,7 @@
     return new AndroidTestResourceBuilder()
         .withSimpleManifestAndAppNameString()
         .addKeepXmlFor("@string/bar", "@drawable/foobar")
+        .addExplicitKeepRuleFileFor("@drawable/barfoo")
         .addRClassInitializeWithDefaultValues(R.string.class, R.drawable.class)
         .build(temp);
   }
@@ -47,7 +49,7 @@
         .addProgramClasses(FooBar.class)
         .addAndroidResources(getTestResources(temp))
         .addKeepMainRule(FooBar.class)
-        .applyIf(optimized, b -> b.enableOptimizedShrinking())
+        .applyIf(optimized, R8TestBuilder::enableOptimizedShrinking)
         .compile()
         .inspectShrunkenResources(
             resourceTableInspector -> {
@@ -57,6 +59,8 @@
               resourceTableInspector.assertContainsResourceWithName("string", "foo");
               // Referenced from keep.xml
               resourceTableInspector.assertContainsResourceWithName("drawable", "foobar");
+              // Referenced from additional keep xml files
+              resourceTableInspector.assertContainsResourceWithName("drawable", "barfoo");
               resourceTableInspector.assertDoesNotContainResourceWithName(
                   "string", "unused_string");
               resourceTableInspector.assertDoesNotContainResourceWithName(
@@ -86,6 +90,7 @@
 
     public static class drawable {
       public static int foobar;
+      public static int barfoo;
       public static int unused_drawable;
     }
   }
diff --git a/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkerLoggingTest.java b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkerLoggingTest.java
new file mode 100644
index 0000000..5ed2e09
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkerLoggingTest.java
@@ -0,0 +1,190 @@
+// 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.androidresources;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResourceBuilder;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ResourceShrinkerLoggingTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameter(1)
+  public boolean optimized;
+
+  @Parameters(name = "{0}, optimized: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withDefaultDexRuntime().withAllApiLevels().build(),
+        BooleanUtils.values());
+  }
+
+  public static AndroidTestResource getTestResources(TemporaryFolder temp) throws Exception {
+    return new AndroidTestResourceBuilder()
+        .withSimpleManifestAndAppNameString()
+        .addRClassInitializeWithDefaultValues(R.string.class, R.drawable.class)
+        .build(temp);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    StringBuilder log = new StringBuilder();
+    testForR8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(FooBar.class)
+        .apply(
+            b ->
+                b.getBuilder()
+                    .setResourceShrinkerConfiguration(
+                        configurationBuilder -> {
+                          if (optimized) {
+                            configurationBuilder.enableOptimizedShrinkingWithR8();
+                          }
+                          configurationBuilder.setDebugConsumer(
+                              (string, handler) -> log.append(string + "\n"));
+                          return configurationBuilder.build();
+                        }))
+        .addAndroidResources(getTestResources(temp))
+        .addKeepMainRule(FooBar.class)
+        .compile()
+        .inspectShrunkenResources(
+            resourceTableInspector -> {
+              resourceTableInspector.assertContainsResourceWithName("string", "bar");
+              resourceTableInspector.assertContainsResourceWithName("string", "foo");
+              resourceTableInspector.assertContainsResourceWithName("drawable", "foobar");
+              resourceTableInspector.assertDoesNotContainResourceWithName(
+                  "string", "unused_string");
+              resourceTableInspector.assertDoesNotContainResourceWithName(
+                  "drawable", "unused_drawable");
+            })
+        .run(parameters.getRuntime(), FooBar.class)
+        .assertSuccess();
+    // TODO(b/360284664): Add (non compatible) logging for optimized shrinking
+    if (!optimized) {
+      // Consistent with the old AGP embedded shrinker
+      List<String> strings = StringUtils.splitLines(log.toString());
+      // string:bar reachable from code
+      for (String dexReachableString : ImmutableList.of("bar", "foo")) {
+        ensureDexReachableResourcesState(strings, "string", dexReachableString, true);
+        ensureResourceReachabilityState(strings, "string", dexReachableString, true);
+        ensureRootResourceState(strings, "string", dexReachableString, true);
+        ensureUnusedState(strings, "string", dexReachableString, false);
+      }
+      // The app name is only reachable from the manifest, not dex
+      ensureDexReachableResourcesState(strings, "string", "app_name", false);
+      ensureResourceReachabilityState(strings, "string", "app_name", true);
+      ensureRootResourceState(strings, "string", "app_name", true);
+      ensureUnusedState(strings, "string", "app_name", false);
+
+      ensureDexReachableResourcesState(strings, "drawable", "unused_drawable", false);
+      ensureResourceReachabilityState(strings, "drawable", "unused_drawable", false);
+      ensureRootResourceState(strings, "drawable", "unused_drawable", false);
+      ensureUnusedState(strings, "drawable", "unused_drawable", true);
+
+      ensureDexReachableResourcesState(strings, "drawable", "foobar", true);
+      ensureResourceReachabilityState(strings, "drawable", "foobar", true);
+      ensureRootResourceState(strings, "drawable", "foobar", true);
+      ensureUnusedState(strings, "drawable", "foobar", false);
+    }
+  }
+
+  private void ensureDexReachableResourcesState(
+      List<String> logStrings, String type, String name, boolean reachable) {
+    // Example line:
+    // Marking drawable:foobar:2130771968 reachable: referenced from classes.dex
+    assertEquals(
+        logStrings.stream()
+            .anyMatch(
+                s ->
+                    s.contains("Marking " + type + ":" + name)
+                        && s.contains("reachable: referenced from")),
+        reachable);
+  }
+
+  private void ensureResourceReachabilityState(
+      List<String> logStrings, String type, String name, boolean reachable) {
+    // Example line:
+    // @packagename:string/bar : reachable=true
+    assertTrue(
+        logStrings.stream()
+            .anyMatch(s -> s.contains(type + "/" + name + " : reachable=" + reachable)));
+  }
+
+  private void ensureRootResourceState(
+      List<String> logStrings, String type, String name, boolean isRoot) {
+    assertEquals(isInSection(logStrings, type, name, "The root reachable resources are:"), isRoot);
+  }
+
+  private void ensureUnusedState(
+      List<String> logStrings, String type, String name, boolean isUnused) {
+    assertEquals(isInSection(logStrings, type, name, "Unused resources are: "), isUnused);
+  }
+
+  private static boolean isInSection(
+      List<String> logStrings, String type, String name, String sectionHeader) {
+    // Example for roots
+    // "The root reachable resources are:"
+    // " drawable:foobar:2130771968"
+    boolean isInSection = false;
+    for (String logString : logStrings) {
+      if (logString.equals(sectionHeader)) {
+        isInSection = true;
+        continue;
+      }
+      if (isInSection) {
+        if (!logString.startsWith(" ")) {
+          return false;
+        }
+        if (logString.startsWith(" " + type + ":" + name)) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  public static class FooBar {
+
+    public static void main(String[] args) {
+      if (System.currentTimeMillis() == 0) {
+        System.out.println(R.drawable.foobar);
+        System.out.println(R.string.bar);
+        System.out.println(R.string.foo);
+      }
+    }
+  }
+
+  public static class R {
+
+    public static class string {
+
+      public static int bar;
+      public static int foo;
+      public static int unused_string;
+    }
+
+    public static class drawable {
+
+      public static int foobar;
+      public static int unused_drawable;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/annotations/NestedAnnotationsUnusedTest.java b/src/test/java/com/android/tools/r8/annotations/NestedAnnotationsUnusedTest.java
new file mode 100644
index 0000000..a807a27
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/annotations/NestedAnnotationsUnusedTest.java
@@ -0,0 +1,75 @@
+// 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.annotations;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.annotations.NestedAnnotationsUsedTest.A;
+import com.android.tools.r8.annotations.NestedAnnotationsUsedTest.TestClass;
+import com.android.tools.r8.utils.StringUtils;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+// This is a regression test for b/359385828.
+@RunWith(Parameterized.class)
+public class NestedAnnotationsUnusedTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  private static final String EXPECTED_OUTPUT = StringUtils.lines("Found");
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepClassRules(A.class)
+        .addKeepRuntimeVisibleAnnotations()
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .inspect(inspector -> assertThat(inspector.clazz(B.class), isAbsent()))
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target({ElementType.TYPE})
+  public @interface A {
+    B b();
+
+    B[] bs() default {};
+  }
+
+  @Retention(RetentionPolicy.RUNTIME)
+  public @interface B {
+    String x();
+  }
+
+  @A(
+      b = @B(x = "1"),
+      bs = {@B(x = "2"), @B(x = "3")})
+  static class TestClass {
+
+    public static void main(String[] args) {
+      System.out.println(TestClass.class.getAnnotation(A.class) != null ? "Found" : "Not found");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/annotations/NestedAnnotationsUsedTest.java b/src/test/java/com/android/tools/r8/annotations/NestedAnnotationsUsedTest.java
new file mode 100644
index 0000000..90ae5bf
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/annotations/NestedAnnotationsUsedTest.java
@@ -0,0 +1,78 @@
+// 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.annotations;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringUtils;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+// This is a regression test for b/359385828.
+@RunWith(Parameterized.class)
+public class NestedAnnotationsUsedTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  private static final String EXPECTED_OUTPUT = StringUtils.lines("1", "2", "3");
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepClassRules(A.class)
+        .addKeepRuntimeVisibleAnnotations()
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .inspect(
+            inspector -> {
+              assertThat(inspector.clazz(B.class), isPresentAndRenamed());
+            })
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target({ElementType.TYPE})
+  public @interface A {
+    B b();
+
+    B[] bs() default {};
+  }
+
+  @Retention(RetentionPolicy.RUNTIME)
+  public @interface B {
+    String x();
+  }
+
+  @A(
+      b = @B(x = "1"),
+      bs = {@B(x = "2"), @B(x = "3")})
+  static class TestClass {
+    public static void main(String[] args) throws Exception {
+      System.out.println(TestClass.class.getAnnotation(A.class).b().x());
+      for (B b : TestClass.class.getAnnotation(A.class).bs()) {
+        System.out.println(b.x());
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkConfig.java b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkConfig.java
index b04a407..0eab29b 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkConfig.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkConfig.java
@@ -3,8 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.benchmarks;
 
+import com.android.tools.r8.utils.IterableUtils;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
+import com.google.common.collect.Iterables;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -12,7 +13,6 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
@@ -26,20 +26,6 @@
       throw new BenchmarkConfigError(
           "Inconsistent single/group benchmark setup: " + benchmark + " and " + other);
     }
-    Set<String> subNames =
-        Sets.union(benchmark.getSubBenchmarks().keySet(), other.getSubBenchmarks().keySet());
-    for (String subName : subNames) {
-      if (!Objects.equals(
-          benchmark.getSubBenchmarks().get(subName), other.getSubBenchmarks().get(subName))) {
-        throw new BenchmarkConfigError(
-            "Inconsistent metrics for sub-benchmark "
-                + subName
-                + " in benchmarks: "
-                + benchmark
-                + " and "
-                + other);
-      }
-    }
     if (!benchmark.getSuite().equals(other.getSuite())) {
       throw new BenchmarkConfigError(
           "Inconsistent suite for benchmarks: " + benchmark + " and " + other);
@@ -182,6 +168,11 @@
       return this;
     }
 
+    public Builder measureComposableCodeSize() {
+      metrics.add(BenchmarkMetric.ComposableCodeSize);
+      return this;
+    }
+
     public Builder measureWarmup() {
       measureWarmup = true;
       return this;
@@ -250,6 +241,16 @@
     this.measureWarmup = measureWarmup;
   }
 
+  public boolean containsMetric(BenchmarkMetric metric) {
+    Iterable<BenchmarkMetric> metrics =
+        isSingleBenchmark() ? getMetrics() : IterableUtils.flatten(getSubBenchmarks().values());
+    return Iterables.contains(metrics, metric);
+  }
+
+  public boolean containsComposableCodeSizeMetric() {
+    return containsMetric(BenchmarkMetric.ComposableCodeSize);
+  }
+
   public BenchmarkIdentifier getIdentifier() {
     return id;
   }
diff --git a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsCollection.java b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsCollection.java
index da3f3d2..8fc8508 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsCollection.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsCollection.java
@@ -24,22 +24,25 @@
   @Override
   public void addRuntimeResult(long result) {
     throw new BenchmarkConfigError(
-        "Unexpected attempt to add a runtime result to a the root of a benchmark with"
-            + " sub-benchmarks");
+        "Unexpected attempt to add a result to a the root of a benchmark with sub-benchmarks");
   }
 
   @Override
   public void addCodeSizeResult(long result) {
     throw new BenchmarkConfigError(
-        "Unexpected attempt to add a runtime result to a the root of a benchmark with"
-            + " sub-benchmarks");
+        "Unexpected attempt to add a result to a the root of a benchmark with sub-benchmarks");
+  }
+
+  @Override
+  public void addComposableCodeSizeResult(long result) {
+    throw new BenchmarkConfigError(
+        "Unexpected attempt to add a result to a the root of a benchmark with sub-benchmarks");
   }
 
   @Override
   public void addResourceSizeResult(long result) {
     throw new BenchmarkConfigError(
-        "Unexpected attempt to add a runtime result to a the root of a benchmark with"
-            + " sub-benchmarks");
+        "Unexpected attempt to add a result to a the root of a benchmark with sub-benchmarks");
   }
 
   @Override
diff --git a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsSingle.java b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsSingle.java
index e15dffb..ad6b1b3 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsSingle.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsSingle.java
@@ -9,13 +9,15 @@
 import it.unimi.dsi.fastutil.longs.LongList;
 import java.io.PrintStream;
 import java.util.Set;
+import java.util.function.LongConsumer;
 
 public class BenchmarkResultsSingle implements BenchmarkResults {
 
-  private String name;
+  private final String name;
   private final Set<BenchmarkMetric> metrics;
   private final LongList runtimeResults = new LongArrayList();
   private final LongList codeSizeResults = new LongArrayList();
+  private final LongList composableCodeSizeResults = new LongArrayList();
 
   public BenchmarkResultsSingle(String name, Set<BenchmarkMetric> metrics) {
     this.name = name;
@@ -30,6 +32,10 @@
     return codeSizeResults;
   }
 
+  public LongList getComposableCodeSizeResults() {
+    return composableCodeSizeResults;
+  }
+
   public LongList getRuntimeResults() {
     return runtimeResults;
   }
@@ -47,6 +53,15 @@
   }
 
   @Override
+  public void addComposableCodeSizeResult(long result) {
+    verifyMetric(
+        BenchmarkMetric.ComposableCodeSize,
+        metrics.contains(BenchmarkMetric.ComposableCodeSize),
+        true);
+    composableCodeSizeResults.add(result);
+  }
+
+  @Override
   public void addResourceSizeResult(long result) {
     addCodeSizeResult(result);
   }
@@ -78,6 +93,10 @@
         BenchmarkMetric.CodeSize,
         metrics.contains(BenchmarkMetric.CodeSize),
         !codeSizeResults.isEmpty());
+    verifyMetric(
+        BenchmarkMetric.ComposableCodeSize,
+        metrics.contains(BenchmarkMetric.ComposableCodeSize),
+        !composableCodeSizeResults.isEmpty());
   }
 
   private void printRunTime(long duration) {
@@ -89,6 +108,11 @@
     System.out.println(BenchmarkResults.prettyMetric(name, BenchmarkMetric.CodeSize, "" + bytes));
   }
 
+  private void printComposableCodeSize(long bytes) {
+    System.out.println(
+        BenchmarkResults.prettyMetric(name, BenchmarkMetric.ComposableCodeSize, "" + bytes));
+  }
+
   @Override
   public void printResults(ResultMode mode, boolean failOnCodeSizeDifferences) {
     verifyConfigAndResults();
@@ -97,6 +121,13 @@
       long result = mode == ResultMode.SUM ? sum : sum / runtimeResults.size();
       printRunTime(result);
     }
+    printCodeSizeResults(codeSizeResults, failOnCodeSizeDifferences, this::printCodeSize);
+    printCodeSizeResults(
+        composableCodeSizeResults, failOnCodeSizeDifferences, this::printComposableCodeSize);
+  }
+
+  private static void printCodeSizeResults(
+      LongList codeSizeResults, boolean failOnCodeSizeDifferences, LongConsumer printer) {
     if (!codeSizeResults.isEmpty()) {
       long size = codeSizeResults.getLong(0);
       if (failOnCodeSizeDifferences) {
@@ -107,7 +138,7 @@
           }
         }
       }
-      printCodeSize(size);
+      printer.accept(size);
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsSingleAdapter.java b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsSingleAdapter.java
index 55936d7..68d6fb8 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsSingleAdapter.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsSingleAdapter.java
@@ -22,6 +22,10 @@
         (codeSizeResult, iteration) -> {
           JsonObject resultObject = new JsonObject();
           resultObject.addProperty("code_size", codeSizeResult);
+          if (!result.getComposableCodeSizeResults().isEmpty()) {
+            resultObject.addProperty(
+                "composable_code_size", result.getComposableCodeSizeResults().getLong(iteration));
+          }
           resultObject.addProperty("runtime", result.getRuntimeResults().getLong(iteration));
           resultsArray.add(resultObject);
         });
diff --git a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsWarmup.java b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsWarmup.java
index 1a4e33c..19c1de8 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsWarmup.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/BenchmarkResultsWarmup.java
@@ -13,6 +13,7 @@
   private final String name;
   private final LongList runtimeResults = new LongArrayList();
   private long codeSizeResult = -1;
+  private long composableCodeSizeResult = -1;
   private long resourceSizeResult = -1;
 
   public BenchmarkResultsWarmup(String name) {
@@ -36,6 +37,20 @@
   }
 
   @Override
+  public void addComposableCodeSizeResult(long result) {
+    if (composableCodeSizeResult == -1) {
+      composableCodeSizeResult = result;
+    }
+    if (composableCodeSizeResult != result) {
+      throw new RuntimeException(
+          "Unexpected Composable code size difference: "
+              + result
+              + " and "
+              + composableCodeSizeResult);
+    }
+  }
+
+  @Override
   public void addResourceSizeResult(long result) {
     if (resourceSizeResult == -1) {
       resourceSizeResult = result;
diff --git a/src/test/java/com/android/tools/r8/benchmarks/appdumps/AppDumpBenchmarkBuilder.java b/src/test/java/com/android/tools/r8/benchmarks/appdumps/AppDumpBenchmarkBuilder.java
index 9454d6e..09934bc 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/appdumps/AppDumpBenchmarkBuilder.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/appdumps/AppDumpBenchmarkBuilder.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.benchmarks.appdumps;
 
+import static junit.framework.TestCase.assertFalse;
+
 import com.android.tools.r8.LibraryDesugaringTestConfiguration;
 import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.TestBase;
@@ -20,7 +22,12 @@
 import com.android.tools.r8.benchmarks.BenchmarkTarget;
 import com.android.tools.r8.dump.CompilerDump;
 import com.android.tools.r8.dump.DumpOptions;
+import com.android.tools.r8.keepanno.annotations.AnnotationPattern;
+import com.android.tools.r8.keepanno.annotations.KeepEdge;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
 import java.io.IOException;
+import java.lang.annotation.RetentionPolicy;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
@@ -31,6 +38,18 @@
 
 public class AppDumpBenchmarkBuilder {
 
+  @KeepEdge(
+      consequences =
+          @KeepTarget(
+              kind = KeepItemKind.ONLY_METHODS,
+              methodAnnotatedByClassName = "androidx.compose.runtime.Composable",
+              constraints = {},
+              constrainAnnotations =
+                  @AnnotationPattern(
+                      name = "androidx.compose.runtime.Composable",
+                      retention = RetentionPolicy.CLASS)))
+  static class KeepComposableAnnotations {}
+
   public static AppDumpBenchmarkBuilder builder() {
     return new AppDumpBenchmarkBuilder();
   }
@@ -99,6 +118,7 @@
         .addDependency(dumpDependency)
         .measureRunTime()
         .measureCodeSize()
+        .measureComposableCodeSize()
         .setTimeout(10, TimeUnit.MINUTES)
         .build();
   }
@@ -158,6 +178,10 @@
     return name + "Code";
   }
 
+  private String nameForComposableCodePart() {
+    return name + "ComposableCode";
+  }
+
   private String nameForLibraryPart() {
     return name + "Library";
   }
@@ -210,6 +234,16 @@
                 options -> options.getOpenClosedInterfacesOptions().suppressAllOpenInterfaces());
   }
 
+  private static ThrowableConsumer<? super R8FullTestBuilder>
+      getKeepComposableAnnotationsConfiguration() {
+    return testBuilder ->
+        testBuilder
+            .addProgramClasses(KeepComposableAnnotations.class)
+            .addKeepClassAndMembersRules("androidx.compose.runtime.Composable")
+            .addKeepRuntimeVisibleAnnotations()
+            .enableExperimentalKeepAnnotations();
+  }
+
   private static BenchmarkMethod runR8(
       AppDumpBenchmarkBuilder builder, ThrowableConsumer<? super R8FullTestBuilder> configuration) {
     return internalRunR8(builder, false, configuration);
@@ -253,20 +287,26 @@
                           })
                       .apply(configuration)
                       .applyIf(
+                          environment.getConfig().containsComposableCodeSizeMetric(),
+                          getKeepComposableAnnotationsConfiguration())
+                      .applyIf(
                           enableResourceShrinking,
                           b ->
                               b.enableOptimizedShrinking()
                                   .setAndroidResourcesFromPath(dump.getAndroidResources()))
                       .applyIf(
                           enableResourceShrinking,
+                          r -> {
+                            assertFalse(environment.getConfig().containsComposableCodeSizeMetric());
+                            r.benchmarkCompile(results.getSubResults(builder.nameForRuntimePart()))
+                                .benchmarkCodeSize(results.getSubResults(builder.nameForCodePart()))
+                                .benchmarkResourceSize(
+                                    results.getSubResults(builder.nameForResourcePart()));
+                          },
                           r ->
-                              r.benchmarkCompile(
-                                      results.getSubResults(builder.nameForRuntimePart()))
-                                  .benchmarkCodeSize(
-                                      results.getSubResults(builder.nameForCodePart()))
-                                  .benchmarkResourceSize(
-                                      results.getSubResults(builder.nameForResourcePart())),
-                          r -> r.benchmarkCompile(results).benchmarkCodeSize(results));
+                              r.benchmarkCompile(results)
+                                  .benchmarkCodeSize(results)
+                                  .benchmarkComposableCodeSize(results));
                 });
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/path/PathConstraintAnalysisUnitTest.java b/src/test/java/com/android/tools/r8/ir/analysis/path/PathConstraintAnalysisUnitTest.java
index 597cca6..8107aff 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/path/PathConstraintAnalysisUnitTest.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/path/PathConstraintAnalysisUnitTest.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.ir.analysis.framework.intraprocedural.DataflowAnalysisResult.SuccessfulDataflowAnalysisResult;
 import com.android.tools.r8.ir.analysis.path.state.ConcretePathConstraintAnalysisState;
 import com.android.tools.r8.ir.analysis.path.state.PathConstraintAnalysisState;
+import com.android.tools.r8.ir.analysis.path.state.PathConstraintKind;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodParameterFactory;
@@ -23,6 +24,9 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.stream.Collectors;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -61,7 +65,10 @@
     // Inspect ENTRY state.
     PathConstraintAnalysisState entryConstraint =
         successfulResult.getBlockExitState(code.entryBlock());
-    assertTrue(entryConstraint.isBottom());
+    assertTrue(entryConstraint.isConcrete());
+
+    ConcretePathConstraintAnalysisState concreteEntryConstraint = entryConstraint.asConcreteState();
+    assertTrue(concreteEntryConstraint.getPathConstraintsForTesting().isEmpty());
 
     // Inspect THEN state.
     PathConstraintAnalysisState thenConstraint =
@@ -69,11 +76,12 @@
     assertTrue(thenConstraint.isConcrete());
 
     ConcretePathConstraintAnalysisState concreteThenConstraint = thenConstraint.asConcreteState();
-    assertEquals(1, concreteThenConstraint.getPathConstraints().size());
-    assertEquals(0, concreteThenConstraint.getNegatedPathConstraints().size());
+    assertEquals(1, getPositivePathConstraints(concreteThenConstraint).size());
+    assertEquals(0, getNegativePathConstraints(concreteThenConstraint).size());
+    assertEquals(0, getDisabledPathConstraints(concreteThenConstraint).size());
 
     ComputationTreeNode thenPathConstraint =
-        concreteThenConstraint.getPathConstraints().iterator().next();
+        getPositivePathConstraints(concreteThenConstraint).iterator().next();
     assertTrue(thenPathConstraint instanceof ComputationTreeUnopCompareNode);
 
     // Inspect ELSE state.
@@ -82,11 +90,12 @@
     assertTrue(elseConstraint.isConcrete());
 
     ConcretePathConstraintAnalysisState concreteElseConstraint = elseConstraint.asConcreteState();
-    assertEquals(0, concreteElseConstraint.getPathConstraints().size());
-    assertEquals(1, concreteElseConstraint.getNegatedPathConstraints().size());
+    assertEquals(0, getPositivePathConstraints(concreteElseConstraint).size());
+    assertEquals(1, getNegativePathConstraints(concreteElseConstraint).size());
+    assertEquals(0, getDisabledPathConstraints(concreteElseConstraint).size());
 
     ComputationTreeNode elsePathConstraint =
-        concreteElseConstraint.getNegatedPathConstraints().iterator().next();
+        getNegativePathConstraints(concreteElseConstraint).iterator().next();
     assertEquals(thenPathConstraint, elsePathConstraint);
 
     // Inspect RETURN state.
@@ -96,10 +105,36 @@
 
     ConcretePathConstraintAnalysisState concreteReturnConstraint =
         returnConstraint.asConcreteState();
-    assertEquals(1, concreteReturnConstraint.getPathConstraints().size());
-    assertEquals(
-        concreteReturnConstraint.getPathConstraints(),
-        concreteReturnConstraint.getNegatedPathConstraints());
+    assertEquals(0, getPositivePathConstraints(concreteReturnConstraint).size());
+    assertEquals(0, getNegativePathConstraints(concreteReturnConstraint).size());
+    assertEquals(1, getDisabledPathConstraints(concreteReturnConstraint).size());
+
+    ComputationTreeNode returnPathConstraint =
+        getDisabledPathConstraints(concreteReturnConstraint).iterator().next();
+    assertEquals(thenPathConstraint, returnPathConstraint);
+  }
+
+  private Set<ComputationTreeNode> getPositivePathConstraints(
+      ConcretePathConstraintAnalysisState state) {
+    return getPathConstraintsOfKind(state, PathConstraintKind.POSITIVE);
+  }
+
+  private Set<ComputationTreeNode> getNegativePathConstraints(
+      ConcretePathConstraintAnalysisState state) {
+    return getPathConstraintsOfKind(state, PathConstraintKind.NEGATIVE);
+  }
+
+  private Set<ComputationTreeNode> getDisabledPathConstraints(
+      ConcretePathConstraintAnalysisState state) {
+    return getPathConstraintsOfKind(state, PathConstraintKind.DISABLED);
+  }
+
+  private Set<ComputationTreeNode> getPathConstraintsOfKind(
+      ConcretePathConstraintAnalysisState state, PathConstraintKind kind) {
+    return state.getPathConstraintsForTesting().entrySet().stream()
+        .filter(entry -> entry.getValue() == kind)
+        .map(Entry::getKey)
+        .collect(Collectors.toSet());
   }
 
   static class Main {
diff --git a/src/test/java/com/android/tools/r8/ir/regalloc/IdenticalAfterRegisterAllocationTest.java b/src/test/java/com/android/tools/r8/ir/regalloc/IdenticalAfterRegisterAllocationTest.java
index 181c0c5..6c68d17 100644
--- a/src/test/java/com/android/tools/r8/ir/regalloc/IdenticalAfterRegisterAllocationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/regalloc/IdenticalAfterRegisterAllocationTest.java
@@ -146,11 +146,6 @@
               }
 
               @Override
-              public boolean isPeepholeOptimizationsEnabled() {
-                return false;
-              }
-
-              @Override
               public boolean shouldFinalizeAfterLensCodeRewriter() {
                 return false;
               }
diff --git a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingWithAssumeNoSideEffectsTest.java b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingWithAssumeNoSideEffectsTest.java
index c57f579..4de5ec7 100644
--- a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingWithAssumeNoSideEffectsTest.java
+++ b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingWithAssumeNoSideEffectsTest.java
@@ -61,6 +61,28 @@
     }
   }
 
+  public static class SubsequentHasNextRunner {
+    public static void main(String[] args) {
+      Iterator<Service> iterator =
+          ServiceLoader.load(Service.class, Service.class.getClassLoader()).iterator();
+      if (iterator.hasNext()) {
+        iterator.next().print();
+        System.out.println("" + iterator.hasNext());
+      }
+    }
+  }
+
+  public static class SubsequentHasNextRunnerNonDominating {
+    public static void main(String[] args) {
+      Iterator<Service> iterator =
+          ServiceLoader.load(Service.class, Service.class.getClassLoader()).iterator();
+      if (iterator.hasNext()) {
+        iterator.next().print();
+      }
+      System.out.println("" + iterator.hasNext());
+    }
+  }
+
   public static class MultipleCallsRunner {
     public static void main(String[] args) {
       Iterator<Service> iterator =
@@ -76,6 +98,7 @@
       Iterator<Service> iterator =
           ServiceLoader.load(Service.class, Service.class.getClassLoader()).iterator();
 
+      // We optimize a subsequent hasNext() only when a prior hasNext() exists.
       iterator.next().print();
       if (iterator.hasNext()) {
         System.out.println("not reached");
@@ -187,6 +210,36 @@
   }
 
   @Test
+  public void testRewritingWithSubsequentHasNext_oneImpl()
+      throws IOException, CompilationFailedException, ExecutionException {
+    doTest(ServiceImpl.class)
+        .addKeepMainRule(SubsequentHasNextRunner.class)
+        .compile()
+        .run(parameters.getRuntime(), SubsequentHasNextRunner.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT + StringUtils.lines("false"))
+        .inspect(
+            inspector -> {
+              assertIteratorPresent(inspector, false);
+              verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+            });
+  }
+
+  @Test
+  public void testRewritingWithSubsequentHasNext_twoImpls()
+      throws IOException, CompilationFailedException, ExecutionException {
+    doTest(ServiceImpl.class, ServiceImpl2.class)
+        .addKeepMainRule(SubsequentHasNextRunner.class)
+        .compile()
+        .run(parameters.getRuntime(), SubsequentHasNextRunner.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT + StringUtils.lines("true"))
+        .inspect(
+            inspector -> {
+              assertIteratorPresent(inspector, false);
+              verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class, ServiceImpl2.class);
+            });
+  }
+
+  @Test
   public void testDoNotRewriteMultipleCalls()
       throws IOException, CompilationFailedException, ExecutionException {
     doTest(ServiceImpl.class)
@@ -275,4 +328,19 @@
               verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
             });
   }
+
+  @Test
+  public void testDoNotRewriteNonDominatedSubsequentHasNext()
+      throws IOException, CompilationFailedException, ExecutionException {
+    doTest(ServiceImpl.class)
+        .addKeepMainRule(SubsequentHasNextRunnerNonDominating.class)
+        .compile()
+        .run(parameters.getRuntime(), SubsequentHasNextRunnerNonDominating.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT + StringUtils.lines("false"))
+        .inspect(
+            inspector -> {
+              assertIteratorPresent(inspector, true);
+              verifyServiceMetaInf(inspector, Service.class, ServiceImpl.class);
+            });
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderTestBase.java b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderTestBase.java
index 809f782..9ce3ecd 100644
--- a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderTestBase.java
+++ b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderTestBase.java
@@ -23,10 +23,10 @@
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import java.io.IOException;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -96,16 +96,7 @@
 
   public void verifyServiceMetaInf(
       CodeInspector inspector, Class<?> serviceClass, Class<?> serviceImplClass) {
-    // Account for renaming, and for the impl to be merged with the interface.
-    String finalServiceName = inspector.clazz(serviceClass).getFinalName();
-    String finalImplName = inspector.clazz(serviceImplClass).getFinalName();
-    if (finalServiceName == null) {
-      finalServiceName = finalImplName;
-    }
-    Map<String, List<String>> actual = getServiceMappings();
-    Map<String, List<String>> expected =
-        ImmutableMap.of(finalServiceName, Collections.singletonList(finalImplName));
-    assertEquals(expected, actual);
+    verifyServiceMetaInf(inspector, serviceClass, serviceImplClass, null);
   }
 
   public void verifyServiceMetaInf(
@@ -113,13 +104,20 @@
       Class<?> serviceClass,
       Class<?> serviceImplClass1,
       Class<?> serviceImplClass2) {
-    // Account for renaming. No class merging should happen.
+    // Account for renaming & that impls can be removed if unused.
     String finalServiceName = inspector.clazz(serviceClass).getFinalName();
     String finalImplName1 = inspector.clazz(serviceImplClass1).getFinalName();
-    String finalImplName2 = inspector.clazz(serviceImplClass2).getFinalName();
+    String finalImplName2 =
+        serviceImplClass2 == null ? null : inspector.clazz(serviceImplClass2).getFinalName();
+    if (finalServiceName == null) {
+      finalServiceName = finalImplName1;
+    }
     Map<String, List<String>> actual = getServiceMappings();
-    Map<String, List<String>> expected =
-        ImmutableMap.of(finalServiceName, Arrays.asList(finalImplName1, finalImplName2));
+    List<String> expectedImpls =
+        finalImplName2 == null
+            ? ImmutableList.of(finalImplName1)
+            : ImmutableList.of(finalImplName1, finalImplName2);
+    Map<String, List<String>> expected = ImmutableMap.of(finalServiceName, expectedImpls);
     assertEquals(expected, actual);
   }
 
diff --git a/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java b/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java
index bf3998d..3412d54 100644
--- a/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java
@@ -47,8 +47,11 @@
 import com.android.tools.r8.utils.SemanticVersion;
 import com.android.tools.r8.utils.SourceFileTemplateProvider;
 import com.android.tools.r8.utils.codeinspector.Matchers;
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.lang.annotation.Annotation;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -982,15 +985,58 @@
       AndroidTestResource testResource, Path output, List<byte[]> classFileData) {
     Path resources = testResource.getResourceZip();
     resourceShrinkerOutput = output;
-    getBuilder()
-        .setAndroidResourceProvider(
-            new ArchiveProtoAndroidResourceProvider(resources, new PathOrigin(resources)));
+    ArchiveProtoAndroidResourceProvider provider = getResourceProvider(testResource);
+    getBuilder().setAndroidResourceProvider(provider);
     getBuilder()
         .setAndroidResourceConsumer(new ArchiveProtoAndroidResourceConsumer(output, resources));
 
     return addProgramClassFileData(classFileData);
   }
 
+  private static ArchiveProtoAndroidResourceProvider getResourceProvider(
+      AndroidTestResource testResource) {
+    Path resources = testResource.getResourceZip();
+    if (testResource.getAdditionalKeepRuleFiles().isEmpty()) {
+      return new ArchiveProtoAndroidResourceProvider(resources, new PathOrigin(resources));
+    }
+    ArchiveProtoAndroidResourceProvider provider =
+        new ArchiveProtoAndroidResourceProvider(resources, new PathOrigin(resources)) {
+          @Override
+          public Collection<AndroidResourceInput> getAndroidResources() throws ResourceException {
+            ArrayList<AndroidResourceInput> resourceInputs =
+                new ArrayList<>(super.getAndroidResources());
+            resourceInputs.addAll(
+                testResource.getAdditionalKeepRuleFiles().stream()
+                    .map(
+                        s ->
+                            new AndroidResourceInput() {
+                              @Override
+                              public Origin getOrigin() {
+                                return Origin.unknown();
+                              }
+
+                              @Override
+                              public ResourcePath getPath() {
+                                return () -> "keep/rule/path";
+                              }
+
+                              @Override
+                              public Kind getKind() {
+                                return Kind.KEEP_RULE_FILE;
+                              }
+
+                              @Override
+                              public InputStream getByteStream() throws ResourceException {
+                                return new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8));
+                              }
+                            })
+                    .collect(Collectors.toList()));
+            return resourceInputs;
+          }
+        };
+    return provider;
+  }
+
   public T setAndroidResourcesFromPath(Path input) throws IOException {
     return setAndroidResourcesFromPath(
         input, getState().getNewTempFile("resourceshrinkeroutput.zip"));
diff --git a/src/test/testbase/java/com/android/tools/r8/R8TestCompileResult.java b/src/test/testbase/java/com/android/tools/r8/R8TestCompileResult.java
index 172fb2b..3ff7487 100644
--- a/src/test/testbase/java/com/android/tools/r8/R8TestCompileResult.java
+++ b/src/test/testbase/java/com/android/tools/r8/R8TestCompileResult.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -12,6 +13,8 @@
 import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.ResourceTableInspector;
 import com.android.tools.r8.benchmarks.BenchmarkResults;
 import com.android.tools.r8.dexsplitter.SplitterTestBase.SplitRunner;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.metadata.R8BuildMetadata;
 import com.android.tools.r8.profile.art.model.ExternalArtProfile;
 import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
@@ -25,7 +28,7 @@
 import com.android.tools.r8.utils.ZipUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.android.tools.r8.utils.codeinspector.Matchers;
+import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
 import com.android.tools.r8.utils.graphinspector.GraphInspector;
 import java.io.IOException;
 import java.io.UncheckedIOException;
@@ -266,21 +269,18 @@
       throws IOException {
     assert getBackend() == runtime.getBackend();
     ClassSubject mainClassSubject = inspector().clazz(SplitRunner.class);
-    assertThat(
-        "Did you forget a keep rule for the main method?", mainClassSubject, Matchers.isPresent());
+    assertThat("Did you forget a keep rule for the main method?", mainClassSubject, isPresent());
     assertThat(
         "Did you forget a keep rule for the main method?",
         mainClassSubject.mainMethod(),
-        Matchers.isPresent());
+        isPresent());
     ClassSubject mainFeatureClassSubject = featureInspector(feature).clazz(mainFeatureClass);
     assertThat(
-        "Did you forget a keep rule for the run method?",
-        mainFeatureClassSubject,
-        Matchers.isPresent());
+        "Did you forget a keep rule for the run method?", mainFeatureClassSubject, isPresent());
     assertThat(
         "Did you forget a keep rule for the run method?",
         mainFeatureClassSubject.uniqueMethodWithOriginalName("run"),
-        Matchers.isPresent());
+        isPresent());
     String[] args = new String[2 + featureDependencies.length];
     args[0] = mainFeatureClassSubject.getFinalName();
     args[1] = feature.toString();
@@ -319,4 +319,29 @@
     results.addCodeSizeResult(applicationSizeWithFeatures);
     return self();
   }
+
+  @Override
+  public R8TestCompileResult benchmarkComposableCodeSize(BenchmarkResults results)
+      throws IOException {
+    int composableCodeSize = getComposableCodeSize(inspector());
+    for (Path feature : features) {
+      composableCodeSize += getComposableCodeSize(featureInspector(feature));
+    }
+    results.addComposableCodeSizeResult(composableCodeSize);
+    return self();
+  }
+
+  private int getComposableCodeSize(CodeInspector inspector) {
+    DexType composableType =
+        inspector.getFactory().createType("Landroidx/compose/runtime/Composable;");
+    int composableCodeSize = 0;
+    for (FoundClassSubject classSubject : inspector.allClasses()) {
+      for (ProgramMethod method : classSubject.getDexProgramClass().directProgramMethods()) {
+        if (method.getAnnotations().hasAnnotation(composableType)) {
+          composableCodeSize += method.getDefinition().getCode().asDexCode().codeSizeInBytes();
+        }
+      }
+    }
+    return composableCodeSize;
+  }
 }
diff --git a/src/test/testbase/java/com/android/tools/r8/TestBuilder.java b/src/test/testbase/java/com/android/tools/r8/TestBuilder.java
index a7c2cd8..5f9d323 100644
--- a/src/test/testbase/java/com/android/tools/r8/TestBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/TestBuilder.java
@@ -50,7 +50,7 @@
     return self();
   }
 
-  public T applyIf(boolean value, ThrowableConsumer<T> consumer) {
+  public T applyIf(boolean value, ThrowableConsumer<? super T> consumer) {
     T self = self();
     if (value) {
       consumer.acceptWithRuntimeException(self);
diff --git a/src/test/testbase/java/com/android/tools/r8/TestCompileResult.java b/src/test/testbase/java/com/android/tools/r8/TestCompileResult.java
index 8f32eae..1f50960 100644
--- a/src/test/testbase/java/com/android/tools/r8/TestCompileResult.java
+++ b/src/test/testbase/java/com/android/tools/r8/TestCompileResult.java
@@ -20,6 +20,7 @@
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.benchmarks.BenchmarkResults;
 import com.android.tools.r8.debug.DebugTestConfig;
+import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApiLevel;
@@ -721,4 +722,9 @@
     results.addCodeSizeResult(app.applicationSize());
     return self();
   }
+
+  public CR benchmarkComposableCodeSize(BenchmarkResults results)
+      throws IOException, ResourceException {
+    throw new Unimplemented();
+  }
 }
diff --git a/src/test/testbase/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java b/src/test/testbase/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
index 80333c3..2f9a852 100644
--- a/src/test/testbase/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
+++ b/src/test/testbase/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
@@ -173,10 +173,13 @@
   public static class AndroidTestResource {
     private final AndroidTestRClass rClass;
     private final Path resourceZip;
+    private final List<String> additionalKeepRuleFiles;
 
-    AndroidTestResource(AndroidTestRClass rClass, Path resourceZip) {
+    AndroidTestResource(
+        AndroidTestRClass rClass, Path resourceZip, List<String> additionalRawXmlFiles) {
       this.rClass = rClass;
       this.resourceZip = resourceZip;
+      this.additionalKeepRuleFiles = additionalRawXmlFiles;
     }
 
     public AndroidTestRClass getRClass() {
@@ -186,6 +189,10 @@
     public Path getResourceZip() {
       return resourceZip;
     }
+
+    public List<String> getAdditionalKeepRuleFiles() {
+      return additionalKeepRuleFiles;
+    }
   }
 
   // Easy traversable resource table.
@@ -296,6 +303,7 @@
     // Used for xml files that we hard code to -v24
     private final Map<String, String> apiSpecificXmlFiles = new TreeMap<>();
     private final Map<String, String> rawXmlFiles = new TreeMap<>();
+    private final List<String> keepRuleFiles = new ArrayList<>();
     private final List<Class<?>> classesToRemap = new ArrayList<>();
     private int packageId = 0x7f;
     private String packageName;
@@ -379,7 +387,16 @@
     }
 
     public AndroidTestResourceBuilder addKeepXmlFor(String... resourceReferences) {
-      addRawXml("keep.xml", String.format(KEEP_XML, String.join(",", resourceReferences)));
+      addRawXml("keep.xml", getKeepXmlContent(resourceReferences));
+      return this;
+    }
+
+    private static String getKeepXmlContent(String[] resourceReferences) {
+      return String.format(KEEP_XML, String.join(",", resourceReferences));
+    }
+
+    public AndroidTestResourceBuilder addExplicitKeepRuleFileFor(String... resourceReferences) {
+      keepRuleFiles.add(getKeepXmlContent(resourceReferences));
       return this;
     }
 
@@ -546,7 +563,7 @@
             }
           });
       return new AndroidTestResource(
-          new AndroidTestRClass(rClassJavaFile, rewrittenRClassFiles), output);
+          new AndroidTestRClass(rClassJavaFile, rewrittenRClassFiles), output, keepRuleFiles);
     }
 
     private String createOverlayableXml() {
diff --git a/src/test/testbase/java/com/android/tools/r8/benchmarks/BenchmarkMetric.java b/src/test/testbase/java/com/android/tools/r8/benchmarks/BenchmarkMetric.java
index 62bc0ba..abf93ce 100644
--- a/src/test/testbase/java/com/android/tools/r8/benchmarks/BenchmarkMetric.java
+++ b/src/test/testbase/java/com/android/tools/r8/benchmarks/BenchmarkMetric.java
@@ -6,6 +6,7 @@
 public enum BenchmarkMetric {
   RunTimeRaw,
   CodeSize,
+  ComposableCodeSize,
   StartupTime;
 
   public String getDartType() {
diff --git a/src/test/testbase/java/com/android/tools/r8/benchmarks/BenchmarkResults.java b/src/test/testbase/java/com/android/tools/r8/benchmarks/BenchmarkResults.java
index 9dcfa97..a925368 100644
--- a/src/test/testbase/java/com/android/tools/r8/benchmarks/BenchmarkResults.java
+++ b/src/test/testbase/java/com/android/tools/r8/benchmarks/BenchmarkResults.java
@@ -13,6 +13,8 @@
   // Append a code size result. This is always assumed to be identical if called multiple times.
   void addCodeSizeResult(long result);
 
+  void addComposableCodeSizeResult(long result);
+
   // Append a resource size result. This is always assumed to be identical if called multiple times.
   void addResourceSizeResult(long result);
 
diff --git a/tools/perf/index.html b/tools/perf/index.html
index ec09f14..73c2931 100644
--- a/tools/perf/index.html
+++ b/tools/perf/index.html
@@ -3,6 +3,7 @@
 <head>
   <meta charset="utf-8">
   <title>R8 perf</title>
+  <link rel="stylesheet" href="stylesheet.css">
 </head>
 <body>
   <div id="benchmark-selectors"></div>
@@ -20,6 +21,7 @@
     </div>
   </div>
   <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
+  <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.2.0"></script>
   <script type="module">
     // Utility methods.
     Array.prototype.any = function(predicate) {
@@ -90,7 +92,9 @@
       }
     }
     if (selectedBenchmarks.size == 0) {
-      selectedBenchmarks.add(benchmarks.values().next().value);
+      const randomBenchmarkIndex = Math.floor(Math.random() * benchmarks.size);
+      const randomBenchmark = Array.from(benchmarks)[randomBenchmarkIndex];
+      selectedBenchmarks.add(randomBenchmark);
     }
     for (const benchmark of benchmarks.values()) {
       const input = document.createElement('input');
@@ -133,6 +137,16 @@
                             .first()
                             .code_size
                         : NaN);
+        const composableCodeSizeData =
+            filteredCommits.map(
+                (c, i) =>
+                    selectedBenchmark in filteredCommits[i].benchmarks
+                        ? filteredCommits[i]
+                            .benchmarks[selectedBenchmark]
+                            .results
+                            .first()
+                            .composable_code_size
+                        : NaN);
         const codeSizeScatterData = [];
         for (const commit of filteredCommits.values()) {
           if (!(selectedBenchmark in commit.benchmarks)) {
@@ -182,7 +196,34 @@
             type: 'line',
             label: 'Code size',
             data: codeSizeData,
+            datalabels: {
+              align: 'end',
+              anchor: 'end'
+            },
             tension: 0.1,
+            yAxisID: 'y',
+            segment: {
+              borderColor: ctx =>
+                  skipped(
+                      ctx,
+                      myChart
+                          ? myChart.data.datasets[ctx.datasetIndex].backgroundColor
+                          : undefined),
+              borderDash: ctx => skipped(ctx, [6, 6]),
+            },
+            spanGaps: true
+          },
+          {
+            benchmark: selectedBenchmark,
+            type: 'line',
+            label: 'Composable size',
+            data: composableCodeSizeData,
+            datalabels: {
+              align: 'start',
+              anchor: 'start'
+            },
+            tension: 0.1,
+            yAxisID: 'y_composable_code_size',
             segment: {
               borderColor: ctx =>
                   skipped(
@@ -199,6 +240,11 @@
             type: 'scatter',
             label: 'Nondeterminism',
             data: codeSizeScatterData,
+            datalabels: {
+              labels: {
+                value: null
+              }
+            },
             radius: 6,
             pointBackgroundColor: 'red'
           },
@@ -207,8 +253,13 @@
             type: 'line',
             label: 'Runtime',
             data: runtimeData,
+            datalabels: {
+              labels: {
+                value: null
+              }
+            },
             tension: 0.1,
-            yAxisID: 'y2',
+            yAxisID: 'y_runtime',
             segment: {
               borderColor: ctx =>
                   skipped(
@@ -225,7 +276,12 @@
             type: 'scatter',
             label: 'Runtime variance',
             data: runtimeScatterData,
-            yAxisID: 'y2'
+            datalabels: {
+              labels: {
+                value: null
+              }
+            },
+            yAxisID: 'y_runtime'
           }
         ]);
       }
@@ -238,7 +294,7 @@
 
     // Legend tracking.
     const legends =
-        new Set(['Code size', 'Nondeterminism', 'Runtime', 'Runtime variance']);
+        new Set(['Code size', 'Composable size', 'Nondeterminism', 'Runtime', 'Runtime variance']);
     const selectedLegends =
         new Set(
               unescape(window.location.hash.substring(1))
@@ -246,6 +302,21 @@
                   .filter(l => legends.has(l)));
     if (selectedLegends.size == 0) {
       legends.forEach(l => selectedLegends.add(l));
+      selectedLegends.delete('Runtime variance')
+    }
+
+    function getDataPercentageChange(context) {
+      var i = context.dataIndex;
+      var value = context.dataset.data[i];
+      var j = i;
+      var previousValue;
+      do {
+        if (j == 0) {
+          return null;
+        }
+        previousValue = context.dataset.data[--j];
+      } while (previousValue === undefined || isNaN(previousValue));
+      return (value - previousValue) / previousValue * 100;
     }
 
     // Chart options.
@@ -254,9 +325,32 @@
           event.native.target.style.cursor =
               chartElement[0] ? 'pointer' : 'default',
       plugins: {
+        datalabels: {
+          backgroundColor: 'rgba(255, 255, 255, 0.7)',
+          borderColor: 'rgba(128, 128, 128, 0.7)',
+          borderRadius: 4,
+          borderWidth: 1,
+          color: context => getDataPercentageChange(context) < 0 ? 'green' : 'red',
+          display: context => {
+            var percentageChange = getDataPercentageChange(context);
+            return percentageChange !== null && Math.abs(percentageChange) >= 0.1;
+          },
+          font: {
+            size: 20,
+            weight: 'bold'
+          },
+          offset: 8,
+          formatter: (value, context) => {
+            var percentageChange = getDataPercentageChange(context);
+            var percentageChangeTwoDecimals = Math.round(percentageChange * 100) / 100;
+            var glyph = percentageChange < 0 ? '▼' : '▲';
+            return glyph + ' ' + percentageChangeTwoDecimals + '%';
+          },
+          padding: 6
+        },
         legend: {
           labels: {
-            filter: function(legendItem, data) {
+            filter: (legendItem, data) => {
               // Only retain the legends for the first selected benchmark. If
               // multiple benchmarks are selected, then use the legends of the
               // first selected benchmark to control all selected benchmarks.
@@ -265,7 +359,7 @@
               return legendItem.datasetIndex < numUniqueLegends;
             },
           },
-          onClick: function legendClickHandler(e, legendItem, legend) {
+          onClick: (e, legendItem, legend) => {
             const clickedLegend = legendItem.text;
             if (selectedLegends.has(clickedLegend)) {
               selectedLegends.delete(clickedLegend);
@@ -277,7 +371,7 @@
         },
         tooltip: {
           callbacks: {
-            title: (context) => {
+            title: context => {
               const elementInfo = context[0];
               var commit;
               if (elementInfo.dataset.type == 'line') {
@@ -288,7 +382,7 @@
               }
               return commit.title;
             },
-            footer: (context) => {
+            footer: context => {
               const elementInfo = context[0];
               var commit;
               if (elementInfo.dataset.type == 'line') {
@@ -317,18 +411,25 @@
             text: 'Code size (bytes)'
           }
         },
-        y2: {
+        y_runtime: {
           position: 'right',
           title: {
             display: true,
             text: 'Runtime (seconds)'
           }
-        }
+        },
+        y_composable_code_size: {
+          position: 'left',
+          title: {
+            display: true,
+            text: 'Composable code size (bytes)'
+          }
+        },
       }
     };
 
     // Setup click handler.
-    canvas.onclick = function (event) {
+    canvas.onclick = event => {
       const points =
           myChart.getElementsAtEventForMode(
               event, 'nearest', { intersect: true }, true);
@@ -340,7 +441,7 @@
     };
 
     // Setup chart navigation.
-    var zoom = { left: 0, right: commits.length };
+    var zoom = { left: Math.max(0, commits.length - 75), right: commits.length };
     for (const urlOption of urlOptions.values()) {
       if (urlOption.startsWith('L')) {
         var left = parseInt(urlOption.substring(1));
@@ -358,7 +459,7 @@
       }
     }
 
-    showMoreLeft.onclick = function (event) {
+    showMoreLeft.onclick = event => {
       if (zoom.left == 0) {
         return;
       }
@@ -370,7 +471,7 @@
       updateChart(true, false);
     };
 
-    showLessLeft.onclick = function (event) {
+    showLessLeft.onclick = event => {
       const currentSize = zoom.right - zoom.left;
       zoom.left = zoom.left + Math.floor(currentSize / 2);
       if (zoom.left >= zoom.right) {
@@ -379,7 +480,7 @@
       updateChart(true, false);
     };
 
-    showLessRight.onclick = function (event) {
+    showLessRight.onclick = event => {
       if (zoom.right == 0) {
         return;
       }
@@ -391,7 +492,7 @@
       updateChart(true, false);
     };
 
-    showMoreRight.onclick = function (event) {
+    showMoreRight.onclick = event => {
       const currentSize = zoom.right - zoom.left;
       zoom.right = zoom.right + currentSize;
       if (zoom.right > commits.length) {
@@ -419,6 +520,13 @@
           const datasetMeta = myChart.getDatasetMeta(datasetIndex);
           datasetMeta.hidden = !selectedLegends.has(datasetMeta.label);
         }
+
+        // Update scales.
+        options.scales.y.display = selectedLegends.has('Code size');
+        options.scales.y_composable_code_size.display = selectedLegends.has('Composable size');
+        options.scales.y_runtime.display =
+            selectedLegends.has('Runtime') || selectedLegends.has('Runtime variance');
+
         // Update chart.
         myChart.update();
       }
@@ -466,7 +574,8 @@
     // Create chart.
     const myChart = new Chart(canvas, {
       data: getData(),
-      options: options
+      options: options,
+      plugins: [ChartDataLabels]
     });
 
     // Hide disabled legends.
diff --git a/tools/perf/stylesheet.css b/tools/perf/stylesheet.css
new file mode 100644
index 0000000..d458469
--- /dev/null
+++ b/tools/perf/stylesheet.css
@@ -0,0 +1,5 @@
+@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap');
+
+body {
+  font-family: 'Roboto';
+}
\ No newline at end of file
diff --git a/tools/run_benchmark.py b/tools/run_benchmark.py
index 6bf0882..f01535f 100755
--- a/tools/run_benchmark.py
+++ b/tools/run_benchmark.py
@@ -18,7 +18,9 @@
     utils.GRADLE_TASK_TESTBASE_WITH_APPLY_MAPPING_JAR,
     utils.GRADLE_TASK_TEST_DEPS_JAR
 ]
-GOLEM_BUILD_TARGETS = [utils.GRADLE_TASK_R8LIB] + GOLEM_BUILD_TARGETS_TESTS
+GOLEM_BUILD_TARGETS = [
+    utils.GRADLE_TASK_R8LIB, utils.GRADLE_TASK_KEEP_ANNO_JAR
+] + GOLEM_BUILD_TARGETS_TESTS
 
 
 def get_golem_resource_path(benchmark):
@@ -46,6 +48,13 @@
         required=True,
         # These should 1:1 with benchmarks/BenchmarkTarget.java
         choices=['d8', 'r8-full', 'r8-force', 'r8-compat'])
+    result.add_argument(
+        '--debug-agent',
+        '--debug_agent',
+        help=
+        'Enable Java debug agent and suspend compilation (default disabled)',
+        default=False,
+        action='store_true')
     result.add_argument('--nolib',
                         '--no-lib',
                         '--no-r8lib',
@@ -119,7 +128,8 @@
             utils.GRADLE_TASK_TEST_JAR, utils.GRADLE_TASK_TEST_DEPS_JAR,
             utils.GRADLE_TASK_TEST_UNZIP_TESTBASE
         ]
-        buildTargets = [utils.GRADLE_TASK_R8] + testBuildTargets
+        buildTargets = [utils.GRADLE_TASK_R8, utils.GRADLE_TASK_KEEP_ANNO_JAR
+                       ] + testBuildTargets
         r8jar = utils.R8_JAR
         testjars = [
             utils.R8_TESTS_JAR, utils.R8_TESTS_DEPS_JAR, utils.R8_TESTBASE_JAR
@@ -157,7 +167,8 @@
     cmd = [
         jdk.GetJavaExecutable(jdkhome), '-Xms8g', '-Xmx8g',
         '-XX:+TieredCompilation', '-XX:TieredStopAtLevel=4',
-        '-DBENCHMARK_IGNORE_CODE_SIZE_DIFFERENCES'
+        '-DBENCHMARK_IGNORE_CODE_SIZE_DIFFERENCES',
+        f'-DBUILD_PROP_KEEPANNO_RUNTIME_PATH={utils.REPO_ROOT}/d8_r8/keepanno/build/classes/java/main'
     ]
     if options.enable_assertions:
         cmd.append('-ea')
@@ -175,6 +186,10 @@
     if options.output:
         cmd.append(f'-DBENCHMARK_OUTPUT={options.output}')
     cmd.extend(['-cp', ':'.join([r8jar] + testjars)])
+    if options.debug_agent:
+        cmd.append(
+            '-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005'
+        )
     cmd.extend([
         'com.android.tools.r8.benchmarks.BenchmarkMainEntryRunner',
         options.benchmark,