Perfettoを用いたAndroidアプリのボトルネックの特定とその改善

ogp

はじめに

ZOZOTOWN本部 ZOZOアプリ部 AndroidチームでZOZOTOWNのAndroidアプリを開発している鈴木です。

本投稿は、ZOZOTOWN AndroidアプリのHome画面に存在する「商品モジュール」実装中に発生したパフォーマンスの低下をPerfettoというツールを用いて特定・改善した事例を紹介します。

Home画面の「商品モジュール」について

Home画面は以下の画像のように、アプリを開いた際、最初に表示される画面のことを指します。商品情報やクーポン情報などを表示することで、ユーザに対して新しい発見の提供や、好みの商品への導線を提供しています。

Home画面

Home画面の内、商品をある括りによって集めたものが「商品モジュール」です。例えば、マルチサイズアイテムと呼ばれるユーザの体型にあった商品を集めた商品モジュールや、最近チェックした商品を集めた商品モジュールなどがあります。

この商品モジュールは、2021/3/18のZOZOTOWNのリニューアルにおいて、以下の画像のようにデザインがリニューアルされ現在のデザインとなっています。

より多くの商品を閲覧できるように、横スクロールが新しく導入されるなど大幅にデザインがアップデートされました。

新・旧商品モジュールの比較

発生した問題

この新・商品モジュールを実装中に、Home画面の表示速度が遅いという問題が発生しました。

具体的には、商品の詳細画面からHome画面に戻る際、Home画面の表示に時間がかかるという問題です。

以下の動画が実際の例です。Home画面に戻る際、コンテンツ表示に時間がかかり、真っ白な画面が長く表示されています。

発生した問題の具体例

このパフォーマンスの低下は、ユーザ体験や売り上げに大きな影響が出ると考え、改善に向けて取り組むことにしました。

調査方法について

商品モジュールの実装において変更した点は基本的にレイアウトのみであったため、レイアウト周りにパフォーマンス低下の原因があると予測をたてて調査することにしました。

調査ツールの選定

リリース日との兼ね合いで、調査にはあまり時間をかけられません。短時間でボトルネックを特定する必要がありました。

商品モジュール内の商品1つ分のレイアウト(以下、商品セルと呼ぶ)はレイアウト構造が複雑です。変更箇所全てに対して計測用コードを実装するような方法は工数が大きくなるため、選択肢から除外して調査しました。

ツールを調査していると、システムトレースを用いることで、アプリのCPU使用率やスレッドの情報を記録できることがわかりました。

Android Developersのシステムトレースの概要によると、Androidのシステムトレースの方法には4種類オプションがあります。

  1. Android StudioのCPU Profiler
  2. System Tracingアプリ
  3. Systraceコマンドラインツール
  4. Perfettoコマンドラインツール

この4手法を、「短時間でトレース可能か」「トレース結果を短時間で確認可能か」という2つの軸で評価しました。

1の「Android StudioのCPU Profiler」は、トレースと結果の可視化がAndroid Studio内で完結するという方法です。実際に使用した結果、当時の筆者の実行環境では、トレース中やトレース結果の確認中にアプリやAndroid Studioの動作が重くなるという問題がありました1。この理由から、短時間でトレースが難しいと判断し、CPU Profilerを用いる方法は候補から除外しました。

2と3については、トレースを端末上で開始し、キャプチャしたデータをPCに移動させて可視化するという方法です。データをPCに移動して、前述のProfilerや後述のPerfetto UIなどで読み込まなければならないという点で、短時間での結果の確認が難しいと判断し候補から除外しました。

4の「Perfettoコマンドラインツール」はPerfetto UIからトレースの設定、Perfettoコマンドの実行、可視化が一貫してできる方法です。前述の3種類と比較して、最も短時間でボトルネックの特定が可能であったため、Perfettoを使用するという判断をしました。

Perfettoとは?

Android 10で導入されたトレースツールであり、Android Debug Bridge(ADB)を介して、パフォーマンス情報を収集できます。

パフォーマンス情報とは、CPU・メモリの使用率や各プロセスの情報等のことを指します。

さらに、Perfetto UIと呼ばれるGUIツールを使用することで、トレースの設定、記録、可視化を一貫して行うことが可能です。

Perfetto UIを用いたAndroidアプリのトレースの流れ

Perfetto UIを用いたトレースの基本的な流れ2は、以下の通りです。

1. 新しいトレースの設定・実行画面を表示

Perfetto UIにアクセスし、ページのサイドバーから「Record new trace」をクリックします。クリック後、以下のような画面が表示され、新しいトレースの設定・実行ができます。

トレースの設定・実行画面

2. トレースに使用する端末を設定

次に、トレースで使用するAndroid端末を設定します。「Add ADB Device」のボタンをクリックして、PCに接続されている端末を選択します。

トレースに使用する端末を設定

3. トレースの設定

トレースの設定は、以下の画像のようにトレースしたい項目を選択していきます。

トレースの設定

4. トレースコマンドの確認

設定完了後に「Recording command」の項目をクリックすると、設定した内容がadbコマンドとして表示されます。

