Control your app from Macrobenchmark

Unlike most Android UI tests, Macrobenchmark tests run in a separate process from the app itself. This is necessary to enable things like stopping the app process and compiling from DEX bytecode to machine code.

You can drive your app's state using the UIAutomator library or other mechanisms that can control the target app from the test process. To expose Compose elements to UI Automator, use Modifier.testTag.

The following example uses a LazyColumn with a testTag:

@Composable
fun ProductListScreen() {
    LazyColumn(
        modifier = Modifier
            .fillMaxSize()
            .testTag("my_lazy_column")    ) {
        items(100) { index ->
            ProductItem(index)
        }
    }
}

The test uses the testTag to find the LazyColumn and fling it:

@Test
fun scrollList() {
    benchmarkRule.measureRepeated(
        packageName = "com.example.myapp",
        metrics = listOf(FrameTimingMetric()),
        iterations = 5,
        setupBlock = {
            uiAutomator {
                pressHome()
                startApp("com.example.myapp")
            }
        }
    ) {
        uiAutomator {
            // Find the Composable using its testTag mapped as a viewIdResourceName
            val lazyColumn = onElement { viewIdResourceName == "my_lazy_column" }

            // Fling the Compose list down
            repeat(3) {
                lazyColumn.fling(Direction.DOWN)
            }
        }
    }
}

Your benchmark doesn't have to scroll the UI. Instead, it can run an animation, for example. It also doesn't need to use UI Automator specifically. It collects performance metrics as long as frames are being produced.

Sometimes you might want to benchmark a specific screen that isn't immediately visible when the app starts, like a details screen or a checkout page deep within your Jetpack Navigation graph.

Because Macrobenchmark runs out-of-process, you can't interact directly with your NavController to swap screens. Instead, your benchmark must simulate a user navigating to that part of the app.

Use the setupBlock to handle the preparation steps, like clicking through an onboarding flow or a menu button. This way, your measureBlock captures the performance metrics of only the target screen.

@Test
fun deepScreenScrollList() {
    benchmarkRule.measureRepeated(
        packageName = "com.example.myapp",
        metrics = listOf(FrameTimingMetric()),
        iterations = 5,
        setupBlock = {
            uiAutomator {
                // 1. Start the app on the home screen
                startApp("com.example.myapp")

                // 2. Navigate to the internal screen by clicking a Compose component
                // (e.g., a card that opens the target list view)
                val settingsButton = onElement { viewIdResourceName == "go_to_list_button" }
                settingsButton.click()

                // 3. Wait until the target screen settles and is fully rendered
                waitForStableInActiveWindow()
            }
        }
    ) {
        uiAutomator {
            // The actual benchmark measurement starts here on the target screen
            val lazyColumn = onElement { viewIdResourceName == "my_lazy_column" }
            lazyColumn.fling(Direction.DOWN)
        }
    }
}

Additional resources

For more information about testing, see the following resources.

Documentation

Views content