mcpField
T
tracing-recompositions-at-runtime

tracing-recompositions-at-runtime

github.com/skydoves開発520126 回インストール19 週間前に更新
リポジトリ

tracing-recompositions-at-runtime について

`skydoves/compose-stability-analyzer`のコンパイル時インストルメンテーションで、Jetpack Composeの過剰な再コンポーズを診断するスキルです。Android開発者向けに、logcatでのパラメーターや状態の変化の確認と、リリース相当ビルドでの追跡を案内します。

主な機能

  1. 01コンパイル時の`@TraceRecomposition`計測
  2. 02パラメーターと状態の変化差分
  3. 03logcatによる再コンポーズ追跡
  4. 04オプションのIDEヒートマップ
  5. 05実行時トグルによる出力制御

活用シーン

  1. 01デバッグ版とリリース版の再コンポーズ数の差の調査
  2. 02リリース相当ビルドでの安定性修正の確認
  3. 03再コンポーズを起こすパラメーターや状態の特定
  4. 04SLOに沿ったコンポーザブルの再コンポーズ数追跡

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/skydoves/compose-performance-skills --skill tracing-recompositions-at-runtime

インストール後、Claude Code で/tracing-recompositions-at-runtimeを入力して読み込まれたか確認します。

詳細

実行時の再コンポジションをトレースする — @TraceRecomposition、logcat、ライブヒートマップ

Layout Inspector は再コンポジションをカウントし、引数の変更理由を表示しますが、動作するのはデバッグビルドのみです。Live Literals とインタープリター実行される Compose ランタイムによって、カウントが膨らむためです。skydoves/compose-stability-analyzer の @TraceRecomposition はコンポーザブルをコンパイル時に計測し、再コンポジションごとの差分(どの状態が変わったか、値がどのように遷移したか)を Recomposition タグで logcat に出力します。この計測は、開発者が有効にしたあらゆるビルドで機能します。リリースビルド(デバッグシンボル付き)も含まれます。また、IntelliJ / Android Studio プラグインのライブ再コンポジションヒートマップにもデータを提供します。

このスキルは、../../recomposition/debugging-recompositions/SKILL.md を補完するリリースモード向けの手段です。Layout Inspector はデバッグビルド専用で、すぐに設定でき、最初のトリアージに適しています。@TraceRecomposition は実際の状態を確認するための手段です。疑わしいコンポーザブルを計測し、リリース相当の R8 ビルドの開発用 APK を配布してユーザージャーニーを実行し、再コンポジションごとのログ行を確認します。

このスキルを使用する場面

  • コンポーザブルが想定より頻繁に再コンポーズされ、Layout Inspector のカウントでは結論が出ない場合(デバッグとリリースでカウントが異なる場合や、疑わしい箇所が Layout Inspector でカバーされないインラインコンポーザブルの場合)。
  • マージ前に、安定性や strong skipping の修正をリリース相当のビルドで確認する必要がある場合。
  • Layout Inspector のツリーをクリックして調べるのではなく、状態やパラメーターごとの変更差分をインラインで出力したい場合。
  • チームでコンポーザブルの再コンポジション回数のベースラインを設定したい場合(SLO の例:「PriceTicker は価格更新ごとに 1 回以下で再コンポーズされ、親のティック更新では再コンポーズされない」)。
  • ユーザーが @TraceRecomposition、「trace recomposition」、「compose-stability-analyzer」、「recomposition logcat」、「recomposition heatmap」、「release-mode recomposition」に言及した場合。

このスキルを使用しない場面

  • 開発者がデバッグビルドで再コンポジション回数を知りたいだけの場合 — Layout Inspector のほうがすぐに設定できます。../../recomposition/debugging-recompositions/SKILL.md を参照してください。
  • 診断の目的がない本番リリースの場合 — 計測を無効にする必要があります。以下の実行時切り替えパターンを参照してください。
  • 今後の安定性のリグレッションを検出する CI ゲートが必要な場合 — 実行時トレースは診断用です。ゲートには ../../stability/enforcing-stability-in-ci/SKILL.md の stabilityCheck を使用します。
  • 必要なのがフレーム単位のタイミングや、エンドツーエンドのユーザー体感パフォーマンスであり、再コンポジション回数ではない場合 — MacrobenchmarkRule と FrameTimingMetric を使用する ../generating-baseline-profiles/SKILL.md が該当します。

