Canonicalize KeepInfo

Change-Id: Ia3e925dbf2c7cbabf32c59c03d9e7cebed3f7c16
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 bacf716..6e97625 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -414,7 +414,7 @@
       reachableVirtualTargets = new IdentityHashMap<>();
 
   /** Collection of keep requirements for the program. */
-  private final MutableKeepInfoCollection keepInfo = new MutableKeepInfoCollection();
+  private final MutableKeepInfoCollection keepInfo;
 
   /**
    * Conditional minimum keep info for classes, fields, and methods, which should only be applied if
@@ -508,6 +508,7 @@
     this.missingClassesBuilder = appView.appInfo().getMissingClasses().builder();
     this.mode = mode;
     this.options = options;
+    this.keepInfo = new MutableKeepInfoCollection(options);
     this.useRegistryFactory = createUseRegistryFactory();
     this.worklist = EnqueuerWorklist.createWorklist(this);
     this.proguardCompatibilityActionsBuilder =
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepClassInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepClassInfo.java
index 3bb0f20..985559d 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepClassInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepClassInfo.java
@@ -186,6 +186,34 @@
     return this.equals(bottom());
   }
 
+  @Override
+  public boolean equalsNoAnnotations(KeepClassInfo other) {
+    return super.equalsNoAnnotations(other)
+        && allowClassInlining == other.internalIsClassInliningAllowed()
+        && allowHorizontalClassMerging == other.internalIsHorizontalClassMergingAllowed()
+        && allowPermittedSubclassesRemoval == other.internalIsPermittedSubclassesRemovalAllowed()
+        && allowRepackaging == other.internalIsRepackagingAllowed()
+        && allowSyntheticSharing == other.internalIsSyntheticSharingAllowed()
+        && allowUnusedInterfaceRemoval == other.internalIsUnusedInterfaceRemovalAllowed()
+        && allowVerticalClassMerging == other.internalIsVerticalClassMergingAllowed()
+        && checkEnumUnboxed == other.internalIsCheckEnumUnboxedEnabled();
+  }
+
+  @Override
+  public int hashCodeNoAnnotations() {
+    int hash = super.hashCodeNoAnnotations();
+    int index = super.numberOfBooleans();
+    hash += bit(allowClassInlining, index++);
+    hash += bit(allowHorizontalClassMerging, index++);
+    hash += bit(allowPermittedSubclassesRemoval, index++);
+    hash += bit(allowRepackaging, index++);
+    hash += bit(allowSyntheticSharing, index++);
+    hash += bit(allowUnusedInterfaceRemoval, index++);
+    hash += bit(allowVerticalClassMerging, index++);
+    hash += bit(checkEnumUnboxed, index);
+    return hash;
+  }
+
   public static class Builder extends KeepInfo.Builder<Builder, KeepClassInfo> {
 
     private boolean allowClassInlining;
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java
index 2483029..1c6cfa5 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java
@@ -71,6 +71,23 @@
     return this.equals(bottom());
   }
 
+  @Override
+  public boolean equalsNoAnnotations(KeepFieldInfo other) {
+    return super.equalsNoAnnotations(other)
+        && (allowFieldTypeStrengthening == other.internalIsFieldTypeStrengtheningAllowed())
+        && (allowRedundantFieldLoadElimination
+            == other.internalIsRedundantFieldLoadEliminationAllowed());
+  }
+
+  @Override
+  public int hashCodeNoAnnotations() {
+    int hash = super.hashCodeNoAnnotations();
+    int index = super.numberOfBooleans();
+    hash += bit(allowFieldTypeStrengthening, index++);
+    hash += bit(allowRedundantFieldLoadElimination, index);
+    return hash;
+  }
+
   public static class Builder extends KeepMemberInfo.Builder<Builder, KeepFieldInfo> {
 
     private boolean allowFieldTypeStrengthening;
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepInfo.java
index d37185a..a085f21 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepInfo.java
@@ -292,6 +292,39 @@
         && typeAnnotationsInfo.isLessThanOrEqualTo(other.internalTypeAnnotationsInfo());
   }
 
+  public boolean equalsNoAnnotations(K other) {
+    return getClass() == other.getClass()
+        && (allowAccessModification == other.internalIsAccessModificationAllowed())
+        && (allowAccessModificationForTesting
+            == other.internalIsAccessModificationAllowedForTesting())
+        && (allowMinification == other.internalIsMinificationAllowed())
+        && (allowOptimization == other.internalIsOptimizationAllowed())
+        && (allowShrinking == other.internalIsShrinkingAllowed())
+        && (allowSignatureRemoval == other.internalIsSignatureRemovalAllowed())
+        && (checkDiscarded == other.internalIsCheckDiscardedEnabled());
+  }
+
+  public int hashCodeNoAnnotations() {
+    int hash = 0;
+    int index = 0;
+    hash += bit(allowAccessModification, index++);
+    hash += bit(allowAccessModificationForTesting, index++);
+    hash += bit(allowMinification, index++);
+    hash += bit(allowOptimization, index++);
+    hash += bit(allowShrinking, index++);
+    hash += bit(allowSignatureRemoval, index++);
+    hash += bit(checkDiscarded, index);
+    return hash;
+  }
+
+  protected int numberOfBooleans() {
+    return 7;
+  }
+
+  protected int bit(boolean bool, int index) {
+    return bool ? 1 << index : 0;
+  }
+
   /** Builder to construct an arbitrary keep info object. */
   public abstract static class Builder<B extends Builder<B, K>, K extends KeepInfo<B, K>> {
 
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepInfoCanonicalizer.java b/src/main/java/com/android/tools/r8/shaking/KeepInfoCanonicalizer.java
new file mode 100644
index 0000000..ada990b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/KeepInfoCanonicalizer.java
@@ -0,0 +1,93 @@
+// 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.shaking;
+
+import com.android.tools.r8.shaking.KeepInfoEquivalenceNoAnnotations.ClassEquivalenceNoAnnotations;
+import com.android.tools.r8.shaking.KeepInfoEquivalenceNoAnnotations.FieldEquivalenceNoAnnotations;
+import com.android.tools.r8.shaking.KeepInfoEquivalenceNoAnnotations.MethodEquivalenceNoAnnotations;
+import com.android.tools.r8.utils.LRUCache;
+import com.google.common.base.Equivalence;
+import java.util.Map;
+
+public abstract class KeepInfoCanonicalizer {
+
+  private static final int CACHE_SIZE = 25;
+
+  public static KeepInfoCanonicalizer newCanonicalizer() {
+    return new KeepInfoConcreteCanonicalizer();
+  }
+
+  public static KeepInfoCanonicalizer newNopCanonicalizer() {
+    return new KeepInfoNopCanonicalizer();
+  }
+
+  public abstract KeepClassInfo canonicalizeKeepClassInfo(KeepClassInfo classInfo);
+
+  public abstract KeepMethodInfo canonicalizeKeepMethodInfo(KeepMethodInfo methodInfo);
+
+  public abstract KeepFieldInfo canonicalizeKeepFieldInfo(KeepFieldInfo fieldInfo);
+
+  static class KeepInfoConcreteCanonicalizer extends KeepInfoCanonicalizer {
+
+    private final ClassEquivalenceNoAnnotations classEquivalence =
+        new ClassEquivalenceNoAnnotations();
+    private final Map<Equivalence.Wrapper<KeepClassInfo>, Equivalence.Wrapper<KeepClassInfo>>
+        keepClassInfos = new LRUCache<>(CACHE_SIZE);
+    private final MethodEquivalenceNoAnnotations methodEquivalence =
+        new MethodEquivalenceNoAnnotations();
+    private final Map<Equivalence.Wrapper<KeepMethodInfo>, Equivalence.Wrapper<KeepMethodInfo>>
+        keepMethodInfos = new LRUCache<>(CACHE_SIZE);
+    private final FieldEquivalenceNoAnnotations fieldEquivalence =
+        new FieldEquivalenceNoAnnotations();
+    private final Map<Equivalence.Wrapper<KeepFieldInfo>, Equivalence.Wrapper<KeepFieldInfo>>
+        keepFieldInfos = new LRUCache<>(CACHE_SIZE);
+
+    private boolean hasKeepInfoAnnotationInfo(KeepInfo<?, ?> info) {
+      return !info.internalAnnotationsInfo().isTop() || !info.internalTypeAnnotationsInfo().isTop();
+    }
+
+    @Override
+    public KeepClassInfo canonicalizeKeepClassInfo(KeepClassInfo classInfo) {
+      if (hasKeepInfoAnnotationInfo(classInfo)) {
+        return classInfo;
+      }
+      return keepClassInfos.computeIfAbsent(classEquivalence.wrap(classInfo), w -> w).get();
+    }
+
+    @Override
+    public KeepMethodInfo canonicalizeKeepMethodInfo(KeepMethodInfo methodInfo) {
+      if (hasKeepInfoAnnotationInfo(methodInfo)
+          || !methodInfo.internalParameterAnnotationsInfo().isTop()) {
+        return methodInfo;
+      }
+      return keepMethodInfos.computeIfAbsent(methodEquivalence.wrap(methodInfo), w -> w).get();
+    }
+
+    @Override
+    public KeepFieldInfo canonicalizeKeepFieldInfo(KeepFieldInfo fieldInfo) {
+      if (hasKeepInfoAnnotationInfo(fieldInfo)) {
+        return fieldInfo;
+      }
+      return keepFieldInfos.computeIfAbsent(fieldEquivalence.wrap(fieldInfo), w -> w).get();
+    }
+  }
+
+  static class KeepInfoNopCanonicalizer extends KeepInfoCanonicalizer {
+
+    @Override
+    public KeepClassInfo canonicalizeKeepClassInfo(KeepClassInfo classInfo) {
+      return classInfo;
+    }
+
+    @Override
+    public KeepMethodInfo canonicalizeKeepMethodInfo(KeepMethodInfo methodInfo) {
+      return methodInfo;
+    }
+
+    @Override
+    public KeepFieldInfo canonicalizeKeepFieldInfo(KeepFieldInfo fieldInfo) {
+      return fieldInfo;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
index a63cce4..d89da04 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
@@ -287,7 +287,9 @@
     // Collection of materialized rules.
     private MaterializedRules materializedRules;
 
-    MutableKeepInfoCollection() {
+    private final KeepInfoCanonicalizer canonicalizer;
+
+    MutableKeepInfoCollection(InternalOptions options) {
       this(
           new IdentityHashMap<>(),
           new IdentityHashMap<>(),
@@ -295,7 +297,10 @@
           new IdentityHashMap<>(),
           new IdentityHashMap<>(),
           new IdentityHashMap<>(),
-          MaterializedRules.empty());
+          MaterializedRules.empty(),
+          options.testing.enableKeepInfoCanonicalizer
+              ? KeepInfoCanonicalizer.newCanonicalizer()
+              : KeepInfoCanonicalizer.newNopCanonicalizer());
     }
 
     private MutableKeepInfoCollection(
@@ -305,7 +310,8 @@
         Map<DexType, KeepClassInfo.Joiner> classRuleInstances,
         Map<DexField, KeepFieldInfo.Joiner> fieldRuleInstances,
         Map<DexMethod, KeepMethodInfo.Joiner> methodRuleInstances,
-        MaterializedRules materializedRules) {
+        MaterializedRules materializedRules,
+        KeepInfoCanonicalizer keepInfoCanonicalizer) {
       this.keepClassInfo = keepClassInfo;
       this.keepMethodInfo = keepMethodInfo;
       this.keepFieldInfo = keepFieldInfo;
@@ -313,6 +319,7 @@
       this.fieldRuleInstances = fieldRuleInstances;
       this.methodRuleInstances = methodRuleInstances;
       this.materializedRules = materializedRules;
+      this.canonicalizer = keepInfoCanonicalizer;
     }
 
     public void setMaterializedRules(MaterializedRules materializedRules) {
@@ -385,7 +392,8 @@
                   methodRuleInstances,
                   lens::getRenamedMethodSignature,
                   KeepMethodInfo::newEmptyJoiner),
-              materializedRules.rewriteWithLens(lens));
+              materializedRules.rewriteWithLens(lens),
+              canonicalizer);
       timing.end();
       return result;
     }
@@ -599,7 +607,7 @@
       fn.accept(joiner);
       KeepClassInfo joined = joiner.join();
       if (!info.equals(joined)) {
-        keepClassInfo.put(clazz.type, joined);
+        keepClassInfo.put(clazz.type, canonicalizer.canonicalizeKeepClassInfo(joined));
       }
     }
 
