RenderingNG アーキテクチャ

Chris Harrelson
Chris Harrelson

ここでは、RenderingNG のコンポーネント ピースの設定方法と、レンダリング パイプラインがどのように流れるかについて説明します。

レンダリングのタスクは、上位レベルから順に次のとおりです。

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

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

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

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

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

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

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

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

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

レンダリング パイプラインの図。
矢印は、各ステージの入力と出力を示します。ステージは色で表され、実行するスレッドまたはプロセスを示します。ステージは、状況に応じて複数の場所で実行される場合があります。そのため、一部のステージは 2 色になっています。緑色のステージはレンダリング プロセスのメインスレッド、黄色はレンダリング プロセスのコンポーザ、オレンジ色のステージはビジュアリゼーション プロセスです。

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

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

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

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

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

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

CPU プロセス

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

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

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

異なるサイトは常に異なるレンダリング プロセスに分離されます。

通常、同じサイトの複数のブラウザのタブまたはウィンドウは、タブが関連している(一方が他方を開いているなど)場合を除き、異なるレンダリング プロセスで処理されます。デスクトップ版 Chromium では、メモリ負荷が高い場合、関連していなくても、同じサイトの複数のタブが同じレンダリング プロセスに配置されることがあります。

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

通常、描画する GPU と画面は 1 つしかないため、Chromium 全体に 1 つの Viz プロセスが存在します。

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

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

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

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

スレッド

スレッドを使用すると、タスクの遅延、パイプラインの並列化、複数のバッファリングがあっても、パフォーマンスの分離と応答性を実現できます。

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

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

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

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

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

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

レンダリング プロセスのスレッド処理アーキテクチャは、次の 3 つの最適化パターンを適用したものです。

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

ブラウザのプロセス

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

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

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

ビジュアリゼーション プロセス

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

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

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

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

コンポーネントの構造

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

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

Blink レンダラの図。

Blink レンダラの場合:

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

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

フレームツリーの図。

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

レンダリング プロセスに応じてフレームを色分けできます。上の画像では、緑色の円はすべて 1 つのレンダリング プロセス内のフレームです。オレンジ色の円は 2 つ目のプロセス内にあり、青色の円は 3 つ目のプロセス内にあります。

ローカル フレームツリー フラグメントは、フレームツリー内の同じ色の接続コンポーネントです。画像には、サイト A 用に 2 つ、サイト B 用に 1 つ、サイト C 用に 1 つのローカル フレームツリーがあります。各ローカル フレームツリーには、独自の 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. メインスレッドのイベントループ ランナーは、ドキュメントのライフサイクルを実行します。
  7. メインスレッドは、結果をコンポジタ スレッドに送信します。
  8. コンポジタ イベントループ ランナーは、コンポジットのライフサイクルを実行します。
  9. ラスタータスクはすべて Viz for Raster に送信されます(多くの場合、これらのタスクは複数あります)。
  10. Viz は GPU でコンテンツをラスタライズします。
  11. Viz はラスタータスクの完了を確認します。注: Chromium は通常、ラスター処理が完了するのを待たず、代わりに同期トークンを使用します。このトークンは、ステップ 15 の実行前にラスタータスクによって解決する必要があります。
  12. コンポジタ フレームが Viz に送信されます。
  13. Viz は、foo.com レンダリング プロセス、bar.com iframe レンダリング プロセス、ブラウザ UI のコンポジタ フレームを集約します。
  14. Viz が抽選をスケジュールします。
  15. Viz は、集約されたコンポジタ フレームを画面に描画します。

タブ 2 で CSS 変換遷移をanimateするには:

  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 #2 のハイパーリンクで click イベントを転送するには:

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

重要なポイント

レンダリングの仕組みを覚えて体得するには、かなりの時間がかかります。

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

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

主要なデータ構造について説明します。これは、コード コンポーネントと同様に RenderingNG にとって重要です。


イラスト: Una Kravets