Jetpack Benchmarkを使ってView#measureにかかる時間を計測する

この記事は、 Android Advent Calendar 2019 - Qiita の23日目です。

はじめに

この記事では、ベンチマークを行うためのJetpack Benchmarkライブラリの紹介と、ベンチマークの例として View#measure にかかる時間の計測を行います。

Jetpack Benchmarkとは

ベンチマークを行うためのライブラリで、Instrumented Tests( androidTest ディレクトリ内のテスト)を書くかのようにベンチマークを行うことができます。

Benchmark app code  |  Android Developers

Jetpack Benchmarkの設定

ベンチマーク用のモジュールを追加

ベンチマーク用のコードを追加するためのモジュールを作成します。
Jetpack Benchmarkを使う際には debuggable=false を指定する必要があるため、モジュールを分けておくと継続的に実行する際に便利です。

プラグインとライブラリを追加

ルートの build.gradleclasspathを設定した後、モジュールの build.gradleプラグインとライブラリを追加します。

classpath "androidx.benchmark:benchmark-gradle-plugin:1.0.0"
apply plugin: 'androidx.benchmark'
androidTestImplementation "androidx.benchmark:benchmark-junit4:1.0.0"

また、モジュール作成時に記述されている defaultConfig 内の testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" を削除しておきます。

debuggableをfalseに設定する

AndroidTest を実行する場合のみ debuggable=false にするためには module/src/androidTest/AndroidManifest.xml を作成後、以下のコードを追加します。

<application
    android:debuggable="false"
    tools:ignore="HardcodedDebugMode"
    tools:replace="android:debuggable"/>

以上で設定は終わりとなります。ここからは実際にベンチマークを取るためのコードを書いていきます。

サンプルのベンチマークを実行

androidTest/java/package ディレクトリに以下のクラスを作成します。

@RunWith(AndroidJUnit4::class)
class ViewMeasureTest {
    @get:Rule
    val benchmarkRule = BenchmarkRule()

    @Test
    fun benchmarkSomeWork() = benchmarkRule.measureRepeated {
        Thread.sleep(100)
    }
}

注目部分は以下の2点です。

  • BenchmarkRule の設定
  • テストコードで benchmarkRule.measureRepeated を使用

measureRepeated 内に実行する処理が、ベンチマークの対象となります。
ベンチマークテストは通常のテストと同様に実行するだけで、ベンチマークをとることができます。今回は結果が分かりやすいように100 msのスリープ処理を実行するようにしました。
実行してみると以下のような出力が行われます。

benchmark:   100,160,468 ns ViewMeasureTest.benchmarkSomeWork

処理に 100,160,468 ns ≒ 100 ms かかったという出力が得られました。

このようにJetpack Benchmarkでは、テストコードと同じ記述方法でベンチマークをとることができます。

View#measureにかかる時間を計測する

もう1つの例として、weight を使った LinearLayout入れ子にした場合としなかった場合の View#measure の計測を行います。 (weight入れ子で使用するとAndroidStudioでは Nested weights are bad for performance というinspectionが表示されます。)

レイアウトファイル

使用したレイアウトファイルは以下の2つです

レイアウトプレビューは以下の画像のようになります。 f:id:scache:20191223004901p:plain

ベンチマーク用コード

以下のコードを実行して計測を行いました。
ベンチマークテストでは measureRepeated 内で実行する処理の計測を行いますが、 runWithTimingDisabled 内に書いた処理は計測から除外することが可能です。

@Test
fun noNestedWeights() {
    benchmarkRule.measureRepeated {
        val container: ViewGroup = runWithTimingDisabled {
            val context = ApplicationProvider.getApplicationContext<Context>()
            LayoutInflater.from(context).inflate(R.layout.layout_no_nested, null) as ViewGroup
        }
        measureAndLayoutWrapLength(container)
    }
}

private fun measureAndLayoutWrapLength(container: ViewGroup) {
    val widthMeasureSpec = MeasureSpec.makeMeasureSpec(360, MeasureSpec.EXACTLY)
    val heightMeasureSpec = MeasureSpec.makeMeasureSpec(640, MeasureSpec.AT_MOST)
    container.measure(widthMeasureSpec, heightMeasureSpec)
    container.layout(
        0, 0, container.measuredWidth,
        container.measuredHeight
    )
}

計測結果

ベンチマークテストを実行した結果は以下のようになりました。 レイアウト構造が少し異なるため一概には言えませんが、 weight入れ子で使用した場合は、しなかった場合と比べて処理に大きく時間がかかっていることが分かります。

入れ子にした場合 : benchmark:       475,000 ns ViewMeasureTest.nestedWeights
入れ子にしない場合: benchmark:       233,802 ns ViewMeasureTest.noNestedWeights

最後に

Jetpack Benchmarkライブラリを使用することで、簡単に安定したベンチマークをとることができます。
今回は View#measure を例にしましたが、公式のサンプルにあるオートボクシングのようにAndroidに依存していないコードのベンチマーク計測も可能です。

公式のサンプルはこちら performance-samples/BenchmarkSample at master · android/performance-samples · GitHub

例で使用したコードなどは以下のレポジトリで確認することができます。 github.com