はじめに
SWETグループiOSチームのkariad(@kariad_uu)です。
本記事はiOSDC 2020 Japanにて発表した「継続的にアプリのパフォーマンスを計測する」の内容を元にブログという形で改めて紹介する記事となります。 発表時のスライドは以下を参照ください。
iOSチームではiOSアプリのパフォーマンス計測に取り組んできました。 iOSアプリのパフォーマンス計測方法はたくさんありますが、中でもInstrumentsを利用したパフォーマンス計測とその自動化について紹介します。
なぜパフォーマンス計測が必要なのか?
取り組んだ計測方法についてご紹介する前にアプリにとってパフォーマンスとその計測がなぜ重要なのかという点を説明します。
起動に時間がかかる、読み込みに時間がかかるアプリは動作が遅くユーザにストレスを与えてしまいます。 短時間で熱くなってしまうアプリでは端末が熱くて持てなくなってしまうだけではなくバッテリーの消費量も大きくなってしまいます。 また場合によってはパフォーマンスが悪いことでアプリのクラッシュが引き起こされる可能性もあります。 こうした問題はアプリのユーザーの離脱を引き起こしてしまう可能性があります。
アプリのパフォーマンスについてAppleが以下のドキュメントを出しています。 https://developer.apple.com/documentation/xcode/improving_your_app_s_performance
このドキュメントによるとパフォーマンス改善のサイクルを回していくことが推奨されています。 改善のサイクルを回すことでパフォーマンスが、ユーザーが離脱してしまうほど悪化する前に発見、修正できます。
Instrumentsでパフォーマンスを計測する
InstrumentsはXCUITestに組み込んで自動的に計測するような自動化の仕組みはありません。 そのため自動化し、継続的にパフォーマンス計測をするためにはツールを組み合わせる必要があります。
Instrumentsを利用して今回の事例で計測する項目は、CPU使用率とThermal State(端末温度)です。1 以前アプリの性能が悪化した際に原因を調査したところ、CPU使用率が高くなることで端末温度が上昇し、クラッシュに繋がることが判明したからです。
そして今回SWETでは次のような構成で自動計測基盤を構築しました。
- 最新のアプリ: Bitriseでビルド&resign
- 実機: HeadSpin
- 計測環境: Jenkins
- アプリ操作用UIテストフレームワーク: XCUITest
- 計測ツール: Instruments CLI
- アプリに負荷をかけるツール: お手製
- 計測結果確認手段: Slack
この構成での実行フローは次のようになります。
- Bitriseにてreleaseビルドでipaを作成
- ipaに対してdevelopment証明書でresign
- HeadSpinにipaを配布、dSYMをArtifactsとして保存
- BitriseからJenkins jobトリガーを実行
- Jenkinsで負荷生成ツールのDL、起動、HeadSpinへの接続等の準備
- 計測開始と完了
- 結果のtraceファイルをXCUITestを用いてSSの撮影とSlackへの投稿
- traceファイルなどをJenkinsのArtifactsとして保存
構成について
なぜこのような構成となったのか、注意するべきポイントとともに、実行フローに沿って説明していきます。
最新のアプリ
コードは毎日変更されます。パフォーマンスが悪化したことをいち早く知るため変更に追従した最新のアプリで計測する必要があります。場合によってはちょっと改善を試したブランチなどもありえます。 またInstrumentsでのデバッグ用にdSYMも必要となるためこれらを毎日ビルドをして用意する必要があります。 しかしただビルドをするだけではなく、Instrumentsで正しく計測できるためにはいくつかの条件を満たす必要もあります。
- コンパイラの最適化をreleaseビルドと同等にする。
- developmentのCertificateでsignする。
Xcodeのデフォルトの設定ではdebugビルドとreleaseビルドではコンパイラの最適化の設定が異なっています。 releaseビルドでは、開発中頻繁にビルドされるdebugビルドよりもアグレッシブな最適化が行われます。最適化の差異はパフォーマンス計測結果の差異に繋がるため2、実際にユーザーが利用するreleaseビルドと同等の最適化設定でビルドされたアプリを対象に計測する必要があります。
またInstrumentsで計測する際にはdevelopmentのCertificateでsignされている必要があり、distributionではエラーとなってしまいます。
これらをまとめると「releaseと同等の最適化設定を利用しながらdevelopmentのCertificateでsign」という条件になります。 これは一見矛盾しているようにも見えます。計測用のビルドのために設定を変える方法もありますが、今回は別の方法を採用することとしました。それがipaのresignです。
ipaのresignとは生成されたipaに対して、名前の通りsignし直すことができます。 今回の例だとreleaseビルドに対してdevelopmentのCertificateでresignします。 resignにはいくつか方法がありますが、今回はfastlaneのresignを利用することにしました。 fastlaneは次のように書くことで簡単にresignが実行できます。
resign( ipa: ipa_path, signing_identity: "iPhone Developer: Xxxxxx Xxxxxxx(FFFFFFFFF)", provisioning_profile: { "com.swet.app" => provisioning_profile_path, "com.swet.app.notificationservice" => provisioning_profile_notification_path }, display_name: "SWET Debug" )
これらのビルドは元々Bitriseを利用していたため、そのままBitriseで実施しています。 そして、Bitriseから作成したipaを計測端末に配布します。
実機
Thermal Stateを計測するためには実機での計測が必要となります。 しかし実機を自前で管理すると次のような理由からコストは高くなってしまいます。
- テストが走っているタイミングで他のテストが実行されないように排他制御の仕組みを持つ必要
- OSのバージョン管理が必要
- 物理的な設置が必要
- 現在のリモート環境において問題が発生した場合に出社が必要となる可能性がある
これらの問題点を解消するためにクラウド型のデバイスファームを利用するという選択肢があります。 クラウド型のデバイスファームはデータセンターなどに設置された実機を利用できるサービスです。その中の一部サービスではWebなどから実際の手元の実機と同じように操作できる機能が提供されています。クラウド型のデバイスファームの有名所ではAWS Device FarmやRemote Testkitなどがありますが、今回はHeadSpinというデバイスファームを利用することにしました。
HeadSpinを選択した理由はすでにSWETで契約済みであり、追加で費用がかからなかったという点が大きいです。 それ以外にもHeadSpinには以下のような便利な機能があるため利用しています。
- Webから実際の画面を見ながら操作できる
- Xcodeから直接Runできる
- Wi-Fiだけではなく特定のSIMで4G回線での利用もできる
- AppiumサーバーがHeadSpin側で用意されている
- 端末単位の契約で他社と共有することがない
HeadSpinはAPIも充実しています。例えば次のようなものがあります。
- ipaのインストールとアンインストール
- デバイスのロック、アンロック
- OSアップデートのポップアップを消す
Bitriseからのipa配布にもこのAPIを利用しています。 その他APIも今回の構成でいくつか利用しています。
計測環境
計測する環境については最大2時間を想定としていたことからBitriseは選択肢に入りませんでした。 (本ブログ執筆時点でBitriseの最大時間は90分) またCircleCIも弊社では契約していますが、Enterprise版を利用しており、macOSでの利用は弊社では利用できません。そうした状況からオンプレで汎用性のあるJenkinsが候補となりました。
Jenkinsを選択したことでJenkins自体の管理が必要になる、という課題が発生します。 しかしSWETにはCI/CDチームが管理しているJenkinsがあります。そのJenkinsに相乗りする形で新たに管理しなくてはいけない項目を増やさずに済みました。このJenkinsはXcodeの新しいバージョンが簡単にインストールできる仕組みなどそれ以外にも便利なしくみが揃っています。またJenkinsで躓いた場合相談できるメンバーが周りにいるという点もJenkinsを採用する上での心強いポイントでした。
そのCI/CDチームがCEDEC 2020で発表した内容がこちらです。 Jenkins周りが気になる方はぜひ御覧ください。
モバイルゲーム開発におけるJenkinsクラウド時代のJenkins構築と管理テクニック
アプリ操作用UIテストフレームワーク
計測のためには、UIテストフレームワークを介してアプリを計測対象画面まで遷移させます。 採用したUIテストフレームワークはXCUITestです。
しかし最初からXCUITestを利用していたわけではありませんでした。 最初はAppium x RSpec(Ruby)を利用していました。
ここでAppiumについて軽く説明します。 AppiumはオープンソースのUIテストフレームワークで様々な言語を利用して実装できます。 その特徴としてWebDriverを用いてアプリを操作します。
上図のとおりAppiumの実行にはAppiumサーバーが必要ですが、相性のよいことにHeadSpin自体がAppiumサーバーの実行環境を用意しています。そこで当初は、AppiumのほうがXCUITestより良い選択肢だと考えていました。
Appiumで起きた問題
Appiumを利用する上でいくつかの問題が発生しました。
- HeadSpin上のAppiumサーバーを利用する場合Xcodeのバージョンを柔軟にコントロールできない
- HeadSpin上のAppiumサーバーを利用する場合traceファイルの転送が必要となり転送先を用意する必要がある
- 原因不明だがconnection errorが発生する
Instrumentsの実行はAppiumサーバーと同じ環境で実行されます。そのため今回の場合はHeadSpin側でInstrumentsも実行されることとなり、向こう側のXcodeのバージョンに依存することとなります。 またInstrumentsの計測結果であるtraceファイルの転送も必要となり、転送先を考える必要がありました。 これらを考慮したときにJenkins上でAppiumサーバーを持つことで、Xcodeのバージョンは簡単に切り替えることができ、traceファイルもJenkinsのArtifactsとして保存すれば良いだけとなることからJenkins側でAppiumサーバーを持つ形へと変更することとなりました。
最後の問題として、負荷をかけ続けた状態で一定時間経つとAppiumで新しいコマンドを実行した際にconnection errorが発生しました。 原因を調査しましたが、詳細はわかりませんでした。しかし前述の理由でAppiumサーバーをJenkinsで立てることになり、Appiumを使い続ける理由がほぼなくなっていたため、XCUITestの挙動を確認することにしました。 すると安定して動作したためXCUITestへ移行したというのが現在です。
アプリに負荷をかけるツール
アプリに負荷をかけ続けた状態でのパフォーマンスを計測することにしました。 そのため、SWETの他チーム(Goチーム)のメンバーと協力して負荷生成ツールを作成しました。 このツールをJenkins上で同時に実行しています。
計測ツール
計測ツールについては以下の理由からInstrumentsを利用することにしました。
- すでにパフォーマンスが問題となっており、ユーザーへ届ける前に問題がないかを知る必要があった
- 問題がある場合は素早くその原因を特定し解決したい
- 一定時間の負荷をかけた状態での端末の温度とCPU使用率の変化を計測したい
これらを満たせるのが現時点ではInsturmentsしかありませんでした。
InstrumentsはXcodeの一部として提供されているツールです。 アプリのパフォーマンスを計測するだけではなく、どこの処理のパフォーマンスが悪いのかまで特定できます。 今回はその特定まで行いたいという動機がInstrumentsを採用した大きな理由の1つです
Instruments実行方法
InstrumentsはInstruments.appとInstruments CLIという2種類に分けることができます。 結果の確認はInstruments.appでしかできませんが、計測だけならばどちらでも可能です。 そのため、コマンド1つで実行できるInstruments CLIを利用しました。 Instruments CLIはAppiumからも利用できます。
計測結果確認手段
Instrumentsではtraceファイルから計測結果を定量的な数値で取得する方法がありません。 (実際にはある程度取れる外部ツールが存在したのですが、この時点では存在を知りませんでした) そのため、結果の確認にはtraceファイルをInstruments appで開き、GUIで確認する必要がありました。 しかし負荷をかけ続けての長時間の計測ともなるとファイルサイズが150〜200MBと大きく、Instruments appで開くまでに1分以上かかることもあります。これを毎回手動で結果確認をするのは面倒です。
その解決策としてInstruments appをXCUITestで操作してスクリーンショットを撮影、Slackに投稿させるという方法を取りました。 Thermal Stateは次のような画面で結果を確認できます。
state | 説明 |
---|---|
Nominal | 平常時、問題がない状態 |
Fair | 対応が必要なほどではないが熱くなり始めている |
Serious | 非常に熱く、アプリ自体にも影響が出始める |
Critical | 今すぐに冷やす必要がある |
またこの各stateの開始時間と終了時間がThermal Stateのグラフを選択することで詳細な情報としてみることができます。
続いてCPU使用率についても実際のスクリーンショットをもとに説明していきます。
CPU使用率に関してはGUIでもとても数値が見づらいです。 CPU使用率の絶対値はグラフでしか確認できません。 しかもそのグラフでさえもグラフの天井へ張り付いたときの数値が固定ではなくその計測回の一番高い数値になります。そのため複数の計測結果のグラフを並べてもグラフだけではどちらのCPU使用率が高いかわかりません。グラフにマウスカーソルを合わせることで初めて具体的な数値がわかります。 またグラフを選択した際に表示される詳細情報では選択した時間での全体のCPU使用率を100%とした場合の具体的な処理毎の使用率内訳が表示されます。つまり全体の使用率の平均などを見ることはGUIでもできません。
これらのスクリーンショットを撮影してSlackに投稿していますが、Thermal Stateについては詳細から各stateの文字列も取得できるため、そこからCriticalに到達したら失敗させるといったことも可能になりました。 一方でCPU使用率についてはスクリーンショットを撮影していますが、それだけではわからずtraceファイルをInstruments appで見なくてはわからないままです。
パフォーマンス計測で注意するべきこと
これらを組み上げる間には手元で確かめたりと様々な試行錯誤を重ねました。その中でパフォーマンス計測していく上で注意する必要があると気がついた点があります。
端末の状態
計測は1パターンだけでなくいくつかのパターンで計測したい場合があります。 しかし連続して同じ端末で計測すると、前の計測で既にThermal Stateが上がりきった状態で計測を開始してしまうため正しい計測結果が得られません。 環境にもよりますが、端末が冷えて前回の影響を受けずに計測するためには30分~1時間の間隔を開ける必要があります。 この問題には私達も直面し、そこで取った対応として端末数を増やして、さらに次のような実行計画を組むことでなるべく時間のロスなく計測パターンに対応させました。
Thermal Stateなど、物理的に影響のある項目を計測する場合には前回の計測から間を空けて、影響を受けないようにすることが必要になります。
外部気温
空調などで完全に管理された空間で無い限り物理的な端末での端末温度の計測は外部気温の影響を受けます。 それにより冬では大丈夫でも夏にはとても熱くなってしまうということもありえます。 性能が悪化していなくてもユーザーからすると使いづらい状況になるため、パフォーマンスの改善したほうが良いでしょう。 また上記で連続して計測する際に端末が熱い状態で開始すると正しく計測できず、クールタイムが必要になると説明しました。このクールタイムも外部気温が高いほど端末は冷えにくくなるため同じ端末で連続して計測する際には注意が必要となります。
これからについて
まだまだ改善していきたいところやまだやれていないことがたくさんあります。 今後やっていきたいことには次のようなものがあります。
- xctraceによる計測結果の確認
- 計測項目の追加と結果確認方法の改善
xctrace
Xcode12からinstrumetsコマンド(Instruments CLI)がdeprecatedとなります。 代わりにxctraceというツールが追加されました。 このxctraceは基本的にはinstrumentsコマンドを置き換えたものになるのですが、新しい機能も追加されています。 その中で最も特徴的なものがXML形式でのデータのexportです。 これによりtraceファイルをInstruments appで開かずとも計測結果のデータを取得できるようになります。 実際にThermal Stateは詳細に表示されていた各stateの開始時間と終了時間がexportできるようになりました。 一方でCPU使用率についてはまだxctraceで取得できません。 これはそもそもグラフでしか表現されていないのが問題かもしれませんが、いつか対応されることを期待しています。
xctraceの使い方
ここでxctraceのexport機能について紹介したいと思います。 以前Xcode12についての記事を書いたのですが、その時点ではまだbeta版であり、なおかつxctraceに関するドキュメントがmanコマンド以外ほぼ存在しないという状態でした。 そのため詳細の紹介を控えたのですがめでたくXcode12がリリースされたため、私が試したexport機能について、私が理解した範囲で紹介します。 また予め断りを入れておきますが、manコマンド以外のドキュメントが無く手探りで試して得られた利用方法のため、正しくない可能性があります。ご了承ください。
xctraceにはrecord、import、export、list、helpの5つコマンドが存在します。 その中でもexportがxctraceから使えるようになったtraceファイルのデータをXMLで出力できるコマンドです。 exportコマンドは次のように使います。例としてTime ProfilerのTemplateで計測した結果を利用します。
いきなり詳細を出力する前に--toc
でTOC(Table of contents、計測項目の一覧と思われるが明確な記載は無いので推測です)を出力します。
$ xcrun xctrace export --input xx.trace --toc
すると次のようなXMLで表現された結果が得られます。
<?xml version="1.0"?> <trace-toc> <run number="1"> <info> <target> <device name="device name" uuid="udid"/> </target> <summary/> </info> <data> <table schema="tick"/> <table schema="device-thermal-state-intervals"/> <table target-pid="ALL" kdebug-match-rule="0" exclude-os-logs="0" schema="region-of-interest" signpost-code-map=""<null>""/> <table schema="os-log" category="PointsOfInterest"/> <table target="ALL" schema="kdebug" codes=""0x1,0x25""/> <table target="ALL" schema="kdebug" codes=""0x2b,0xd8""/> <table target="ALL" schema="kdebug" codes=""0x2b,0xdc""/> <table target="ALL" schema="kdebug" codes=""0x1f,0x7""/> <table codes=""0x2d,*"" schema="kdebug" target="ALL" callstack="user"/> <table target="ALL" schema="kdebug" codes=""0x2b,0x87""/> <table codes=""0x31,0xca"" schema="kdebug" target="ALL"/> <table category="InduceCondition" schema="os-signpost" subsystem=""com.apple.ConditionInducer.LowSeverity""/> <table target-pid="ALL" kdebug-match-rule="0" exclude-os-logs="0" schema="roi-metadata" signpost-code-map=""<null>""/> <table schema="life-cycle-period" target-pid="ALL"/> <table codes=""0x2b,0x65"" schema="kdebug"/> <table codes=""0x1,0xa"" schema="kdebug" callstack="user"/> <table schema="os-signpost" category="PointsOfInterest"/> <table schema="tick" frequency="1"/> <table codes=""46,2"" schema="kdebug" callstack="user"/> <table sample-rate-micro-seconds="1000" all-thread-states="NO" schema="time-sample" target="ALL" callstack="user"/> <table codes=""0x21,0xa"" schema="kdebug" callstack="user"/> <table schema="gcd-perf-event" target-pid="ALL"/> <table schema="os-signpost-arg" category="PointsOfInterest"/> <table target-pid="ALL" exclude-os-logs="0" schema="global-poi-layout" signpost-code-map=""<null>"" colorize-by-arg4="0"/> <table target-pid="ALL" kdebug-match-rule="0" schema="global-roi-layout" signpost-code-map=""<null>"" colorize-by-arg4="0"/> <table schema="os-log-arg" category="PointsOfInterest"/> <table target-pid="ALL" high-frequency-sampling="0" schema="time-profile" needs-kernel-callstack="0" record-waiting-threads="0"/> <table target-pid="ALL" schema="kdebug-signpost" signpost-code-map=""<null>""/> <table schema="thread-name"/> </data> </run> </trace-toc>
詳細を見るためにはこのTOC XMLの階層を--xpath
オプションでたどる必要があります。
例えばこのTOCからThermal Stateの詳細を得るためのコマンドが次になります。
$ xcrun xctrace export --input xx.trace --xpath '/trace-toc/run[@number="1"]/data/table[@schema="device-thermal-state-intervals"]'
このコマンドを実行すると以下のような結果が得られます。
<?xml version="1.0"?> <trace-query-result> <node xpath='//trace-toc[1]/run[1]/data[1]/table[2]'> <schema name="device-thermal-state-intervals"> <col> <mnemonic>start</mnemonic> <name>Start</name> <engineering-type>start-time</engineering-type> </col> <col> <mnemonic>duration</mnemonic> <name>Duration</name> <engineering-type>duration</engineering-type> </col> <col> <mnemonic>end</mnemonic> <name>End</name> <engineering-type>start-time</engineering-type> </col> <col> <mnemonic>thermal-state</mnemonic> <name>Thermal State</name> <engineering-type>thermal-state</engineering-type> </col> <col> <mnemonic>track-label</mnemonic> <name>Track</name> <engineering-type>string</engineering-type> </col> <col> <mnemonic>is-induced</mnemonic> <name>Is Induced</name> <engineering-type>boolean</engineering-type> </col> <col> <mnemonic>narrative</mnemonic> <name>Narrative</name> <engineering-type>narrative</engineering-type> </col> </schema> <row> <start-time id="1" fmt="16:03.938.891">963938891916</start-time> <duration id="2" fmt="12.28 min">737085646124</duration> <start-time id="3" fmt="28:21.024.538">1701024538040</start-time> <thermal-state id="4" fmt="Serious">Serious</thermal-state> <string id="5" fmt="Current">Current</string> <boolean id="6" fmt="No">0</boolean> <narrative id="7" fmt="Serious thermal state"> <thermal-state ref="4"/> <narrative-text id="8" fmt=" thermal state"> thermal state</narrative-text> </narrative> </row> <row> <start-time id="9" fmt="09:48.940.922">588940922875</start-time> <duration id="10" fmt="6.25 min">374997969041</duration> <start-time ref="1"/> <thermal-state id="11" fmt="Fair">Fair</thermal-state> <string ref="5"/> <boolean ref="6"/> <narrative id="12" fmt="Fair thermal state"> <thermal-state ref="11"/> <narrative-text ref="8"/> </narrative> </row> <row> <start-time id="13" fmt="00:00.000.000">0</start-time> <duration id="14" fmt="9.82 min">588940922875</duration> <start-time ref="9"/> <thermal-state id="15" fmt="Nominal">Nominal</thermal-state> <string ref="5"/> <boolean ref="6"/> <narrative id="16" fmt="Nominal thermal state"> <thermal-state ref="15"/> <narrative-text ref="8"/> </narrative> </row> </node> </trace-query-result>
中身を見てみると、Thermal stateの変化について出力されています。 開始から9.82minがNominal、その直後から6.25minがFair、最後にSeriousが12.28min、あることがわかります。 これはInstruments.appのGUIでThermal stateを選択した際の詳細情報として表示されていたものと同等のものが出力されているように見えます。
例としてThermal stateで試しましたがschemaにある項目であればすべて表示可能です。しかしTime Profilerなどはとてつもない長さのXMLが出力されるなど、正直あまり使い勝手がいいとは言えない気はします。 まだ最初のリリースなのでこれからに期待したいところです。
他項目の計測と結果確認方法の改善
現在他項目の計測も進めています。 項目によってはInstrumentsではなく別の方法で計測するのですが、結果をBigQueryに格納し、DataStudioで閲覧できるように準備中です。 今回の計測についてもこのフォーマットに合わせて閲覧できると計測結果がまとめて確認できるようになるためデータの可視化というのを進めていきたいと考えています。 その他MetricKitなど実際のユーザ環境での計測も検討しています。
まとめ
SWETが取り組んだパフォーマンス計測についてiOSDC Japan 2020で発表した内容をもとに計測の一例を紹介しました。
すべてのアプリがThermal stateを見る必要があるかというと必ずしもそうではないかもしれません。 大切なのは自分のアプリがどんな課題を持ちうるか検討した上で早めにパフォーマンス計測の仕組みを導入しておくことです。
今回紹介した計測でもさらなる改善や異なる観点での計測も考えています。 これからもパフォーマンス計測についてSWETでは取り組んでいきますのでさらなる知見が溜まったらまたブログ等で発信していきたいと思います。
最後に今年のiOSDCではViewの表示、バッテリーなど他にもパフォーマンスに関する発表がありました。 他社がどのようにパフォーマンス計測に対して取り組んでいるかを知ることができ、とても有意義でした。 発表については聞いてくださった皆様ありがとうございます。 当日質問できなかった、このブログを読んで気になることができた等あれば片山までTwitterでぜひお気軽に聞いてください。
-
InstrumentsはCPU使用率とThermal State以外にも様々な項目を計測可能です。↩
-
パフォーマンスへの影響についてはWWDC2019「Getting Started with Instruments」のセッションでも述べられています。↩