RenderingNG アーキテクチャ

Chris Harrelson
Chris Harrelson

ここでは、RenderingNG のコンポーネント部分の設定方法と、レンダリング パイプラインの流れについて説明します。

レンダリングの上位レベルでは、次のタスクを行います。

  1. 画面上のピクセルにコンテンツをレンダリングします。
  2. コンテンツの視覚効果をアニメーション化します。
  3. 入力に応じてスクロールする。
  4. デベロッパー スクリプトや他のサブシステムが応答できるように、適切な場所に効率的に入力をルーティングする。

レンダリングされるコンテンツは、各ブラウザタブとブラウザ インターフェースのフレームツリーです。タッチ スクリーン、マウス、キーボード、その他のハードウェア デバイスからの未加工の入力イベントのストリーム。

各フレームには次のものが含まれます。

  • DOM 状態
  • CSS
  • キャンバス
  • 画像、動画、フォント、SVG などの外部リソース

フレームは、HTML ドキュメントとその URL です。ブラウザタブに読み込まれたウェブページには、最上位フレーム、最上位ドキュメントに含まれる各 iframe の子フレーム、およびその子孫の再帰的な iframe が含まれます。

視覚効果とは、スクロール、変換、クリップ、フィルタ、不透明度、ブレンドなど、ビットマップに適用されるグラフィック操作のことです。

アーキテクチャ コンポーネント

RenderingNG では、これらのタスクは複数のステージとコード コンポーネントに論理的に分割されます。コンポーネントは、最終的にそれらのスレッド内のさまざまな CPU プロセス、スレッド、サブコンポーネントに配置されます。それぞれが、すべてのウェブ コンテンツに対して信頼性スケーラブルなパフォーマンス拡張性を達成するうえで重要な役割を果たします。

パイプライン構造のレンダリング

レンダリング パイプラインの図。
矢印は各ステージの入力と出力を示しています。ステージは色で示され、どのスレッドまたはプロセスが実行されているかがわかります。状況によってはステージが複数の場所で実行される場合があります。そのため、一部のステージでは 2 色が使用されます。緑色のステージはレンダリング プロセスのメインスレッド、黄色はレンダリング プロセスのコンポジター、オレンジ色のステージは viz プロセスです。

レンダリングはパイプラインに沿って進み、その過程で多数のステージとアーティファクトが作成されます。各ステージは、レンダリング内で明確に定義された 1 つのタスクを実行するコードを表します。アーティファクトは、ステージの入力または出力であるデータ構造です。

ステージは次のとおりです。

  1. アニメーション化: 計算されたスタイルを変更し、宣言型のタイムラインに基づいて、時間の経過とともにプロパティ ツリーを変更します。
  2. スタイル: CSS を DOM に適用し、計算されたスタイルを作成します。
  3. レイアウト: 画面上の DOM 要素のサイズと位置を決定し、不変のフラグメント ツリーを作成します。
  4. 事前ペイント: プロパティ ツリーを計算し、必要に応じて既存のディスプレイ リストと GPU テクスチャ タイルを無効化します。
  5. スクロール: プロパティ ツリーを変更することで、ドキュメントとスクロール可能な DOM 要素のスクロール オフセットを更新します。
  6. ペイント: DOM から GPU テクスチャ タイルをラスター化する方法を記述するディスプレイ リストを計算します。
  7. commit: プロパティ ツリーとディスプレイ リストをコンポジタ スレッドにコピーします。
  8. レイヤ化: ディスプレイ リストを合成レイヤリストに分割し、独立したラスタライズとアニメーションを実現します。
  9. ラスター、デコード、ペイント ワークレット: ディスプレイ リスト、エンコード画像、ペイント ワークレット コードをそれぞれ GPU テクスチャ タイルに変換します。
  10. Activate: GPU タイルを画面に描画して配置する方法を表すコンポジタ フレームを、視覚効果とともに作成します。
  11. 集約: 表示されているすべてのコンポジター フレームからのコンポジター フレームを、単一のグローバル コンポジター フレームに結合します。
  12. Draw: 集約されたコンポジタ フレームを GPU で実行し、画面上にピクセルを作成します。

