JankStats ライブラリ

JankStats ライブラリは、アプリのパフォーマンスの問題を追跡、分析するのに役立ちます。ジャンクとは、アプリフレームのレンダリングに時間がかかりすぎる状況を指します。JanStats ライブラリには、アプリのジャンク統計情報に関するレポートが用意されています。

機能

JankStats は、Android 7(API レベル 24)以降では FrameMetrics API、それ以前のバージョンでは OnPreDrawListener など、既存の Android プラットフォーム機能を基礎として構築されています。これらのメカニズムにより、アプリはフレームが完了するまでにかかる時間を追跡できます。JanksStats ライブラリには、ジャンク ヒューリスティックと UI の状態という、より動的で使いやすい 2 つの機能が追加されています。

ジャンク ヒューリスティック

FrameMetrics を使用してフレーム持続時間を追跡することはできますが、FrameMetrics では実際にジャンクが発生しているか判別することはできません。JankStats には、ジャンクが発生したタイミングを判断するための、設定変更もできる内部メカニズムがあり、より有用なレポートを迅速に作成できます。

UI の状態

アプリでパフォーマンスの問題が発生する場合、通常はその状況を把握する必要が生じます。たとえば、FrameMetrics を使用する複雑なマルチスクリーン アプリを開発していて、そのアプリのフレームに大きなジャンクが頻繁に発生することが判明した場合、問題の発生個所、ユーザーが行っていた操作、それらの再現方法を把握することで、状況を確認します。

JankStats では、この問題を解決するために state API を導入しました。この API を使うと、ライブラリと通信してアプリのアクティビティに関する情報を入手できます。JankStats がジャンクのあるフレームに関する情報をログに記録すると、ジャンク レポートにアプリの現在の状態が追加されます。

使用方法

JankStats の使用を開始するには、各 Window のライブラリをインスタンス化して有効にします。各 JankStats オブジェクトは Window 内のデータのみを追跡します。ライブラリをインスタンス化するには、Window インスタンスと OnFrameListener リスナーが必要です。これらはいずれもクライアントに指標を送信するために使用されます。リスナーはフレームごとに FrameData で呼び出され、以下の詳細情報を示します。

  • フレームの開始時間
  • 持続時間
  • フレームをジャンクと見なすかどうか
  • フレーム中のアプリの状態に関する情報を含む文字列ペアのセット

JankStats をさらに有用なものとするには、FrameData でのレポート用に、関連する UI の状態情報をアプリからライブラリに入力する必要があります。この処理は、すべての状態管理ロジックと API が有効な PerformanceMetricsState API を介して(JankStats を直接使用せずに)実行できます。

初期化

JankStats ライブラリの使用を開始するには、まず Gradle ファイルに JankStats の依存関係を追加します。

implementation "androidx.metrics:metrics-performance:1.0.0-beta01"

次に、Window ごとに JankStats を初期化して有効にします。アクティビティがバックグラウンドに移行したら、JankStats の追跡を一時停止する必要があります。アクティビティのオーバーライドで JankStats オブジェクトを作成して有効にします。

class JankLoggingActivity : AppCompatActivity() {

    private lateinit var jankStats: JankStats


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ...
        // metrics state holder can be retrieved regardless of JankStats initialization
        val metricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(binding.root)

        // initialize JankStats for current window
        jankStats = JankStats.createAndTrack(window, jankFrameListener)

        // add activity name as state
        metricsStateHolder.state?.putState("Activity", javaClass.simpleName)
        // ...
    }

上記の例では、JankStats オブジェクトを作成した後に、現在のアクティビティに関する状態情報を注入しています。この JankStats オブジェクト用に今後作成されるすべての FrameData レポートにも、アクティビティの情報が含まれるようになります。

JankStats.createAndTrack メソッドは Window オブジェクトへの参照を取得します。これは、Window 内のビュー階層のプロキシであり、Window 自体のプロキシでもあります。jankFrameListener は、内部でプラットフォームから JankStats にその情報を送るときと同じスレッドで呼び出されます。

JankStats オブジェクトで追跡とレポートを有効にするには、isTrackingEnabled = true を呼び出します。追跡はデフォルトで有効になっていますが、アクティビティを一時停止すると無効になります。その場合は、必ず追跡を再度有効にしてから続行してください。追跡を停止するには、isTrackingEnabled = false を呼び出します。

override fun onResume() {
    super.onResume()
    jankStats.isTrackingEnabled = true
}

override fun onPause() {
    super.onPause()
    jankStats.isTrackingEnabled = false
}

レポート

JankStats ライブラリは、有効な JankStats オブジェクトの OnFrameListener に、各フレームのすべての追跡データをレポートします。アプリは、後でアップロードするためにこのデータを保存、集約できます。詳しくは、集約のセクションに記載されている例をご覧ください。

フレームごとのレポートを受け取るには、アプリに OnFrameListener を作成して渡す必要があります。このリスナーはフレームごとに呼び出され、進行中のジャンクデータをアプリに渡します。

