轉譯 NG 深入解析:BlinkNG

Stefan Zager
Stefan Zager
Chris Harrelson
Chris Harrelson

Blink 是指 Chromium 實作的網路平台,涵蓋所有轉譯前的轉譯階段 (完成合成器修訂版本)。如要進一步瞭解閃爍轉譯架構,請參閱本系列的先前的文章

Blink 起源於 WebKit 的分支,它本身是 KHTML 的分支,其歷史可追溯至 1998 年。這個套件包含 Chromium 中一些最舊 (且最重要的) 程式碼,而在 2014 年之前,就明確展示了使用時間。今年,我們在 BlinkNG 規劃了一系列宏大專案,設計目的是解決組織和 Blink 程式碼結構長久以來的缺陷。本文將探討 BlinkNG 及其組成專案:我們採取的措施、他們完成的是什麼、形塑設計的指導原則,以及他們日後可負擔的改善機會。

BlinkNG 前後的轉譯管道。

轉譯 NG 前轉譯

Blink 中的轉譯管道在概念上一般可分為多個階段 (stylelayout Paint 等等),但抽象化的障礙物已外洩。大致來說,與算繪相關的資料,由長效型且可變動的物件組成。這些物件隨時都可以修改,也可用於修改,而且經常因連續的算繪更新而回收再利用。很難確實回答簡單的問題,例如:

  • 是否需要更新樣式、版面配置或油漆的輸出內容?
  • 這些資料何時會獲得「最終」值呢?
  • 何時可以修改這些資料?
  • 何時會刪除這個物件?

以下列舉幾個例子:

樣式會根據樣式表產生 ComputedStyle;但 ComputedStyle 不可變動;在某些情況下,可能會被後續的管道階段修改。

樣式會產生 LayoutObject 的樹狀結構,然後版面配置會以大小和位置資訊為這些物件加上註解。在某些情況下,版面配置甚至會修改樹狀結構。沒有明確區分 layout 的輸入和輸出內容。

「樣式」會產生決定「合成」的配件資料結構,並由 style 之後各個階段修改這些資料結構。

在較低層級的算繪資料類型,大多是由特殊樹狀結構 (例如 DOM 樹狀結構、樣式樹狀結構、版面配置樹狀結構、繪製屬性樹狀結構)。而轉譯階段則是以遞迴樹走動實作。在理想情況下,應「納入」樹狀步道:在處理指定樹狀結構時,我們不應存取位於該節點根樹狀結構的樹狀子目錄之外的任何資訊。在導入之前,這就不是真的;樹狀結構會傾向從所處理節點的祖系取得經常存取的資訊。因此系統相當脆弱,容易出錯。要從任何位置開始樹步,而是樹根基底,也是不可能的任務。

最後,轉譯管道中有許多入口在程式碼中,包括:強制由 JavaScript 觸發的版面配置、在文件載入期間觸發的部分更新、強制更新以準備事件目標、顯示系統要求的排程更新,以及僅開放測試程式碼的特殊 API 等等。轉譯管道中甚至有幾個遞迴可重新傳送的路徑,也就是從另一個階段中跳到一個階段的起始路徑。每個 RAM 都有各自的慣用行為,在某些情況下,算繪結果會視觸發算繪更新的方式而定。

異動內容

BlinkNG 由許多大型和小型子專案組成,其共同目標為消除前述架構缺陷。這些專案會分享幾項指導原則,讓轉譯管道更像是實際管道:

  • 統一進入點:我們應該一律在開頭進入管線。
  • 功能階段:每個階段中都應該有定義明確的輸入和輸出內容,其行為必須「功能性」,也就是確定性且可重複,而輸出結果應只依賴定義的輸入內容。
  • 常數輸入內容:任何階段的輸入內容都應該在階段執行時有效。
  • 不可變動的輸出內容:階段完成後,在轉譯更新的其餘部分,其輸出內容應無法變更。
  • 檢查點一致性:在每個階段結束時,到目前為止產生的轉譯資料應處於自我一致性狀態。
  • 簡化工作:每個項目只會計算一次。

完整的 BlinkNG 子專案清單會讓閱讀過程變得很繁瑣,但以下列舉幾個會發生的情況。

文件生命週期

DocumentLifecycle 類別會透過轉譯管道追蹤我們的進度。它可讓我們執行基本檢查,強制執行前述的不變體,例如:

  • 如果是修改 ComputedStyle 屬性,則文件生命週期必須為 kInStyleRecalc
  • 如果 DocumentLifecycle 狀態為 kStyleClean 以上,則 NeedsStyleRecalc() 必須為任何附加節點傳回 false
  • 進入「繪畫」生命週期階段時,生命週期狀態必須為 kPrePaintClean

