blob: 08842e8f7e66663c41a8ac24807a7628004a7b1e [file] [log] [blame] [view]
# Benchmarking in AndroidX
[TOC]
The public documentation at
[d.android.com/benchmark](http://d.android.com/benchmark) explains how to use
the library - this page focuses on specifics to writing libraries in the
AndroidX repo, and our continuous testing / triage process.
This page is for MICRO benchmarks measuring CPU performance of small sections of
code. If you're looking for measuring startup or jank, see the guide for
MACRObenchmarks [here](/docs/macrobenchmarking.md).
### Writing the benchmark
Benchmarks are just regular instrumentation tests! Just use the
[`BenchmarkRule`](https://developer.android.com/reference/kotlin/androidx/benchmark/junit4/BenchmarkRule)
provided by the library:
<section class="tabs">
#### Kotlin {.new-tab}
```kotlin
@RunWith(AndroidJUnit4::class)
class ViewBenchmark {
@get:Rule
val benchmarkRule = BenchmarkRule()
@Test
fun simpleViewInflate() {
val context = InstrumentationRegistry
.getInstrumentation().targetContext
val inflater = LayoutInflater.from(context)
val root = FrameLayout(context)
benchmarkRule.measure {
inflater.inflate(R.layout.test_simple_view, root, false)
}
}
}
```
#### Java {.new-tab}
```java
@RunWith(AndroidJUnit4.class)
public class ViewBenchmark {
@Rule
public BenchmarkRule mBenchmarkRule = new BenchmarkRule();
@Test
public void simpleViewInflate() {
Context context = InstrumentationRegistry
.getInstrumentation().getTargetContext();
final BenchmarkState state = mBenchmarkRule.getState();
LayoutInflater inflater = LayoutInflater.from(context);
FrameLayout root = new FrameLayout(context);
while (state.keepRunning()) {
inflater.inflate(R.layout.test_simple_view, root, false);
}
}
}
```
</section>
## Project structure
As in the public documentation, benchmarks in the AndroidX repo are test-only
library modules. Differences for AndroidX repo:
1. Module *must* apply `id("androidx.benchmark")` in the plugin block
1. Module *should* live in `integration-tests` group directory to follow
convention
1. Module name *should* end with `-benchmark` in `settings.gradle` to follow
convention
1. Module *should not* contain non-benchmark tests to avoid wasting resources
in benchmark postsubmit
Applying the benchmark plugin give you benefits from the AndroidX plugin:
* Inclusion in microbenchmark CI runs
* AOT Compilation of module (local and CI) for stability
* Disable ANR avoidance in local runs (so you always get method traces)
But note that these can be detrimental for non-benchmark code.
### I'm lazy and want to start quickly
Start by copying one of the following non-Compose projects:
* [navigation-benchmark](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:navigation/navigation-benchmark/)
* [recyclerview-benchmark](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:recyclerview/recyclerview-benchmark/)
Many Compose libraries already have benchmark modules:
* [Compose UI Benchmarks](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/benchmark/)
* [Compose Runtime Benchmarks](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/runtime/runtime/compose-runtime-benchmark/)
* [Compose Material Benchmarks](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/material/material/benchmark/)
* [Wear Compose Material Benchmarks](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:wear/compose/compose-material/benchmark/)
## Profiling
See the
[public profiling guide](https://developer.android.com/studio/profile/benchmark#profiling)
for more details.
Jetpack benchmark supports capturing profiling information by setting
instrumentation arguments. Stack sampling and method tracing can be performed
either from CLI or Studio invocation.
### Set Arguments in Gradle
Args can be set in your benchmark's `build.gradle`, which will affect both
Studio / command-line gradlew runs. Runs from Studio will link result traces
that can be opened directly from the IDE.
```
android {
defaultConfig {
// must be one of: 'None', 'StackSampling', or 'MethodTracing'
testInstrumentationRunnerArgument 'androidx.benchmark.profiling.mode', 'StackSampling'
}
}
```
### Set Arguments on Command Line
Args can also be passed from CLI. Here's an example which runs the
`androidx.compose.material.benchmark.CheckboxesInRowsBenchmark#draw` method with
`StackSampling` profiling:
```
./gradlew compose:material:material-benchmark:cC \
-P android.testInstrumentationRunnerArguments.androidx.benchmark.profiling.mode=StackSampling \
-P android.testInstrumentationRunnerArguments.class=androidx.compose.material.benchmark.CheckboxesInRowsBenchmark#draw
```
The command output will tell you where to look for the file on your host
machine:
```
04:33:49 I/Benchmark: Benchmark report files generated at
/androidx-main/out/ui/ui/integration-tests/benchmark/build/outputs/connected_android_test_additional_output
```
To inspect the captured trace, open the appropriate `*.trace` file in that
directory with Android Studio, using `File > Open`.
NOTE For stack sampling, it's recommended to profile on Android Q(API 29) or
higher, as this enables the benchmark library to use
[Simpleperf](https://android.googlesource.com/platform/system/extras/+/master/simpleperf/doc/)
when capturing samples.
For more information on the `StackSampling` and `MethodTracing` profiling modes,
see the
[Studio Profiler recording configuration docs](https://developer.android.com/studio/profile/record-traces#configurations),
specifically "Sample C/C++ Functions" (called "Callstack sample" in recent
versions), and Java Method Tracing.
![Sample flame chart](benchmarking_images/profiling_flame_chart.png "Sample flame chart")
### Advanced: Connected Studio Profiler
Profiling for allocations requires Studio to capture, and a debuggable build. Do
not commit the following changes.
First, set your benchmark to be debuggable in your benchmark module's
`androidTest/AndroidManifest.xml`:
```
<application
...
android:debuggable="false"
tools:ignore="HardcodedDebugMode"/>
```
Note that switching to the debug variant will likely not work, as Studio will
fail to find the benchmark as a test source.
Next select `ConnectedAllocation` in your benchmark module's `build.gradle`:
```
android {
defaultConfig {
// --- Local only, don't commit this! ---
// pause for manual profiler connection before/after a single run of
// the benchmark loop, after warmup
testInstrumentationRunnerArgument 'androidx.benchmark.profiling.mode', 'ConnectedAllocation'
}
}
```
Run `File > Sync Project with Gradle Files`, or sync if Studio asks you. Now any
benchmark runs in that project will permit debuggable, and pause before and
after the test, to allow you to connect a profiler and start recording, and then
stop recording.
#### Running and Profiling
After the benchmark test starts, you have about 20 seconds to connect the
profiler:
1. Click the profiler tab at the bottom
1. Click the plus button in the top left, `<device name>`, `<process name>`
1. Click the memory section, and right click the window, and select `Record
allocations`.
1. Approximately 20 seconds later, right click again and select `Stop
recording`.
If timed correctly, you'll have started and stopped collection around the single
run of your benchmark loop, and see all allocations in detail with call stacks
in Studio.
## Minification / R8
As many Android apps don't yet enable R8, the default for microbenchmarks in
AndroidX is to run with R8 disabled to measure worst-case performance. It may
still be useful to run your microbenchmarks with R8 enabled locally however, and
that is supported experimentally. To do this in your microbench module, set the
**androidTest** minification property:
```
android {
buildTypes.release.androidTest.enableMinification = true
}
```
Then, if you see any errors from classes not found at runtime, you can add
proguard rules
[here](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/benchmark-utils/proguard-rules.pro),
or in a similar place for your module.