レンダリング パイプラインのステージは、不要な場合はスキップできます。たとえば、視覚効果とスクロールのアニメーションでは、レイアウト、プリペイント、ペイントをスキップできます。そのため、図ではアニメーションとスクロールに黄色と緑色のドットが付いています。視覚効果のためにレイアウト、プリペイント、ペイントをスキップできる場合は、それらをすべてコンポジタ スレッドで実行し、メインスレッドをスキップできます。

ブラウザ UI のレンダリングはここでは直接表していませんが、同じパイプラインの簡易版と考えることができます(実際、実装では多くのコードが共有されています)。動画(直接表示されていない)は通常、フレームを GPU テクスチャ タイルにデコードしてからコンポジタ フレームと描画ステップに接続する独立したコードでレンダリングされます。

プロセスとスレッドの構造

CPU プロセス

複数の CPU プロセスを使用することで、サイト間およびブラウザの状態からパフォーマンスとセキュリティを分離し、GPU ハードウェアから安定性とセキュリティを分離できます。

CPU プロセスのさまざまな部分の図

  • レンダリング プロセスでは、1 つのサイトとタブの組み合わせに対する入力のレンダリング、アニメーション化、スクロール、ルーティングを行います。レンダリング プロセスがいくつかあります。
  • ブラウザ プロセスは、ブラウザ UI への入力(アドレスバー、タブのタイトル、アイコンなど)をレンダリング、アニメーション化、ルーティングし、残りのすべての入力を適切なレンダリング プロセスにルーティングします。ブラウザ プロセスは 1 つです。
  • Viz プロセスは、複数のレンダリング プロセスとブラウザ プロセスからの合成を集約します。GPU を使用してラスター化と描画を行います。Viz プロセスは 1 つあります。

サイトが異なれば、レンダリング プロセスも常に異なります。

通常、同じサイトの複数のブラウザタブまたはウィンドウは、互いに関連するレンダリング プロセス(一方が他方を開いているなど)でない限り、異なるレンダリング プロセスで実行されます。パソコンのメモリ負荷が高い場合、Chromium では、関連性がない場合でも、同じサイトの複数のタブを同じレンダリング プロセスに入れることがあります。

1 つのブラウザタブ内では、異なるサイトのフレームは常に異なるレンダリング プロセスに含まれますが、同じサイトのフレームは常に同じレンダリング プロセスに含まれます。レンダリングの観点から見ると、複数のレンダリング プロセスを使用する重要な利点は、クロスサイトの iframe とタブが互いにパフォーマンスを分離できることです。さらに、オリジンではさらなる分離にオプトインできます。

通常、描画先となる GPU と画面は 1 つのみであるため、Chromium 全体で Viz プロセスは 1 つだけです。

Viz を独自のプロセスに分離すると、GPU ドライバやハードウェアのバグが発生しても安定性が向上します。また、セキュリティの分離にも役立ちます。Vulkan などの GPU API や一般的なセキュリティは重要です。

ブラウザには多数のタブとウィンドウがあり、そのすべてにブラウザ UI ピクセルが描画されるため、なぜブラウザ プロセスが 1 つだけあるのか、不思議に思われるかもしれません。これは、一度に 1 つのみがフォーカスされるためです。実際、非表示のブラウザタブはほとんどの場合無効になり、GPU メモリはすべて破棄されます。ただし、複雑なブラウザ UI レンダリング機能がレンダリング プロセスにも実装されるようになってきています(WebUI と呼ばれます)。これは、パフォーマンスを分離する理由ではなく、Chromium のウェブ レンダリング エンジンの使いやすさを活用するためのものです。