@@ -617,7 +625,7 @@
       fn.accept(joiner);
       KeepMethodInfo joined = joiner.join();
       if (!info.equals(joined)) {
-        keepMethodInfo.put(method.getReference(), joined);
+        keepMethodInfo.put(method.getReference(), canonicalizer.canonicalizeKeepMethodInfo(joined));
       }
     }
 
@@ -635,7 +643,7 @@
       fn.accept(joiner);
       KeepFieldInfo joined = joiner.join();
       if (!info.equals(joined)) {
-        keepFieldInfo.put(field.getReference(), joined);
+        keepFieldInfo.put(field.getReference(), canonicalizer.canonicalizeKeepFieldInfo(joined));
       }
     }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepInfoEquivalenceNoAnnotations.java b/src/main/java/com/android/tools/r8/shaking/KeepInfoEquivalenceNoAnnotations.java
new file mode 100644
index 0000000..d1b3e72
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/KeepInfoEquivalenceNoAnnotations.java
@@ -0,0 +1,28 @@
+// 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.shaking;
+
+import com.google.common.base.Equivalence;
+
+public class KeepInfoEquivalenceNoAnnotations<T extends KeepInfo<?, T>> extends Equivalence<T> {
+
+  @Override
+  protected boolean doEquivalent(T a, T b) {
+    return a.equalsNoAnnotations(b);
+  }
+
+  @Override
+  protected int doHash(T a) {
+    return a.hashCodeNoAnnotations();
+  }
+
+  static class ClassEquivalenceNoAnnotations
+      extends KeepInfoEquivalenceNoAnnotations<KeepClassInfo> {}
+
+  static class MethodEquivalenceNoAnnotations
+      extends KeepInfoEquivalenceNoAnnotations<KeepMethodInfo> {}
+
+  static class FieldEquivalenceNoAnnotations
+      extends KeepInfoEquivalenceNoAnnotations<KeepFieldInfo> {}
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepMemberInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepMemberInfo.java
index ab20efa..887dff9 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepMemberInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepMemberInfo.java
@@ -90,6 +90,25 @@
     }
   }
 