前提条件

  • 計測するコンポーザブルを所有するモジュールに、Gradle プラグイン com.github.skydoves.compose.stability.analyzer(最新バージョン、v0.7.3 以降)が追加されていること。
  • マニフェストで Application サブクラスを宣言し、onCreate() から ComposeStabilityAnalyzer.setEnabled(...) を呼び出せること。
  • 同じモジュールから Compose Compiler を利用できること — org.jetbrains.kotlin.plugin.compose が適用済みであること(Kotlin 2.0 以降)。
  • 実行時切り替えから読み取れる BuildConfig フィールドまたは機能フラグがあること。BuildConfig.DEBUG でも動作しますが、本番環境相当のプロファイリングビルドには独自の BuildConfig.ENABLE_RECOMPOSITION_TRACE が推奨されます。
  • IntelliJ / Android Studio のヒートマップを使用する場合:JetBrains マーケットプレイスから compose-stability-analyzer プラグインをインストール済みであること(または GitHub リポジトリからローカルでビルド済みであること)。
  • 実行時トレースを使用する前に、デバッグ時点で疑わしいコンポーザブルに名前を付けているよう、../../recomposition/debugging-recompositions/SKILL.md の内容を把握していること。

ワークフロー

1. Gradle プラグインを適用する

モジュールの build.gradle.kts で次のように指定します。

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("org.jetbrains.kotlin.plugin.compose")
    alias(libs.plugins.compose.stability.analyzer)
}

gradle/libs.versions.toml では次のように指定します。

[versions]
composeStabilityAnalyzer = "0.7.3"

[plugins]
compose-stability-analyzer = { id = "com.github.skydoves.compose.stability.analyzer", version.ref = "composeStabilityAnalyzer" }
2. アナライザーを設定する

プラグインブロックと同じ build.gradle.kts に追加します。

composeStabilityAnalyzer {
    enabled.set(true) // compile-time switch; runtime toggle below gates emission
}

enabled.set(true) は、コンパイラーによる計測コードの織り込みを制御します。enabled.set(false) にすると、@TraceRecomposition アノテーションは一切効果を持ちません。すべてのビルドタイプで有効にしたままでも問題ありません。実際の本番環境でのゲートは、ランタイムトグルです。

3. トレースするコンポーザブルにアノテーションを付ける
import com.skydoves.compose.stability.runtime.TraceRecomposition

@TraceRecomposition(traceStates = true)
@Composable
fun PriceTicker(price: Price) {
    Text(price.formatted)
}

traceStates = true にすると、差分の対象がパラメーターだけでなく、コンポーザブル本体内での mutableStateOf の読み取りにも広がります。最初の調査では true にし、原因が分かったらログを簡潔に保つため false に切り替えます。

4. Application.onCreate() にランタイムトグルを追加する
import com.skydoves.compose.stability.runtime.ComposeStabilityAnalyzer

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG)
    }
}

この呼び出しがないと、アノテーションを付けたすべてのコンポーザブルが、リリースビルドでも logcat に出力されます。本番用 APK で出力されないよう、トグルを BuildConfig.DEBUG またはカスタムの BuildConfig.ENABLE_RECOMPOSITION_TRACE で制御します。

5. 症状を再現して logcat を確認する
adb logcat -s Recomposition:D

価格が 99.0 から 99.5 に変わるアニメーション付き PriceTicker の出力例です(例示用です。正確なログ形式はアナライザーのバージョンによって異なります)。

