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

Scratchファイルを使って気軽に実行可能なコードを保存する

概要

この記事ではIntellijでサポートされたKotlinのScratchファイルについて紹介を行います
Scratchファイル自体はKotlinに限らずJavaなどもサポートされています

Scratchファイルとは

Kotlinなどの実行可能なコードを保存するファイルで、デフォルトではプロジェクト外に保存されます。 プロジェクト内にあるファイルと違う点は、あらかじめプロジェクトをビルドしておけば、Scratchファイルのビルドだけで実行可能なため、実行までの時間が速いことです。

IntelliJIDE機能の一つです

実行環境

  • IntelliJ IDEA 2018.3.1 (Community Edition)
  • Kotlin Plugin 1.3.10-release-IJ2018.3-1

Hellow World!

  1. プロジェクトにファイルを追加する時と同じように New -> Scratch File を選択 f:id:scache:20181214142223p:plain:w300

  2. Scratchファイルの形式が選べるので Kotlin を選択
    f:id:scache:20181214142253p:plain:w300

  3. ファイルが作成されるので Use classpath of Module からモジュールを選択してコードを記述します
    f:id:scache:20181214143310p:plain:w600

実行してみる

左上にある実行ボタンを押すとコードが実行され、エディタ上に結果が表示されます
※実行前にはモジュールのBuildをするか Make Before Run にチェックする必要があります
各行ごとに出力が表示されるのがとても便利です
f:id:scache:20181214143243p:plain:w600

選択したモジュール内のクラスを使う

Scratchファイルでは、モジュールの中で定義されているクラスを使用することが可能です
例えば以下のようなクラスが定義されている場合、Scratchファイルでも使用することが可能です

package com.github.sckm

data class Person(
    val name: String,
    val age: Int
)

f:id:scache:20181214163219p:plain:w600

ライブラリを使用する

ライブラリを使用する際は、Scratchファイル用に依存を追加することはできず、
使用するモジュールの build.gradle などで依存を追加する必要があります

使用例

自分がよく使うパターンとしては、

  • プロジェクトでたまに使うAPIのレスポンスを確認したい時
    • IDE上でできるので1番よく使用しています
  • ちょっとした処理の動作を確認したい時
    • テストコードとして実行すると時間がかかるプロジェクトで時間を節約可能

Scratchファイルの保存場所

~/Library/Preferences/IdeaIC2018.3/scratches/

プロジェクトとは関係無い場所に保存がされるため間違えてGitリポジトリに含めてしまうという心配もありません

もちろん、IDE上からでもファイルを開くこともできます f:id:scache:20181214153353p:plain:w400

まとめ

  • コード実行時に新しくプロジェクトを作ったり、Git配下にファイルを追加する必要が無い
  • モジュール内のクラスを使用可能
  • テストコードと似た感覚でカジュアルに実行可能

参考

Scratches - Help | IntelliJ IDEA
Tuning IntelliJ IDEA - Help | IntelliJ IDEA

ブログに実行可能なKotlinコードを貼り付ける

ブログなどのサイト上で実行可能なKotlinコードを表示するためのスクリプト(Kotlin Playground)がJetBrainsから公開され、 Try Kotlin や Kotlinのリファレンスにあるような機能を自分のサイトでも使うことが可能になりました。

f:id:scache:20180428035614p:plain:w300

all - Kotlin Programming Language

使い方

scriptの記述

codeタグ内にコードを記述する場合は以下のコードをヘッダーなどに記述します。

<script src="https://unpkg.com/kotlin-playground@1" data-selector="code"></script>

上の記述ではcodeタグすべてにKotlinPlaygroundが適用されてしまいます。
はてなブログMarkdownで使う場合には以下のようにKotlinコードのみで適用されるようにするのが良いでしょう。
以後の説明ではこちらを使っていると仮定して進めていきます。

<script src="https://unpkg.com/kotlin-playground@1"></script>
<script>
  document.addEventListener('DOMContentLoaded', function() {
    KotlinPlayground('.lang-kotlin');
  });
</script>

以上で準備は終了になります。

コードの記述

はてなブログMarkdownの場合は、以下のようにコードを記述します f:id:scache:20180428035057p:plain:w300

実際の表示

以下にKotlin Playgroundを使ったコードを表示しています。

fun main(args : Array<String>) {
    println("Hello Kotliner!")
    println("Click this green button at the top right!")
}

最後に

  • はてなブログなど、自分で自由にいじるのが難しい環境では少し扱いずらい部分がある
  • KotlinPlaygroundが少し重い?

さらに詳しい情報は以下を参照してください

Embedding Kotlin Playground | Kotlin Blog

github.com

SequenceとList(Array)

TL;DR

Sequence1つの値に対する操作が全て行われた後、必要なら次のが処理される。
Listすべての値に対して1つの操作が行われた後、必要なら次の操作が行われる

Kotlinでのデータの集合

Kotlinにはデータの集合を操作するためのクラスとして、List, Array, Sequenceがあります。
ListやArrayは他の多くに言語にもあるため想像がつくかと思います。では、Sequenceとは何なのでしょうか?
Sequenceを理解するためにList, Arrayとの違いをみていきましょう。

List vs Array

ListArrayはそれぞれの要素に対して操作を行うという観点ではほとんど同じと言えます。大きな違いとしてはArrayJavaの配列型と同等の扱いになります。Javaとの互換性を保つためにArrayがあると推測できます。

以降の文ではArrayは無視してSequenceListについて書いていきます。

Sequence vs List