在實作 BlinkNG 的過程中,我們有系統地刪除了違反這些不變數的程式碼路徑,並在整個程式碼中加入更多斷言,以確保不會迴歸。

如果您曾仔細看著低階轉譯程式碼,或許會想問自己:「那是怎麼到這裡?」如前所述,進入轉譯管道有許多進入點。過去,這類路徑包括遞迴和回歸的呼叫路徑,以及我們在中階階段進入管道的位置,而非從頭開始。在 BlinkNG 期間,我們分析了這些呼叫路徑,並判定這些路徑在兩種基本情境下都能縮減:

  • 所有轉譯資料都需要更新,例如為多媒體廣告產生新像素,或針對事件指定目標執行命中測試時。
  • 我們需要特定查詢的最新值,無須更新所有轉譯資料即可解決。其中包括大部分的 JavaScript 查詢,例如 node.offsetTop

轉譯管道現在只有兩個進入點,分別對應這兩種情況。重構程式碼路徑已移除或重構,且無法再從中階階段進入管道。這樣可以消除了轉譯更新發生時間與方式的神秘面紗,更輕鬆地理解系統行為。

管線樣式、版面配置和預先繪製

整體而言,繪製前的轉譯階段會造成以下情況:

  • 執行樣式瀑布演算法,計算 DOM 節點的最終樣式屬性。
  • 產生代表文件方塊階層的版面配置樹狀結構。
  • 決定所有方塊的大小和位置資訊。
  • 將子像素幾何圖形四捨五入或對齊整個像素邊界,以便進行繪製。
  • 確定複合層的屬性 (肯定層轉換、篩選器、不透明度或其他任何可以 GPU 加速的項目)。
  • 判斷哪些內容自上次繪製階段以來有哪些變化,需要重新繪製或重新繪製 (繪畫無效)。

這份清單並未改變,但在 BlinkNG 之前大多以臨時方式完成,且分散於多個算繪階段,具有大量重複功能且內建效率低下。舉例來說,style 階段一直主要負責計算節點的最終樣式屬性,但在少數特殊情況下,我們必須等到 style 階段完成後,才無法決定最終的樣式屬性值。算繪過程中,也沒有正式或可強制執行的時間點,因此必須確定樣式資訊完整且無法變更。

發生 BlinkNG 問題還有另一個很好的例子,就是繪製撤銷。先前在繪製前的所有算繪階段中,油漆撤銷是一體的,修改樣式或版面配置程式碼時,很難知道需要變更哪些繪製撤銷邏輯,而且很容易犯錯導致缺乏適當錯誤或過度無效的錯誤。如果想進一步瞭解舊版繪製撤銷系統的細節,請參閱 LayoutNG 相關系列文章。

將子像素版面配置幾何圖形貼至整個像素界線進行繪製,就是我們多次以相同功能實作相同功能,以及執行許多多餘的工作。還有一個像素間隔的程式碼路徑,是繪製系統使用的路徑,而每當我們需要在繪製程式碼之外進行一次性的像素化座標時,就會使用一個完全獨立的程式碼路徑。無庸置疑,每項導入方式都有各自的錯誤,但結果不一定一致。由於這項資訊並未快取,因此系統有時會重複執行完全相同的運算,這會再次對效能造成負擔。

這裡有一些重要專案,已經消除了算繪階段的架構缺陷。

Project Squad:改變風格階段

這項專案在風格階段中解決了兩大缺陷,導致資料無法完全傳播:

樣式階段有兩個主要輸出內容:ComputedStyle,包含對 DOM 樹狀結構執行 CSS 串聯演算法的結果;和 LayoutObjects 的樹狀結構,用於建立版面配置階段的作業順序。從概念上來說,請嚴格執行串聯演算法,然後再產生版面配置樹狀結構。但在之前,這兩項作業是交錯的Project Squad 成功將這兩個階段劃分為不同的連續階段。

先前,ComputedStyle 在樣式重新計算期間不會一律取得最終值;在某些情況下,ComputedStyle 已在後續管道階段更新。Project Squad 成功重構這些程式碼路徑,因此在樣式階段之後絕不會修改 ComputedStyle

LayoutNG:簡化版面配置階段

