diff options
author | 2025-03-12 14:26:09 -0700 | |
---|---|---|
committer | 2025-03-13 16:01:27 -0700 | |
commit | e18b7781056a39fb65260ac87a5fc98ed28a93dd (patch) | |
tree | 8f316f6ea5fc4b695be22438c84e6182fbb24e94 | |
parent | c6af79cff3599a387961cdc98e2c8c152ce3ba21 (diff) |
Update "stats" generation
- Stop hardcoding test directory path in ravenwood-stats-collector.sh
- Don't count native methods kept by KeepNativeFilter as "supported"
- Hide some of the "obvious" classes and methods
(annotations classes, Object methods, etc etc)
Flag: EXEMPT host test change only
Bug: 402797626
Test: $ANDROID_BUILD_TOP/frameworks/base/ravenwood/scripts/run-ravenwood-tests.sh -s
Change-Id: I0a12ec51cc6cc852bc2d1bde68021c34c2c8bb59
9 files changed, 174 insertions, 69 deletions
diff --git a/ravenwood/scripts/ravenwood-stats-collector.sh b/ravenwood/scripts/ravenwood-stats-collector.sh index b83216af95fe..c2bf8d82e272 100755 --- a/ravenwood/scripts/ravenwood-stats-collector.sh +++ b/ravenwood/scripts/ravenwood-stats-collector.sh @@ -29,21 +29,46 @@ mkdir -p $out_dir mkdir -p $keep_all_dir mkdir -p $dump_dir -# Where the input files are. -path=$ANDROID_BUILD_TOP/out/host/linux-x86/testcases/ravenwood-stats-checker/x86_64/ -timestamp="$(date --iso-8601=seconds)" +stats_checker_module="ravenwood-stats-checker" +minfo=$OUT/module-info.json -m() { - ${ANDROID_BUILD_TOP}/build/soong/soong_ui.bash --make-mode "$@" -} +timestamp="$(date --iso-8601=seconds)" -# Building this will generate the files we need. -m ravenwood-stats-checker +# First, use jq to get the output files from the checker module. This will be something like this: +# +# --- +# out/host/linux-x86/nativetest64/ravenwood-stats-checker/framework-configinfrastructure_apis.csv +# out/host/linux-x86/nativetest64/ravenwood-stats-checker/framework-configinfrastructure_dump.txt +# : +# out/host/linux-x86/nativetest64/ravenwood-stats-checker/hoststubgen_services.core_stats.csv +# out/host/linux-x86/nativetest64/ravenwood-stats-checker/ravenwood-stats-checker +# --- +# Then, use grep to find the script's path (the last line in the above examle) +script_path="$( + jq -r ".\"$stats_checker_module\".installed | .[]" $minfo | + grep '/ravenwood-stats-checker$' +)" + +if [[ "$script_path" == "" ]] ; then + echo "Error: $stats_checker_module script not found from $minfo" + exit 1 +fi + +# This is the directory where our input files are. +script_dir="$ANDROID_BUILD_TOP/$(dirname "$script_path")" + +# Clear it before (re-)buildign the script, to make sure we won't have stale files. +rm -fr "$script_dir" + +# Then build it, which will also collect the input files in the same dir. +echo "Collecting the input files..." +m "$stats_checker_module" # Start... -cd $path +echo "Files directory is: $script_dir" +cd "$script_dir" dump() { local jar=$1 @@ -55,6 +80,7 @@ dump() { collect_stats() { local out="$1" + local desc="$2" { # Copy the header, with the first column appended. echo -n "Jar,Generated Date," @@ -66,11 +92,12 @@ collect_stats() { dump "framework-statsd" framework-statsd_stats.csv } > "$out" - echo "Stats CVS created at $out" + echo "Stats CVS created at $out$desc" } collect_apis() { local out="$1" + local desc="$2" { # Copy the header, with the first column appended. echo -n "Jar,Generated Date," @@ -82,12 +109,12 @@ collect_apis() { dump "framework-statsd" framework-statsd_apis.csv } > "$out" - echo "API CVS created at $out" + echo "API CVS created at $out$desc" } -collect_stats $stats -collect_apis $apis +collect_stats $stats " (import it as 'ravenwood_stats')" +collect_apis $apis " (import it as 'ravenwood_supported_apis')" cp *keep_all.txt $keep_all_dir echo "Keep all files created at:" diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenStats.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenStats.kt index 9045db210495..ea8c25b6833c 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenStats.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenStats.kt @@ -15,13 +15,24 @@ */ package com.android.hoststubgen +import com.android.hoststubgen.asm.ClassNodes import com.android.hoststubgen.asm.getOuterClassNameFromFullClassName import com.android.hoststubgen.asm.getPackageNameFromFullClassName import com.android.hoststubgen.filters.FilterPolicyWithReason +import com.android.hoststubgen.filters.StatsLabel import org.objectweb.asm.Opcodes import java.io.PrintWriter -open class HostStubGenStats { +/** + * TODO This is for the legacy API coverage stats CSV that shows how many APIs are "supported" + * in each class with some heuristics. We created [ApiDumper] later, which dumpps all methods + * with the "supported" status. We should update the coverage dashboard to use the [ApiDumper] + * output and remove this class, once we port all the heuristics to [ApiDumper] as well. + * (For example, this class ignores non-public and/or abstract methods, but [ApiDumper] shows + * all of them in the same way. We should probably mark them as "Boring" or maybe "Ignore" + * for [ApiDumper]) + */ +open class HostStubGenStats(val classes: ClassNodes) { data class Stats( var supported: Int = 0, var total: Int = 0, @@ -30,14 +41,6 @@ open class HostStubGenStats { private val stats = mutableMapOf<String, Stats>() - data class Api( - val fullClassName: String, - val methodName: String, - val methodDesc: String, - ) - - private val apis = mutableListOf<Api>() - fun onVisitPolicyForMethod( fullClassName: String, methodName: String, @@ -45,16 +48,16 @@ open class HostStubGenStats { policy: FilterPolicyWithReason, access: Int ) { - if (policy.policy.isSupported) { - apis.add(Api(fullClassName, methodName, descriptor)) - } - // Ignore methods that aren't public if ((access and Opcodes.ACC_PUBLIC) == 0) return // Ignore methods that are abstract if ((access and Opcodes.ACC_ABSTRACT) != 0) return + // Ignore methods where policy isn't relevant - if (policy.isIgnoredForStats) return + val statsLabel = policy.statsLabel + if (statsLabel == StatsLabel.Ignored) return + + val cn = classes.findClass(fullClassName) ?: return val packageName = getPackageNameFromFullClassName(fullClassName) val className = getOuterClassNameFromFullClassName(fullClassName) @@ -70,7 +73,7 @@ open class HostStubGenStats { val packageStats = stats.getOrPut(packageName) { Stats() } val classStats = packageStats.children.getOrPut(className) { Stats() } - if (policy.policy.isSupported) { + if (statsLabel == StatsLabel.Supported) { packageStats.supported += 1 classStats.supported += 1 } diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/dumper/ApiDumper.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/dumper/ApiDumper.kt index 5e4e70f0cbaa..bb8cdccafaa6 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/dumper/ApiDumper.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/dumper/ApiDumper.kt @@ -25,6 +25,7 @@ import com.android.hoststubgen.csvEscape import com.android.hoststubgen.filters.FilterPolicy import com.android.hoststubgen.filters.FilterPolicyWithReason import com.android.hoststubgen.filters.OutputFilter +import com.android.hoststubgen.filters.StatsLabel import com.android.hoststubgen.log import org.objectweb.asm.Type import org.objectweb.asm.tree.ClassNode @@ -44,7 +45,10 @@ class ApiDumper( val descriptor: String, ) - private val javaStandardApiPolicy = FilterPolicy.Keep.withReason("Java standard API") + private val javaStandardApiPolicy = FilterPolicy.Keep.withReason( + "Java standard API", + StatsLabel.Supported, + ) private val shownMethods = mutableSetOf<MethodKey>() @@ -53,7 +57,7 @@ class ApiDumper( */ fun dump() { pw.printf("PackageName,ClassName,FromSubclass,DeclareClass,MethodName,MethodDesc" + - ",Supported,Policy,Reason\n") + ",Supported,Policy,Reason,SupportedLabel\n") classes.forEach { classNode -> shownMethods.clear() @@ -68,23 +72,36 @@ class ApiDumper( methodClassName: String, methodName: String, methodDesc: String, - policy: FilterPolicyWithReason, + classPolicy: FilterPolicyWithReason, + methodPolicy: FilterPolicyWithReason, ) { + if (methodPolicy.statsLabel == StatsLabel.Ignored) { + return + } + // Label hack -- if the method is supported, but the class is boring, then the + // method is boring too. + var methodLabel = methodPolicy.statsLabel + if (methodLabel == StatsLabel.SupportedButBoring + && classPolicy.statsLabel == StatsLabel.SupportedButBoring) { + methodLabel = classPolicy.statsLabel + } + pw.printf( - "%s,%s,%d,%s,%s,%s,%d,%s,%s\n", + "%s,%s,%d,%s,%s,%s,%d,%s,%s,%s\n", csvEscape(classPackage), csvEscape(className), if (isSuperClass) { 1 } else { 0 }, csvEscape(methodClassName), csvEscape(methodName), csvEscape(methodDesc), - if (policy.policy.isSupported) { 1 } else { 0 }, - policy.policy, - csvEscape(policy.reason), + methodLabel.statValue, + methodPolicy.policy, + csvEscape(methodPolicy.reason), + methodLabel, ) } - private fun isDuplicate(methodName: String, methodDesc: String): Boolean { + private fun shownAlready(methodName: String, methodDesc: String): Boolean { val methodKey = MethodKey(methodName, methodDesc) if (shownMethods.contains(methodKey)) { @@ -98,6 +115,12 @@ class ApiDumper( dumpClass: ClassNode, methodClass: ClassNode, ) { + val classPolicy = filter.getPolicyForClass(dumpClass.name) + if (classPolicy.statsLabel == StatsLabel.Ignored) { + return + } + log.d("Class ${dumpClass.name} -- policy $classPolicy") + val pkg = getPackageNameFromFullClassName(dumpClass.name).toHumanReadableClassName() val cls = getClassNameFromFullClassName(dumpClass.name).toHumanReadableClassName() @@ -112,23 +135,23 @@ class ApiDumper( } } // If we already printed the method from a subclass, don't print it. - if (isDuplicate(method.name, method.desc)) { + if (shownAlready(method.name, method.desc)) { return@forEach } - val policy = filter.getPolicyForMethod(methodClass.name, method.name, method.desc) + val methodPolicy = filter.getPolicyForMethod(methodClass.name, method.name, method.desc) // Let's skip "Remove" APIs. Ideally we want to print it, just to make the CSV // complete, we still need to hide methods substituted (== @RavenwoodReplace) methods // and for now we don't have an easy way to detect it. - if (policy.policy == FilterPolicy.Remove) { + if (methodPolicy.policy == FilterPolicy.Remove) { return@forEach } val renameTo = filter.getRenameTo(methodClass.name, method.name, method.desc) dumpMethod(pkg, cls, isSuperClass, methodClass.name.toHumanReadableClassName(), - renameTo ?: method.name, method.desc, policy) + renameTo ?: method.name, method.desc, classPolicy, methodPolicy) } // Dump super class methods. @@ -155,10 +178,13 @@ class ApiDumper( dump(dumpClass, methodClass) return } - if (methodClassName.startsWith("java/") || - methodClassName.startsWith("javax/") - ) { - dumpStandardClass(dumpClass, methodClassName) + + // Dump overriding methods from Java standard classes, except for the Object methods, + // which are obvious. + if (methodClassName.startsWith("java/") || methodClassName.startsWith("javax/")) { + if (methodClassName != "java/lang/Object") { + dumpStandardClass(dumpClass, methodClassName) + } return } log.w("Super class or interface $methodClassName (used by ${dumpClass.name}) not found.") @@ -188,12 +214,12 @@ class ApiDumper( val methodDesc = Type.getMethodDescriptor(method) // If we already printed the method from a subclass, don't print it. - if (isDuplicate(methodName, methodDesc)) { + if (shownAlready(methodName, methodDesc)) { return@forEach } dumpMethod(pkg, cls, true, methodClassName, - methodName, methodDesc, javaStandardApiPolicy) + methodName, methodDesc, javaStandardApiPolicy, javaStandardApiPolicy) } } catch (e: ClassNotFoundException) { log.w("JVM type $methodClassName (used by ${dumpClass.name}) not found.") diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/FilterPolicy.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/FilterPolicy.kt index 81c26ffdf1f4..c3c870f59347 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/FilterPolicy.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/FilterPolicy.kt @@ -155,7 +155,10 @@ enum class FilterPolicy(val policyStringOrPrefix: String) { /** * Create a [FilterPolicyWithReason] with a given reason. */ - fun withReason(reason: String): FilterPolicyWithReason { - return FilterPolicyWithReason(this, reason) + fun withReason( + reason: String, + statsLabelOverride: StatsLabel? = null, + ): FilterPolicyWithReason { + return FilterPolicyWithReason(this, reason, statsLabelOverride = statsLabelOverride) } } diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/FilterPolicyWithReason.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/FilterPolicyWithReason.kt index b10165b835f2..7358a0bfb3e6 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/FilterPolicyWithReason.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/FilterPolicyWithReason.kt @@ -16,32 +16,54 @@ package com.android.hoststubgen.filters /** + * How each entry should be handled on the dashboard. + */ +enum class StatsLabel(val statValue: Int, val label: String) { + /** Entry shouldn't show up in the dashboard. */ + Ignored(-1, ""), + + /** Entry should be shown as "not supported" */ + NotSupported(0, "NotSupported"), + + /** + * Entry should be shown as "supported", but are too "boring" to show on the dashboard, + * e.g. annotation classes. + */ + SupportedButBoring(1, "Boring"), + + /** Entry should be shown as "supported" */ + Supported(2, "Supported"), +} + +/** * Captures a [FilterPolicy] with a human-readable reason. */ data class FilterPolicyWithReason ( - val policy: FilterPolicy, - val reason: String = "", + val policy: FilterPolicy, + val reason: String = "", + private val statsLabelOverride: StatsLabel? = null ) { /** * Return a new [FilterPolicy] with an updated reason, while keeping the original reason * as an "inner-reason". */ - fun wrapReason(reason: String): FilterPolicyWithReason { - return FilterPolicyWithReason(policy, "$reason [inner-reason: ${this.reason}]") + fun wrapReason(reason: String, statsLabelOverride: StatsLabel? = null): FilterPolicyWithReason { + return FilterPolicyWithReason( + policy, + "$reason [inner-reason: ${this.reason}]", + statsLabelOverride = statsLabelOverride, + ) } override fun toString(): String { - return "[$policy - reason: $reason]" + return "[$policy/$statsLabel - reason: $reason]" } - /** 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-to") + val statsLabel: StatsLabel get() { + statsLabelOverride?.let { return it } + if (policy.isSupported) { + return StatsLabel.Supported } + return StatsLabel.NotSupported + } } diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt index d44d016f7c5b..1145da635606 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt @@ -48,7 +48,11 @@ class ImplicitOutputFilter( // If the outer class needs to be in impl, it should be in impl too. val outerPolicy = outermostFilter.getPolicyForClass(cn.outerClass) if (outerPolicy.policy.needsInOutput) { - return FilterPolicy.KeepClass.withReason("anonymous-inner-class") + // We keep this class, but don't need to show it in the dashboard. + return FilterPolicy.KeepClass.withReason( + "anonymous-inner-class", + StatsLabel.Ignored, + ) } } return null @@ -62,6 +66,15 @@ class ImplicitOutputFilter( // Use the implicit policy, if any. getClassImplicitPolicy(cn)?.let { return it } + // If it's an annotation class and we need to keep it, then + // change the reason to hide it from the stats. + if (cn.isAnnotation() && fallback.policy.needsInOutput) { + return FilterPolicy.KeepClass.withReason( + "is-annotation", + StatsLabel.Ignored, + ) + } + return fallback } @@ -102,14 +115,20 @@ class ImplicitOutputFilter( if (cn.isEnum()) { mn?.let { mn -> if (isAutoGeneratedEnumMember(mn)) { - return memberPolicy.withReason(classPolicy.reason).wrapReason("is-enum") + return memberPolicy.withReason(classPolicy.reason).wrapReason( + "is-enum", + StatsLabel.Ignored, + ) } } } // Keep (or stub) all members of annotations. if (cn.isAnnotation()) { - return memberPolicy.withReason(classPolicy.reason).wrapReason("is-annotation") + return memberPolicy.withReason(classPolicy.reason).wrapReason( + "is-annotation", + StatsLabel.Ignored, + ) } mn?.let { @@ -117,7 +136,8 @@ class ImplicitOutputFilter( // For synthetic methods (such as lambdas), let's just inherit the class's // policy. return memberPolicy.withReason(classPolicy.reason).wrapReason( - "is-synthetic-method" + "is-synthetic-method", + StatsLabel.Ignored, ) } } diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/KeepNativeFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/KeepNativeFilter.kt index 00e7d77fa6e7..57309b49a2cd 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/KeepNativeFilter.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/KeepNativeFilter.kt @@ -22,6 +22,9 @@ import com.android.hoststubgen.asm.isNative * For native methods that weren't handled by outer filters, we keep it so that * native method registration will not crash at runtime. Ideally we shouldn't need * this, but in practice unsupported native method registrations do occur. + * + * Native methods kept by this filter will all have a "Keep" policy, but they won't show + * up as "supported" in the stats dashboard beucase we set reallySupported to false. */ class KeepNativeFilter( private val classes: ClassNodes, @@ -34,7 +37,7 @@ class KeepNativeFilter( ): FilterPolicyWithReason { return classes.findMethod(className, methodName, descriptor)?.let { mn -> if (mn.isNative()) { - FilterPolicy.Keep.withReason("native-preserve") + FilterPolicy.Keep.withReason("native-preserve", StatsLabel.NotSupported) } else { null } diff --git a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt index 2edcb2a6c199..3291ff6b8bc6 100644 --- a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt +++ b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt @@ -31,12 +31,13 @@ import org.apache.commons.compress.archivers.zip.ZipFile class HostStubGen(val options: HostStubGenOptions) { fun run() { val errors = HostStubGenErrors() - val stats = HostStubGenStats() val inJar = ZipFile(options.inJar.get) // Load all classes. val allClasses = ClassNodes.loadClassStructures(inJar, options.inJar.get) + val stats = HostStubGenStats(allClasses) + // Dump the classes, if specified. options.inputJarDumpFile.ifSet { log.iTime("Dump file created at $it") { diff --git a/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py b/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py index 761748265726..0c2269ab5e0d 100755 --- a/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py +++ b/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py @@ -33,7 +33,7 @@ def run_diff(file1, file2): '--ignore-space-change', # Ignore the class file version. - '--ignore-matching-lines=^ *\(major\|minor\) version:$', + '--ignore-matching-lines=^ *\\(major\\|minor\\) version:$', # We shouldn't need `--ignore-matching-lines`, but somehow # the golden files were generated without these lines for b/388562869, |