古い Android デバイスの場合、WebView で使用すると、レンダリングとブラウザのプロセスは共有されます(Android の Chromium には当てはまらず、WebView のみです)。WebView では、ブラウザ プロセスは埋め込みアプリとも共有され、WebView にはレンダリング プロセスは 1 つだけです。

保護された動画コンテンツをデコードするためのユーティリティ プロセスが存在する場合もあります。このプロセスは前の図には示されていません。

スレッド

スレッドは、遅いタスク、パイプラインの並列化、複数のバッファリングにもかかわらず、パフォーマンスの分離と応答性を達成するのに役立ちます。

レンダリング プロセスの図。

  • メインスレッドは、スクリプト、レンダリング イベント ループ、ドキュメントのライフサイクル、ヒットテスト、スクリプト イベントのディスパッチ、HTML や CSS などのデータ形式の解析を行います。
    • メインスレッド ヘルパー。エンコードやデコードが必要な画像ビットマップや blob の作成などのタスクを実行します。
    • ウェブワーカー: 実行スクリプトと、OffscreenCanvas のレンダリング イベント ループ。
  • コンポジタ スレッドは、入力イベントの処理、ウェブ コンテンツのスクロールとアニメーションの実行、ウェブ コンテンツの最適なレイヤ化の計算、画像のデコード、ペイント ワークレット、ラスタータスクの調整を行います。
    • コンポジタ スレッド ヘルパー: Viz ラスタータスクを調整し、画像デコード タスク、ペイント ワークレット、フォールバック ラスターを実行します。
  • メディア、デマルチプレクサー、オーディオ出力スレッドは、動画ストリームとオーディオ ストリームのデコード、処理、同期を行います。(動画はメインのレンダリング パイプラインと並行して実行されます)。

メインスレッドとコンポジタ スレッドを分離することは、アニメーションとスクロールのパフォーマンスをメインスレッドの処理から分離するうえで非常に重要です。

同じサイトの複数のタブやフレームが同じプロセスに入ることはありますが、レンダリング プロセスごとにメインスレッドは 1 つだけです。ただし、さまざまなブラウザ API で実行される作業からパフォーマンスは分離されます。たとえば、Canvas API での画像ビットマップと blob の生成は、メインスレッドのヘルパー スレッドで実行されます。

同様に、コンポジタ スレッドはレンダリング プロセスごとに 1 つだけです。一般的に、コンポジタ スレッドで高コストのオペレーションはすべてコンポジタ ワーカー スレッドまたは Viz プロセスに委任され、この作業は入力ルーティング、スクロール、アニメーションと並行して行えるため、1 つしかないことは問題ではありません。コンポジタ ワーカー スレッドは Viz プロセスで実行されるタスクを調整しますが、どこでもの GPU アクセラレーションは、ドライバのバグなど、Chromium では制御できない理由で失敗する可能性があります。このような場合、ワーカー スレッドは CPU のフォールバック モードで処理を実行します。

コンポジタ ワーカー スレッドの数は、デバイスの機能によって異なります。たとえば、デスクトップはモバイル デバイスよりも CPU コアの数が多く、バッテリーの制約が少ないため、一般的により多くのスレッドを使用します。これはスケールアップとスケールダウンの例です。

レンダリング プロセスのスレッド アーキテクチャには、次の 3 つの異なる最適化パターンが適用されます。

  • ヘルパー スレッド: 長時間実行サブタスクを追加スレッドに送信して、親スレッドが他の同時リクエストに対する応答性を維持します。メインスレッドのヘルパー スレッドとコンポジタのヘルパー スレッドは、この手法の好例です。
  • マルチ バッファリング: 新しいコンテンツのレンダリング中に以前にレンダリングされたコンテンツを表示し、レンダリングのレイテンシを隠します。コンポジタ スレッドはこの手法を使用します。
  • パイプラインの並列化: レンダリング パイプラインを複数の場所で同時に実行します。これにより、スクロールとアニメーションが高速になります。メインスレッドのレンダリング更新が進行中の場合でも、スクロールとアニメーションを並行して実行できます。