private val jankFrameListener = JankStats.OnFrameListener { frameData ->
    // A real app could do something more interesting, like writing the info to local storage and later on report it.
    Log.v("JankStatsSample", frameData.toString())
}

リスナーは FrameData オブジェクトを使用して、ジャンクに関するフレームごとの情報を提供します。これには、リクエストされたフレームに関する次の情報が含まれます。

  • isjank: フレームでジャンクが発生したかどうかを示すブール値のフラグ。
  • frameDurationUiNanos: フレームの長さ(ナノ秒単位)。
  • frameStartNanos: フレームの開始時刻(ナノ秒単位)。
  • states: フレーム中のアプリの状態。

Android 12(API レベル 31)以降を使用している場合は、次のようにしてフレームの長さに関するデータをさらに公開できます。

リスナーで StateInfo を使用して、アプリの状態に関する情報を保存します。

OnFrameListener は、内部でフレームごとの情報を JankStats に送るときと同じスレッドで呼び出されます。Android バージョン 6(API レベル 23)以前では、メイン(UI)スレッドです。Android バージョン 7(API レベル 24)以降では、FrameMetrics で作成、使用されるスレッドです。いずれの場合も、そのスレッドでパフォーマンス問題が生じないよう、コールバックを処理してすぐに戻すことが重要です。

また、データレポート用の新しいオブジェクトを割り当てずに済むよう、コールバックで送信された FrameData オブジェクトはフレームごとに再利用されます。つまり、コールバックが戻るとすぐにオブジェクトは無効かつ古くなったと見なされるため、そのデータをコピーして他の場所にキャッシュする必要があります。

集約

アプリコードでフレームごとのデータを集約する場合は、自身の裁量で情報の保存とアップロードが行えます。保存とアップロードに関する詳細情報は、JankStats API アルファ版リリースでは提供されませんが、JankAggregatorActivityGitHub リポジトリで利用可能)を使用して、フレームごとのデータをより大きなコレクションに集約するための予備的なアクティビティを表示することが可能です。

JankAggregatorActivityJankStatsAggregator クラスを使用して、JankStats OnFrameListener メカニズムの上に独自の報告メカニズムを階層化し、より高いレベルの抽象化を行って、多数のフレームにわたって収集される情報のみをレポートします。

JankAggregatorActivity は JankStats オブジェクトを直接作成する代わりに JankStatsAggregator オブジェクトを作成します。これにより、独自の JankStats オブジェクトが内部で作成されます。

class JankAggregatorActivity : AppCompatActivity() {

    private lateinit var jankStatsAggregator: JankStatsAggregator


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ...
        // Metrics state holder can be retrieved regardless of JankStats initialization.
        val metricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(binding.root)

        // Initialize JankStats with an aggregator for the current window.
        jankStatsAggregator = JankStatsAggregator(window, jankReportListener)

        // Add the Activity name as state.
        metricsStateHolder.state?.putState("Activity", javaClass.simpleName)
    }

JankAggregatorActivity では、同様のメカニズムが追跡の一時停止と再開に使用されます。その際、pause() イベントをシグナルとして追加し、issueJankReport() を呼び出すことでレポートを発行します。これは、アプリでジャンクの状態をキャプチャするうえで、ライフサイクルの変更が適切なタイミングであると想定されるためです。

override fun onResume() {
    super.onResume()
    jankStatsAggregator.jankStats.isTrackingEnabled = true
}

override fun onPause() {
    super.onPause()
    // Before disabling tracking, issue the report with (optionally) specified reason.
    jankStatsAggregator.issueJankReport("Activity paused")
    jankStatsAggregator.jankStats.isTrackingEnabled = false
}

上記のサンプルコードは、アプリが JankStats を有効にしてフレームデータを受け取るために必要なものをすべて示しています。

状態の管理

他の API を呼び出して JankStats をカスタマイズすることもできます。たとえば、アプリの状態情報を注入し、ジャンクが発生するそれらのフレームにコンテキストを提供することでフレームデータの利便性を高めることが可能です。

この静的メソッドで、特定のビュー階層の現在の MetricsStateHolder オブジェクトを取得します。

PerformanceMetricsState.getHolderForHierarchy(view: View): MetricsStateHolder

有効な階層内の任意のビューを使用できます。内部的には、ビュー階層に関連付けられた既存の Holder オブジェクトの有無を確認します。この情報は、階層の最上位のビューにキャッシュされます。該当するオブジェクトが存在しない場合は、getHolderForHierarchy() によって作成されます。

静的 getHolderForHierarchy() メソッドを使用すると、後で取得するためにホルダー インスタンスをどこかにキャッシュする必要がなくなります。また、コード内のどこからでも(元のインスタンスにアクセスできないライブラリ コードからでも)既存の状態オブジェクトを簡単に取得できるようになります。

戻り値は、状態オブジェクトそのものではなく、ホルダー オブジェクトであることに注意してください。ホルダー内の状態オブジェクトの値は JankStats によってのみ設定されます。つまり、アプリが、そのビュー階層を含むウィンドウの JankStats オブジェクトを作成すると、状態オブジェクトが作成、設定されます。それ以外の場合、JankStats が情報を追跡しなければ、状態オブジェクトは不要です。また、アプリやライブラリのコードで状態を注入する必要もありません。

