1 /*
<lambda>null2  * Copyright 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.benchmark.macro
18 
19 import android.os.Build
20 import androidx.annotation.RequiresApi
21 import androidx.annotation.RestrictTo
22 import androidx.benchmark.DeviceInfo
23 import androidx.benchmark.Shell
24 import androidx.benchmark.macro.BatteryCharge.hasMinimumCharge
25 import androidx.benchmark.macro.PowerMetric.Companion.deviceSupportsHighPrecisionTracking
26 import androidx.benchmark.macro.PowerRail.hasMetrics
27 import androidx.benchmark.macro.perfetto.BatteryDischargeQuery
28 import androidx.benchmark.macro.perfetto.FrameTimingQuery
29 import androidx.benchmark.macro.perfetto.FrameTimingQuery.SubMetric
30 import androidx.benchmark.macro.perfetto.FrameTimingQuery.getFrameSubMetrics
31 import androidx.benchmark.macro.perfetto.MemoryCountersQuery
32 import androidx.benchmark.macro.perfetto.MemoryUsageQuery
33 import androidx.benchmark.macro.perfetto.PowerQuery
34 import androidx.benchmark.macro.perfetto.StartupTimingQuery
35 import androidx.benchmark.macro.perfetto.camelCase
36 import androidx.benchmark.traceprocessor.Slice
37 import androidx.benchmark.traceprocessor.TraceProcessor
38 import androidx.test.platform.app.InstrumentationRegistry
39 
40 /** Metric interface. */
41 sealed class Metric {
42     internal open fun configure(captureInfo: CaptureInfo) {}
43 
44     internal open fun start() {}
45 
46     internal open fun stop() {}
47 
48     /** After stopping, collect metrics */
49     internal abstract fun getMeasurements(
50         captureInfo: CaptureInfo,
51         traceSession: TraceProcessor.Session
52     ): List<Measurement>
53 
54     /**
55      * Contextual information about the environment where metrics are captured, such as [apiLevel]
56      * and [targetPackageName].
57      *
58      * @property apiLevel `Build.VERSION.SDK_INT` at time of capture.
59      * @property targetPackageName Package name of the app process being measured.
60      * @property testPackageName Package name of the test/benchmarking process.
61      * @property startupMode StartupMode for the target application, if the app was forced to launch
62      *   in a specific state, `null` otherwise.
63      * @property artMainlineVersion ART mainline version, or `-1` if on a OS version without ART
64      *   mainline (<30). `null` if captured from a fixed trace, where mainline version is unknown.
65      */
66     @ExperimentalMetricApi
67     class CaptureInfo(
68         val apiLevel: Int,
69         val targetPackageName: String,
70         val testPackageName: String,
71         val startupMode: StartupMode?,
72 
73         // allocations for tests not relevant, not in critical path
74         @Suppress("AutoBoxing")
75         @get:Suppress("AutoBoxing")
76         val artMainlineVersion: Long? = expectedArtMainlineVersion(apiLevel),
77     ) {
78         init {
79             val expectedArtMainlineVersion = expectedArtMainlineVersion(apiLevel)
80             if (expectedArtMainlineVersion != null) {
81                 // require exact match
82                 require(artMainlineVersion == expectedArtMainlineVersion) {
83                     "For API level $apiLevel, expected artMainlineVersion to be $expectedArtMainlineVersion, observed $artMainlineVersion"
84                 }
85             }
86         }
87 
88         companion object {
89             internal fun expectedArtMainlineVersion(apiLevel: Int) =
90                 when {
91                     apiLevel == 30 -> 1L
92                     apiLevel < 30 -> -1
93                     // can't reason about other levels, since low ram go devices
94                     // may not have mainline updates enabled at all, e.g. wembley
95                     else -> null
96                 }
97 
98             /**
99              * Constructs a CaptureInfo for a local run on the current device, from the current
100              * process.
101              *
102              * @param targetPackageName Package name of the app being measured.
103              * @param startupMode StartupMode for the target application, if the app was forced to
104              *   launch in a specific state, `null` otherwise.
105              */
106             @JvmStatic
107             fun forLocalCapture(targetPackageName: String, startupMode: StartupMode?) =
108                 CaptureInfo(
109                     apiLevel = Build.VERSION.SDK_INT,
110                     artMainlineVersion = DeviceInfo.artMainlineVersion,
111                     targetPackageName = targetPackageName,
112                     testPackageName =
113                         InstrumentationRegistry.getInstrumentation().context.packageName,
114                     startupMode = startupMode
115                 )
116         }
117     }
118 
119     /**
120      * Represents a Metric's measurement of a single iteration.
121      *
122      * To validate results in tests, use [assertEqualMeasurements]
123      */
124     @ConsistentCopyVisibility // Mirror copy()'s visibility with that of the constructor
125     @ExperimentalMetricApi
126     @Suppress("DataClassDefinition")
127     data class Measurement
128     internal constructor(
129         /**
130          * Unique name of the metric, should be camel case with abbreviated suffix, e.g.
131          * `startTimeNs`
132          */
133         val name: String,
134         /**
135          * Measurement values captured by the metric, length constraints defined by
136          * [requireSingleValue].
137          */
138         val data: List<Double>,
139         /**
140          * True if the [data] param is a single value per measurement, false if it contains an
141          * arbitrary number of samples.
142          */
143         val requireSingleValue: Boolean
144     ) {
145 
146         /**
147          * Represents a measurement with a single value captured per iteration.
148          *
149          * For example, in a startup Macrobenchmark, [StartupTimingMetric] returns a single
150          * measurement for `timeToInitialDisplayMs`.
151          */
152         constructor(
153             name: String,
154             data: Double
155         ) : this(name, listOf(data), requireSingleValue = true)
156 
157         /**
158          * Represents a measurement with a value sampled an arbitrary number of times per iteration.
159          *
160          * For example, in a jank Macrobenchmark, [FrameTimingMetric] can return multiple
161          * measurements for `frameOverrunMs` - one for each observed frame.
162          *
163          * When measurements are merged across multiple iterations, percentiles are extracted from
164          * the total pool of samples: P50, P90, P95, and P99.
165          */
166         constructor(
167             name: String,
168             dataSamples: List<Double>
169         ) : this(name, dataSamples, requireSingleValue = false)
170 
171         init {
172             require(!requireSingleValue || data.size == 1) {
173                 "Metric.Measurement must be in multi-sample mode, or include only one data item"
174             }
175         }
176     }
177 }
178 
Longnull179 private fun Long.nsToDoubleMs(): Double = this / 1_000_000.0
180 
181 /**
182  * Metric which captures timing information from frames produced by a benchmark, such as a scrolling
183  * or animation benchmark.
184  *
185  * This outputs the following measurements:
186  * * `frameOverrunMs` (Requires API 31) - How much time a given frame missed its deadline by.
187  *   Positive numbers indicate a dropped frame and visible jank / stutter, negative numbers indicate
188  *   how much faster than the deadline a frame was.
189  * * `frameDurationCpuMs` - How much time the frame took to be produced on the CPU - on both the UI
190  *   Thread, and RenderThread. Note that this doesn't account for time before the frame started
191  *   (before Choreographer#doFrame), as that data isn't available in traces prior to API 31.
192  * * `frameCount` - How many total frames were produced. This is a secondary metric which can be
193  *   used to understand *why* the above metrics changed. For example, when removing unneeded frames
194  *   that were incorrectly invalidated to save power, `frameOverrunMs` and `frameDurationCpuMs` will
195  *   often get worse, as the removed frames were trivial. Checking `frameCount` can be a useful
196  *   indicator in such cases.
197  *
198  * Generally, prefer tracking and detecting regressions with `frameOverrunMs` when it is available,
199  * as it is the more complete data, and accounts for modern devices (including higher, variable
200  * framerate rendering) more naturally.
201  */
202 @Suppress("CanSealedSubClassBeObject")
203 class FrameTimingMetric : Metric() {
204     override fun getMeasurements(
205         captureInfo: CaptureInfo,
206         traceSession: TraceProcessor.Session
207     ): List<Measurement> {
208         val frameData =
209             FrameTimingQuery.getFrameData(
210                 session = traceSession,
211                 captureApiLevel = captureInfo.apiLevel,
212                 packageName = captureInfo.targetPackageName
213             )
214         return frameData
215             .getFrameSubMetrics(captureInfo.apiLevel)
216             .filterKeys { it == SubMetric.FrameDurationCpuNs || it == SubMetric.FrameOverrunNs }
217             .map {
218                 Measurement(
219                     name =
220                         if (it.key == SubMetric.FrameDurationCpuNs) {
221                             "frameDurationCpuMs"
222                         } else {
223                             "frameOverrunMs"
224                         },
225                     dataSamples = it.value.map { timeNs -> timeNs.nsToDoubleMs() }
226                 )
227             } + listOf(Measurement("frameCount", frameData.size.toDouble()))
228     }
229 }
230 
231 /**
232  * Version of FrameTimingMetric based on 'dumpsys gfxinfo' instead of trace data.
233  *
234  * Added for experimentation in contrast to FrameTimingMetric, as the platform accounting of frame
235  * drops currently behaves differently from that of FrameTimingMetric.
236  *
237  * Likely to be removed when differences in jank behavior are reconciled between this class, and
238  * [FrameTimingMetric].
239  *
240  * Note that output metrics do not match perfectly to FrameTimingMetric, as individual frame times
241  * are not available, only high level, millisecond-precision statistics.
242  */
243 @ExperimentalMetricApi
244 class FrameTimingGfxInfoMetric : Metric() {
245     private lateinit var packageName: String
246     private val helper = JankCollectionHelper()
247     private var metrics = mutableMapOf<String, Double>()
248 
configurenull249     override fun configure(captureInfo: CaptureInfo) {
250         this.packageName = captureInfo.targetPackageName
251         helper.addTrackedPackages(packageName)
252     }
253 
startnull254     override fun start() {
255         try {
256             helper.startCollecting()
257         } catch (exception: RuntimeException) {
258             // Ignore the exception that might result from trying to clear GfxInfo
259             // The current implementation of JankCollectionHelper throws a RuntimeException
260             // when that happens. This is safe to ignore because the app being benchmarked
261             // is not showing any UI when this happens typically.
262 
263             // Once the MacroBenchmarkRule has the ability to setup the app in the right state via
264             // a designated setup block, we can get rid of this.
265             if (!Shell.isPackageAlive(packageName)) {
266                 error(exception.message ?: "Assertion error, $packageName not running")
267             }
268         }
269     }
270 
stopnull271     override fun stop() {
272         helper.stopCollecting()
273 
274         // save metrics on stop to attempt to more closely match perfetto based metrics
275         metrics.clear()
276         metrics.putAll(helper.metrics)
277     }
278 
279     /**
280      * Used to convert keys from platform to JSON format.
281      *
282      * This both converts `snake_case_format` to `camelCaseFormat`, and renames for clarity.
283      *
284      * Note that these will still output to inst results in snake_case, with `MetricNameUtils` via
285      * [androidx.benchmark.MetricResult.putInBundle].
286      */
287     private val keyRenameMap =
288         mapOf(
289             "frame_render_time_percentile_50" to "gfxFrameTime50thPercentileMs",
290             "frame_render_time_percentile_90" to "gfxFrameTime90thPercentileMs",
291             "frame_render_time_percentile_95" to "gfxFrameTime95thPercentileMs",
292             "frame_render_time_percentile_99" to "gfxFrameTime99thPercentileMs",
293             "gpu_frame_render_time_percentile_50" to "gpuFrameTime50thPercentileMs",
294             "gpu_frame_render_time_percentile_90" to "gpuFrameTime90thPercentileMs",
295             "gpu_frame_render_time_percentile_95" to "gpuFrameTime95thPercentileMs",
296             "gpu_frame_render_time_percentile_99" to "gpuFrameTime99thPercentileMs",
297             "missed_vsync" to "vsyncMissedFrameCount",
298             "deadline_missed" to "deadlineMissedFrameCount",
299             "deadline_missed_legacy" to "deadlineMissedFrameCountLegacy",
300             "janky_frames_count" to "jankyFrameCount",
301             "janky_frames_legacy_count" to "jankyFrameCountLegacy",
302             "high_input_latency" to "highInputLatencyFrameCount",
303             "slow_ui_thread" to "slowUiThreadFrameCount",
304             "slow_bmp_upload" to "slowBitmapUploadFrameCount",
305             "slow_issue_draw_cmds" to "slowIssueDrawCommandsFrameCount",
306             "total_frames" to "gfxFrameTotalCount",
307             "janky_frames_percent" to "gfxFrameJankPercent",
308             "janky_frames_legacy_percent" to "jankyFramePercentLegacy"
309         )
310 
311     /** Filters output to only frameTimeXXthPercentileMs and totalFrameCount */
312     private val keyAllowList =
313         setOf(
314             "gfxFrameTime50thPercentileMs",
315             "gfxFrameTime90thPercentileMs",
316             "gfxFrameTime95thPercentileMs",
317             "gfxFrameTime99thPercentileMs",
318             "gfxFrameTotalCount",
319             "gfxFrameJankPercent",
320         )
321 
getMeasurementsnull322     override fun getMeasurements(
323         captureInfo: CaptureInfo,
324         traceSession: TraceProcessor.Session
325     ): List<Measurement> {
326         return metrics
327             .map {
328                 val prefix = "gfxinfo_${packageName}_"
329                 val keyWithoutPrefix = it.key.removePrefix(prefix)
330 
331                 if (keyWithoutPrefix != it.key && keyRenameMap.containsKey(keyWithoutPrefix)) {
332                     Measurement(keyRenameMap[keyWithoutPrefix]!!, it.value)
333                 } else {
334                     throw IllegalStateException("Unexpected key ${it.key}")
335                 }
336             }
337             .filter { keyAllowList.contains(it.name) }
338     }
339 }
340 
341 /**
342  * Captures app startup timing metrics.
343  *
344  * This outputs the following measurements:
345  * * `timeToInitialDisplayMs` - Time from the system receiving a launch intent to rendering the
346  *   first frame of the destination Activity.
347  * * `timeToFullDisplayMs` - Time from the system receiving a launch intent until the application
348  *   reports fully drawn via [android.app.Activity.reportFullyDrawn]. The measurement stops at the
349  *   completion of rendering the first frame after (or containing) the `reportFullyDrawn()` call.
350  *   This measurement may not be available prior to API 29.
351  */
352 @Suppress("CanSealedSubClassBeObject")
353 class StartupTimingMetric : Metric() {
getMeasurementsnull354     override fun getMeasurements(
355         captureInfo: CaptureInfo,
356         traceSession: TraceProcessor.Session
357     ): List<Measurement> {
358         return StartupTimingQuery.getFrameSubMetrics(
359                 session = traceSession,
360                 captureApiLevel = captureInfo.apiLevel,
361                 targetPackageName = captureInfo.targetPackageName,
362 
363                 // Pick an arbitrary startup mode if unspecified. In the future, consider throwing
364                 // an
365                 // error if startup mode not defined
366                 startupMode = captureInfo.startupMode ?: StartupMode.COLD
367             )
368             ?.run {
369                 mapOf(
370                         "timeToInitialDisplayMs" to timeToInitialDisplayNs.nsToDoubleMs(),
371                         "timeToFullDisplayMs" to timeToFullDisplayNs?.nsToDoubleMs()
372                     )
373                     .filterValues { it != null }
374                     .map { Measurement(it.key, it.value!!) }
375             } ?: emptyList()
376     }
377 }
378 
379 /** Captures app startup timing metrics. */
380 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
381 @Suppress("CanSealedSubClassBeObject")
382 @RequiresApi(29)
383 class StartupTimingLegacyMetric : Metric() {
getMeasurementsnull384     override fun getMeasurements(
385         captureInfo: CaptureInfo,
386         traceSession: TraceProcessor.Session
387     ): List<Measurement> {
388         // Acquires perfetto metrics
389         val traceMetrics = traceSession.getTraceMetrics("android_startup")
390         val androidStartup =
391             traceMetrics.android_startup
392                 ?: throw IllegalStateException("No android_startup metric found.")
393         val appStartup =
394             androidStartup.startup.firstOrNull { it.package_name == captureInfo.targetPackageName }
395                 ?: throw IllegalStateException(
396                     "Didn't find startup for pkg " +
397                         "${captureInfo.targetPackageName}, found startups for pkgs: " +
398                         "${androidStartup.startup.map { it.package_name }}"
399                 )
400 
401         // Extract app startup
402         val measurements = mutableListOf<Measurement>()
403 
404         val durMs = appStartup.to_first_frame?.dur_ms
405         if (durMs != null) {
406             measurements.add(Measurement("startupMs", durMs))
407         }
408 
409         val fullyDrawnMs = appStartup.report_fully_drawn?.dur_ms
410         if (fullyDrawnMs != null) {
411             measurements.add(Measurement("fullyDrawnMs", fullyDrawnMs))
412         }
413 
414         return measurements
415     }
416 }
417 
418 /**
419  * Metric which captures results from a Perfetto trace with custom [TraceProcessor] queries.
420  *
421  * This is a more customizable version of [TraceSectionMetric] which can perform arbitrary queries
422  * against the captured PerfettoTrace.
423  *
424  * Sample metric which finds the duration of the first "activityResume" trace section for the traced
425  * package:
426  * ```
427  * class ActivityResumeMetric : TraceMetric() {
428  *     override fun getMeasurements(
429  *         captureInfo: CaptureInfo,
430  *         traceSession: TraceProcessor.Session
431  *     ): List<Measurement> {
432  *         val rowSequence = traceSession.query(
433  *             """
434  *             SELECT
435  *                 slice.name as name,
436  *                 slice.ts as ts,
437  *                 slice.dur as dur
438  *             FROM slice
439  *                 INNER JOIN thread_track on slice.track_id = thread_track.id
440  *                 INNER JOIN thread USING(utid)
441  *                 INNER JOIN process USING(upid)
442  *             WHERE
443  *                 process.name LIKE ${captureInfo.targetPackageName}
444  *                     AND slice.name LIKE "activityResume"
445  *             """.trimIndent()
446  *         )
447  *         // this metric queries a single slice type to produce submetrics, but could be extended
448  *         // to capture timing of every component of activity lifecycle
449  *         val activityResultNs = rowSequence.firstOrNull()?.double("dur")
450  *         return if (activityResultMs != null) {
451  *             listOf(Measurement("activityResumeMs", activityResultNs / 1_000_000.0))
452  *         } else {
453  *             emptyList()
454  *         }
455  *     }
456  * }
457  * ```
458  *
459  * @see TraceProcessor
460  * @see TraceProcessor.Session
461  * @see TraceProcessor.Session.query
462  */
463 @ExperimentalMetricApi
464 abstract class TraceMetric : Metric() {
465     /**
466      * Get the metric result for a given iteration given information about the target process and a
467      * TraceProcessor session
468      */
getMeasurementsnull469     public abstract override fun getMeasurements(
470         captureInfo: CaptureInfo,
471         traceSession: TraceProcessor.Session
472     ): List<Measurement>
473 }
474 
475 /**
476  * Captures the time taken by named trace section - a named begin / end pair matching the provided
477  * [sectionName].
478  *
479  * Select how matching sections are resolved into a duration metric with [mode], and configure if
480  * sections outside the target process are included with [targetPackageOnly].
481  *
482  * The following TraceSectionMetric counts the number of JIT method compilations that occur within a
483  * trace:
484  * ```
485  * TraceSectionMetric(
486  *     sectionName = "JIT Compiling %",
487  *     mode = TraceSectionMetric.Mode.Sum
488  * )
489  * ```
490  *
491  * Note that non-terminating slices in the trace (where duration = -1) are always ignored by this
492  * metric.
493  *
494  * @see androidx.tracing.Trace.beginSection
495  * @see androidx.tracing.Trace.endSection
496  * @see androidx.tracing.trace
497  */
498 @ExperimentalMetricApi
499 class TraceSectionMetric
500 @JvmOverloads
501 constructor(
502     /**
503      * Section name or pattern to match.
504      *
505      * "%" can be used as a wildcard, as this is supported by the underlying [TraceProcessor] query.
506      * For example `"JIT %"` will match a section named `"JIT compiling int
507      * com.package.MyClass.method(int)"` present in the trace.
508      */
509     private val sectionName: String,
510     /**
511      * Defines how slices matching [sectionName] should be confirmed to metrics, by default uses
512      * [Mode.Sum] to count and sum durations of all matching trace sections.
513      */
514     private val mode: Mode = Mode.Sum,
515     /** Metric label, defaults to [sectionName]. */
516     private val label: String = sectionName,
517     /** Filter results to trace sections only from the target process, defaults to true. */
518     private val targetPackageOnly: Boolean = true
519 ) : Metric() {
520     sealed class Mode(internal val name: String) {
521         /**
522          * Captures the duration of the first instance of `sectionName` in the trace.
523          *
524          * When this mode is used, no measurement will be reported if the named section does not
525          * appear in the trace.
526          */
527         object First : Mode("First")
528 
529         /**
530          * Captures the sum of all instances of `sectionName` in the trace.
531          *
532          * When this mode is used, a measurement of `0` will be reported if the named section does
533          * not appear in the trace.
534          */
535         object Sum : Mode("Sum")
536 
537         /**
538          * Reports the maximum observed duration for a trace section matching `sectionName` in the
539          * trace.
540          *
541          * When this mode is used, no measurement will be reported if the named section does not
542          * appear in the trace.
543          */
544         object Min : Mode("Min")
545 
546         /**
547          * Reports the maximum observed duration for a trace section matching `sectionName` in the
548          * trace.
549          *
550          * When this mode is used, no measurement will be reported if the named section does not
551          * appear in the trace.
552          */
553         object Max : Mode("Max")
554 
555         /**
556          * Counts the number of observed instances of a trace section matching `sectionName` in the
557          * trace.
558          *
559          * When this mode is used, a measurement of `0` will be reported if the named section does
560          * not appear in the trace.
561          */
562         object Count : Mode("Count")
563 
564         /**
565          * Average duration of trace sections matching `sectionName` in the trace.
566          *
567          * When this mode is used, a measurement of `0` will be reported if the named section does
568          * not appear in the trace.
569          */
570         object Average : Mode("Average")
571 
572         /**
573          * Internal class to prevent external exhaustive when statements, which would break as we
574          * add more to this sealed class.
575          */
576         internal object WhenPrevention : Mode("N/A")
577     }
578 
579     override fun getMeasurements(
580         captureInfo: CaptureInfo,
581         traceSession: TraceProcessor.Session
582     ): List<Measurement> {
583         val slices =
584             traceSession.querySlices(
585                 sectionName,
586                 packageName = if (targetPackageOnly) captureInfo.targetPackageName else null
587             )
588 
589         return when (mode) {
590             Mode.First -> {
591                 val slice = slices.firstOrNull()
592                 if (slice == null) {
593                     emptyList()
594                 } else listOf(Measurement(name = label + "FirstMs", data = slice.dur / 1_000_000.0))
595             }
596             Mode.Sum -> {
597                 listOf(
598                     Measurement(
599                         name = label + "SumMs",
600                         // note, this duration assumes non-reentrant slices
601                         data = slices.sumOf { it.dur } / 1_000_000.0
602                     ),
603                     Measurement(name = label + "Count", data = slices.size.toDouble())
604                 )
605             }
606             Mode.Min -> {
607                 if (slices.isEmpty()) {
608                     emptyList()
609                 } else
610                     listOf(
611                         Measurement(
612                             name = label + "MinMs",
613                             data = slices.minOf { it.dur } / 1_000_000.0
614                         )
615                     )
616             }
617             Mode.Max -> {
618                 if (slices.isEmpty()) {
619                     emptyList()
620                 } else
621                     listOf(
622                         Measurement(
623                             name = label + "MaxMs",
624                             data = slices.maxOf { it.dur } / 1_000_000.0
625                         )
626                     )
627             }
628             Mode.Count -> {
629                 listOf(Measurement(name = label + "Count", data = slices.size.toDouble()))
630             }
631             Mode.Average -> {
632                 listOf(
633                     Measurement(
634                         name = label + "AverageMs",
635                         data = slices.sumOf { it.dur } / 1_000_000.0 / slices.size
636                     )
637                 )
638             }
639             Mode.WhenPrevention -> throw IllegalStateException("WhenPrevention should be unused")
640         }
641     }
642 }
643 
644 /**
645  * Captures metrics about ART method/class compilation and initialization.
646  *
647  * JIT Compilation, Class Verification, and (on supported devices) Class Loading.
648  *
649  * For more information on how ART compilation works, see
650  * [ART Runtime docs](https://source.android.com/docs/core/runtime/configure).
651  *
652  * ## JIT Compilation
653  * As interpreted (uncompiled) dex code from the APK is run, some methods will be Just-In-Time (JIT)
654  * compiled, and this compilation is traced by ART. This does not apply to methods AOT compiled
655  * either from Baseline Profiles, Warmup Profiles, or Full AOT.
656  *
657  * The number of traces and total duration (reported as `artJitCount` and `artJitSumMs`) indicate
658  * how many uncompiled methods were considered hot by the runtime, and were JITted during
659  * measurement.
660  *
661  * Note that framework code on the system image that is not AOT compiled on the system image may
662  * also be JITted, and will also show up in this metric. If you see this metric reporting non-zero
663  * values when compiled with [CompilationMode.Full] or [CompilationMode.Partial], this may be the
664  * reason.
665  *
666  * Some methods can't be AOTed or JIT compiled. Generally these are either methods too large for the
667  * Android runtime compiler, or due to a malformed class definition.
668  *
669  * ## Class Loading
670  * Class Loading tracing requires either API 35, or API 31+ with ART mainline version >=
671  * `341511000`. If a device doesn't support these tracepoints, the measurements will not be reported
672  * in Studio UI or in JSON results. You can check your device's ART mainline version with:
673  * ```
674  * adb shell cmd package list packages --show-versioncode --apex-only art
675  * ```
676  *
677  * Classes must be loaded by ART in order to be used at runtime. In [CompilationMode.None] and
678  * [CompilationMode.Full], this is deferred until runtime, and the cost of this can significantly
679  * slow down scenarios where code is run for the first time, such as startup.
680  *
681  * In `CompilationMode.Partial(warmupIterations=...)` classes captured in the warmup profile (used
682  * during the warmup iterations) are persisted into the `.art` file at compile time to allow them to
683  * be preloaded during app start, before app code begins to execute. If a class is preloaded by the
684  * runtime, it will not appear in traces.
685  *
686  * Even if a class is captured in the warmup profile, it will not be persisted at compile time if
687  * any of the superclasses are not in the app's profile (extremely unlikely) or the Boot Image
688  * profile (for Boot Image classes).
689  *
690  * The number of traces and total duration (reported as `artClassLoadCount` and `artClassLoadSumMs`)
691  * indicate how many classes were loaded during measurement, at runtime, without preloading at
692  * compile time.
693  *
694  * These tracepoints are slices of the form `Lcom/example/MyClassName;` for a class named
695  * `com.example.MyClassName`.
696  *
697  * Class loading is not affected by class verification.
698  *
699  * ## Class Verification
700  * Most usages of a class require classes to be verified by the runtime (some usage only require
701  * loading). Typically all classes in a release APK are verified at install time, regardless of
702  * [CompilationMode].
703  *
704  * The number of traces and total duration (reported as `artVerifyClass` and `artVerifyClassSumMs`)
705  * indicate how many classes were verified during measurement, at runtime.
706  *
707  * There are two exceptions however:
708  * 1) If install-time verification fails for a class, it will remain unverified, and be verified at
709  *    runtime.
710  * 2) Debuggable=true apps are not verified at install time, to save on iteration speed at the cost
711  *    of runtime performance. This results in runtime verification of each class as it's loaded
712  *    which is the source of much of the slowdown between a debug app and a release app. As
713  *    Macrobenchmark treats `debuggable=true` as a measurement error, this won't be the case for
714  *    `ArtMetric` measurements unless you suppress that error.
715  *
716  * Some classes will be verified at runtime rather than install time due to limitations in the
717  * compiler and runtime or due to being malformed.
718  */
719 @RequiresApi(24)
720 class ArtMetric : Metric() {
getMeasurementsnull721     override fun getMeasurements(
722         captureInfo: CaptureInfo,
723         traceSession: TraceProcessor.Session
724     ): List<Measurement> {
725         return traceSession
726             .querySlices("JIT Compiling %", packageName = captureInfo.targetPackageName)
727             .asMeasurements("artJit") +
728             traceSession
729                 .querySlices("VerifyClass %", packageName = captureInfo.targetPackageName)
730                 .asMeasurements("artVerifyClass") +
731             if (
732                 DeviceInfo.isClassLoadTracingAvailable(
733                     sdkInt = captureInfo.apiLevel,
734                     artVersion = captureInfo.artMainlineVersion
735                 )
736             ) {
737                 traceSession
738                     .querySlices("L%/%;", packageName = captureInfo.targetPackageName)
739                     .asMeasurements("artClassLoad")
740             } else emptyList()
741     }
742 
asMeasurementsnull743     private fun List<Slice>.asMeasurements(label: String) =
744         listOf(
745             Measurement(
746                 name = label + "SumMs",
747                 // note, this duration assumes non-reentrant slices,
748                 // which is true for art trace sections
749                 data = sumOf { it.dur } / 1_000_000.0
750             ),
751             Measurement(name = label + "Count", data = size.toDouble())
752         )
753 }
754 
755 /**
756  * Captures the change of power, energy or battery charge metrics over time for specified duration.
757  * A configurable output of power, energy, subsystems, and battery charge will be generated.
758  * Subsystem outputs will include the sum of all power or energy metrics within it. A metric total
759  * will also be generated for power and energy, as well as a metric which is the sum of all
760  * unselected metrics.
761  *
762  * @param type Either [Type.Energy] or [Type.Power], which can be configured to show components of
763  *   system power usage, or [Type.Battery], which will halt charging of device to measure power
764  *   drain.
765  *
766  * For [Type.Energy] or [Type.Power], the sum of all categories will be displayed as a `Total`
767  * metric. The sum of all unrequested categories will be displayed as an `Unselected` metric. The
768  * subsystems that have not been categorized will be displayed as an `Uncategorized` metric. You can
769  * check if the local device supports this high precision tracking with
770  * [deviceSupportsHighPrecisionTracking].
771  *
772  * For [Type.Battery], the charge for the start of the run and the end of the run will be displayed.
773  * An additional `Diff` metric will be displayed to indicate the charge drain over the course of the
774  * test.
775  *
776  * The metrics will be stored in the format `<type><name><unit>`. This outputs measurements like the
777  * following:
778  *
779  * Power metrics example:
780  * ```
781  * powerCategoryDisplayUw       min       128.2,   median       128.7,   max       129.8
782  * powerComponentCpuBigUw       min         1.9,   median         2.9,   max         3.4
783  * powerComponentCpuLittleUw    min        65.8,   median        76.2,   max        79.7
784  * powerComponentCpuMidUw       min        10.8,   median        13.3,   max        13.6
785  * powerTotalUw                 min       362.4,   median       395.2,   max       400.6
786  * powerUnselectedUw            min       155.3,   median       170.8,   max       177.8
787  * ```
788  *
789  * Energy metrics example:
790  * ```
791  * energyCategoryDisplayUws     min    610,086.0,   median    623,183.0,   max    627,259.0
792  * energyComponentCpuBigUws     min      9,233.0,   median     13,566.0,   max     16,536.0
793  * energyComponentCpuLittleUws  min    318,591.0,   median    368,211.0,   max    379,106.0
794  * energyComponentCpuMidUws     min     52,143.0,   median     64,462.0,   max     64,893.0
795  * energyTotalUws               min  1,755,261.0,   median  1,880,687.0,   max  1,935,402.0
796  * energyUnselectedUws          min    752,111.0,   median    813,036.0,   max    858,934.0
797  * ```
798  *
799  * Battery metrics example:
800  * ```
801  * batteryDiffMah       min         2.0,   median         2.0,   max         4.0
802  * batteryEndMah        min     3,266.0,   median     3,270.0,   max     3,276.0
803  * batteryStartMah      min     3,268.0,   median     3,274.0,   max     3,278.0
804  * ```
805  *
806  * This measurement is not available prior to API 29.
807  */
808 @RequiresApi(29)
809 @ExperimentalMetricApi
810 class PowerMetric(private val type: Type) : Metric() {
811 
812     companion object {
813         internal const val MEASURE_BLOCK_SECTION_NAME = "measureBlock"
814 
815         @JvmStatic
Batterynull816         fun Battery(): Type.Battery {
817             return Type.Battery()
818         }
819 
820         @JvmStatic
Energynull821         fun Energy(
822             categories: Map<PowerCategory, PowerCategoryDisplayLevel> = emptyMap()
823         ): Type.Energy {
824             return Type.Energy(categories)
825         }
826 
827         @JvmStatic
Powernull828         fun Power(
829             categories: Map<PowerCategory, PowerCategoryDisplayLevel> = emptyMap()
830         ): Type.Power {
831             return Type.Power(categories)
832         }
833 
834         /**
835          * Returns true if the current device can be used for high precision [Power] and [Energy]
836          * metrics.
837          *
838          * This can be used to change behavior or fall back to lower precision tracking:
839          * ```
840          * metrics = listOf(
841          *     if (PowerMetric.deviceSupportsHighPrecisionTracking()) {
842          *         PowerMetric(Type.Energy()) // high precision tracking
843          *     } else {
844          *         PowerMetric(Type.Battery()) // fall back to less precise tracking
845          *     }
846          * )
847          * ```
848          *
849          * Or to skip a test when detailed tracking isn't available:
850          * ```
851          * @Test fun myDetailedPowerBenchmark {
852          *     assumeTrue(PowerMetric.deviceSupportsHighPrecisionTracking())
853          *     macrobenchmarkRule.measureRepeated (
854          *         metrics = listOf(PowerMetric(Type.Energy(...)))
855          *     ) {
856          *         ...
857          *     }
858          * }
859          * ```
860          */
861         @JvmStatic
deviceSupportsHighPrecisionTrackingnull862         fun deviceSupportsHighPrecisionTracking(): Boolean =
863             hasMetrics(throwOnMissingMetrics = false)
864 
865         /**
866          * Returns true if [Type.Battery] measurements can be performed, based on current device
867          * charge.
868          *
869          * This can be used to change behavior or throw a clear error before metric configuration,
870          * or to skip the test, e.g. with `assumeTrue(PowerMetric.deviceBatteryHasMinimumCharge())`
871          */
872         @JvmStatic
873         fun deviceBatteryHasMinimumCharge(): Boolean =
874             hasMinimumCharge(throwOnMissingMetrics = false)
875     }
876 
877     /**
878      * Configures the PowerMetric request.
879      *
880      * @param categories A map which is used to configure which metrics are displayed. The key is a
881      *   `PowerCategory` enum, which configures the subsystem category that will be displayed. The
882      *   value is a `PowerCategoryDisplayLevel`, which configures whether each subsystem in the
883      *   category will have metrics displayed independently or summed for a total metric of the
884      *   category.
885      */
886     sealed class Type(var categories: Map<PowerCategory, PowerCategoryDisplayLevel> = emptyMap()) {
887         class Power(powerCategories: Map<PowerCategory, PowerCategoryDisplayLevel> = emptyMap()) :
888             Type(powerCategories)
889 
890         class Energy(energyCategories: Map<PowerCategory, PowerCategoryDisplayLevel> = emptyMap()) :
891             Type(energyCategories)
892 
893         class Battery : Type()
894     }
895 
configurenull896     override fun configure(captureInfo: CaptureInfo) {
897         if (type is Type.Energy || type is Type.Power) {
898             hasMetrics(throwOnMissingMetrics = true)
899         } else {
900             hasMinimumCharge(throwOnMissingMetrics = true)
901         }
902     }
903 
startnull904     override fun start() {
905         if (type is Type.Battery) {
906             Shell.executeScriptSilent("setprop power.battery_input.suspended true")
907         }
908     }
909 
stopnull910     override fun stop() {
911         if (type is Type.Battery) {
912             Shell.executeScriptSilent("setprop power.battery_input.suspended false")
913         }
914     }
915 
getMeasurementsnull916     override fun getMeasurements(
917         captureInfo: CaptureInfo,
918         traceSession: TraceProcessor.Session
919     ): List<Measurement> {
920         // collect metrics between trace point flags
921         val slice =
922             traceSession.querySlices(MEASURE_BLOCK_SECTION_NAME, packageName = null).firstOrNull()
923                 ?: return emptyList()
924 
925         if (type is Type.Battery) {
926             return getBatteryDischargeMetrics(traceSession, slice)
927         }
928 
929         return getPowerMetrics(traceSession, slice)
930     }
931 
getBatteryDischargeMetricsnull932     private fun getBatteryDischargeMetrics(
933         session: TraceProcessor.Session,
934         slice: Slice
935     ): List<Measurement> {
936         val metrics = BatteryDischargeQuery.getBatteryDischargeMetrics(session, slice)
937         return metrics.map { measurement ->
938             Measurement(getLabel(measurement.name), measurement.chargeMah)
939         }
940     }
941 
getPowerMetricsnull942     private fun getPowerMetrics(session: TraceProcessor.Session, slice: Slice): List<Measurement> {
943         val metrics = PowerQuery.getPowerMetrics(session, slice)
944 
945         val metricMap: Map<String, Double> = getSpecifiedMetrics(metrics)
946         if (metricMap.isEmpty()) {
947             return emptyList()
948         }
949 
950         val extraMetrics: Map<String, Double> = getTotalAndUnselectedMetrics(metrics)
951 
952         return (metricMap + extraMetrics).map { Measurement(it.key, it.value) }
953     }
954 
getLabelnull955     private fun getLabel(metricName: String, displayType: String = ""): String {
956         return when (type) {
957             is Type.Power -> "power${displayType}${metricName}Uw"
958             is Type.Energy -> "energy${displayType}${metricName}Uws"
959             is Type.Battery -> "battery${metricName}Mah"
960         }
961     }
962 
getTotalAndUnselectedMetricsnull963     private fun getTotalAndUnselectedMetrics(
964         metrics: Map<PowerCategory, PowerQuery.CategoryMeasurement>
965     ): Map<String, Double> {
966         return mapOf(
967                 getLabel("Total") to
968                     metrics.values.fold(0.0) { total, next -> total + next.getValue(type) },
969                 getLabel("Unselected") to
970                     metrics
971                         .filter { (category, _) -> !type.categories.containsKey(category) }
972                         .values
973                         .fold(0.0) { total, next -> total + next.getValue(type) }
974             )
975             .filter { (_, measurement) -> measurement != 0.0 }
976     }
977 
getSpecifiedMetricsnull978     private fun getSpecifiedMetrics(
979         metrics: Map<PowerCategory, PowerQuery.CategoryMeasurement>
980     ): Map<String, Double> {
981         return metrics
982             .filter { (category, _) -> type.categories.containsKey(category) }
983             .map { (category, measurement) ->
984                 val sectionName = if (category == PowerCategory.UNCATEGORIZED) "" else "Category"
985                 when (type.categories[category]) {
986                     // if total category specified, create component of sum total of category
987                     PowerCategoryDisplayLevel.TOTAL ->
988                         listOf(
989                             getLabel(category.toString().camelCase(), sectionName) to
990                                 measurement.components.fold(0.0) { total, next ->
991                                     total + next.getValue(type)
992                                 }
993                         )
994                     // if breakdown, append all ComponentMeasurements metrics from category
995                     else ->
996                         measurement.components.map { component ->
997                             getLabel(component.name, "Component") to component.getValue(type)
998                         }
999                 }
1000             }
1001             .flatten()
1002             .associate { pair -> Pair(pair.first, pair.second) }
1003     }
1004 }
1005 
1006 /**
1007  * Metric for tracking the memory usage of the target application.
1008  *
1009  * There are two modes for measurement - `Last`, which represents the last observed value during an
1010  * iteration, and `Max`, which represents the largest sample observed per measurement.
1011  *
1012  * By default, reports:
1013  * * `memoryRssAnonKb` - Anonymous resident/allocated memory owned by the process, not including
1014  *   memory mapped files or shared memory.
1015  * * `memoryRssAnonFileKb` - Memory allocated by the process to map files.
1016  * * `memoryHeapSizeKb` - Heap memory allocations from the Android Runtime, sampled after each GC.
1017  * * `memoryGpuKb` - GPU Memory allocated for the process.
1018  *
1019  * By passing a custom `subMetrics` list, you can enable other [SubMetric]s.
1020  */
1021 @ExperimentalMetricApi
1022 class MemoryUsageMetric(
1023     private val mode: Mode,
1024     private val subMetrics: List<SubMetric> =
1025         listOf(
1026             SubMetric.HeapSize,
1027             SubMetric.RssAnon,
1028             SubMetric.RssFile,
1029             SubMetric.Gpu,
1030         )
1031 ) : TraceMetric() {
1032     enum class Mode {
1033         /**
1034          * Select the last available sample for each value. Useful for inspecting the final state of
1035          * e.g. Heap Size.
1036          */
1037         Last,
1038 
1039         /**
1040          * Select the maximum value observed.
1041          *
1042          * Useful for inspecting the worst case state, e.g. finding worst heap size during a given
1043          * scenario.
1044          */
1045         Max
1046     }
1047 
1048     enum class SubMetric(
1049         /** Name of counter in trace. */
1050         internal val counterName: String,
1051         /**
1052          * False if the metric is represented in the trace in bytes, and must be divided by 1024 to
1053          * be converted to KB.
1054          */
1055         internal val alreadyInKb: Boolean
1056     ) {
1057         HeapSize("Heap size (KB)", alreadyInKb = true),
1058         RssAnon("mem.rss.anon", alreadyInKb = false),
1059         RssFile("mem.rss.file", alreadyInKb = false),
1060         RssShmem("mem.rss.shmem", alreadyInKb = false),
1061         Gpu("GPU Memory", alreadyInKb = false)
1062     }
1063 
getMeasurementsnull1064     override fun getMeasurements(
1065         captureInfo: CaptureInfo,
1066         traceSession: TraceProcessor.Session
1067     ): List<Measurement> {
1068 
1069         val suffix = mode.toString()
1070         return MemoryUsageQuery.getMemoryUsageKb(
1071                 session = traceSession,
1072                 targetPackageName = captureInfo.targetPackageName,
1073                 mode = mode
1074             )
1075             ?.mapNotNull {
1076                 if (it.key in subMetrics) {
1077                     Measurement("memory${it.key}${suffix}Kb", it.value.toDouble())
1078                 } else {
1079                     null
1080                 }
1081             } ?: listOf()
1082     }
1083 }
1084 
1085 /** Captures the number of page faults over time for a target package name. */
1086 @ExperimentalMetricApi
1087 class MemoryCountersMetric : TraceMetric() {
getMeasurementsnull1088     override fun getMeasurements(
1089         captureInfo: CaptureInfo,
1090         traceSession: TraceProcessor.Session
1091     ): List<Measurement> {
1092         val metrics =
1093             MemoryCountersQuery.getMemoryCounters(
1094                 session = traceSession,
1095                 targetPackageName = captureInfo.targetPackageName
1096             ) ?: return listOf()
1097 
1098         return listOf(
1099             Measurement("minorPageFaults", metrics.minorPageFaults),
1100             Measurement("majorPageFaults", metrics.majorPageFaults),
1101             Measurement("pageFaultsBackedBySwapCache", metrics.pageFaultsBackedBySwapCache),
1102             Measurement("pageFaultsBackedByReadIO", metrics.pageFaultsBackedByReadIO),
1103             Measurement("memoryCompactionEvents", metrics.memoryCompactionEvents),
1104             Measurement("memoryReclaimEvents", metrics.memoryReclaimEvents),
1105         )
1106     }
1107 }
1108