ブラウザのプロセス

レンダリングおよび合成スレッドと、レンダリングおよび合成スレッド ヘルパーの関係を示すブラウザのプロセス図。

  • レンダリングと合成のスレッドは、ブラウザ UI での入力に応答し、他の入力を適切なレンダリング プロセスにルーティングして、ブラウザ UI のレイアウトとペイントを行います。
  • レンダリングと合成スレッド ヘルパーは、画像デコード タスクと、フォールバックのラスターまたはデコードを実行します。

ブラウザ プロセスのレンダリングと合成スレッドは、メインスレッドとコンポジタ スレッドが 1 つに結合されている点を除き、レンダリング プロセスのコードと機能と似ています。この場合、必要なスレッドは 1 つだけです。設計上存在しないため、長いメインスレッド タスクからパフォーマンスを分離する必要はないからです。

Viz のプロセス

Viz プロセスには、GPU メインスレッドとディスプレイ コンポジタ スレッドが含まれます。

  • GPU メインスレッドは、ディスプレイ リストと動画フレームを GPU テクスチャ タイルにラスター化し、コンポジタ フレームを画面に描画します。
  • ディスプレイ コンポジタ スレッドは、各レンダリング プロセスとブラウザ プロセスからの合成を集約し、画面に表示するために単一のコンポジタ フレームに最適化します。

ラスターと描画はどちらも GPU リソースに依存しており、確実にマルチスレッドで GPU を使用することは難しいため、通常、ラスターと描画は同じスレッドで実行されます(新しい Vulkan 標準を開発する動機の一つは、GPU へのマルチスレッド アクセスを簡単にすることです)。Android WebView では、ネイティブ アプリに WebView を埋め込む方法により、描画用の OS レベルのレンダリング スレッドが別途存在します。他のプラットフォームにも、今後このようなスレッドが存在する可能性があります。

ディスプレイ コンポジタは、常に応答可能である必要があり、GPU メインスレッドで速度低下の潜在的な原因をブロックしない必要があるため、別のスレッドにあります。GPU メインスレッドの速度が低下する原因の 1 つは、ベンダー固有の GPU ドライバなどの Chromium 以外のコードの呼び出しです。これは予測が難しい方法で遅くなる可能性があります。

コンポーネントの構造

各レンダリング プロセスのメインスレッドまたはコンポジタ スレッド内には、構造化された方法で相互にやり取りする論理ソフトウェア コンポーネントがあります。

レンダリング プロセスのメインスレッド コンポーネント

Blink レンダラの図。

Blink レンダラでの場合:

  • ローカル フレーム ツリー フラグメントは、ローカル フレームのツリーとフレーム内の DOM を表します。
  • DOM と Canvas API コンポーネントには、これらすべての API の実装が含まれています。
  • ドキュメント ライフサイクル ランナーは、commit ステップまでのレンダリング パイプライン ステップを実行します。
  • 入力イベントのヒットテストとディスパッチ コンポーネントは、ヒットテストを実行してイベントのターゲットとする DOM 要素を特定し、入力イベントのディスパッチ アルゴリズムとデフォルトの動作を実行します。

レンダリング イベントループのスケジューラとランナーは、イベントループで何を、いつ実行するかを決定します。デバイスのディスプレイと同じ頻度でレンダリングが行われるようにスケジュール設定します。

フレームツリーの図。

ローカル フレームツリー フラグメントは少し複雑です。フレームツリーは、再帰的にメインページとその子 iframe です。フレームがレンダリングされる場合、フレームはレンダリング プロセスに対してローカルであり、それ以外の場合はリモートです。

レンダリング プロセスに応じてフレームに色を付けることができます。上の画像では、緑色の円は 1 つのレンダリング プロセス内のすべてのフレームを示しています。オレンジ色の円は 2 つ目のレンダリング プロセス、青色の円は 3 つ目のレンダリング プロセス中のフレームです。

