hoststubgen: Emit stats for dashboarding.

As we expand our audience, developers will be interested in knowing
what APIs are supported through a top-down dashboard view that we
can continually update over time.

This change emits a statistics CSV that can be easily bulk-imported
to generate a dashboard.

Bug: 322895594
Test: TH
Change-Id: Idea55b64cdb79e9a49f63340f83a1b395f8e5ec7
diff --git a/Ravenwood.bp b/Ravenwood.bp
index d13c4d7..93febca4 100644
--- a/Ravenwood.bp
+++ b/Ravenwood.bp
@@ -33,6 +33,7 @@
         "@$(location ravenwood/ravenwood-standard-options.txt) " +
 
         "--debug-log $(location hoststubgen_framework-minus-apex.log) " +
+        "--stats-file $(location hoststubgen_framework-minus-apex_stats.csv) " +
 
         "--out-impl-jar $(location ravenwood.jar) " +
 
@@ -56,6 +57,7 @@
         "hoststubgen_dump.txt",
 
         "hoststubgen_framework-minus-apex.log",
+        "hoststubgen_framework-minus-apex_stats.csv",
     ],
     visibility: ["//visibility:private"],
 }
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
index 97e09b8..06eeb47c 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
@@ -49,6 +49,7 @@
 class HostStubGen(val options: HostStubGenOptions) {
     fun run() {
         val errors = HostStubGenErrors()
+        val stats = HostStubGenStats()
 
         // Load all classes.
         val allClasses = loadClassStructures(options.inJar.get)
@@ -80,7 +81,14 @@
                 options.enableClassChecker.get,
                 allClasses,
                 errors,
+                stats,
         )
+
+        // Dump statistics, if specified.
+        options.statsFile.ifSet {
+            PrintWriter(it).use { pw -> stats.dump(pw) }
+            log.i("Dump file created at $it")
+        }
     }
 
     /**
@@ -237,6 +245,7 @@
             enableChecker: Boolean,
             classes: ClassNodes,
             errors: HostStubGenErrors,
+            stats: HostStubGenStats,
             ) {
         log.i("Converting %s into [stub: %s, impl: %s] ...", inJar, outStubJar, outImplJar)
         log.i("ASM CheckClassAdapter is %s", if (enableChecker) "enabled" else "disabled")
@@ -254,7 +263,8 @@
                         while (inEntries.hasMoreElements()) {
                             val entry = inEntries.nextElement()
                             convertSingleEntry(inZip, entry, stubOutStream, implOutStream,
-                                    filter, packageRedirector, enableChecker, classes, errors)
+                                    filter, packageRedirector, enableChecker, classes, errors,
+                                    stats)
                         }
                         log.i("Converted all entries.")
                     }
@@ -287,6 +297,7 @@
             enableChecker: Boolean,
             classes: ClassNodes,
             errors: HostStubGenErrors,
+            stats: HostStubGenStats,
             ) {
         log.d("Entry: %s", entry.name)
         log.withIndent {
@@ -300,7 +311,7 @@
             // If it's a class, convert it.
             if (name.endsWith(".class")) {
                 processSingleClass(inZip, entry, stubOutStream, implOutStream, filter,
-                        packageRedirector, enableChecker, classes, errors)
+                        packageRedirector, enableChecker, classes, errors, stats)
                 return
             }
 
@@ -354,6 +365,7 @@
             enableChecker: Boolean,
             classes: ClassNodes,
             errors: HostStubGenErrors,
+            stats: HostStubGenStats,
             ) {
         val classInternalName = entry.name.replaceFirst("\\.class$".toRegex(), "")
         val classPolicy = filter.getPolicyForClass(classInternalName)
@@ -370,7 +382,7 @@
                     stubOutStream.putNextEntry(newEntry)
                     convertClass(classInternalName, /*forImpl=*/false, bis,
                             stubOutStream, filter, packageRedirector, enableChecker, classes,
-                            errors)
+                            errors, stats)
                     stubOutStream.closeEntry()
                 }
             }
@@ -383,7 +395,7 @@
                     implOutStream.putNextEntry(newEntry)
                     convertClass(classInternalName, /*forImpl=*/true, bis,
                             implOutStream, filter, packageRedirector, enableChecker, classes,
-                            errors)
+                            errors, stats)
                     implOutStream.closeEntry()
                 }
             }