D/Recomposition: [Recomposition #1] PriceTicker
D/Recomposition:   ├─ [param] price: Price changed (Price(99.0) → Price(99.5))
D/Recomposition: [Recomposition #2] PriceTicker
D/Recomposition:   ├─ [param] price: Price unchanged (skipped via strong-skipping equals)

# の後の数字はインスタンスごとのカウンターで、コンポーザブルの再起動スコープの存続期間を通じた累積値です。画面に表示されてから 2 秒しか経っていないのに [Recomposition #50] と出力されるコンポーザブルは、間違いなく問題の原因です。

6. Android Studio でライブヒートマップを開く(任意)

JetBrains Marketplace から Compose Stability Analyzer プラグインをインストールします。プラグインをインストールしてアプリを実行すると、エディターのガターにある各 @TraceRecomposition アノテーション付きコンポーザブルの横に、色分けされたバッジが表示されます。目安となるしきい値(例示用です。現在の範囲はプラグインの設定パネルを参照してください):

  • 緑 — 現在のセッションでの再コンポーズが 10 回未満。
  • 黄 — 再コンポーズが 10~50 回。
  • 赤 — 再コンポーズが 50 回超。

バッジをクリックすると、各 [Recomposition #N] エントリーとその差分を一覧表示するサイドパネルに移動します。このパネルは logcat と同じ内容を表示しますが、コンポーザブルのインスタンスごとにグループ化されるため、logcat をスクロールしなくても、問題を起こしている LazyColumn の行を見つけられます。

7. 上流の修正にたどり着く

ランタイムトレースでは、コンポーザブルの名前と変化するパラメーターが分かります。修正すべき箇所は別にあります。

  • 型が不安定なためにパラメーターが再コンポーズされる場合 → ../../stability/diagnosing-compose-stability/SKILL.md と ../../stability/stabilizing-compose-types/SKILL.md。
  • キャプチャされたラムダまたは Flow が原因でパラメーターが再コンポーズされる場合 → ../../recomposition/using-strong-skipping-correctly/SKILL.md と ../../side-effects/collecting-flows-safely/SKILL.md。
  • 状態が誤ったフェーズ(Composition、Layout、Draw)で読み取られている場合 → ../../recomposition/deferring-state-reads/SKILL.md。
  • Layout Inspector に、対処が必要な Argument Change Reason のステータスが表示された場合 → ../../recomposition/debugging-recompositions/SKILL.md。

修正を反映したら、トレースを再実行します。同じシナリオで、修正後の logcat に最初の [Recomposition #1] が 1 件だけ表示され、それ以降のエントリーがないことを確認してください。

8. CI で将来のリグレッションを防ぐ

@TraceRecomposition は診断用です。次のリグレッションを防ぐのは CI の役割です。../../stability/enforcing-stability-in-ci/SKILL.md に従って CI で stabilityCheck を有効にすると、以前はスキップ可能だったコンポーザブルがスキップ不可になった場合にビルドが失敗します。

パターン

パターン:疑わしい箇所にアノテーションを付け、デバッグ情報付きリリースビルドを実行して差分を確認する
// RIGHT
@TraceRecomposition(traceStates = true)
@Composable
fun PriceTicker(price: Price) {
    Text(price.formatted)
}

logcat の出力例です(例示用です。正確なログ形式はアナライザーのバージョンによって異なります。価格は 1 秒ごとに変化し、親のティックごとに周囲の行が 1 回再コンポーズされます)。

D/Recomposition: [Recomposition #3] PriceTicker
D/Recomposition:   ├─ [param] price: Price changed (Price(99.0) → Price(99.5))
D/Recomposition: [Recomposition #4] PriceTicker
D/Recomposition:   ├─ [param] price: Price unchanged
D/Recomposition:   ├─ [reason] parent restart scope re-invoked; strong-skipping equals matched

2 つ目のエントリーが望ましい状態です。親のティックが発生し、equals() ガードが実行され、本体の実行がスキップされています。

パターン:ビルドフラグでランタイムトグルを制御する
// WRONG
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        // ComposeStabilityAnalyzer.setEnabled(...) is never called.
        // Every annotated composable emits to logcat in every build, including release.
    }
}
// WRONG because: shipping with tracing enabled adds logcat I/O on every recomposition,
// which adds nontrivial overhead on hot composables (LazyColumn rows in particular)
// and pollutes user-installed-app logs on shared devices.
// RIGHT
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG)
    }
}

デバッグシンボル付きリリースのプロファイリング APK では、BuildConfig.DEBUG ではなく、専用のフラグを使用してください。

// RIGHT — explicit profiling flag, decoupled from debug
ComposeStabilityAnalyzer.setEnabled(BuildConfig.ENABLE_RECOMPOSITION_TRACE)
パターン:本番リリースに @TraceRecomposition アノテーションを含めない
// WRONG
// Production release with @TraceRecomposition still annotated on hot composables and
// ComposeStabilityAnalyzer.setEnabled(true) hard-coded in Application.
// WRONG because: logcat I/O on every recomposition adds nontrivial overhead on hot
// composables. The instrumentation also captures parameter values into log strings,
// which can leak PII if a composable receives a user model.
// RIGHT — annotation present, runtime gate keeps it dormant in release
@TraceRecomposition(traceStates = true)
@Composable
fun PriceTicker(price: Price) { Text(price.formatted) }

// Application:
ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG) // dormant in release
パターン:原因が判明したら traceStates を控えめにする
// First investigation — verbose
@TraceRecomposition(traceStates = true)
@Composable
fun Feed(state: FeedState) { /* ... */ }
// Cause identified, fix shipped, keep the annotation as a tripwire — quieter
@TraceRecomposition(traceStates = false)
@Composable
fun Feed(state: FeedState) { /* ... */ }

traceStates = true にすると、本文内でのすべての mutableStateOf の読み取り遷移がログに出力されます。最初のトリアージでは非常に役立ちますが、原因が判明した後はノイズになります。false に切り替えると、状態ごとの差分は出力せず、再コンポジションごとのカウンターは維持されます(引き続き異常を検知するのに役立ちます)。

パターン:ランタイムトレースと CI の安定性ゲートを組み合わせる

@TraceRecomposition は、開発者が今まさに追跡しているリグレッションを見つけます。チームメイトが来週持ち込むリグレッションには何も対処しません。stabilityCheck と組み合わせてください。

