Emit keep stats in metadata

Fixes: b/367558466
Change-Id: I14a805d955eeaefc7d4ec6d17b1eab3c89132dd2
diff --git a/src/main/java/com/android/tools/r8/metadata/R8BuildMetadata.java b/src/main/java/com/android/tools/r8/metadata/R8BuildMetadata.java
index 6d118ab..27f8792 100644
--- a/src/main/java/com/android/tools/r8/metadata/R8BuildMetadata.java
+++ b/src/main/java/com/android/tools/r8/metadata/R8BuildMetadata.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.metadata.impl.R8OptionsImpl;
 import com.android.tools.r8.metadata.impl.R8ResourceOptimizationOptionsImpl;
 import com.android.tools.r8.metadata.impl.R8StartupOptimizationOptionsImpl;
+import com.android.tools.r8.metadata.impl.R8StatsMetadataImpl;
 import com.google.gson.GsonBuilder;
 import com.google.gson.JsonDeserializer;
 import java.util.List;
@@ -34,6 +35,7 @@
             deserializeTo(R8BaselineProfileRewritingOptionsImpl.class))
         .registerTypeAdapter(R8CompilationInfo.class, deserializeTo(R8CompilationInfoImpl.class))
         .registerTypeAdapter(R8DexFileMetadata.class, deserializeTo(R8DexFileMetadataImpl.class))
+        .registerTypeAdapter(R8StatsMetadata.class, deserializeTo(R8StatsMetadataImpl.class))
         .registerTypeAdapter(
             R8FeatureSplitMetadata.class, deserializeTo(R8FeatureSplitMetadataImpl.class))
         .registerTypeAdapter(
@@ -85,6 +87,8 @@
    */
   R8StartupOptimizationOptions getStartupOptizationOptions();
 
+  R8StatsMetadata getStatsMetadata();
+
   String getVersion();
 
   String toJson();
diff --git a/src/main/java/com/android/tools/r8/metadata/R8StatsMetadata.java b/src/main/java/com/android/tools/r8/metadata/R8StatsMetadata.java
new file mode 100644
index 0000000..6b446ba
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/metadata/R8StatsMetadata.java
@@ -0,0 +1,16 @@
+// 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.metadata;
+
+import com.android.tools.r8.keepanno.annotations.KeepForApi;
+
+@KeepForApi
+public interface R8StatsMetadata {
+
+  float getNoObfuscationPercentage();
+
+  float getNoOptimizationPercentage();
+
+  float getNoShrinkingPercentage();
+}
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/BuildMetadataFactory.java b/src/main/java/com/android/tools/r8/metadata/impl/BuildMetadataFactory.java
index 1bb4869..bcf50d4 100644
--- a/src/main/java/com/android/tools/r8/metadata/impl/BuildMetadataFactory.java
+++ b/src/main/java/com/android/tools/r8/metadata/impl/BuildMetadataFactory.java
@@ -47,7 +47,7 @@
     return R8BuildMetadataImpl.builder()
         .setOptions(new R8OptionsImpl(options))
         .setBaselineProfileRewritingOptions(R8BaselineProfileRewritingOptionsImpl.create(options))
-        .setCompilationInfo(R8CompilationInfoImpl.create(executorService, options))
+        .setCompilationInfo(R8CompilationInfoImpl.create(appView, executorService))
         .applyIf(
             options.isGeneratingDex(), builder -> builder.setDexFilesMetadata(baseVirtualFiles))
         .applyIf(
@@ -58,6 +58,7 @@
         .setResourceOptimizationOptions(R8ResourceOptimizationOptionsImpl.create(options))
         .setStartupOptimizationOptions(
             R8StartupOptimizationOptionsImpl.create(options, baseVirtualFiles))
+        .setStatsMetadata(R8StatsMetadataImpl.create(appView))
         .setVersion(Version.LABEL)
         .build();
   }
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/R8BuildMetadataImpl.java b/src/main/java/com/android/tools/r8/metadata/impl/R8BuildMetadataImpl.java
index edabf58..c24ba36 100644
--- a/src/main/java/com/android/tools/r8/metadata/impl/R8BuildMetadataImpl.java
+++ b/src/main/java/com/android/tools/r8/metadata/impl/R8BuildMetadataImpl.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.metadata.R8Options;
 import com.android.tools.r8.metadata.R8ResourceOptimizationOptions;
 import com.android.tools.r8.metadata.R8StartupOptimizationOptions;
+import com.android.tools.r8.metadata.R8StatsMetadata;
 import com.android.tools.r8.utils.ListUtils;
 import com.google.gson.Gson;
 import com.google.gson.annotations.Expose;
@@ -53,6 +54,10 @@
   private final List<R8DexFileMetadata> dexFilesMetadata;
 
   @Expose
+  @SerializedName("statsMetadata")
+  private final R8StatsMetadata statsMetadata;
+
+  @Expose
   @SerializedName("featureSplitsMetadata")
   private final R8FeatureSplitsMetadata featureSplitsMetadata;
 
@@ -73,6 +78,7 @@
       R8BaselineProfileRewritingOptions baselineProfileRewritingOptions,
       R8CompilationInfo compilationInfo,
       List<R8DexFileMetadata> dexFilesMetadata,
+      R8StatsMetadata statsMetadata,
       R8FeatureSplitsMetadata featureSplitsMetadata,
       R8ResourceOptimizationOptions resourceOptimizationOptions,
       R8StartupOptimizationOptions startupOptimizationOptions,
@@ -81,6 +87,7 @@
     this.baselineProfileRewritingOptions = baselineProfileRewritingOptions;
     this.compilationInfo = compilationInfo;
     this.dexFilesMetadata = dexFilesMetadata;
+    this.statsMetadata = statsMetadata;
     this.featureSplitsMetadata = featureSplitsMetadata;
     this.resourceOptimizationOptions = resourceOptimizationOptions;
     this.startupOptimizationOptions = startupOptimizationOptions;
@@ -127,6 +134,11 @@
   }
 
   @Override
