Macrobenchmark からアプリを制御する

ほとんどの Android UI テストと異なり、Macrobenchmark テストはアプリ自体とは別個のプロセスで実行されます。これは、アプリプロセスの停止や、DEX バイトコードからマシンコードへのコンパイルなどを行うために必要です。

UIAutomator ライブラリか、テストプロセスからターゲット アプリを制御できるその他のメカニズムを使用して、アプリの状態を変更できます。EspressoActivityScenario などのアプローチは、アプリと共有するプロセスでの実行を想定しているため、Macrobenchmark では機能しません。

次の例では、RecyclerView を検出するためにそのリソース ID を使用し、何度か下にスクロールしています。

Kotlin

@Test
fun scrollList() {
    benchmarkRule.measureRepeated(
        // ...
        setupBlock = {
            // Before starting to measure, navigate to the UI to be measured
            val intent = Intent("$packageName.RECYCLER_VIEW_ACTIVITY")
            startActivityAndWait(intent)
        }
    ) {
        val recycler = device.findObject(By.res(packageName, "recycler"))
        // Set gesture margin to avoid triggering gesture navigation
        // with input events from automation.
        recycler.setGestureMargin(device.displayWidth / 5)

        // Scroll down several times
        repeat(3) { recycler.fling(Direction.DOWN) }
    }
}

Java

@Test
public void scrollList() {
    benchmarkRule.measureRepeated(
        // ...
        /* setupBlock */ scope -> {
            // Before starting to measure, navigate to the UI to be measured
            val intent = Intent("$packageName.RECYCLER_VIEW_ACTIVITY")
            scope.startActivityAndWait();
            return Unit.INSTANCE;
        },
        /* measureBlock */ scope -> {
            UiDevice device = scope.getDevice();
            UiObject2 recycler = device.findObject(By.res(scope.getPackageName(), "recycler"));

            // Set gesture margin to avoid triggering gesture navigation
            // with input events from automation.
            recycler.setGestureMargin(device.getDisplayWidth() / 5);

            // Fling the recycler several times
            for (int i = 0; i < 3; i++) {
                recycler.fling(Direction.DOWN);
            }

            return Unit.INSTANCE;
        }
    );
}

ベンチマークで UI をスクロールする必要はありません。たとえば、代わりにアニメーションを実行できます。また、特に UI Automator を使用する必要もありません。ビューシステムによってフレーム(Jetpack Compose によって生成されるフレームを含む)が生成されている間、パフォーマンス指標が収集されます。

場合によっては、アプリ内の外部から直接アクセスできない部分をベンチマークする必要があります。たとえば、内部アクティビティ(exported=false とマークされている)にアクセスする場合、Fragment にナビゲートする場合、UI の一部をスワイプする場合などがあります。ベンチマークは、ユーザーが操作する場合と同様に、アプリのそのような部分に「手動」でナビゲートする必要があります。

これは、setupBlock{} 内のコードを変更して目的の効果(ボタンのクリックやスワイプなど)を含めることで実現できます。measureBlock{} には、実際にベンチマークを行う UI 操作のみを含めます。

Kotlin

@Test
fun nonExportedActivityScrollList() {
    benchmarkRule.measureRepeated(
        // ...
        setupBlock = setupBenchmark()
    ) {
        // ...
    }
}

private fun setupBenchmark(): MacrobenchmarkScope.() -> Unit = {
    // Before starting to measure, navigate to the UI to be measured
    startActivityAndWait()

    // click a button to launch the target activity.
    // While we use resourceId here to find the button, you could also use
    // accessibility info or button text content.
    val selector = By.res(packageName, "launchRecyclerActivity")
    if (!device.wait(Until.hasObject(selector), 5_500)) {
        fail("Could not find resource in time")
    }
    val launchRecyclerActivity = device.findObject(selector)
    launchRecyclerActivity.click()

    // wait until the activity is shown
    device.wait(
        Until.hasObject(By.clazz("$packageName.NonExportedRecyclerActivity")),
        TimeUnit.SECONDS.toMillis(10)
    )
}

Java

@Test
public void scrollList() {
    benchmarkRule.measureRepeated(
        // ...
        /* setupBlock */ scope -> {
            // Before starting to measure, navigate to the default activity
            scope.startActivityAndWait();

            // click a button to launch the target activity.
            // While we use resourceId here to find the button, you could also use
            // accessibility info or button text content.
            UiObject2 launchRecyclerActivity = scope.getDevice().findObject(
                By.res(packageName, "launchRecyclerActivity")
            )
            launchRecyclerActivity.click();

            // wait until activity is shown
            scope.getDevice().wait(
                Until.hasObject(By.clazz("$packageName.NonExportedRecyclerActivity")),
                10000L
            )

            return Unit.INSTANCE;
        },
        /* measureBlock */ scope -> {
            // ...
        }
    );
}