描画パフォーマンス
アプリケーションでは、ユーザの操作に合わせて様々な動きが発生する。TableView/CollectionView のスクロールや画面遷移、その他ボタンや UI 要素に対するジェスチャに応じたアニメーションなどが考えられる。
hitch とは、フレームが予定より遅れてスクリーンに表示されることを指す。hitch が生じると、アニメーションが途中で急にジャンプした、あるいは突っかかたような感覚になる。 Render Loop
概要
ユーザがデバイスを操作すると、操作イベントがアプリに通知される。するとアプリは UI を更新し、UI を OS に通知する。OS はディスプレイに UI を表示する。といったループのことを、Render Loop と呼ぶ。hitch は、Render Loop 内でフレームが予定通りに表示されなかった場合に発生するとも言える。
Refresh rate
Render Loop は、デバイスの refresh rate に応じて発生する。これは iPhone, iPad では 60Hz、iPad Pro では 120Hz となる。60 Hz だと、1 秒間に 60 フレームが表示可能であり、フレームの表示は 16.67 ms 毎におこなわれる。120 Hz の場合も同様に、フレームの表示は 8.33 ms 毎におこなわれる。
table:refresh rate
Device Refresh rate Frame duration
iPhone, iPad 60Hz 16.67ms
iPad Pro 120Hz 8.33ms
Render Loop の詳細
フレームの開始時にハードウェアは VSYNC と呼ばれるイベントを送出する。このイベントは新しいフレームが用意されなければならないことを示すもので、フレームの開始タイミング毎に送出され、このタイミングでディスプレイ上のフレームが差し替えられる。Refresh rate に応じて、16.67/8.33 ms 毎に発生する。
Render Loop にてディスプレイにフレームが表示されるまでは、大きく 3 段階に分けられる。
1. App: ユーザのタッチイベントをハンドリングし、UI を変更する
2. Render Server: App とは別プロセスで、UI のレンダリングを行う
3. Display: ディスプレイ上に UI を表示する
いずれの段階も、基本的には 1 フレーム内に処理を収める必要がある。また、この方式は、実際に画面に UI が表示されるまでに 2 フレームを必要としているため、Double bufferging と呼ばれる。hitch を生じさせないための fallback モードとして、Render server に 2 フレーム利用する Triple buffering モードも用意されている。
https://gyazo.com/c3cde3f6d6d4f2ca031c3fa0ad9f7c11
各段階で行われることをさらに細かく見ていくと、以下の 5 つの段階になる。
1. Event: App がタッチイベントをハンドリングして、UI に必要な変更を決定する
イベントの種類は、タッチジェスチャや通信、キーボード、タイマーなど多岐に渡る
UI への変更は、即ち View hierarchy の状態の変更になる
2. Commit: App が UI を更新して、レンダリングのために Render Server に提出する
描画の更新が必要な Layer tree を解決して、Render Server に渡す
3. Render prepare: Render server が GPU での描画のための準備を行う
GPU が処理しやすいよう、Layer tree を背面のものから前面のものに順に並び替える
4. Render execute: Render server が GPU で UI を画像としてレンダリングする
GPU によって Layer を最終的に 1 毎の画像にする
Layer によってはレンダリングに時間がかかり、ボトルネックになることがある
5. Display: ディスプレイ上に UI が表示される
https://gyazo.com/5755b13c59b21e5112bf88a1c137fa40
フレームレートを達成しつつ低レイテンシーを維持するために、実際にはこれらの流れは並列で行われている。
https://gyazo.com/f76988e2d277340b6c07b26967f6af8c
Event & Commit
流れ
1. View hierarchy は、イベント受信まで待機する
2. イベントを受信すると、View hierarchy に変更を加える
background color を変えるとか、frame を変えるとか
3. Commit Transaction が開始する
draw(rect:) や layoutSubviews() が適宜呼び出される
Commit Transaction
Commit Transaction には 5 つの段階が存在する。
1. Layout
layout が必要な全ての View に対して、layoutSubviews() が呼び出される
layout が必要かどうか?は以下で判定される
View の位置が変わる (frame, bounds, transform)
View が追加/削除される
明示的に setNeedsLayout() が呼び出される
2. Display
display が必要な全ての View に対して、draw(rect:) が呼び出される
display が必要かどうか?は以下で判定される
draw(rect:) が override された View が view hierarchy に追加される
setNeedsDisplay() が明示的に呼び出される
3. Prepare
大きい画像の場合は時間がかかる
GPU が直接扱えない color format だった場合、変換される
オリジナルの画像のポインタを渡す代わりに画像のコピーを行うことになるので、時間的にもメモリ的にもコストがかかる
4. Commit
View hierarchy が再起的にパッケージされ、Render server に送信される
深い View hierarchy の場合、パッケージにより時間がかかる
Best Practice
TODO: かく
Render prepare & execute
概要
commit フェーズにて、更新されたレイヤーツリーが Render Server に送信される。この送信を commit と呼ぶ。commit 後の Render の prepare, execute 内で行われる処理は、下記のようになっている。
Render prepare
GPU が処理可能なシンプルな工程のパイプラインに整理する
アニメーションや視覚効果の解釈もこのタイミングで行われる
Render execute
パイプライン内の各ステップを GPU で描画する
GPU が最終的な画像をコンパイルし、表示可能にする
Offscreen Pass
Render prepare フェーズでは、レイヤーのツリーに対し、兄弟から兄弟、親から子へと順番に処理し、テクスチャを描画していく
ただし、影が存在する場合、GPU は影を描画する範囲を特定するために、一度別のテクスチャに影を描画する対象を別途描画して、その上に影を描き、それを元のテクスチャにコピーする工程が入る
この、最終的なテクスチャとは別のテクスチャで描画行うことを Offscreen Rendering と呼ぶ
このように、GPU がレイヤーを別の場所で描写して、最終的なテクスチャにコピーすることを Offscreen Pass と呼ぶ
Offscreen Pass が積み重なると Render が遅れて hitch の原因になる
Offscreen Pass の最適化が考えられるタイプが 4 つある
Shadowing
影の描画範囲を決定するために、影の元になるレイヤーを Offscreen Rendering する
Masking
あるサブツリーのマスキングが必要な場合、まずサブツリー全体を Offscreen Rendering し、マスク内のピクセルのみをコピーする
ユーザが目にしないピクセルをレンダーする場合がある
Rounded rectangles
Masking とほとんど同じ。角丸ではない状態のレイヤーを Offscreen Rendering する必要が出てくる
Visual effects
UIKit では、Vibrancy と Blur エフェクトがある 効果をかける対象のレイヤーを Offscreen Rendering する必要がある
基本的には、この Offscreen Pass の発生をなるだけ避けるようにすると良い
Offscreen Pass の数は、Instruments や view hierarchy debugger で確認できる
Commit hitch: App で発生する hitch
Render hitch: Render server で発生する hitch
TODO: 2 つの hitch の解消方法と best practice についてかく
https://gyazo.com/73258d32149f3c1eed4b21b3a56a7397
Timeline pane には、Render loop に関連するイベントが時系列で可視化されている
table:Track
Track 概要
Hitches ヒッチの発生と持続時間。フレームの準備で超過した時間
User Events hitch したフレームに関連するユーザイベント
Commits Commit フェーズの時間とその処理時間
Renders
GPU
Frame Lifetimes フレームをイベントから表示まで構成するのにかかった時間
Built-in Display ディスプレイ上の全てのフレームと VSYNC のタイミング
Hitch 7 に着目すると、GPU phase が超過しているために Hitch が発生してしまっていることがわかる。
https://gyazo.com/fa02cac66efb416df9f9b5a7c70e9ca4
Detail pane には、各 Hitch の細かい情報を表示できる。Hitch の持続時間と Acceptable latency の時間もわかる。ここで、Acceptable latency は、上図の Frame Lifetime から Hitch Duration を差し引いた時間となる。
https://gyazo.com/b4f671d0a4bfe06ad3c0c3f4d17a7c3e
(WIP)
Hitch について評価する
hitch した時間の絶対値は、単一の hitch を評価するときは有益だが、一連のアニメーションにおける hitch 時間の総計はあまり有益ではない。デバイスによってアニメーションにかかる時間や refresh rate 及びフレーム数も変わってくるため、比較可能な値とならないため。また、iOS デバイスは常に画面を更新するわけではなく、Render server に commit が行われなければフレームは表示されない。 Apple では、hitch time ratio を計測することにしている。hitch time ratio は、ある区間内で発生した hitch 時間の合計を、その区間の時間で割った値。つまり、秒間 の hitch 時間がどの程度かを図ることができるもので、表現としては hitch 時間 (ms) / sec となる。 Apple では、この hitch time ratio がユーザに与える影響度を 3 段階に分けている。
table:hitch time ratio
影響度 hitch time ratio
Critical >= 10ms/s
Warning 5..10 ms/s
Good <= 5ms/s
参考