這項「特別專案」是 RenderingNG 的其中一項基石,是版面配置轉譯階段的完整重寫作業。我們不會在這裡處理整項專案,但對於 BlinkNG 整個專案有一些值得注意之處:

  • 先前,版面配置階段會收到由樣式階段建立的 LayoutObject 樹狀結構,並使用大小和位置資訊為樹狀結構加上註解。因此,沒有明確分隔輸出內容與輸入內容。LayoutNG 導入了片段樹狀結構,這是版面配置的主要唯讀輸出內容,可做為後續轉譯階段的主要輸入內容。
  • LayoutNG 將容器屬性導入版面配置:計算指定 LayoutObject 的大小和位置時,我們不會再查看根位於該物件的子樹狀結構之外。系統會預先計算指定物件版面配置所需的所有資訊,並以唯讀輸入內容提供給演算法。
  • 先前在極端情況下,版面配置演算法無法完全正常運作:演算法的結果取決於最新版面配置的更新期限。LayoutNG 已解決這些情況。

預漆階段

先前沒有正式的預繪繪製階段,只是版面配置後作業的收納袋就夠了。「預繪製」階段從認識到少數相關函式,適合在版面配置完成後將幾個相關函式實作為版面配置樹狀結構的系統化週遊;最重要的:

  • 發出繪製無效通知:如果資訊不完整,會在版面配置中正確執行繪製撤銷作業。如果將內容分割為兩個不同的程序,就能更容易取得正確,且效率極高:在樣式和版面配置期間,可以將內容標示為「可能需要套用無效色彩」的簡易標記。在預製樹狀圖中,我們會檢查這些標記並視情況進行撤銷作業。
  • 產生繪製屬性樹狀結構:具體說明程序。
  • 運算及記錄像素填充的繪製地點:所記錄的結果可用於套用階段,也可供任何需要這些結果的下游程式碼使用,無需額外計算。

地產樹:一致的幾何圖形

我們在 RenderingNG 推出「資源樹狀結構」,是為了處理捲動的複雜性,因為網路上與所有其他視覺特效的結構不同。在屬性樹狀結構之前,Chromium 的合成器使用了單一「圖層」階層結構,表示合成內容的幾何關係,但此做法很快就脫離了各項功能的複雜性,例如 position:fixed 就變得明顯。圖層階層會增加其他非本機指標,並指出「捲動父項」或「clip 父項」很久以前就難以理解程式碼

屬性樹狀結構會分別表示與其他所有視覺效果的溢位捲動和裁剪切面,藉此修正這個問題。進而正確建立網站真正的視覺和捲動結構。下一個,「全部」我們必須在屬性樹狀結構上方實作演算法 (例如複合圖層的螢幕空間轉換,或是判斷哪些圖層已捲動,哪些圖層則沒有捲動)。

事實上,我們很快就發現程式碼中還有許多其他地方出現類似的幾何問題。(索引鍵資料結構發布作業提供更完整的清單)。其中幾個項目重複實作了合成器程式碼正在執行的操作。都屬於不同的錯誤子集但沒有人能夠正確模擬真正的網站結構接著,解題方法會變得清楚明瞭:將所有幾何演算法集中在同一處,並重構所有程式碼即可使用。

這些演算法依靠屬性樹狀結構,因此屬性樹狀結構是「索引鍵」資料結構,也就是 RenderingNG 在整個管道中使用的結構。因此,為了實現集中式幾何程式碼的這個目標,我們必須更早在管道中預先導入屬性樹狀結構的概念,並變更所有依附於這些樹狀結構需要預先繪製的 API,才能加以執行。

這個故事也是 BlinkNG 重構模式的另一個層面:識別重要運算、重構避免重複,以及建立定義明確的管道階段,以便建立資料結構來提供這些內容。如果有所有必要資訊,系統會在當下計算屬性樹狀結構;我們會確保在後續的算繪階段執行時,屬性樹狀結構不會變更。

顏料後合成的複合材料:管線粉刷和合成

「分層」是指找出哪些 DOM 內容進入專屬複合圖層的程序,而後者代表的是 GPU 紋理。在 RenderingNG 之前,分層作業會在繪製前執行,而不是之後 (請參閱這裡瞭解目前的管道;請注意順序異動)。首先,我們會先決定 DOM 要放入哪個複合圖層的部分,然後繪製這些紋理的顯示清單。當然,決策取決於多項因素,例如哪些 DOM 元素用於製作動畫或捲動,或是曾進行 3D 轉換,以及哪些元素出現的位置。