# RIGHT — both layers in place
- @TraceRecomposition annotates suspect composables; runtime toggle gated on BuildConfig.DEBUG.
- ./gradlew :app:stabilityCheck runs in CI per ../../stability/enforcing-stability-in-ci/SKILL.md.
- The .stability baseline updates only after a deliberate review.
# WRONG — diagnosis without prevention
- @TraceRecomposition is the only mechanism in place.
- Next PR introduces an unstable type; nothing in CI catches it; the perf regression
  ships and is found again at runtime weeks later.

必須ルール

  • 必須: ComposeStabilityAnalyzer.setEnabled(...) は、ビルド設定フィールド(BuildConfig.DEBUG または専用の BuildConfig.ENABLE_RECOMPOSITION_TRACE)かフィーチャーフラグで制御してください。setEnabled(true) をハードコードしてはいけません。
  • 必須: ランタイムトレースを、../../stability/enforcing-stability-in-ci/SKILL.md に記載されている stabilityCheck CI ゲートと組み合わせてください。ランタイムトレースは診断に使い、CI ゲートは次のリグレッションを防ぎます。どちらか一方だけでは、ワークフローとして不完全です。
  • 禁止: @TraceRecomposition による計測を有効にしたまま、本番リリースを出荷してはいけません。アノテーションをコンポーザブルに残してもかまいませんが、本番ビルドでは ComposeStabilityAnalyzer.setEnabled(...) が false に解決される必要があります。
  • 禁止: コンテキスト(どのシナリオか、どのデバイスか、リリースビルドかデバッグビルドか)なしに、[Recomposition #N] の回数を厳格な SLO として扱ってはいけません。代わりに、固定したシナリオで回数の差分を追跡してください。「修正後の株価ティッカーのシナリオでは、修正前の30件に対して1件だけ出力される」のようにします。
  • 必須: 調査結果を報告するときは、コンポーザブルと変更されたパラメーターの両方に言及してください(「price が Changed と報告されるため、PriceTicker は親のティックごとに再コンポジションされるが、Price データクラスには @Immutable が付いており、equals() は一致するはずです」)。「再コンポジションが多い」だけでは調査結果とは言えません。
  • 推奨: 最初の調査では traceStates = true から始め、ログを詳しく記録してください。原因が判明したら false に設定し、トレースをノイズの少ない異常検知用の仕組みにしてください。
  • 推奨: IntelliJ/Android Studio プラグインをインストールし、ガターのヒートマップを表示してください(参考となる範囲:緑は <10、黄色は 10–50、赤は 50+。現在のプラグイン設定で確認してください)。logcat を grep しなくても、問題のあるコンポーザブルを見つけられます。
  • 推奨: @TraceRecomposition アノテーションは、対象を絞って意図的に選んだコンポーザブル(画面内で頻繁に再コンポジションされるコンポーザブル、LazyColumn の行コンポーザブルなど)に付けてください。すべてのコンポーザブルに付けると、ヒートマップの信号対雑音比が損なわれます。

検証

  • 計測対象のコンポーザブルを所有するモジュールに com.github.skydoves.compose.stability.analyzer プラグインが適用されている。
  • composeStabilityAnalyzer { enabled.set(true) } が設定されている。
  • Application.onCreate() が ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG)(または別のビルドフラグによるゲート)を呼び出す。この呼び出しは true にハードコードされていない。
  • 少なくとも1つのコンポーザブルに @TraceRecomposition(traceStates = true) が付いている。
  • シナリオを再現しているときに adb logcat -s Recomposition:D を実行すると、[Recomposition #N] <ComposableName> の行が出力される。
  • 出力される各行にパラメーターまたは状態が記載され、changed (oldValue → newValue) または unchanged と報告される。
  • リリースビルド(またはゲートをオフにした状態)では、adb logcat -s Recomposition:D を実行しても何も出力されず、計測が無効になっている。
  • ../../stability/enforcing-stability-in-ci/SKILL.md に従って CI の安定性ゲート(./gradlew :app:stabilityCheck)が設定されており、次のリグレッションがリリース前に検出される。

参考資料

Layout Inspector と Argument Change Reasons を使ったデバッグ時のエントリーポイントについては、../../recomposition/debugging-recompositions/SKILL.md を参照してください。次の安定性のリグレッションを防ぐ CI ゲートについては、../../stability/enforcing-stability-in-ci/SKILL.md を参照してください。問題のあるパラメーターが特定された後に原因へ対処する方法については、../../stability/diagnosing-compose-stability/SKILL.md、../../stability/stabilizing-compose-types/SKILL.md、../../recomposition/using-strong-skipping-correctly/SKILL.md、および ../../recomposition/deferring-state-reads/SKILL.md を参照してください。エンドツーエンドのユーザー体感パフォーマンス測定(フレームタイミング、コールドスタートアップ)については、../generating-baseline-profiles/SKILL.md を参照してください。

コメント