+  public R8StatsMetadata getStatsMetadata() {
+    return statsMetadata;
+  }
+
+  @Override
   public String getVersion() {
     return version;
   }
@@ -142,6 +154,7 @@
     private R8BaselineProfileRewritingOptions baselineProfileRewritingOptions;
     private R8CompilationInfo compilationInfo;
     private List<R8DexFileMetadata> dexFilesMetadata;
+    private R8StatsMetadata statsMetadata;
     private R8FeatureSplitsMetadata featureSplitsMetadata;
     private R8ResourceOptimizationOptions resourceOptimizationOptions;
     private R8StartupOptimizationOptions startupOptimizationOptions;
@@ -180,6 +193,11 @@
       return this;
     }
 
+    public Builder setStatsMetadata(R8StatsMetadata statsMetadata) {
+      this.statsMetadata = statsMetadata;
+      return this;
+    }
+
     public Builder setFeatureSplitsMetadata(R8FeatureSplitsMetadata featureSplitsMetadata) {
       this.featureSplitsMetadata = featureSplitsMetadata;
       return this;
@@ -208,6 +226,7 @@
           baselineProfileRewritingOptions,
           compilationInfo,
           dexFilesMetadata,
+          statsMetadata,
           featureSplitsMetadata,
           resourceOptimizationOptions,
           startupOptimizationOptions,
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/R8CompilationInfoImpl.java b/src/main/java/com/android/tools/r8/metadata/impl/R8CompilationInfoImpl.java
index e68225d..f5f9094 100644
--- a/src/main/java/com/android/tools/r8/metadata/impl/R8CompilationInfoImpl.java
+++ b/src/main/java/com/android/tools/r8/metadata/impl/R8CompilationInfoImpl.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.metadata.impl;
 
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.keepanno.annotations.AnnotationPattern;
 import com.android.tools.r8.keepanno.annotations.FieldAccessFlags;
 import com.android.tools.r8.keepanno.annotations.KeepConstraint;
@@ -38,7 +40,8 @@
   }
 
   public static R8CompilationInfoImpl create(
-      ExecutorService executorService, InternalOptions options) {
+      AppView<? extends AppInfoWithClassHierarchy> appView, ExecutorService executorService) {
+    InternalOptions options = appView.options();
     assert options.created > 0;
     long buildTime = System.nanoTime() - options.created;
     return new R8CompilationInfoImpl(buildTime, ThreadUtils.getNumberOfThreads(executorService));
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/R8StatsMetadataImpl.java b/src/main/java/com/android/tools/r8/metadata/impl/R8StatsMetadataImpl.java
new file mode 100644
index 0000000..05b8e2f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/metadata/impl/R8StatsMetadataImpl.java
@@ -0,0 +1,119 @@
+// 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.metadata.impl;
+
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ProgramDefinition;
+import com.android.tools.r8.keepanno.annotations.AnnotationPattern;
+import com.android.tools.r8.keepanno.annotations.FieldAccessFlags;
+import com.android.tools.r8.keepanno.annotations.KeepConstraint;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.UsedByReflection;
+import com.android.tools.r8.metadata.R8StatsMetadata;
+import com.android.tools.r8.shaking.KeepInfo;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.InternalOptions;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+@UsedByReflection(
+    description = "Keep and preserve @SerializedName for correct (de)serialization",
+    constraints = {KeepConstraint.LOOKUP},
+    constrainAnnotations = @AnnotationPattern(constant = SerializedName.class),
+    kind = KeepItemKind.CLASS_AND_FIELDS,
+    fieldAccess = {FieldAccessFlags.PRIVATE},
+    fieldAnnotatedByClassConstant = SerializedName.class)
+public class R8StatsMetadataImpl implements R8StatsMetadata {
+
+  @Expose
+  @SerializedName("noObfuscationPercentage")
+  private final float noObfuscationPercentage;
+
+  @Expose
+  @SerializedName("noOptimizationPercentage")
+  private final float noOptimizationPercentage;
+
+  @Expose
+  @SerializedName("noShrinkingPercentage")
+  private final float noShrinkingPercentage;
+
+  private R8StatsMetadataImpl(
+      float noObfuscationPercentage, float noOptimizationPercentage, float noShrinkingPercentage) {
+    this.noObfuscationPercentage = noObfuscationPercentage;
+    this.noOptimizationPercentage = noOptimizationPercentage;
+    this.noShrinkingPercentage = noShrinkingPercentage;
+  }
+
+  public static R8StatsMetadataImpl create(AppView<? extends AppInfoWithClassHierarchy> appView) {
+    Counters counters = Counters.create(appView);
+    return new R8StatsMetadataImpl(
+        counters.getNoObfuscationPercentage(),
+        counters.getNoOptimizationPercentage(),
+        counters.getNoShrinkingPercentage());
+  }
+
+  @Override
+  public float getNoObfuscationPercentage() {
+    return noObfuscationPercentage;
+  }
+
+  @Override
+  public float getNoOptimizationPercentage() {
+    return noOptimizationPercentage;
+  }
+
+  @Override
+  public float getNoShrinkingPercentage() {
+    return noShrinkingPercentage;
+  }
+
+  private static class Counters {
+
+    private int itemsCount = 0;
+    private int noObfuscationCount = 0;
+    private int noOptimizationCount = 0;
+    private int noShrinkingCount = 0;
+
+    private Counters() {}
+
+    static Counters create(AppView<? extends AppInfoWithClassHierarchy> appView) {
+      Counters counters = new Counters();
+      for (DexProgramClass clazz : appView.appInfo().classes()) {
+        counters.add(appView, clazz);
+        clazz.forEachProgramMember(member -> counters.add(appView, member));
+      }
+      return counters;
+    }
+
+    private void add(
+        AppView<? extends AppInfoWithClassHierarchy> appView, ProgramDefinition definition) {
+      KeepInfo<?, ?> keepInfo = appView.getKeepInfo(definition);
+      InternalOptions options = appView.options();
+      itemsCount++;
+      noObfuscationCount += BooleanUtils.intValue(!keepInfo.isMinificationAllowed(options));
+      noOptimizationCount += BooleanUtils.intValue(!keepInfo.isOptimizationAllowed(options));
+      noShrinkingCount += BooleanUtils.intValue(!keepInfo.isShrinkingAllowed(options));
+    }
+
+    float getNoObfuscationPercentage() {
+      return toPercentageWithTwoDecimals(noObfuscationCount);
+    }
+
+    float getNoOptimizationPercentage() {
+      return toPercentageWithTwoDecimals(noOptimizationCount);
+    }
+
+    float getNoShrinkingPercentage() {
+      return toPercentageWithTwoDecimals(noShrinkingCount);
+    }
+
+    float toPercentageWithTwoDecimals(int count) {
+      // Multiply by 100 twice to get percentage with two decimals.
+      float number = (float) (count * 100 * 100) / itemsCount;
+      return (float) Math.round(number) / 100;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/metadata/R8BuildMetadataTest.java b/src/test/java/com/android/tools/r8/metadata/R8BuildMetadataTest.java
index 1365c88..719306b 100644
--- a/src/test/java/com/android/tools/r8/metadata/R8BuildMetadataTest.java
+++ b/src/test/java/com/android/tools/r8/metadata/R8BuildMetadataTest.java
@@ -89,6 +89,7 @@
 
   private void inspectDeserializedBuildMetadata(R8BuildMetadata buildMetadata) {
     assertNotNull(buildMetadata.getBaselineProfileRewritingOptions());
+    assertNotNull(buildMetadata.getCompilationInfo());
     assertNotNull(buildMetadata.getOptions());
     assertNotNull(buildMetadata.getOptions().getKeepAttributesOptions());
     assertEquals(
@@ -113,6 +114,7 @@
     } else {
       assertNull(startupOptimizationOptions);
     }
+    assertNotNull(buildMetadata.getStatsMetadata());
     assertEquals(Version.LABEL, buildMetadata.getVersion());
   }