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