Open1

【Google I/O 2023】Debugging Jetpack Compose

watabeewatabee

Debugging Jetpack Compose

https://www.youtube.com/watch?v=Kp-aiSU8qCU&list=PLOU2XLYxmsIIwZQkAPhJZg8jaNrrHk1DH&index=38

Debugging Mindset (0:26~)

デバッグのプロセス。

Define -> Reproduce -> Validate Assumptions -> Fix

  • 何が問題か?
    • まず最初に解決しようとしている問題が何であるかを定義
  • 問題の再現
    • 再現可能な例を作る
  • 仮説の検証
    • 問題を再現して、ツールを使って仮説を検証する
  • 修正する

Commom Compose Issues (1:38~)

Composable が recompose されていて、どのパラメータが変更されて recompose が発生したかわからない場合

仮説 : recomposition の種類

  • Direct recomposition
  • Indirect recomposition
  • Unskippable recomposition

以下は Direct recomposition の例。

@Composable
fun DirectRecomposition() {
    var count by remember { mutableStateOf(1) }
    Text("$count")
    Button(onClick = { count++ }) {
        Text("Increment")
    }
}

以下は Indirect recomposition の例。

@Composable
fun IndirectRecomposition() {
    var count by remember { mutableStateOf(0) }
    val doubled = count * 2
    
    MyText(doubled)
    
    Button(onClick = { count++ }) {
        Text("Increment")
    }
}

@Composable
fun MyText(count: Int) {
    Text("$count")
}

以下は Indirect recomposition の例。

@Composable
fun IndirectRecomposition() {
    var count by remember { mutableStateOf(0) }
    val doubled = count * 2
    
    MyText(doubled)
    
    Button(onClick = { count++ }) {
        Text("Increment")
    }
}

@Composable
fun MyText(count: Int) {
    Text("$count")
}

3番目の例。
この例の場合、count が変更されると MyList は recompose される。

@Composable
fun UnstableRecomposition() {
    var count by remember { mutableStateOf(0) }
    Text("$count")
    
    var list by remember { mutableStateOf(listOf(1, 2, 3)) }
    MyList(list)
    
    Button(onClick = { count++ }) {
        Text("Increment")
    }
}

@Composable
fun MyList(list: List<Int>) { ... }

Android Studio Hedgehog ではデバッガに recomposition state という機能が追加された。
ブレークポイントがヒットすると、Composable の各引数ごとに変更状態を確認できる。
この状態は以下のものがある。

  • Unchanged : この引数は変更されていない
  • Changed : この引数は異なる値に変更されている
  • Uncertain : Compose はこの引数が変更されたかどうかを評価している
  • Static : Compose はこの引数を変更がないものとみなしている
  • Unstable : 引数は unstable type

4:53~ デモが確認できる。

頻繁に Recomposition が発生する

スクロールやアニメーションの最中に Composable が recompose される。

@Composable
fun ScrollingList() {
    val listState = rememberLazyListState()
    LazyRow { ... }
    
    Log.d(TAG, "List recompose ${listState.firstVisibleItemIndex}")
    // 値を直接参照せず、ラムダで返すようにする
    // MyComposable(offset = listState.firstVisibleItemIndex)
    MyComposable(offset = { listState.firstVisibleItemIndex })
}

@Composable fun MyComposable(offset: () -> Int) { ... }

goo.gle/compose-performance を参照。

ただし上記の場合、ログで値が直接参照されているのでスクロールした時に引き続き recompose が発生する。

この場合以下のように SideEffect でラップする。

SideEffect {
    Log.d(TAG, "List recompose ${listState.firstVisibleItemIndex}")
}

異なる端末で UI の問題点を知るには?

Android Studio Hedgehog では、Compose プレビューの Visual Lint がサポートされた。

レイアウトを複数のスクリーンサイズで確認することができ、何か問題がある場合は Lint が警告かエラーを教えてくれる。

Jank が発生するのはなぜか? (8:35~)

R8 を有効でかつ baseline profile が設定されたリリースモードで実行して確認する。

Layout Inspector で Recompose が発生した回数が見れるので、回数が多いものがないか確認する。

デバッガで recomposition state を確認する。もし Unstable のものがあった場合、Composable は頻繁に recomposition されている可能性がある。
=> このような問題を解決するにあたって、まず最初にテストを書いてパフォーマンスを計測する。(Macrobenchmark のライブラリが使える)
=> Unstable のパラメータが List だった場合、PersistentList に変更してみる。これでリストは Immutable であるとみなされる。
=> 変更後に再度パフォーマンステストを実行して効果を測定する。

間違った仮説

Pager のアイテムは recomposition をスキップしているという間違った仮説。

  • 修正を goo.gle/jetcaster-pager で確認できる
  • ブログ : goo.gle/compose-stability-explained
  • Baseline profiles : goo.gle/baseline-profiles

Summary

  • Layout Inspector
  • Macrobenchmark
    • テストを書いて、修正がパフォーマンス改善を保証することを確認する
  • Debugger
    • どのパラメータが変わって recomposition を発生させているかを確認できる

どこに最適化の焦点を当てるべきか? (14:00~)

パフォーマンスモニタリングでフリーズしているフレームがあるようだが、何を修正してよいかがわからない場合。

Tracing を使う。
Tracing には2つのタイプがある。

  • System trace
    • オーバーヘッドが少ない
    • 時間計測に向いている
    • マークしたイベントのみをトレースできる
    • 15:27~ デモが見れる
    • goo.gle/compose-tracing
  • Method trace
    • オーバーヘッドがある
    • 全てのメソッドコールが対象
    • system trace の後に使うのがよい
    • 15:45~ デモが見れる

Android Studio では現在 system trace で Composable 関数を見れるようになった。
有効にするためには Gradle ファイルに依存関係を設定する。

implementation("androidx.compose.runtime:runtime-tracing:<version>")

以下は recomposition の system trace の簡単なバージョンの例。

各バーはその下にあるバーの合計時間を表している。
また、バーは関数呼び出しのスタックトレースに対応している。
MyImage と MyButton の間が空いているが、system trace はマークされたもののみを表示しているので、これは system trace が追跡できなかったコードが実行されていることを意味する。

17:32 からデモ。

確認したいコードを trace("...") { ... } で囲むことによって、system trace に反映される。