這造成了重大問題,因為程式碼中須有循環依附元件增加或減少,而這對轉譯管道而言是一大問題。讓我們透過範例瞭解原因。假設我們需要「作廢」繪製作業,這代表我們必須重新繪製顯示清單,然後再按一次光柵。而需要作廢的原因可能是 DOM 中的變更,或樣式/版面配置有所變更。但當然,我們只想取消已實際變更的部分。這表示系統會找出受影響的複合圖層,然後撤銷這些圖層的部分或所有顯示清單。

這表示撤銷作業取決於 DOM、樣式、版面配置和過去的圖層化決定 (過去:上一個算繪影格的意義)。不過,目前的分層設定也取決於所有因素。此外,由於我們沒有所有分層資料的副本,因此很難判斷過去和日後的分層設定差異。我們最後得到很多程式碼 造成循環推理這有時會導致程式碼邏輯不正確或有誤,甚至導致當機或安全性問題 (如果不夠謹慎)。

為處理這種情況,我們及早推出了 DisableCompositingQueryAsserts 物件的概念。在大多數情況下,如果程式碼嘗試查詢過去的分層決定,就會導致斷言失敗,如果瀏覽器處於偵錯模式,就會讓瀏覽器當機。如此一來,我們就能避免引入新的錯誤。而在需要查詢過往分層決策的程式碼時,程式碼的正當性都會加入程式碼,只要分配 DisableCompositingQueryAsserts 物件即可允許存取。

我們計劃陸續停用所有呼叫站 DisableCompositingQueryAssert 物件,然後宣告安全且正確的程式碼。但我們發現,有些來電基本上不可能移除,只要分層進行就沒問題。(我們最近只能將其移除!)這是「塗料之後」專案的第一個原因。我們學到了這一點,即使有明確定義的作業管道階段,只要作業在管道中的錯誤位置,最終還是容易卡住。

產生複合粉刷完成後的第二個原因是基本合成錯誤。如要指出這項錯誤,DOM 元素並非 1:1 完整呈現網頁內容的效率或完整圖層配置。此外,由於合成作業是在繪製前完成,因此較難或更少依賴 DOM 元素,不顯示清單或屬性樹狀結構。這與我們導入屬性樹狀結構的原因非常類似,而且就和屬性樹狀結構一樣,如果找出適當的管道階段、在正確時間執行,並提供正確的金鑰資料結構,解決方案就會直接淘汰。和屬性樹狀結構一樣,這是確保繪製階段完成後,所有後續管道階段的輸出內容都無法變更。

優點

如您所見,定義明確的轉譯管道能夠帶來巨大的長期效益。除了您預期的結果外,

  • 大幅提高可靠性:這項功能相當簡單明瞭。介面更加簡潔,而且介面明確且易於理解,使用者更容易理解、撰寫及測試。而這也更加可靠。此外,還能讓程式碼更安全穩定,不僅能減少當機次數,也會減少使用釋放後錯誤造成的錯誤。
  • 擴大測試涵蓋範圍:在 BlinkNG 期間,我們為套件新增了多項測試。這包括提供重點內部驗證的單元測試。迴歸測試可避免新舊錯誤發生 (許多錯誤!),以及廣泛維護的 Web Platform 測試套件,供所有瀏覽器評估是否符合網路標準。
  • 擴充更容易:如果系統被細分為清楚的元件,就不必學習任何細節層級的其他元件,即可取得當前元件的進展。如此一來,所有人都可以輕鬆為轉譯程式碼增添價值,而無須精通熟練的專家,也能輕鬆理解整個系統的行為。
  • 效能:要最佳化以空白程式碼編寫的演算法並不容易,但若沒有這類管道,就幾乎無法達成通用執行緒捲動和動畫等更大規模的工作,或是用於隔離網站隔離的程序與執行緒。平行處理可協助我們大幅改善效能,但也非常複雜。
  • 產生及遏制:BlinkNG 推出了幾項新功能,以新穎的方式練習管道。舉例來說,如果只想在預算到期前執行轉譯管道,該怎麼做?或者,如果子樹狀結構目前與使用者並無關,還是要略過轉譯?這就是 content-visibility CSS 屬性所能啟用的功能。或者,該如何讓元件的樣式取決於版面配置?也就是「容器查詢」

個案研究:容器查詢

容器查詢是眾所期待的網路平台功能 (多年來是 CSS 開發人員最常要求的功能)。既然如此,為什麼還不存在?這是因為執行容器查詢時,必須非常小心地瞭解和控制樣式和版面配置程式碼之間的關係。一起來仔細看看。

