【Google I/O 2023】Debugging Jetpack Compose
Debugging Jetpack Compose
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 に反映される。