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

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

コンポーネントの構造

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

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

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. コンテンツの視覚効果をある状態から別の状態にAnimateします。
  3. 入力に応じてスクロールします。
  4. デベロッパー スクリプトや他のサブシステムが応答できるように、入力を適切な場所に効率的にルーティングします。

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

  1. デベロッパー スクリプトが foo.com のレンダリング プロセスで DOM を変更します。
  2. Blink レンダラは、レンダリングが必要であることをコンポジタに伝えます。
  3. コンポジタは、レンダリングが必要であることを Viz に伝えます。
  4. Viz はレンダリングの開始をコンポジタに返します。
  5. コンポジタは開始シグナルを Blink レンダラに転送します。
  6. メインスレッドのイベントループ ランナーは、ドキュメントのライフサイクルを実行します。
  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。