容器查詢允許套用至元素的樣式,取決於祖系的配置大小。由於廣告版面配置是在版面配置期間計算的,這表示我們需要在版面配置後重新計算樣式。而是在版面配置前執行樣式重新計算!「雞蛋和雞蛋」是無法執行 BlinkNG 前容器查詢的完整原因。

我們該如何解決這個問題?它不是回溯的管道依附性,而是與專案 (如繪製塗料後 Composite) 解決的相同問題嗎?更糟的是,如果新樣式變更了祖系的大小,該怎麼辦?這樣有時候不會造成無限迴圈嗎?

原則上,使用包含 CSS 屬性可解決循環依附元件,進而允許在元素外轉譯,不必仰賴該元素子樹狀結構中的算繪作業。這表示容器套用的新樣式不會影響容器的大小,因為容器查詢需要包含

但實際上是不夠的,因此必須引入比大小遏制更弱的遏制方法。這是因為容器查詢容器只能根據內嵌尺寸從單一方向調整大小 (通常為區塊),因此新增了內嵌大小控制的概念。但是,從該章節非常長的附註可以看到,如果可以納入內嵌大小控制,長期以來都無法清楚顯示。

在抽象規格語言中,可描述抑制是一件事,正確實作也是個好方法。請回想一下,BlinkNG 的目標之一是針對構成算繪主要邏輯的樹狀步道提出抑制原則:在穿越子樹時,子樹狀結構外部不需要提供任何資訊。在此情況下 (並非真的意外),如果顯示程式碼遵守遏制原則,就可以大幅更簡潔、更容易導入 CSS 遏制。

未來:off-main-thread 合成...還有更多作品!

這裡顯示的轉譯管道,其實比目前的 RenderingNG 實作還要快。這會顯示分層化是離開主執行緒,而目前在主執行緒上。然而,執行這些動作只是一段時間才完成,現在複雜作業只需完成繪製,而圖層化作業就完成了。

為了瞭解這的重要性,以及是否有其他可能的原因,我們需要考量轉譯引擎的架構,從較高的測試點著手。改善 Chromium 效能的最長期障礙之一,就是轉譯器的主執行緒同時處理主要應用程式邏輯 (即執行指令碼) 和大量轉譯。因此,主執行緒經常處於工作狀態,而主要執行緒壅塞情形往往是整個瀏覽器的瓶頸。

好消息是,這項做法可以試試!這一部分 Chromium 架構的歷史可追溯至 KHTML 天,當時單一執行緒執行是主要的程式設計模型。隨著消費者等級裝置中普遍使用多核心處理器,單一執行緒假設已完整導入 Blink (舊稱 WebKit) 中。我們一直想為算繪引擎推出更多執行緒,這是舊版系統不可能的任務。「轉譯 NG」的主要目標之一是釐清這個漏洞,並將轉譯工作 (部分或全部) 移至其他執行緒 (或執行緒)。

BlinkNG 即將完成,我們已開始探索這個領域;非封鎖 Commit 是變更轉譯器執行緒模型的第一步。編譯器修訂版本 (或只是 commit) 是主執行緒與合成器執行緒之間的同步處理步驟。在修訂期間,我們會複製主執行緒上產生的轉譯資料,供在合成器執行緒上執行的下游合成程式碼使用。在進行這項同步處理時,在合成器執行緒上執行複製程式碼時,主執行緒執行作業就會停止。這麼做是為了確保主執行緒在合成器執行緒複製時,不會修改其轉譯資料。

非封鎖修訂功能會省去主執行緒停止並等待修訂階段結束的情況:在合成器執行緒上並行執行修訂時,主執行緒會繼續執行工作。非封鎖修訂版本的淨影響將減少專門在主執行緒上轉譯工作所需的時間,可減少主執行緒的壅塞情形,以及提升效能。截至本文撰寫時間 (2022 年 3 月) 為止,我們已推出非封鎖「修訂」作業的原型,並準備進行深入分析,以評估這項做法對成效的影響。

等待 Wings 是「Off-main-thread Compositing」,目的是透過將「分層」從主執行緒移至背景工作執行緒,讓算繪引擎與插圖相符。與非封鎖修訂版本一樣,這項做法可減少轉譯工作負載,減少主執行緒的壅塞情形。除非有塗抹技術的繪製改良,否則這類專案永遠可能不可能實現。

還有更多專案 (原定會)!我們終於有能力嘗試重新分配算繪工作,我們很期待看到可行的!