実際の処理を見ると違いがわかりやすいので、
(1, 2, 3, 4)の値を持ったSequenceとListに対して以下の処理を行う場合の動作をみていきます

  1. それぞれの要素を二乗する : map{v -> v * v}
  2. それぞれの要素に1を足す : map{v -> v + 1}
  3. 値が5の要素を除く : filter{v != 5}
  4. 残った要素から最初に2つを取り出す: take(2)
  5. 1〜4の結果を出力: foreach{ v -> println(v)}

処理過程

操作の過程を以下の表に示します。

f:id:scache:20180419000703p:plain:w300f:id:scache:20180419000707p:plain:w300

Sequenceの4の列がすべて空白になっています(書き忘れじゃないですよ)。
今回の場合、Sequenceでは値4に対してまったく操作が行われません。ここがListとの大きな違いとなります。

Sequenceでは遅延評価が行われ、値1に対する操作(map -> map -> filter -> take)がすべて終わった後に値2,3の操作が行われます。
13のtake(2)が終わった段階でtake(2)の結果が決定するため値4の処理は行われません。

一方、Listの場合は値14に対してmap(v*v)の操作を行った結果のListを作成します。作成されたListに対してさらにmap(v+1)の操作, さらにfilter(), take()と続きます。

f:id:scache:20180419000652p:plain:w300f:id:scache:20180419000656p:plain:w300

まとめ

Sequence1つの値に対する操作が全て行われた後、必要なら次のが処理される。
Listすべての値に対して1つの操作が行われた後、必要なら次の操作が行われる

※ Sequenceは実際に値が必要になるまで処理が行われない(遅延される)などの違いもあります。

参考

kotlin.sequences - Kotlin Programming Language

文字列中の変数の値を表示するプラグインを作った

プラグインの概要

Kotlinの文字列中の変数が定数値の場合に、その値を文字列中に表示するプラグインです。
↓の例では$tagの表示がtagの値であるtag nameになります。

f:id:scache:20180404100244g:plain  

プラグイン説明

インストール方法

以下の画像のように、IntellijやAndroidStudioの設定から
PluginsBrowse Repositories... をクリックし、ConstStringPlaceHolderで検索することでインストールができます。

f:id:scache:20180404230859p:plain:w300
f:id:scache:20180404230836p:plain:w300

使い方

定数値を表示するには、文字列中の変数にカーソルを移動した後にCollapse(右クリック→FoldingCollapse)をします。

f:id:scache:20180404235225g:plain
f:id:scache:20180404234946g:plain

複数の値を一度に変換する場合は、範囲を選択後にCollapse All(右クリック→FoldingCollapse All)をします。

f:id:scache:20180405001433g:plain

課題

v0.0.1時点では、

  • Javaコードでは使用できない
  • ファイルを開いた時点ではすべてExpandされた状態になる

の課題があるので今後は上記を改善していきたいです。

data classで配列を使う時はequalsをオーバーライドしよう [Kotlin]

data classとは

data classは以下のようにクラスにdataをつけて宣言することで、
コンストラクタで宣言されたプロパティを使ったequals/hashCode/toString関数などを自動生成してくれます。

data class Book(
    val title: String,
    val page: Int
)

val book1 = Book("book", 100)
val book2 = Book("book", 100)
book1.toString()  // => Book(title=book, page=100)
book1 == book2    // => true

toString()で返って来る文字列にプロパティの値が入っていたり、
==(equalsが呼ばれる)を使った比較ができたりします。

data class内で配列を宣言した場合

↓のようにプロパティに配列型を宣言した場合にも、同様にequals()などの関数が自動で生成されます。

data class Book(
    val title: String,
    val author: Array<String>
)
    
val book1 = Book("book", arrayOf("Alice","Bob"))
val book2 = Book("book", arrayOf("Alice","Bob"))
book1.toString() // Book(title=book, author=[Alice, Bob])
book1 == book2   // false

しかし、先ほどとは違いbook1 == book2の結果がfalseになってしまいます。

なぜ配列を使うと比較がうまくできないのか?

配列を使ったdata classをJavaデコンパイルすると、
Array<String>String[]に、data classのequals内の配列の比較はarray1.equals(array2)と同等のコードになります。

Javaの配列型のequals()メソッドは、要素の比較は行わず配列型のインスタンスが同じかどうかを判定するため、配列内の値が等しいだけではtrueになりません。

対処法

equals()メソッドをオーバーライドしてArrays#equlas()を使うことでこの問題を解決することが可能です。

IntellijやAndroidStudioでKotlinPluginを入れている場合(最近のバージョンでは初めから入っています)は、Inspectionが表示され以下のようなequals()/hashCode()を自動生成することが可能になっています。 f:id:scache:20180321002505p:plain

override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (javaClass != other?.javaClass) return false
    other as Book
    if (title != other.title) return false
    if (!Arrays.equals(author, other.author)) return false
    return true
}
override fun hashCode(): Int {
    var result = title.hashCode()
    result = 31 * result + Arrays.hashCode(author)
    return result
}

最後に

data classで配列の比較も良い感じに生成してくれるとありがたいですが、影響がでかいためv2が来るまで変更されることはないと個人的には思います。

Gradle Kotlin DSLを書くための参考になるサイトやページのメモ

Gradle Kotlin DSL

メリット

  • 1回ビルドできる状態にもっていけばあとはコードの補完が効く
  • Groovyを知らなくてもKotlinで書くことができる

デメリット

  • gradle(Groovy版)からKotlinDSLに直すのがつらい
  • あまり使っている人を見ないので思わぬバグを踏む可能性が高い

公式サイト

github.com

Android関係