@@ -403,6 +415,7 @@
             enableChecker: Boolean,
             classes: ClassNodes,
             errors: HostStubGenErrors,
+            stats: HostStubGenStats,
             ) {
         val cr = ClassReader(input)
 
@@ -420,6 +433,7 @@
                 enablePostTrace = options.enablePostTrace.get,
                 enableNonStubMethodCallDetection = options.enableNonStubMethodCallDetection.get,
                 errors = errors,
+                stats = stats,
         )
         outVisitor = BaseAdapter.getVisitor(classInternalName, classes, outVisitor, filter,
                 packageRedirector, forImpl, visitorOptions)
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
index d2ead18..9f5d524 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
@@ -108,6 +108,8 @@
         var enablePostTrace: SetOnce<Boolean> = SetOnce(false),
 
         var enableNonStubMethodCallDetection: SetOnce<Boolean> = SetOnce(false),
+
+        var statsFile: SetOnce<String?> = SetOnce(null),
 ) {
     companion object {
 
@@ -252,6 +254,8 @@
                         "--verbose-log" -> setLogFile(LogLevel.Verbose, nextArg())
                         "--debug-log" -> setLogFile(LogLevel.Debug, nextArg())
 
+                        "--stats-file" -> ret.statsFile.setNextStringArg()
+
                         else -> throw ArgumentsException("Unknown option: $arg")
                     }
                 } catch (e: SetOnce.SetMoreThanOnceException) {
@@ -387,6 +391,7 @@
               enablePreTrace=$enablePreTrace,
               enablePostTrace=$enablePostTrace,
               enableNonStubMethodCallDetection=$enableNonStubMethodCallDetection,
+              statsFile=$statsFile,
             }
             """.trimIndent()
     }
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt
new file mode 100644
index 0000000..fe4072f
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.hoststubgen
+
+import com.android.hoststubgen.asm.toHumanReadableClassName
+import com.android.hoststubgen.filters.FilterPolicyWithReason
+import java.io.PrintWriter
+
+open class HostStubGenStats {
+    data class Stats(
+            var supported: Int = 0,
+            var total: Int = 0,
+            val children: MutableMap<String, Stats> = mutableMapOf<String, Stats>(),
+    )
+
+    private val stats = mutableMapOf<String, Stats>()
+
+    fun onVisitPolicyForMethod(fullClassName: String, policy: FilterPolicyWithReason) {
+        if (policy.isIgnoredForStats) return
+
+        val packageName = resolvePackageName(fullClassName)
+        val className = resolveClassName(fullClassName)
+
+        val packageStats = stats.getOrPut(packageName) { Stats() }
+        val classStats = packageStats.children.getOrPut(className) { Stats() }
+
+        if (policy.policy.isSupported) {
+            packageStats.supported += 1
+            classStats.supported += 1
+        }
+        packageStats.total += 1
+        classStats.total += 1
+    }
+
+    fun dump(pw: PrintWriter) {
+        pw.printf("PackageName,ClassName,SupportedMethods,TotalMethods\n")
+        stats.forEach { (packageName, packageStats) ->
+            if (packageStats.supported > 0) {
+                packageStats.children.forEach { (className, classStats) ->
+                    pw.printf("%s,%s,%d,%d\n", packageName, className,
+                            classStats.supported, classStats.total)
+                }
+            }
+        }
+    }
+
+    private fun resolvePackageName(fullClassName: String): String {
+        val start = fullClassName.lastIndexOf('/')
+        return fullClassName.substring(0, start).toHumanReadableClassName()
+    }
+
+    private fun resolveClassName(fullClassName: String): String {
+        val start = fullClassName.lastIndexOf('/')
+        val end = fullClassName.indexOf('$')
+        if (end == -1) {
+            return fullClassName.substring(start + 1)
+        } else {
+            return fullClassName.substring(start + 1, end)
+        }
+    }
+}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt
index 9317996..4d21106 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt
@@ -111,6 +111,16 @@
             }
         }
 
+    /** Returns whether a policy is considered supported. */
+    val isSupported: Boolean
+        get() {
+            return when (this) {
+                // TODO: handle native method with no substitution as being unsupported
+                Stub, StubClass, Keep, KeepClass, SubstituteAndStub, SubstituteAndKeep -> true
+                else -> false
+            }
+        }
+
     fun getSubstitutionBasePolicy(): FilterPolicy {
         return when (this) {
             SubstituteAndKeep -> Keep
@@ -136,4 +146,4 @@
     fun withReason(reason: String): FilterPolicyWithReason {
         return FilterPolicyWithReason(this, reason)
     }
-}
\ No newline at end of file
+}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicyWithReason.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicyWithReason.kt
index b64a2f5..53eb5a8 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicyWithReason.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicyWithReason.kt
@@ -63,4 +63,15 @@
     override fun toString(): String {
         return "[$policy - reason: $reason]"
     }
-}
\ No newline at end of file
+
+    /** Returns whether this policy should be ignored for stats. */
+    val isIgnoredForStats: Boolean
+        get() {
+            return reason.contains("anonymous-inner-class")
+                    || reason.contains("is-annotation")
+                    || reason.contains("is-enum")
+                    || reason.contains("is-synthetic-method")
+                    || reason.contains("special-class")
+                    || reason.contains("substitute-from")
+        }
+}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
index ea7d1d0..78b13fd 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
@@ -119,14 +119,14 @@
             if (cn.isEnum()) {
                 mn?.let { mn ->
                     if (isAutoGeneratedEnumMember(mn)) {
-                        return memberPolicy.withReason(classPolicy.reason).wrapReason("enum")
+                        return memberPolicy.withReason(classPolicy.reason).wrapReason("is-enum")
                     }
                 }
             }
 
             // Keep (or stub) all members of annotations.
             if (cn.isAnnotation()) {
-                return memberPolicy.withReason(classPolicy.reason).wrapReason("annotation")
+                return memberPolicy.withReason(classPolicy.reason).wrapReason("is-annotation")
             }
 
             mn?.let {
@@ -134,7 +134,7 @@
                     // For synthetic methods (such as lambdas), let's just inherit the class's
                     // policy.
                     return memberPolicy.withReason(classPolicy.reason).wrapReason(
-                            "synthetic method")
+                            "is-synthetic-method")
                 }
             }
         }
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
index 7fdd944..6ad83fb 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
@@ -132,23 +132,24 @@
                                             throw ParseException(
                                                     "Policy for AIDL classes already defined")
                                         }
-                                        aidlPolicy = policy.withReason("$FILTER_REASON (AIDL)")
+                                        aidlPolicy = policy.withReason(
+                                                "$FILTER_REASON (special-class AIDL)")
                                     }
                                     SpecialClass.FeatureFlags -> {
                                         if (featureFlagsPolicy != null) {
                                             throw ParseException(
                                                     "Policy for feature flags already defined")
                                         }
-                                        featureFlagsPolicy =
-                                                policy.withReason("$FILTER_REASON (feature flags)")
+                                        featureFlagsPolicy = policy.withReason(
+                                                "$FILTER_REASON (special-class feature flags)")
                                     }
                                     SpecialClass.Sysprops -> {
                                         if (syspropsPolicy != null) {
                                             throw ParseException(
                                                     "Policy for sysprops already defined")
                                         }
-                                        syspropsPolicy =
-                                                policy.withReason("$FILTER_REASON (sysprops)")
+                                        syspropsPolicy = policy.withReason(
+                                                "$FILTER_REASON (special-class sysprops)")
                                     }
                                 }
                             }
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt
index 21cfd4b..c20aa8b 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt
@@ -16,6 +16,7 @@
 package com.android.hoststubgen.visitors
 
 import com.android.hoststubgen.HostStubGenErrors
+import com.android.hoststubgen.HostStubGenStats
 import com.android.hoststubgen.LogLevel
 import com.android.hoststubgen.asm.ClassNodes
 import com.android.hoststubgen.asm.UnifiedVisitor
@@ -50,6 +51,7 @@
      */
     data class Options (
             val errors: HostStubGenErrors,
+            val stats: HostStubGenStats,
             val enablePreTrace: Boolean,
             val enablePostTrace: Boolean,
             val enableNonStubMethodCallDetection: Boolean,
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
index 416b782..beca945 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
@@ -141,6 +141,11 @@
             substituted: Boolean,
             superVisitor: MethodVisitor?,
     ): MethodVisitor? {
+        // Record statistics about visiting this method when visible.
+        if ((access and Opcodes.ACC_PRIVATE) == 0) {
+            options.stats.onVisitPolicyForMethod(currentClassName, policy)
+        }
+
         // Inject method log, if needed.
         var innerVisitor = superVisitor