トレースコマンドの表示

5. トレースの実行

最後に「Start Recording」をクリックします。そうすることで、4. トレースコマンドの確認の項目で表示されたadbコマンドが端末上で実行されます。

トレースの実行

6. トレース結果の可視化

トレースが終了すると、設定内容にもとづいてトレース結果が可視化されます。

トレース結果の可視化

問題の調査

実際にPerfettoを用いて調査した内容について述べます。

トレース内容

トレースは、実際に問題があった区間の内容としました。

具体的には、下図の通り、商品の詳細画面からHome画面のコンテンツが表示される区間としました。

トレース区間

Perfettoのトレース設定

Perfetto UI上で行ったトレース設定について説明します。

Perfeto UI上で「Record new trace」を選択し、「Recording settings」において以下の画像のようにそれぞれ設定をしました。

Recording settings

①の「Recording mode」では、バッファへ書き込まれるトレースデータの記録方式を設定します。今回は「Stop when full」を選択しました。これはトレースデータがバッファ上限を超えた際、書き込みをストップするという方式です。今回は後述のバッファサイズを十分確保できるためこの方式としました。

②の「In-memory buffer size」では、トレースデータを書き込むためのバッファサイズを設定します。試しに計測をした結果、Perfetto UIが設定したデフォルト値「64MB」で問題なくトレース可能であったため「64MB」としました。

③の「Max duration」では、トレースの時間を指定します。今回は、商品の詳細画面からHome画面のコンテンツが表示される間のトレースになるため、5(s)としました。

次に、「Android apps & svcs」を選択し、以下の画像のように設定しました。

Android apps & svcs

④の「Atrace userspace anntaions」を有効にし、⑤の「View System」を選択しました。これにより、Linuxカーネルに組み込まれているftraceと呼ばれるトレース機能を使用して、レイアウト生成処理のイベントをキャプチャできるようになります。

設定が完了したら「Recording command」でトレース用のコマンドを取得します。

Recording command

⑥において、設定した①〜⑤の内容がadbコマンドとして出力されました。

下記が出力されたコマンドです3

adb shell perfetto \
  -c - --txt \
  -o /data/misc/perfetto-traces/trace \
<<EOF

buffers: {
    size_kb: 63488
    fill_policy: DISCARD
}
buffers: {
    size_kb: 2048
    fill_policy: DISCARD
}
data_sources: {
    config {
        name: "linux.ftrace"
        ftrace_config {
            ftrace_events: "ftrace/print"
            atrace_categories: "view"
        }
    }
}
duration_ms: 5000

EOF

⑦においてトレースする端末を選択し、⑧の「Start Recording」からトレースを開始します。

実行結果

トレース結果の内、①で示した区間が、商品セルをinflateしている箇所です。

商品セル1つ分のトレース結果

inflateの内訳を見ると、②で示している区間において比較的時間がかかっていることがわかりました。

以下が②の部分を拡大した画像となっており、②はMaterialCardViewのinflate処理であることがわかりました。

商品セル1つ分のトレース結果拡大

さらに、以下のように、Home画面で表示される商品セルの数だけ(50以上)inflateが発生していることがわかりました。 商品モジュールのトレース結果

問題の改善

トレース結果より、下記2つの改善方針の案を考えました。

  1. 商品セルのinflate時間を短くする
  2. 商品セルのinflate回数を減らす

改善方針の検討

どちらの方法も、一定の改善見込みがありました。

1. 商品セルのinflate時間を短くする

Androidアプリでは、下記2つの計算後に、レイアウトの描画が行われます。

  1. measureパス
  2. layoutパス

measureパスでは、View, ViewGroupのサイズ計算が行われます。各View, ViewGroupがonMeasure()メソッドにより自身のサイズを申告することで、Viewツリーの全ノードの幅と高さを決定します。

layoutパスでは、View, ViewGroupの配置座標の計算が行われます。measureパスにより決定した各ノードのサイズ情報と親ノードの特徴(例えば、LinearLayoutでは子ノードを一列に並べるなど)をもとに、Viewツリーの全ノードの座標を決定します。

2つの計算が終了すると、各View, ViewGroupがdraw()メソッドを呼び出し、レイアウトを描画していきます。

さて、以下のトレース結果からもわかるように、inflateはmeasureパスにおいて実行されています。

実行結果

つまり、inflateの速度はmeasureパスの速度にも影響し、最終的にはレイアウトの描画にも影響します。

inflate時間の短縮に取り組むことで、パフォーマンスの改善が期待できます。

2. 商品セルのinflate回数を減らす

実行結果の通り、Home画面で表示される商品セルの数だけinflateが実行されていました。

画面内に表示されていない商品セルについても、Home画面表示の際にinflateされていることから、この回数を減らすことでパフォーマンスの改善が期待できます。

改善方針の決定

今回はスケジュールの都合から、短時間で実装可能な方針のみ着手することとしました。

1.「商品セルのinflate時間を短くする」は、短時間で実装可能と判断しました。

