現代ブラウザアーキテクチャ#
レンダリングパイプラインの紹介を始める前に、Chromium のブラウザアーキテクチャと Chromium のプロセスモデルについて前提知識を紹介する必要があります。
2 つの公式#
公式 1:ブラウザ = ブラウザエンジン + サービス
- Safari = WebKit + その他のコンポーネント、ライブラリ、サービス
- Chrome = Chromium + Google サービス統合
- Microsoft Edge (Chromium) = Chromium + Microsoft サービス統合
- Yandex Browser = Chromium + Yandex サービス統合
- 360 セキュリティブラウザ = Trident + Chromium + 360 サービス統合
- Chromium = Blink + V8 + その他のコンポーネント、ライブラリ、サービス
公式 2:エンジン = レンダリングエンジン + JavaScript エンジン + その他
Browser | Rendering Engine | JavaScript Engine |
---|---|---|
Internet Explorer | Trident (MSHTML) | JScript/Chakra |
Microsoft Edge | EdgeHTML → Blink | Chakra → V8 |
Firefox | Gecko | SpiderMonkey |
Safari | KHTML → WebKit | JavaScriptCore |
Chrome | WebKit → Blink | V8 |
Opera | Presto → WebKit → Blink | Carakan → V8 |
ここでは、Firefox とすでに廃止された IE を除いて、市場に出回っているほとんどのブラウザが Blink + V8 または WebKit + JavaScriptCore のルートに進化していることがわかります。
レンダリングエンジン#
HTML、CSS、JavaScript を解析し、ページをレンダリングする役割を担っています。
Firefox を例に挙げると、以下の作業グループがあります:
- Document parser (HTML と XML を処理)
- コンテンツモデルを持つレイアウトエンジン
- スタイルシステム (CSS などを処理)
- JavaScript ランタイム (SpiderMonkey)
- 画像ライブラリ
- ネットワーキングライブラリ (Necko)
- Win32、X、Mac 用のプラットフォーム固有のグラフィックスレンダリングとウィジェットセット
- ユーザー設定ライブラリ
- Mozilla プラグイン API (NPAPI) で Navigator プラグインインターフェースをサポート
- Open Java Interface (OJI)、Sun Java 1.2 JVM を使用
- RDF バックエンド
- フォントライブラリ
- セキュリティライブラリ (NSS)
次に、WebKit の発展の歴史を見てみましょう。
Apple は 2001 年に KHTML を基に WebKit を開発し、Safari のエンジンとして使用しました。その後、Google は 2008 年に WebKit を基に独自の Chromium を開発しました。その時の Chrome のレンダリングエンジンも WebKit を使用していました。2010 年、Apple は WebKit をアップグレードして再構築し、現在の WKWebView と Safari のレンダリングエンジンである WebKit2 が誕生しました。2013 年、Google は WebKit を基に独自のレンダリングエンジン Blink を開発し、現在の Chromium のレンダリングエンジンとなりました。オープンソースライセンスの関係で、現在 Blink のソースコードを見ると、Apple や WebKit の影響が多く見られます。
WebKit の進化のルートは以下の図のようになります:
Web Platform Tests のテストレポートによると、Chromium レンダリングエンジンの互換性も非常に良好です:
JavaScript エンジン#
JavaScript エンジンは通常、ブラウザ内のレンダリングエンジンに組み込まれたモジュールとして存在しますが、その独立性も非常に高く、他の場所に移植して使用することもできます。
ここでは、業界で有名な JavaScript エンジンをいくつか挙げます:
- SpiderMonkey: Mozilla の JavaScript エンジンで、C/C++ で書かれており、Firefox の JavaScript エンジンとして使用されています。
- Rhino: Mozilla のオープンソース JavaScript エンジンで、Java で書かれています。
- Nashorn: Oracle Java Development Kit (JDK) 8 から内蔵された JavaScript エンジンで、Java で書かれています。
- JavaScriptCore: WebKit に内蔵された JavaScript エンジンで、システムが開発者に提供するものです。iOS モバイルアプリケーションは JavaScriptCore を直接ゼロインクリメントで導入できます(ただし、このシナリオでは JIT を有効にできません)。
- ChakraCore: Microsoft のオープンソース JavaScript エンジンで、現在は Edge で Chromium を全面的に使用しています。そのため、Edge の iOS モバイル版を除いて(Chromium の iOS 版は JavaScriptCore を JavaScript エンジンとして使用)、他の Edge では V8 エンジンが使用されています。
- V8: Google のオープンソース JavaScript エンジンで、C++ で書かれており、Chromium(またはさらに言えば Blink)の内蔵 JavaScript エンジンであり、Android システムの WebView の内蔵エンジンでもあります(Android WebView も Chromium ですから、笑)。性能が優れており、JIT を有効にした後の性能は他のエンジンを圧倒します。また、ES 構文の互換性も優れています(後の表を参照)。
- JerryScript: Samsung がオープンソースで開発した JavaScript エンジンで、IoT.js で使用されています。
- Hermes: Facebook がオープンソースで開発した JavaScript エンジンで、React Native などのハイブリッド UI システム向けに設計されています。バイトコードを直接ロードすることをサポートし、JS のロード時間を短縮し、TTI を最適化します。また、エンジンはバイトコードを最適化し、インクリメンタルロードをサポートし、中低端機に優しい設計になっています。ただし、JIT をサポートしていないため、グルー言語のインタープリターとして存在します(モバイルの JS エンジンは JIT の使用を制限します。JIT を有効にすると、ウォームアップ時間が長くなり、ページの初回表示時間に影響を与え、パッケージサイズやメモリ使用量が増加します)。
- QuickJS: FFmpeg の作者 Fabrice Bellard が開発したもので、非常に小さなサイズ(210 KB)で、互換性も良好です。直接バイトコードを生成し、C のネイティブモジュールをインポートすることをサポートし、性能も優れています。シングルコアマシンで 300μs という非常に低い起動時間を持ち、メモリ使用量も非常に少なく、参照カウントを使用してメモリ管理が優れています。QuickJS はハイブリッドアーキテクチャ、ゲームスクリプトシステム、または他の組み込みシステムに非常に適しています。
各エンジンの性能は以下の図に示されています:
ECMAScript 標準のサポート状況:
Chromium プロセスモデル#
Chromium には 5 種類のプロセスがあります:
- ブラウザプロセス:1 つ
- ユーティリティプロセス:1 つ
- Viz プロセス:1 つ
- プラグインプロセス:複数
- レンダープロセス:複数
Chrome 拡張のプラグインプロセスを除けば、レンダリングに強く関連しているのはブラウザプロセス、レンダープロセス、Viz プロセスです。次に、これら 3 つのプロセスを重点的に見ていきます。
レンダープロセス#
- 数量:複数
- 職責:単一のタブ内の単一のサイト(クロスサイト iframe の状況に注意)のレンダリング、アニメーション、スクロール、入力イベントなどを担当します。
- スレッド:
- メインスレッド x 1
- 合成スレッド x 1
- ラスタースレッド x 1
- ワーカースレッド x N
レンダープロセスが担当する領域は WebContent です:
メインスレッド#
職責:
- JavaScript の実行
- イベントループ
- ドキュメントライフサイクル
- ヒットテスト
- イベントスケジューリング
- HTML、CSS などのデータ形式の解析
合成スレッド#
職責:
- 入力ハンドラーとヒットテスター
- Web コンテンツ内のスクロールとアニメーション
- Web コンテンツの最適なレイヤーの計算
- 画像のデコード、描画、ラスタライズタスクの調整(ヘルパー)
合成スレッドのヘルパーの数は CPU コアの数に依存します。
ブラウザプロセス#
- 数量:1 つ
- 職責:ブラウザ UI(WebContent の UI を含まない)のすべての機能を担当し、レンダリング、アニメーション、ルーティング、入力イベントなどを含みます。
- スレッド:
- レンダー&合成スレッド
- レンダー&合成スレッドヘルパー
Viz プロセス#
- 数量:1 つ
- 職責:レンダープロセスとブラウザプロセスから生成されたviz::CompositorFrameを受け取り、合成(Aggregate)し、最終的に GPU を使用して合成結果を画面に表示します。
- スレッド:
- GPU メインスレッド
- 表示合成スレッド
Chromium のプロセスモード#
- Process-per-site-instance:古いバージョンのデフォルト戦略で、あるページから別の新しいページを開いた場合、新しいページと現在のページが同じサイト(ルートドメインとプロトコルが同じ)に属する場合、これらの 2 つのページは 1 つのレンダープロセスを共有します。
- Process-per-site
- Process-per-tab:現在のバージョンのデフォルト戦略で、各タブに 1 つのレンダープロセスを起動します。ただし、サイト内部のクロスサイト iframe も新しいレンダープロセスを起動します。以下の例を参照してください。
- シングルプロセス:単一プロセスモードで、起動パラメータは制御可能で、デバッグに使用されます。
例:
現在、3 つのタブがあり、それぞれ foo.com、bar.com、baz.com の 3 つのサイトを開いていると仮定します。bar.com と baz.com は iframe を含まないが、foo.com は含む場合、そのコードは以下のようになります:
したがって、Process-per-tab モードに従うと、最終的なプロセスモデルは以下の図のようになります:
Chromium レンダリングパイプライン#
前提知識の紹介が完了したので、この記事の核心部分である Chromium レンダリングパイプラインを開始します。
レンダリングパイプラインとは、ネットワークから受け取ったバイトコードを受け取り、それを一歩一歩処理して画面上のピクセルに変換するプロセスです。整理した結果、以下の 13 のプロセスが含まれます:
- 解析
- スタイル
- レイアウト
- プレペイント
- ペイント
- コミット
- 合成
- タイリング
- ラスタ
- アクティベート
- 描画
- 集約
- 表示
各プロセスのモジュールとスレッドを整理し、最終的なパイプラインを以下の図に示します:
以下で、一歩一歩見ていきましょう。
注:この記事は概要に属するため、簡潔さを求め、ソースコードを貼り付けることはありませんが、ソースコードに関与する部分にはソースコードリンクを付け、読者が自分で索引を参照できるようにします。また、いくつかの段階では、より詳細なプロセス分析記事を執筆しており、対応する章の冒頭にリンクを貼ります。興味のある読者は、詳細を読むためにクリックできます。
解析#
本節では、Parsing に関するこのシリーズの記事《Chromium Rendering Pipeline - Parsing》を読むことをお勧めします。
- モジュール:blink
- プロセス:レンダープロセス
- スレッド:メインスレッド
- 職責:ブラウザプロセスのネットワークスレッドから送られてくるバイトを解析し、処理を経て DOM ツリーを生成します。
- 入力:バイト
- 出力:DOM ツリー
この段階でのデータフローは次のようになります:バイト → 文字 → トークン → ノード → オブジェクトモデル(DOM ツリー)
データフローの各転換を整理すると、以下の 5 つの段階が得られます:
- 読み込み:Blink がネットワークスレッドからバイトを受信します。
- 変換:HTMLParser がバイトを文字に変換します。
- トークン化:文字を W3C 標準のトークンに変換します。
- 字句解析:字句解析を通じてトークンを Element オブジェクトに変換します。
- DOM 構築:構築された Element オブジェクトを使用して DOM ツリーを構築します。
読み込み#
職責:Blink がネットワークスレッドからバイトを受信します。
プロセス:
- ブラウザプロセスがウェブページの内容をダウンロードします
- レンダープロセスのコンテンツモジュールに渡します
- blink::DocumentLoader
- blink::HTMLDocumentParser
変換#
職責:バイトを文字に解析します。
コアスタック:
トークン化#
職責:文字をトークンに解析します。
コア関数:
この段階では、link、script、img タグを解析すると、ネットワークリクエストが発生します。また、script を解析する際には、解析された JavaScript が実行されるまで HTML の解析を続行しない必要があります。なぜなら、JavaScript が DOM ツリーの構造を変更する可能性があるからです(例えば、document.write()
など)。
字句解析#
職責:トークンを Element に解析します。
コア関数:
この段階では、スタック構造を使用して Node(HTML タグ)を保存し、後で DOM ツリーを構築できるようにします。例えば、HTMLToken::StartTag
タイプのトークンに対しては、ProcessStartTag
を呼び出してスタックにプッシュし、HTMLToken::EndTag
タイプのトークンに対しては、ProcessEndTag
を呼び出してスタックからポップします。
以下のような DOM ツリーに対して:
各 Node のプッシュとポップのプロセスは以下のようになります:
DOM 構築#
職責:Element を DOM ツリーにインスタンス化します。
最終的な DOM ツリーのデータ構造は、blink::TreeScope
からプレビューできます:
DevTools を使用してページの解析プロセスを確認できます:
ただし、このフレームグラフでは C++ 側のスタック呼び出しは表示されません。カーネル側のスタックの詳細を確認したい場合は、Perfettoを使用してページを録画し分析できます。これにより、C++ 側のスタックの状況を確認できるだけでなく、各呼び出しが属するスレッドや、プロセス間通信時に通信を発信した関数呼び出しと受信した関数呼び出しを示すことができます。
解析が完了した後、フローチャートを改善できます:
スタイル#
- モジュール:blink
- プロセス:レンダープロセス
- スレッド:メインスレッド
- 職責:スタイルエンジンが DOM を遍歴し、CSSOM と照合してスタイル分析(解決)とスタイル再計算(再計算)を行い、レンダーツリーを構築します。
- 入力:DOM ツリー
- 出力:レンダーツリー
レンダーツリーはレンダーオブジェクトで構成され、各レンダーオブジェクトは DOM ノードに対応し、DOM に計算スタイル(ComputedStyle)情報が追加されます。
計算スタイルは DevTools で直接確認でき、CSS デバッグ時によく使用されます。
コア関数:Document::UpdateStyleAndLayout(レイアウト部分はまだ見なくて大丈夫です)
この関数のロジックは以下の図に示されています。この計算スタイルを生成する段階をスタイル再計算(style recalc)と呼びます:
スタイルの完全なフローは以下の図に示されています:
3 つの段階に分けることができます:
- CSS の読み込み
- CSS の解析
- CSS の計算
CSS の読み込み#
コアスタックの出力:
DOM が構築された後、すぐに HTML ページがレンダリングされるわけではなく、CSS の処理が完了するのを待つ必要があります。CSS が読み込まれた後にスタイル再計算などの後続のプロセスが行われるため、CSS がない状態でスタイルのない DOM をレンダリングするのは無意味です。
ブラウザは DOM と CSSOM の両方を持つまでレンダリングをブロックします。 ——Render blocking CSS
CSS の解析#
CSS 解析に関与するデータフローは次のようになります:バイト → 文字 → トークン → StyleRule → RuleMap。バイトの処理については前述の通り、ここでは後続のプロセスに重点を置きます。
最初は:文字 → トークン。
CSS に関与するトークンは以下の図のようになります:
FunctionToken には追加の計算が必要であることに注意してください。例えば、Blink の下層では RGBA32 を使用して Color を保存しています(CSSColor::Create)。私の微基準テストの結果によると、Hex を RGBA32 に変換する方が rgb () よりも約 15% 効率が良いです。
次のステップは:トークン → StyleRule。
StyleRules = セレクタ(selectors) + プロパティセット(properties)。
CSS セレクタの解析は右から左に行われることに注意してください。
例えば、以下の CSS に対して:
解析結果は以下のようになります:
ここで、Blink のデフォルトスタイルについても触れておきます。Blink にはデフォルトスタイルを適用するためのルールがあり、読み込み順序はhtml.css(デフォルトスタイル)→ quirk.css(怪異スタイル)→ android/linux/mac.css(各 OS スタイル)→ other.css(ビジネススタイル)です。
さらに内蔵 CSS の読み込み順序については、blink_resources.grdの設定を参照してください。
最後に、StyleRule → RuleMap。
すべての StyleRule はセレクタタイプに応じて異なる Map に保存されます。これにより、比較時に最初のセレクタに一致するすべてのルールを迅速に取得でき、各ルールは次のセレクタが現在の要素に一致するかどうかを確認します。
- RuleMap id_rules_: id セレクタの RuleMap
- RuleMap class_rules_: クラスセレクタの RuleMap
- RuleMap attr_rules_: 属性セレクタの RuleMap
- RuleMap tag_rules_: タグセレクタの RuleMap
- RuleMap ua_shadow_pseudo_element_rules_: 擬似クラスセレクタの RuleMap
読むことをお勧めします:blink/renderer/core/css/rule_set.h
CSS の計算#
- 産物:ComputedStyle
なぜ CSS スタイルを計算する必要があるのでしょうか?それは、複数のセレクタのスタイルが DOM ノードに命中する可能性があり、親要素のプロパティや UA が提供するプロパティを継承する必要があるからです。
ステップ:
- 命中したセレクタを見つける
- スタイルを設定する
注意すべきは、最終的にスタイルを適用する優先順位の順序です:
ソースコード:ElementRuleCollector::CompareRules :
スタイルを適用する優先順位はセレクタの優先順位の合計であることは知られていますが、これは内部の第 2 レベルの優先順位に過ぎません。最初の 3 つの優先順位が完全に同じ場合、最終的に適用されるスタイルはスタイルの宣言時期に依存します —— 宣言が後ろにあるほど優先順位が高くなります。
以下の図を参照してください:
ここでの h1 のクラスは、main-heading 2 main-heading
と書かれていても順序を入れ替えても、タイトルは青色になります。なぜなら、.main-heading2
の宣言が後ろにあるため、優先順位が高くなるからです。
レイアウト#
- モジュール:blink
- プロセス:レンダープロセス
- スレッド:メインスレッド
- 職責:Element の幾何学的属性、すなわち位置とサイズを処理します。
- 入力:レンダーツリー
- 出力:レイアウトツリー
レイアウトオブジェクトはレンダーオブジェクトの幾何学的属性を記録します。
1 つのレイアウトオブジェクトには、次のようなレイアウト矩形属性が追加されます:
- x
- y
- width
- height
ただし、レイアウトオブジェクトと DOM ノードは 1:1 の関係ではないことに注意してください。理由は以下の図に示されています:
レイアウトプロセスのコア関数:Document::UpdateStyleAndLayout 、このステップを経ると DOM ツリーはレイアウトツリーに変わります。以下の図のコードを参照してください:
各レイアウトオブジェクトノードは位置とサイズ情報を記録します:
レイアウト(reflow)を避けることで、ページのパフォーマンスを向上させることができます。では、どのようにリフローを減らすことができるのでしょうか?主旨は複数のリフローを統合し、最終的にレンダーツリーにフィードバックすることです。具体的には以下の対策があります:
- スタイルではなくクラス名を直接変更する → CSSOM の再生成と合成を避ける
- 頻繁にリフローする要素を「オフライン」にする
- リフローを引き起こす属性を置き換える
- リフローの影響範囲を個別のレイヤー内に制御する
リフローを初めて / 二回目に引き起こす属性については、CSS Triggersを参照できます:
各ブラウザエンジンが属性を処理する方法は異なるため、パフォーマンスを最適化する必要がある場合は、この表を参照して、最適化できる CSS 属性があるかどうかを確認できます。
プレペイント#
- モジュール:blink
- プロセス:レンダープロセス
- スレッド:メインスレッド
- 職責:プロパティツリーを生成し、合成スレッドで使用できるようにし、特定のリソースの重複ラスタを避けます。
- 入力:レイアウトツリー
- 出力:プロパティツリー
プロパティツリーに基づいて、Chromium は特定のノードの変換、クリッピング、エフェクト、スクロールを個別に操作でき、子ノードに影響を与えないようにします。
コア関数:
新しいバージョンの Chromium は CAP(composite after paint)モードに変更されました
プロパティツリーは以下の 4 つのツリーを含みます:
ペイント#
- モジュール:blink
- プロセス:レンダープロセス
- スレッド:メインスレッド
- 職責:Blink が cc の描画インターフェースに接続し、ペイントを行い、cc モジュールのデータソースcc::Layerを生成します。
- 入力:レイアウトオブジェクト
- 出力:PaintLayer (cc::Layer)
注意:cc = content collator(コンテンツ編成器)、Chromium コンポジタではありません。
コア関数:
ペイント段階では、レイアウトツリー内のレイアウトオブジェクトを描画命令に変換し、これらの操作をcc::DisplayItemListに封装し、その後 cc::PictureLayer に注入します。
display item list を生成するプロセスもスタック構造の遍歴です:
次に、以下の HTML を例に挙げます:
対応する生成された display items は以下の図のようになります:
最後に、cc::Layer について紹介します。これは主スレッドで実行され、1 つのレンダープロセス内に 1 つの cc::Layer ツリーしか存在しません。
cc::Layer は矩形領域内の UI を表し、以下のサブクラスは異なるタイプの UI データを表します:
- cc::PictureLayer:自描画型の UI コンポーネントを実現するために使用され、外部が cc::ContentLayerClient インターフェースを実装してcc::DisplayItemListオブジェクトを提供することを許可します。これは一連の描画操作のリストを表し、cc のパイプラインを経て 1 つ以上のviz::TileDrawQuadに変換され、viz::CompositorFrameに保存されます。
- cc::TextureLayer:viz のviz::TextureDrawQuadに対応し、Raster を使用して独自のロジックを持つ UI コンポーネントが使用できます。例えば Flash プラグインや WebGL などです。
- cc::UIResourceLayer/cc::NinePatchLayer:TextureLayer に似ており、ソフトウェアレンダリングに使用されます。
- cc::SurfaceLayer/cc::VideoLayer(廃止):viz のviz::SurfaceDrawQuadに対応し、他の CompositorFrame を埋め込むために使用されます。Blink の iframe やビデオプレーヤーはこのレイヤーを使用して実現できます。
- cc::SolidColorLayer:単色の UI コンポーネントを表示するために使用されます。
コミット#
- モジュール:cc
- プロセス:レンダープロセス
- スレッド:合成スレッド
- 職責:ペイント段階の生成物データ([cc::Layer](https://source.chromium.org/chromium/chromium/src/+/main:cc/layers/layer.h;l=86;drc=416bc691f0aab3a1f344b673df60a00b26cd482a;bpv=1;bpt=1?q=cc::Layer&sq=&ss=chromium/chromium/src))を合成スレッドに提出します。
- 入力:cc::Layer(メインスレッド)
- 出力:LayerImpl(合成スレッド)
コア関数:PushPropertiesTo
コアロジックはLayerTreeHostのデータを LayerTreeHostImpl にコミットすることです。コミットメッセージを受信した場所でブレークポイントを設定すると、スタックは以下のようになります:
合成#
- モジュール:cc
- プロセス:レンダープロセス
- スレッド:合成スレッド
- 職責:ページ全体を特定のルールに従って複数の独立したレイヤーに分割し、更新を隔離しやすくします。
- 入力:PaintLayer (cc::Layer)
- 出力:GraphicsLayer
コア関数:
なぜ合成スレッドが必要なのでしょうか?それでは、このステップがなければ、ペイント後に直接ラスタライズして画面に表示した場合はどうなるでしょうか:
直接ラスタライズして画面に表示すると、ラスタライズに必要なデータソースがさまざまな理由で垂直同期信号が来る前に準備できていない場合、フレームが失われ、「ジャギー」が発生します。
もちろん、ジャギーを避けるために、Chromium は各段階で非常に一般的な最適化を行っています —— キャッシュ。以下の図のように、スタイル、レイアウト、ペイント、ラスタの各段階で対応するキャッシュ戦略が実施され、不要なレンダリングを避け、ジャギーが発生する可能性を減らします:
しかし、これだけのキャッシュ最適化を行っても、単純なスクロールはすべてのピクセルを再度ペイント + ラスタすることになります!
合成段階でレイヤーを分割した結果、Chromium はレンダリング時に必要なレイヤーのみを操作し、他のレイヤーは合成に参加するだけで済むため、レンダリング効率が向上します:
以下の図のように:
wobble クラスに transform アニメーションがある場合、この全体の div ノードは独立した GraphicsLayer であり、アニメーションはこの部分のレイヤーのみをレンダリングすれば済みます。
また、DevTools のレイヤーツールを使用してすべてのレイヤーを確認できます。これにより、このレイヤーが生成された理由、メモリ使用量、これまでに何回描画されたかを確認でき、メモリとレンダリング効率の最適化に役立ちます。
これにより、CSS アニメーションのパフォーマンスが優れている理由が解明されます。なぜなら、合成スレッドが参加し、プロパティツリーに基づいて合成されたレイヤーが CSS アニメーションを合成スレッドで処理できるからです。また、will-change を使用して合成スレッドに事前に通知することで、レイヤーの統合を最適化できます。ただし、この方法は万能ではなく、各レイヤーは一定のメモリを消費します。
合成スレッドは入力イベントを処理する能力も持っています。以下の図のように、ブラウザプロセスからのさまざまなイベントをリッスンします:
ただし、JavaScript でイベントリスナーを登録した場合、入力イベントはメインスレッドに転送されて処理されます。
タイリング#
- モジュール:cc
- プロセス:レンダープロセス
- スレッド:合成スレッド
- 職責:1 つのcc::PictureLayerImplを異なるスケールレベル、異なるサイズに分割し、複数のcc::TileTaskタスクをラスタスレッドに処理させます。
- 入力:LayerImpl(合成スレッド)
- 出力:cc::TileTask(ラスタスレッド)
タイル(Tiling)はラスタの基本作業単位であり、この段階でレイヤー(LayerImpl)はタイルに分割されます。コミットが完了した後、必要に応じてタイルタスクcc::RasterTaskImplが作成され、これらのタスクはラスタスレッドで実行されます。
コア関数:PrepareTiles
この段階では、cc::TileTask タスクをラスタスレッドに提出して分割レンダリング(Tile Rendering)を行います。分割レンダリングとは、ウェブページのキャッシュを 256x256 または 512x512 の小さなブロックに分割し、分割してレンダリングすることを指します。
分割レンダリングの必要性は以下の 2 つの側面で明らかになります:
- GPU 合成は通常 OpenGL ES テクスチャを使用して実装されるため、この時のキャッシュは実際にはテクスチャ(GL Texture)であり、多くの GPU はテクスチャのサイズに制限があります。GPU は任意のサイズのキャッシュをサポートできません。
- 分割キャッシュは、ブラウザが統一されたバッファプールを使用してキャッシュを管理するのを容易にします。小さなバッファプールのキャッシュはすべての WebView で共有されます。ウェブページを開くときは、バッファプールに小さなキャッシュを要求し、ウェブページを閉じるときはこれらのキャッシュが回収されます。
前の環境での分割はマクロ的にレンダリング効率を向上させるのに対し、分割はミクロ的にレンダリング効率を向上させます。
Chromium の分割レンダリング戦略には以下の最適化ポイントもあります:
- ビューポートに近いタイルを優先的に描画します:ラスタはタイルと可視ビューポートの距離に基づいて優先順位を設定し、近いものが優先的にラスタされ、遠いものはラスタの優先順位が低下します。
- 最初にタイルを合成する際に、解像度を下げてテクスチャの合成とアップロードにかかる時間を短縮します。
タイルタスクを提出する位置でブレークポイントを設定すると、この段階の完全なスタックが表示されます:
ラスタ#
- モジュール:cc
- プロセス:レンダープロセス
- スレッド:ラスタースレッド
- 職責:ラスタ段階では、各 TileTask を実行し、最終的にリソースを生成します。このリソースは LayerImpl(cc::PictureLayerImpl)に記録されます。これはDisplayItemList内の描画操作を viz のCompositorFrameに Playback します。
- 入力:cc::TileTask
- 出力:LayerImpl(cc::PictureLayerImpl)
推奨読書:cc/raster/
これらのカラー値ビットマップは OpenGL 参照と共に GPU のメモリに保存されます(GPU もラスタライズを行うことができ、ハードウェアアクセラレーションです)。
さらに、ラスタには画像デコードの能力も含まれます:
ラスタのコアクラスcc::RasterBufferProviderには以下のいくつかの重要なサブクラスがあります:
- cc::GpuRasterBufferProvider:GPU を使用してラスタを行い、ラスタの結果は直接 SharedImage に保存されます。
- cc::OneCopyRasterBufferProvider:Skia を使用してラスタを行い、結果は最初に GpuMemoryBuffer に保存され、その後 GpuMemoryBuffer 内のデータが CopySubTexture を介してリソースの SharedImage にコピーされます。
- cc::ZeroCopyRasterBufferProvider:Skia を使用してラスタを行い、結果は GpuMemoryBuffer に保存され、その後 GpuMemoryBuffer を使用して SharedImage を直接作成します。
- cc::BitmapRasterBufferProvider:Skia を使用してラスタを行い、結果は共有メモリに保存されます。
GPU Shared Image#
いわゆる SharedImage メカニズムは本質的にGPU のデータストレージ能力を抽象化したものであり、アプリケーションが直接データを GPU メモリに保存し、GPU からデータを直接読み取ることを許可し、shared group 境界を越えることを許可します。初期の Chromium では Mailbox メカニズムが使用されていましたが、現在のモジュールはほとんどがGPU Shared Imageに再構築されています。
GPU Shared Image にはクライアント側とサービス側があり、クライアント側はブラウザ / レンダリング / GPU プロセスなどが含まれ、クライアント側は複数存在する可能性があります。一方、サービス側は 1 つだけで、GPU プロセスで実行されます。アーキテクチャ図は以下のようになります:
Chromium で SharedImage メカニズムが使用されるいくつかのシナリオ:
- CC モジュール:まず画面を SharedImage にラスタし、その後 Viz に送信して合成します。
- OffscreenCanvas:まず Canvas の内容を SharedImage にラスタし、その後 Viz に送信して合成します。
- 画像処理 / レンダリング:あるスレッドが画像を GPU にデコードし、別のスレッドが GPU を使用して画像を変更またはレンダリングします。
- ビデオ再生:あるスレッドがビデオを GPU にデコードし、別のスレッドがレンダリングします。
ラスタ化戦略#
合成とラスタの 2 つの段階は同期して行われる(注意:同期が同じスレッドで行われる必要はありません)か非同期で行われるかによって、同期ラスタ化と非同期ラスタ化に分かれます。非同期ラスタ化はすべて分割して行われるため、非同期分割ラスタ化とも呼ばれます。
同期ラスタ化は、Android、iOS、Flutter が使用する同期ラスタ化メカニズムであり、これらは追加のピクセルバッファをサポートして間接的なラスタ化を行います。
同期ラスタ化のレンダリングパイプラインは非常にシンプルで、以下の図のようになります:
非同期ラスタ化は現在のブラウザと WebView が採用している戦略であり、特定の特殊なレイヤー(Canvas、Video など)を除いて、レイヤーは分割ラスタ化されます。各ラスタ化タスクは、対応するレイヤーの対応する分割領域内の描画命令を実行し、結果をその分割のピクセルバッファに書き込みます。さらに、ラスタ化と合成は同じスレッドで実行されず、非同期で行われます。合成中に特定の分割がラスタ化を完了していない場合、その分割は空白を保持するか、チェッカーボードの形状を描画します。
2 つのラスタ化戦略にはそれぞれ利点と欠点があり、以下の表に示されています:
同期ラスタ化 | 非同期ラスタ化 | |
---|---|---|
メモリ使用量 | 非常に良好 | 非常に悪い |
初回表示性能 | 良好 | 一般 |
動的変化するコンテンツのレンダリング効率 | 高い | 低い |
レイヤーアニメーション | 一般 | 慣性アニメーションに絶対的な優位性 |
ラスタ化性能 | 低端機ではやや弱い | 良好 |
メモリ使用量において、同期ラスタ化は絶対的な優位性を持ち、非同期ラスタ化は非常にメモリを消費します。基本的に、ブラウザエンジンの性能の大部分はメモリを交換することで実現されています。
初回表示性能において、同期ラスタ化のパイプラインはより洗練されており、複雑なスケジューリングタスクがないため、より早く画面に表示されます。しかし、この向上は実際には限られており、初回表示性能において、同期ラスタ化は非同期ラスタ化に比べて理論的には 1、2 フレーム早く完了することができ、20 ミリ秒程度の差です(もちろん、ここで非同期ラスタ化のリソースもローカルで読み込まれています)。
動的変化するコンテンツの場合、ページの内容が絶えず変化することを意味し、非同期ラスタ化の中間キャッシュの大部分が無効になります。同期ラスタ化のパイプラインはより洗練されているため、この部分の再レンダリング効率も高くなります。
レイヤーアニメーションに関しては、非同期ラスタ化が絶対的な優位性を持っています。前述のプロパティツリーと合成により、再レンダリングのレイヤー範囲を制御でき、効率が非常に高くなります。非同期ラスタ化は、特に複雑なページアニメーションの場合にその利点を発揮します。慣性スクロールの場合、非同期ラスタ化はビューポート外の領域を事前にラスタ化して体験を最適化します。しかし、同期ラスタ化もそれぞれの特性を持ち、iOS、Android、Flutter では、セルレベルの再利用メカニズムを強調してスクロール効果を最適化します。
最後に、ラスタ化性能において、同期ラスタ化はより高い性能要求を持ち、大量の CPU 計算が必要であるため、低端機ではフレームが持続的に失われる可能性があります。しかし、スマートフォンの CPU 性能が向上するにつれて、同期ラスタ化戦略の優位性がより明確になります。なぜなら、非同期ラスタ化に対して絶対的なメモリの優位性を持ち、慣性アニメーションに対しても再利用メカニズムを通じて解決できるため、全体的な優位性はかなり明確です。
さらに、非同期ラスタ化には、ページが白くなる、スクロール中に DOM が非同期で更新されるなどの回避できない問題もあります。
アクティベート#
- モジュール:cc
- プロセス:レンダープロセス
- スレッド:合成スレッド
- 職責:バッファメカニズムを実装し、描画段階の操作前にラスタのデータが準備されていることを確認します。具体的には、レイヤーツリーを [Pending Tree](https://source.chromium.org/chromium/chromium/src/+/main:cc/trees/layer_tree_host_impl.h;l=656;d