この記事は Chet Haase による Android Developers - Medium の記事 " JankStats Goes Alpha " を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。
イラスト : Virginia Poltrack
ジャンク(名詞): アプリケーションのパフォーマンスが悪いこと。フレーム落ちが発生したり、UI の動作が断続的になったりして、悪いユーザー エクスペリエンスにつながる。「不幸なユーザー」も参照。
パフォーマンスの問題をデバッグするのは、難しいことです。多くの場合、どこから始めればよいか、どのツールを使うか、ユーザーにどんな問題が起きているのか、その問題が実際のデバイスにどう出現しているのかは、明確ではありません。
Android チームは、ここ数年をかけて、問題のさまざまな部分をデバッグするツールを増やしてきました。たとえば、起動パフォーマンス (英語) の分析、特定のコードパスのテスト、特定のユースケースのテストや最適化、IDE のビジュアル プロファイラなどです。これらはすべて開発時のテスト用で、ローカルで確認できる問題をデバッグして修正する際に役立ちます。
一方、Google Play の Android Vitals、そして Firebase は、どちらもダッシュボードを提供しており、そこで実際のユーザーのデバイスでアプリがどのように動作しているかを確認できます。
しかしそれでも、実環境のアプリで発生する問題の見つけ方を知るのは難しいことです。とりわけ、ユーザーのデバイスで発生し、自分の席で快適に利用できる便利な開発マシンだけでは見ることができない問題はそう言えます。パフォーマンス ダッシュボードは役立ちますが、ユーザーに問題が発生しているときに、そこで何が起こっているかがわかるほどの詳しい情報が提供されるとは限りません。
そこで登場するのが、JankStats です。これは、ユーザーのデバイスで発生しているアプリのパフォーマンスの問題を計測して報告することに特化した AndroidX 初めてのライブラリです。
JankStats はかなり小さな API で、根本的な目的が 3 つあります。それは、フレームごとのパフォーマンス情報をキャプチャすること、アプリを(開発環境だけでなく)ユーザーのデバイスで実行すること、そしてパフォーマンスの問題が起きたときにアプリで発生していることを計測したり報告したりできるようにすることです。
Android プラットフォームでは、フレームのパフォーマンス データを取得する方法がすでに提供されています。たとえば、API 24 以降では FrameMetrics を使えます。また、最近のリリースでは、さらに詳しい情報を提供できる機能が追加されています。それ以前のリリースで実行している場合も、精度は落ちますが、さまざまな手法で有用なタイミング情報を取得できます。
そのため、すべてのリリースで動作する独自のフレーム時間ロジックが必要なら、さまざまな API バージョンでさまざまなメカニズムを実装しなければなりません。しかし、そうする代わりに 1 つの JankStats API を使うだけで、それが皆さんの代わりにすべてをしてくれます。さらに、その他の機能も提供されます。
JankStats では、1 つの API でフレームごとの時間を報告できるので、簡単に計測できます。内部的には、適切なメカニズム(API 24 以降の FrameMetrics (英語) など)に委譲しています。データがどこから来るかを気にする必要はなく、単に JankStats にどのくらい時間がかかったかを聞けばいいだけです。すると、コールバックでその情報が返されます。
実際に JankStats のデータを生成したりリスニングしたりするのも、それと同じくらい簡単です。生成して、あとはリスニングして待てばいいだけです(厳密に言えば、待つのはコードですが)。JankStats のサンプル JankLoggingActivity から、この手順の例を紹介しましょう。
val jankFrameListener = JankStats.OnFrameListener { frameData -> // real app would do something more interesting than log this... Log.v("JankStatsSample", frameData.toString())}jankStats = JankStats.createAndTrack( window, Dispatchers.Default.asExecutor(), jankFrameListener,)
val jankFrameListener = JankStats.OnFrameListener { frameData ->
// real app would do something more interesting than log this...
Log.v("JankStatsSample", frameData.toString())
}
jankStats = JankStats.createAndTrack(
window,
Dispatchers.Default.asExecutor(),
jankFrameListener,
)
Log.v()
JankStats.OnFrameListener: FrameData(frameStartNanos=827233150542009, frameDurationUiNanos=27779985, frameDurationCpuNanos=31296985, isJank=false, states=[Activity: JankLoggingActivity])JankStats.OnFrameListener: FrameData(frameStartNanos=827314067288736, frameDurationUiNanos=89903592, frameDurationCpuNanos=94582592, isJank=true, states=[RecyclerView: Dragging, Activity: JankLoggingActivity])JankStats.OnFrameListener: FrameData(frameStartNanos=827314167288732, frameDurationUiNanos=88641926, frameDurationCpuNanos=91526926, isJank=true, states=[RecyclerView: Settling, RecyclerView: Dragging, Activity: JankLoggingActivity])JankStats.OnFrameListener: FrameData(frameStartNanos=827314183945923, frameDurationUiNanos=4731405, frameDurationCpuNanos=8283405, isJank=false, states=[RecyclerView: Settling, Activity: JankLoggingActivity])
JankStats.OnFrameListener: FrameData(frameStartNanos=827233150542009, frameDurationUiNanos=27779985, frameDurationCpuNanos=31296985, isJank=false, states=[Activity: JankLoggingActivity])
JankStats.OnFrameListener: FrameData(frameStartNanos=827314067288736, frameDurationUiNanos=89903592, frameDurationCpuNanos=94582592, isJank=true, states=[RecyclerView: Dragging, Activity: JankLoggingActivity])
JankStats.OnFrameListener: FrameData(frameStartNanos=827314167288732, frameDurationUiNanos=88641926, frameDurationCpuNanos=91526926, isJank=true, states=[RecyclerView: Settling, RecyclerView: Dragging, Activity: JankLoggingActivity])
JankStats.OnFrameListener: FrameData(frameStartNanos=827314183945923, frameDurationUiNanos=4731405, frameDurationCpuNanos=8283405, isJank=false, states=[RecyclerView: Settling, Activity: JankLoggingActivity])
ログに出力された frameData には、いくつかの興味深い点があります。
frameData
isJank=true
JankLoggingActivity
Thread.sleep()
FrameMetrics
Activity
JankStats は、最近のベンチマーク用ライブラリとは異なり、ユーザーのデバイスから結果を取得できるように作られています。開発マシンで問題をデバッグできるのはすばらしいことですが、実世界の実ユーザーが、制約の異なるさまざまなデバイスでアプリを使っている状況では、開発マシンでのデバッグは役に立ちません。
JankStats では、アプリを計測して必要なパフォーマンス データを提供する API や、そのデータをアップロードしてオフラインで分析できるようにするための報告メカニズムが準備されています。
最後に(このライブラリの本当に新しい注目点はここです)、JankStats では、パフォーマンスの問題が起きたときに、アプリで実際に何が起きていたのかを把握できる仕組みが提供されています。私たちがよく耳にする不満は、既存のツールやダッシュボード、アプローチでは、ユーザーが経験しているパフォーマンスの問題に関する十分な コンテキスト が得られないというものです。
たとえば、FrameMetrics API(API 24 で導入され、JankStats で内部的に使用されています)は、フレームの描画にどのくらい時間がかかったかを教えてくれるので、そこからジャンク情報を導き出すことができます。しかし、そのときにアプリで何が起こっていたかはわかりません。FrameMetrics などのパフォーマンス測定ツールを組み込み、コードを計測しようとしているのは皆さんなので、問題を見つけるのは皆さんの仕事です。しかし、皆さんは忙しくて、このような内部インフラストラクチャを構築することはできません。そのため、ジャンクの計測は行われず、パフォーマンスの問題が残り続けるのが一般的です。
同じように、Android Vitals ダッシュボードは、アプリでパフォーマンスの問題が発生していることは教えてくれますが、その問題が発生しているときにアプリが何をしていたかは教えてくれません。そのため、その情報を使って何をすればよいのかを知るのは困難です。
JankStats では、PerformanceMetricsState API が導入されます。これはシンプルなメソッドの集まりで、任意のタイミングでアプリの中で起きていることを教えてもらうように、システムに依頼します。結果は String のペアで表され、特定の Activity や Fragment がアクティブなタイミングや、RecyclerView がスクロールされるタイミングなどを知ることができます。
PerformanceMetricsState API
String
Fragment
RecyclerView
次の例は、JankStats サンプルのコードです。RecyclerView を計測し、その情報を JankStats に渡す方法を示しています。
val scrollListener = object : RecyclerView.OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { val metricsState = metricsStateHolder?.state ?: return when (newState) { RecyclerView.SCROLL_STATE_DRAGGING -> { metricsState.addState("RecyclerView", "Dragging") } RecyclerView.SCROLL_STATE_SETTLING -> { metricsState.addState("RecyclerView", "Settling") } else -> { metricsState.removeState("RecyclerView") } } }}
val scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView,
newState: Int)
{
val metricsState = metricsStateHolder?.state ?: return
when (newState) {
RecyclerView.SCROLL_STATE_DRAGGING -> {
metricsState.addState("RecyclerView", "Dragging")
RecyclerView.SCROLL_STATE_SETTLING -> {
metricsState.addState("RecyclerView", "Settling")
else -> {
metricsState.removeState("RecyclerView")
この状態は、アプリのどこからでも(あるいは、別のライブラリからでも)挿入できます。JankStats は、結果を報告するときにこの状態を取得します。そのため、JankStats からレポートを取得すると、各フレームでかかった時間だけでなく、そのフレームでユーザーが何をしていたのかまでわかります。その操作がジャンクの原因になっているかもしれません。
JankStats をさらに詳しく知るためのリソースをいくつか紹介します。
AndroidX プロジェクト : JankStats は、AndroidX の androidx.metrics ライブラリにあります。
ドキュメント : デベロッパー サイトに新しいデベロッパー ガイドがあり、そこで JankStats の使い方が説明されています。
サンプルコード : このプロジェクトにサンプルがあります。JankStats オブジェクトをインスタンス化してリスニングする方法や、重要な UI 状態情報を使ってアプリを計測する方法が示されています。
performance-samples/JankStatsSample at main · android/performance-samples
バグ : ライブラリの問題を見つけた方や、API のリクエストがある方は、バグを送信してください。
JankStats は、最初のアルファ版がリリースされたばかりです。つまり、「私たちはこの API と機能を 1.0 リリースに妥当なものだと考えていますが、ぜひ試してみて感想をお知らせください」という意味です。
今後、JankStats で行いたいと考えていることは他にもあります。たとえば、集計メカニズムのようなものの追加や、既存のアップロード サービスとの同期などです。しかし、基本機能を搭載した最初のバージョンを公開し、皆さんの使い方や要望を確認したいと思いました。現在の基本的な状態でも、十分役に立つことを期待しています。簡単に計測して UI 状態情報を記録できるだけでも、何の機能もないよりはよいはずです。
ぜひこれを入手してお試しください。そして、問題があればお知らせください。いち早くパフォーマンスの問題を見つけて修正しましょう!ユーザーは皆さんを待っています。