ローカル フレームツリー フラグメントは、フレームツリー内の同じ色の連結コンポーネントです。この画像には 4 つのローカル フレームツリーがあります。2 つはサイト A 用、1 つはサイト B 用、1 つはサイト C 用です。 各ローカル フレームツリーは、独自の Blink レンダラ コンポーネントを取得します。ローカル フレームツリーの Blink レンダラは、他のローカル フレームツリーと同じレンダリング プロセスにあるとは限りません。前述のように、レンダリング プロセスの選択方法によって決まります。

レンダリング プロセスのコンポジタ スレッド構造

レンダリング プロセスのコンポジタ コンポーネントを示す図。

レンダリング プロセスのコンポジタ コンポーネントには、次のものがあります。

  • 合成されたレイヤリスト、表示リスト、プロパティ ツリーを管理するデータハンドラ
  • レンダリング パイプラインのアニメーション、スクロール、複合、ラスター、デコードとアクティブ化のステップを実行するライフサイクル ランナー。(アニメーションとスクロールは、メインスレッドとコンポジタの両方で行うことができます)。
  • 入力とヒットテストのハンドラは、合成レイヤの解像度で入力処理とヒットテストを実行し、コンポジタ スレッドでスクロール操作を実行できるかどうかと、どのレンダリング プロセスのヒットテストをターゲットにするかを判断します。

実際のアーキテクチャ例

この例では、3 つのタブがあります。

タブ 1: foo.com

<html>
  <iframe id=one src="foo.com/other-url"></iframe>
  <iframe  id=two src="bar.com"></iframe>
</html>

タブ 2: bar.com

<html>
 …
</html>

タブ 3: baz.com html <html> … </html>

これらのタブのプロセス、スレッド、コンポーネントの構造は次のようになります。

タブのプロセスの図。

レンダリングの 4 つの主要なタスクについて、それぞれ例を 1 つずつ見ていきましょう。留意点:

  1. コンテンツを画面上のピクセルにレンダリングします。
  2. コンテンツの視覚効果をある状態から別の状態にアニメーション化します。
  3. 入力に応じてスクロールする。
  4. デベロッパー スクリプトや他のサブシステムが応答できるように、入力を適切な場所に効率的にルーティングします。

タブ 1 の変更した DOM をレンダリングするには:

  1. 開発者スクリプトは、foo.com のレンダリング プロセスで DOM を変更します。
  2. Blink レンダラは、レンダリングを行う必要があることをコンポジタに伝えます。
  3. コンポジタは、レンダリングを行う必要があることを Viz に伝えます。
  4. Viz はレンダリングの開始をコンポジタに返します。
  5. コンポジタは開始シグナルを Blink レンダラに転送します。
  6. main スレッドのイベント ループランナーは、ドキュメントのライフサイクルを実行します。
  7. メインスレッドが結果をコンポジタ スレッドに送信します。
  8. コンポジタ イベント ループ ランナーは、合成ライフサイクルを実行します。
  9. ラスタータスクはすべてラスター用の Viz に送信されます(多くの場合、これらのタスクは複数あります)。
  10. Viz は GPU 上でコンテンツをラスター化します。
  11. Viz がラスタータスクの完了を確認します。注: Chromium は多くの場合、ラスターの完了を待たず、代わりに同期トークンと呼ばれるものを使用します。同期トークンは、ステップ 15 を実行する前にラスタータスクによって解決される必要があります。
  12. コンポジタ フレームが Viz に送信されます。
  13. Viz は、foo.com レンダリング プロセス、bar.com iframe レンダリング プロセス、ブラウザ UI のコンポジタ フレームを集約します。
  14. Viz が抽選のスケジュールを設定します。
  15. Viz が、集約されたコンポジタ フレームを画面に描画します。