+  @Override
+  public boolean equalsNoAnnotations(K other) {
+    return super.equalsNoAnnotations(other)
+        && (allowValuePropagation == other.internalIsValuePropagationAllowed());
+  }
+
+  @Override
+  public int hashCodeNoAnnotations() {
+    int hash = super.hashCodeNoAnnotations();
+    int index = super.numberOfBooleans();
+    hash += bit(allowValuePropagation, index);
+    return hash;
+  }
+
+  @Override
+  protected int numberOfBooleans() {
+    return super.numberOfBooleans() + 1;
+  }
+
   public abstract static class Joiner<
           J extends Joiner<J, B, K>, B extends Builder<B, K>, K extends KeepMemberInfo<B, K>>
       extends KeepInfo.Joiner<J, B, K> {
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepMethodInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepMethodInfo.java
index a40c7e6..0711d8a 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepMethodInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepMethodInfo.java
@@ -280,6 +280,52 @@
     return this.equals(bottom());
   }
 
+  @Override
+  public boolean equalsNoAnnotations(KeepMethodInfo other) {
+    return super.equalsNoAnnotations(other)
+        && allowThrowsRemoval == other.internalIsThrowsRemovalAllowed()
+        && allowClassInlining == other.internalIsClassInliningAllowed()
+        && allowClosedWorldReasoning == other.internalIsClosedWorldReasoningAllowed()
+        && allowCodeReplacement == other.internalIsCodeReplacementAllowed()
+        && allowConstantArgumentOptimization
+            == other.internalIsConstantArgumentOptimizationAllowed()
+        && allowInlining == other.internalIsInliningAllowed()
+        && allowMethodStaticizing == other.internalIsMethodStaticizingAllowed()
+        && allowParameterRemoval == other.internalIsParameterRemovalAllowed()
+        && allowParameterReordering == other.internalIsParameterReorderingAllowed()
+        && allowParameterTypeStrengthening == other.internalIsParameterTypeStrengtheningAllowed()
+        && allowReprocessing == other.internalIsReprocessingAllowed()
+        && allowReturnTypeStrengthening == other.internalIsReturnTypeStrengtheningAllowed()
+        && allowSingleCallerInlining == other.internalIsSingleCallerInliningAllowed()
+        && allowUnusedArgumentOptimization == other.internalIsUnusedArgumentOptimizationAllowed()
+        && allowUnusedReturnValueOptimization
+            == other.internalIsUnusedReturnValueOptimizationAllowed()
+        && allowParameterNamesRemoval == other.internalIsParameterNamesRemovalAllowed();
+  }
+
+  @Override
+  public int hashCodeNoAnnotations() {
+    int hash = super.hashCodeNoAnnotations();
+    int index = super.numberOfBooleans();
+    hash += bit(allowThrowsRemoval, index++);
+    hash += bit(allowClassInlining, index++);
+    hash += bit(allowClosedWorldReasoning, index++);
+    hash += bit(allowCodeReplacement, index++);
+    hash += bit(allowConstantArgumentOptimization, index++);
+    hash += bit(allowInlining, index++);
+    hash += bit(allowMethodStaticizing, index++);
+    hash += bit(allowParameterRemoval, index++);
+    hash += bit(allowParameterReordering, index++);
+    hash += bit(allowParameterTypeStrengthening, index++);
+    hash += bit(allowReprocessing, index++);
+    hash += bit(allowReturnTypeStrengthening, index++);
+    hash += bit(allowSingleCallerInlining, index++);
+    hash += bit(allowUnusedArgumentOptimization, index++);
+    hash += bit(allowUnusedReturnValueOptimization, index++);
+    hash += bit(allowParameterNamesRemoval, index);
+    return hash;
+  }
+
   public static class Builder extends KeepMemberInfo.Builder<Builder, KeepMethodInfo> {
 
     private boolean allowThrowsRemoval;
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 02881e6..16b3ca9 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -2472,6 +2472,7 @@
     public boolean allowUnusedDontWarnRules = true;
     public boolean alwaysUseExistingAccessInfoCollectionsInMemberRebinding = true;
     public boolean alwaysUsePessimisticRegisterAllocation = false;
+    public boolean enableKeepInfoCanonicalizer = true;
     public boolean enableBridgeHoistingToSharedSyntheticSuperclass = false;
     public boolean enableCheckCastAndInstanceOfRemoval = true;
     public boolean enableDeadSwitchCaseElimination = true;
diff --git a/src/main/java/com/android/tools/r8/utils/LRUCache.java b/src/main/java/com/android/tools/r8/utils/LRUCache.java
new file mode 100644
index 0000000..acf4c00
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/LRUCache.java
@@ -0,0 +1,23 @@
+// 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.utils;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class LRUCache<K, V> extends LinkedHashMap<K, V> {
+
+  private final int maxSize;
+
+  public LRUCache(int maxSize) {
+    super(maxSize + 1, 0.75F, true);
+    this.maxSize = maxSize;
+  }
+
+  @Override
+  protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
+    return this.size() > this.maxSize;
+  }
+}