商品セルのinflateにおいて一番時間がかかっているMaterialCardViewの使用を中止できる見込みが立ったためです。

2.「商品セルのinflate回数を減らす」は、短時間での実装は難しいと判断しました。

2のためには、以下のようにRecyclerView in RecyclerViewの構造にレイアウトを組み直す必要がありました。この構造にすることで、初回のレイアウト描画時はファーストビューに必要なコンテンツのみ描画処理され、inflate回数を減らすことができます。

RecyclerView in RecyclerVxiewのイメージ

レイアウト構造変更に伴う修正、RecyclerView in RecyclerViewを実現可能なライブラリの使用を検討・調査するなど、実装コストが高いと判断しました。

上記より、 1.「商品セルのinflate時間を短くする」 のみを着手することにしました。

改善方法

MaterialCardViewは、新・商品モジュールにおける商品セルの角丸を実装するために追加されており、セルのRootのViewとなっていました。

実装されていたコードのサンプルは下記の通りです。

<!-- レイアウトファイル -->
<com.google.android.material.card.MaterialCardView
    android:id="@+id/listItem"
        :
        >
     <!-- 商品情報(商品画像, 値段, ブランド名, etc.) -->
        :
</com.google.android.material.card.MaterialCardView>
// 角丸付与のロジック
// 商品セル右側の上下
private fun setRoundCornersOnRightSides() {
    val CORNER_ROUND_DP = 10L
    val corner_round_px: Float = dpToPx(CORNER_ROUND_DP)

    binding.listItem.shapeAppearanceModel = ShapeAppearanceModel()
        .toBuilder()
        .setAllCornerSizes(0F)
        .setTopRightCorner(CornerFamily.ROUNDED, corner_round_px)
        .setBottomRightCorner(CornerFamily.ROUNDED, corner_round_px)
        .build()
}
    :
// 商品セル左側の上下
private fun setRoundCornersOnLeftSides() {
    :

// 商品セルの上下左右
    :

MaterialCardViewを用いると、ShapeAppearanceModelを用いて簡単に角丸を実装できます。

しかし、パフォーマンスを犠牲にしてまで利用するものではないため、FrameLayoutのbackgroundに角丸のdrawableを当てる方法で代替しました。

実装コードのサンプルは下記の通りです。

<!-- 商品セルのレイアウト -->
<FrameLayout
    android:id="@+id/listItem"
        :        
        >
    <!-- 商品情報(商品画像, 値段, ブランド名, etc.) -->
        :        
</FrameLayout>
// 角丸付与のロジック
// 商品セル右側の上下
private fun setRoundCornersOnRightSides(context: Context) {
    binding.listItem.run {
        background = ContextCompat.getDrawable(context, R.drawable.bg_corner_right)
        foreground = ContextCompat.getDrawable(context, R.drawable.fg_corner_right)
    }
}
    :
// 商品セル左側の上下
private fun setRoundCornersOnLeftSides(context: Context) {
    :

// 商品セルの上下左右
    :

結果

パフォーマンスの比較方法

パフォーマンスが改善できているか確認するため、下記項目を改善方針の実装前後で比較しました。

  • 端末を操作したときの体感での比較
  • 商品セルのinflateの平均時間の比較

体感での比較は、実際にアプリを操作したときの動作比較です。

inflateの平均時間は、Perfettoを用いて3回トレースした際の、商品セルのinflate平均時間を比較しています。

体感での比較

チームで実際に操作して検証したところ、「微妙な差ではあるものの、対応後の方が速いように思う」という評価が集まりました。

商品セルのinflateの平均時間の比較

定量的にも評価します。

対応前
トレース(回目) 平均inflate時間(ms)
1 10.50
2 10.40
3 10.78
対応後
トレース(回目) 平均inflate時間(ms)
1 8.59
2 8.68
3 8.23

対応前はトレース3回の平均は10.56ms、対応後は8.5msでした。

1つ1つはたった2msと劇的ではないものの、商品セルの描画全体では2ms × 50セル = 100msほど改善される結果となりました。

まとめ

本投稿ではPerfettoと呼ばれるツールを用いて、実装中に感じたパフォーマンスの低下を特定・改善した事例について紹介しました。

対応後の実装では、体感で劇的な改善とはなりませんでしたが、全体で100msほど表示速度が改善されました。

加えて、現在は問題の改善で述べた「商品セルのinflate回数を減らす」改善も実施し、RecyclerView in RecyclerViewのレイアウト構造となりました。これにより、更にパフォーマンスが改善されつつあります。

また、当時は開発環境の問題から諦めたAndroid StudioのCPU Profilerも、現在の筆者の開発環境では快適に動作するようになりました。これの使用も検討中です。

おわりに

ZOZOテクノロジーズでは、Androidエンジニアを募集しています。

まずは、以下のリンクから、お気軽にカジュアル面談にご応募ください! hrmos.co


  1. 現在(2021/8/30時点)は問題なく確認できています。
  2. Quickstart: Record traces on Android - Perfetto Tracing Docs
  3. コード内の各フィールドについては、公式ドキュメントで詳しく説明されています。
カテゴリー