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