このアプローチにより、JankStats が後で入力できるホルダーを取得できます。外部コードはいつでもホルダーに照会できます。呼び出し元はこの軽量の Holder オブジェクトをキャッシュし、内部状態プロパティの値に応じて、キャッシュを使っていつでも状態を設定できます。たとえば、以下のサンプルコードでは、ホルダーの内部の state プロパティが null 以外の場合にのみ状態が設定されます。

val metricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(binding.root)
// ...
metricsStateHolder.state?.putState("Activity", javaClass.simpleName)

UI やアプリの状態を管理するために、putState メソッドと removeState メソッドをアプリで使用して状態を注入(または削除)できます。JankStats は、これらの呼び出しのタイムスタンプをログに記録します。フレームが状態の開始時間と終了時間に重なる場合、JankStats はその状態情報をフレームのタイミング データとともにレポートします。

どの状態に対しても、key(「RecyclerView」などの状態のカテゴリ)と value(「スクロール」などのその時点の状況に関する情報)の 2 つの情報を追加します。

無効となった状態を removeState() メソッドで削除し、誤った情報や誤解を招く情報がフレームデータでレポートされないようにします。

以前に追加された keyputState() を呼び出すと、その状態の既存の value が新しい値に置き換えられます。

putSingleFrameState() バージョンの状態 API では、次にレポートされるフレームで、一度だけログに記録される状態を追加します。その後、その状態はシステムで自動的に削除されるので、コードに古い状態が誤って含まれることはありません。JankStats は単一フレームの状態を自動的に削除するため、removeState() に相当する singleFrame は存在しないことに注意してください。

private val scrollListener = object : RecyclerView.OnScrollListener() {
    override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
        // check if JankStats is initialized and skip adding state if not
        val metricsState = metricsStateHolder?.state ?: return

        when (newState) {
            RecyclerView.SCROLL_STATE_DRAGGING -> {
                metricsState.putState("RecyclerView", "Dragging")
            }
            RecyclerView.SCROLL_STATE_SETTLING -> {
                metricsState.putState("RecyclerView", "Settling")
            }
            else -> {
                metricsState.removeState("RecyclerView")
            }
        }
    }
}

状態に使用するキーは、後で分析できるよう十分に有意なものである必要があります。特に、状態の key が以前に追加されたものと同じであると、その以前の値は置き換えられるため、アプリやライブラリで異なるインスタンスを持つ可能性があるオブジェクトには一意の key 名を使用するようにしてください。たとえば、5 つの異なる RecyclerView を持つアプリでは、RecyclerView ごとに RecyclerView を使用するのではなく、各 RecyclerView に識別可能なキーを提供することで、フレームデータがどのインスタンスを参照しているかを結果データから簡単に見分けられません。

ジャンク ヒューリスティック

ジャンクとみなされるものを判別するための内部アルゴリズムを調整するには、jankHeuristicMultiplier プロパティを使用します。

デフォルトでは、現在のリフレッシュ レートの 2 倍の時間がかかるフレームがジャンクとして定義されています。アプリのレンダリング時間に関する情報が完全に明確ではないため、リフレッシュ レートを超えるものはジャンクとして処理されません。したがって、バッファを追加して、重大なパフォーマンスの問題を引き起こす場合にのみ問題を報告するようにすることをおすすめします。

これらの値はいずれも、それらのメソッドを介して変更できます。アプリの状況により近づけたり、テストの必要に応じてジャンクの有無を切り替えたりすることが可能です。

Jetpack Compose での使用方法

現在、Compose で JankStats を使用するの必要なセットアップはほとんどありません。構成変更後も PerformanceMetricsState を保持するには、次の状態を覚えておいてください。

/**
 * Retrieve MetricsStateHolder from compose and remember until the current view changes.
 */
@Composable
fun rememberMetricsStateHolder(): PerformanceMetricsState.Holder {
    val view = LocalView.current
    return remember(view) { PerformanceMetricsState.getHolderForHierarchy(view) }
}

JankStats を使用するには、現在の状態を次のように stateHolder に追加します。

val metricsStateHolder = rememberMetricsStateHolder()

// Reporting scrolling state from compose should be done from side effect to prevent recomposition.
LaunchedEffect(metricsStateHolder, listState) {
    snapshotFlow { listState.isScrollInProgress }.collect { isScrolling ->
        if (isScrolling) {
            metricsStateHolder.state?.putState("LazyList", "Scrolling")
        } else {
            metricsStateHolder.state?.removeState("LazyList")
        }
    }
}

Jetpack Compose アプリで JankStats を使用する方法について詳しくは、パフォーマンス サンプルアプリをご覧ください。

フィードバックを送信

以下のリソースを通じてフィードバックやアイデアをお寄せください。

Issue Tracker
Google がバグを修正できるよう問題を報告します。