タブ 2 の CSS 変換遷移をアニメーション化するには:

  1. bar.com レンダリング プロセスのコンポジタ スレッドは、既存のプロパティ ツリーを変更することで、コンポジタ イベントループでアニメーションをオンにします。これにより、コンポジタのライフサイクルが再実行されます。(ラスタータスクとデコードタスクが発生する場合もありますが、ここでは説明しません)。
  2. コンポジタ フレームが Viz に送信されます。
  3. Viz は、foo.com レンダリング プロセス、bar.com レンダリング プロセス、ブラウザ UI のコンポジタ フレームを集約します。
  4. Viz が抽選のスケジュールを設定します。
  5. Viz が、集約されたコンポジタ フレームを画面に描画します。

タブ 3 のウェブページをスクロールするには:

  1. 一連の input イベント(マウス、タップ、キーボード)がブラウザ プロセスに到着します。
  2. 各イベントは、baz.com のレンダリング プロセスのコンポジタ スレッドにルーティングされます。
  3. コンポジタは、メインスレッドがイベントを認識する必要があるかどうかを判断します。
  4. 必要に応じて、イベントがメインスレッドに送信されます。
  5. メインスレッドが input イベント リスナー(pointerdowntouchstarpointermovetouchmovewheel)を起動して、リスナーがイベントで preventDefault を呼び出すかどうかを確認します。
  6. メインスレッドは、preventDefault がコンポジタに呼び出されたかどうかを返します。
  7. イベントがない場合、入力イベントはブラウザ プロセスに返されます。
  8. ブラウザ プロセスが他の最近のイベントと組み合わせてスクロール操作に変換します。
  9. スクロール操作が再び baz.com のレンダリング プロセスのコンポジタ スレッドに送信されます。
  10. そこでスクロールが適用され、bar.com レンダリング プロセスのコンポジタ スレッドがコンポジタ イベントループでアニメーションをオンにします。これにより、プロパティ ツリーのスクロール オフセットが変更され、コンポジタのライフサイクルが再実行されます。また、メインスレッドに scroll イベントを発行するように指示します(ここでは示していません)。
  11. コンポジタ フレームが Viz に送信されます。
  12. Viz は、foo.com レンダリング プロセス、bar.com レンダリング プロセス、ブラウザ UI のコンポジタ フレームを集約します。
  13. Viz が抽選のスケジュールを設定します。
  14. Viz が、集約されたコンポジタ フレームを画面に描画します。

タブ 1 の iframe #two のハイパーリンクで click イベントをルーティングするには:

  1. input イベント(マウス、タップ、キーボード)がブラウザ プロセスに到着します。近似ヒットテストを実行して、bar.com iframe レンダリング プロセスがクリックを受信する必要があるかどうかを判断し、そこに送信します。
  2. bar.com のコンポジタ スレッドは、click イベントを bar.com のメインスレッドにルーティングし、レンダリング イベントループ タスクをスケジュールして処理します。
  3. bar.com のメインスレッド ヒット用の入力イベント プロセッサは、iframe 内のどの DOM 要素がクリックされたかを判断するためにテストを行い、click イベントを発生させてスクリプトを監視します。preventDefaultが聞こえない場合は、ハイパーリンクに移動します。
  4. ハイパーリンクのリンク先ページが読み込まれると、前述の「変更された DOM」の例と同様の手順により、新しい状態がレンダリングされます。(この後の変更はここでは示していません)。

重要なポイント

レンダリングの仕組みを覚えて理解するには、多くの時間がかかります。

最も重要なポイントは、レンダリング パイプラインが、慎重なモジュール化と細部への配慮により、多数の自己完結型のコンポーネントに分割されたことです。これらのコンポーネントは、スケーラブルなパフォーマンス拡張性の機会を最大化するために、並列プロセスとスレッドに分割されました。

各コンポーネントは、最新のウェブアプリのパフォーマンスと機能を有効にするうえで重要な役割を果たします。

主要なデータ構造についてお読みください。これは、RenderingNG にとってコード コンポーネントと同様に重要です。


イラスト: Una Kravets。