Android Nomad #43 - Harnessing the Power of Macrobenchmarks
Advanced Techniques for Compose Macrobenchmarks
As Jetpack Compose continues to evolve, so too must the techniques we use to ensure its performance in production apps. Macrobenchmarks are powerful tools for analyzing end-to-end app performance and are crucial when making optimizations, especially during migrations from Views to Compose.
Why Macrobenchmarks for Compose?
Compose's declarative nature can significantly impact performance when not optimized correctly. Since Compose handles UI rendering differently from traditional Views, it's crucial to measure the time it takes to produce frames, process animations, and render UI elements. Macrobenchmarks let you evaluate how well your app’s performance holds up in real-world scenarios, especially under complex user interactions.
While simple macrobenchmarks can give you frame timings, using advanced techniques allows you to get even more granular insights. You can capture metrics like startup time, scrolling performance, and battery consumption.
Setting Up Advanced Macrobenchmarks
To start implementing advanced macrobenchmarks in Compose, you can use the module wizard in Android Studio. The wizard creates a module that is pre-configured for benchmarking, with a benchmark directory added and debuggable set to false. You can learn more about this here.
Implementing Macrobenchmarks for Compose
Now, let's create a macrobenchmark for the following:
1. Measuring App Startup Time
App startup is a critical performance metric that users notice immediately. Using macrobenchmarks, you can measure cold or warm startup times to ensure your Compose-heavy screens aren’t adding too much overhead.
Here’s how you can measure cold startup time:
@RunWith(AndroidJUnit4::class)
@LargeTest
class AppStartupBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun measureColdStartup() {
benchmarkRule.measureRepeated(
packageName = "com.example.myapp",
metrics = listOf(StartupTimingMetric()),
iterations = 5,
startupMode = StartupMode.COLD
) {
startActivityAndWait(Intent(Intent.ACTION_MAIN).apply {
setClassName(packageName, "com.example.myapp.MainActivity")
})
}
}
}- Cold Startup: This is when the app is completely closed, and the OS needs to launch it from scratch.
- StartupTimingMetric captures the time it takes for the app to reach a usable state after launching.
You can also measure warm startup by setting the startupMode to StartupMode.WARM.
2. Frame Timing for Complex Animations
Compose shines when it comes to creating smooth animations, but they can also be resource-intensive if not handled properly. Macrobenchmarks allow you to measure frame timing during animations, giving you insights into how smooth or janky the animations are.
@RunWith(AndroidJUnit4::class)
@LargeTest
class AnimationBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun measureFrameTimingDuringAnimation() {
benchmarkRule.measureRepeated(
packageName = "com.example.myapp",
metrics = listOf(FrameTimingMetric()),
iterations = 10
) {
startActivityAndWait(Intent(Intent.ACTION_MAIN).apply {
setClassName(packageName, "com.example.myapp.MyComposeAnimationActivity")
})
// Simulate user interaction to trigger the animation
val composeButton = device.findObject(UiSelector().text("Start Animation"))
composeButton.click()
device.waitForIdle()
}
}
}This test captures frame timing during an animation, letting you know if any frames take longer than expected to render (causing jank).
3. Measuring Scrolling Performance
Scrolling is one of the most common user interactions in mobile apps. When using Compose for building lists (e.g., LazyColumn), it’s essential to measure how smooth the scrolling is, especially with large datasets.
@RunWith(AndroidJUnit4::class)
@LargeTest
class ScrollingPerformanceBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun measureScrollPerformance() {
benchmarkRule.measureRepeated(
packageName = "com.example.myapp",
metrics = listOf(FrameTimingMetric()),
iterations = 5
) {
startActivityAndWait(Intent(Intent.ACTION_MAIN).apply {
setClassName(packageName, "com.example.myapp.MyComposeListActivity")
})
val listView = device.findObject(UiSelector().className("androidx.compose.ui.platform.ComposeView"))
listView.swipeUp(100)
device.waitForIdle()
}
}
}This setup captures frame timings during scrolling, helping you identify if there are any delays or dropped frames while navigating through a long list.
4. Custom Metrics with Battery and Memory Consumption
For more advanced analysis, you can create custom metrics by measuring battery or memory consumption during specific interactions or processes.
Here’s an example to track memory usage:
@RunWith(AndroidJUnit4::class)
@LargeTest
class MemoryUsageBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun measureMemoryUsage() {
benchmarkRule.measureRepeated(
packageName = "com.example.myapp",
metrics = listOf(MemoryMetric()),
iterations = 5
) {
startActivityAndWait(Intent(Intent.ACTION_MAIN).apply {
setClassName(packageName, "com.example.myapp.MyComposeHeavyActivity")
})
}
}
}The MemoryMetric() tracks the memory consumption during the activity’s lifecycle, helping you spot memory leaks or high memory usage in your Compose-based screens.
5. Testing Across Different Device Configurations
Compose performance can vary significantly across devices with different screen sizes, refresh rates, or processing power. To make your benchmarks more robust, it’s important to run them on a variety of devices or emulators.
You can set up a test matrix to run your macrobenchmarks across different devices and Android versions to ensure your app performs well on all configurations.
Summary: Best Practices for Compose Macrobenchmarks
- Use Multiple Metrics: Don’t rely solely on frame timings. Include startup time, memory usage, and scrolling performance in your benchmarks.
- Run Multiple Iterations: Performance can vary slightly between runs, so always set up multiple iterations for your tests to get a reliable average.
- Simulate Real-World User Interactions: Macrobenchmarks are most useful when simulating actual user flows like screen navigation, scrolling, and animations.
- Test on Real Devices: While emulators are useful, running benchmarks on physical devices gives a more accurate picture of your app’s performance in the hands of users.
By leveraging advanced techniques for macrobenchmarks, you can ensure that your Compose-based UIs are not only functional but also fast, fluid, and optimized for performance across different devices. This is crucial as you scale your app to reach broader audiences while maintaining a high-quality user experience.