Blink 是指 Chromium 採用的網路平台,包含合成之前的所有轉譯階段 (包括合成器修訂版本)。如要進一步瞭解閃爍轉譯架構,請參閱本系列文章中的這篇文章。
Blink 原本就是 WebKit 的分支,它本身是 KHTML 的分支,其歷史可追溯至 1998 年。其中包含 Chromium 中最舊 (且最關鍵) 的部分程式碼,而到了 2014 年,它確實會顯示程式碼的存在時間。同年,我們在名為 BlinkNG 的橫幅開展了一系列宏大的專案,目標是解決組織中長期存在的缺陷以及 Blink 程式碼結構。本文將介紹 BlinkNG 及相關組成專案:我們完成這些專案的原因、完成的成果、制定設計指導原則的指導原則,以及日後可進行的改進。
轉譯 NG 之前的版本
Blink 中的轉譯管道在概念上一直分為多個階段 (style、layout、paint 等),但抽象化障礙已浮漏。大致來說,與算繪相關的資料是由長期的可變動物件組成。這些物件隨時都有可能修改過,而且經常被連續轉譯更新回收並重複使用。我們無法可靠地回答以下這類簡單的問題:
- 是否需要更新樣式、版面配置或繪製的輸出內容?
- 這些資料何時會成為「最終」價值?
- 可以在何時修改這些資料?
- 這個物件何時會刪除?
其中有許多範例,包括:
樣式會根據樣式表產生 ComputedStyle
,但 ComputedStyle
無法變更;在某些情況下,則會在之後的管道階段修改。
Style 會產生 LayoutObject
的樹狀結構,然後版面配置會使用大小和位置資訊為這些物件加上註解。在某些情況下,版面配置甚至會修改樹狀結構。版面配置的輸入和輸出內容之間沒有明確區隔。
樣式會產生決定「合成」的配件資料結構,且會在 style 之後每個階段中修改這些資料結構。
在較低層級,算繪資料類型主要由特殊樹狀結構 (例如 DOM 樹狀結構、樣式樹狀結構、版面配置樹狀結構、繪製屬性樹狀結構) 組成;轉譯階段則是以遞迴樹散步的方式實作。理想情況下,樹步道應包含:處理指定的樹狀結構節點時,我們不應存取位於該節點子樹狀結構中的任何資訊。這從以前不是真正的轉譯模型,而是從正在處理的節點祖系中經常存取的資訊。這使得系統變得十分脆弱,而且容易出錯。從任何位置都能開始對樹,只不過是樹木的根部。
最後,在整個程式碼中,有不少要塞進轉譯管道的難度包括:由 JavaScript 觸發的版面配置、文件載入期間觸發的部分更新、為了準備事件指定而強制更新、顯示系統要求的排程更新,以及只向測試程式碼公開的特殊 API 等等。轉譯管道中有一些「遞迴」和「重複」路徑 (也就是從另一個階段中間跳到某個階段的起始處)。每個 VM 預設坡道都有各自的慣用行為,在某些情況下,算繪結果則取決於觸發算繪更新的方式。
異動內容
BlinkNG 由許多大大小小的專案組成,其共同的目標在於消除先前所述的結構缺陷。這些專案共用一些指導原則,旨在讓轉譯管道更接近實際管道:
- 統一進入點:我們一律應在開頭輸入管道。
- 功能階段:每個階段都應有定義明確的輸入和輸出內容,而且其行為應為「功能性」,也就是可確定且可重複,而且輸出內容只取決於定義的輸入內容。
- 常數輸入內容:在階段執行期間,任何階段的輸入內容都必須有效一致。
- 不可變動的輸出:階段完成後,其輸出內容對於其餘轉譯更新的部分應該無法變更。
- 檢查點一致性:在每個階段結束時,到目前為止產生的轉譯資料應該處於自我一致的狀態。
- 簡化工作:只計算一次工作一次。
一份完整的 BlinkNG 子專案清單會造成繁瑣的讀數,不過下列是一些特殊的結果。
文件生命週期
DocumentLifecycle 類別可用來追蹤轉譯管道的進度。這可讓我們執行基本檢查,強制執行上述的不變體,例如:
- 如果我們要修改 ComputedStyle 屬性,則文件生命週期必須是
kInStyleRecalc
。 - 如果 DocumentLifecycle 狀態為
kStyleClean
以上,則NeedsStyleRecalc()
必須針對任何附加的節點傳回 false。 - 進入 paint 生命週期階段時,生命週期狀態必須為
kPrePaintClean
。
在實作 BlinkNG 的過程中,我們有系統地刪除違反這些不變的程式碼路徑,並在整個程式碼中加入了更多斷言,確保不會迴歸。
如果您曾遇見兔子洞,查看低階轉譯程式碼,不妨問問自己:「這是怎麼到來的?」如前文所述,轉譯管道有許多不同的進入點。先前,這包括遞迴和重新呼叫的呼叫路徑,以及以中繼階段進入管道的位置,而非從頭開始。在 BlinkNG 的過程中,我們分析了這些呼叫路徑,並確定這些路徑全都可以減少到兩種基本情境:
- 所有顯示資料都必須更新,例如產生多媒體廣告的新像素,或針對指定事件進行命中測試時。
- 我們需要特定查詢的最新值,以便在不更新所有顯示資料的情況下回答。這包括大部分的 JavaScript 查詢,例如
node.offsetTop
。
轉譯管道現在只有兩個進入點,對應這兩種情境。移除或重構的重複程式碼路徑已移除,且無法再從中繼階段開始進入管道。這已經消除了算繪更新作業的時機和方式等諸多問題,讓您更輕鬆地瞭解系統的行為。
管線樣式、版面配置和預先繪製
整體而言,paint 之前的轉譯階段會負責下列事項:
- 執行樣式瀑布演算法,計算 DOM 節點的最終樣式屬性。
- 產生代表文件方塊階層的版面配置樹狀結構。
- 決定所有方塊的大小和位置資訊。
- 將子像素形狀的幾何圖形四捨五入或對齊至整個像素邊界,以便繪圖。
- 判斷複合層的屬性 (自然轉換、濾鏡、不透明度或其他任何可 GPU 加速的功能)。
- 判斷內容自上一次繪製階段以來發生過哪些變更,需要繪製或重新繪製 (繪製無效)。
這份清單並未變更,但在 BlinkNG 之前,大部分工作都是以臨時方式進行,並分散至多個轉譯階段,且內建許多重複功能,而且效率不彰。舉例來說,style 階段一直主要負責計算節點的最終樣式屬性,但在少數特殊案例中,我們只有在 style 階段完成後才會確定最終樣式屬性值。在轉譯過程中,我們並沒有正式或可執行的階段,可以說是樣式資訊完整且不可變的。
「套用無效預先連結」問題的另一個例子是繪製無效問題。先前,繪製無效現象在整個轉譯階段都是在繪製前發生。修改樣式或版面配置程式碼時,我們很難判斷需要哪些繪製無效邏輯需要哪些變更,因此很容易犯下錯誤,導致出現不足或無效的錯誤。如要進一步瞭解舊版繪製無效系統的細節,請參閱 LayoutNG 系列文章。
將子像素版面配置幾何圖形,貼齊用於繪畫的整個像素邊界,就是一種例子,我們多次實作相同的功能,並且執行大量多餘的工作。繪製系統使用一個像素貼齊程式碼路徑,以及每當需要在繪製程式碼以外即時計算像素相隔座標時,即可使用完全獨立的程式碼路徑。不用說,每個實作項目都有各自的錯誤,結果也不一定一致。由於這項資訊沒有快取內容,因此有時可能會重複執行相同的運算,這反而會增加效能。
以下列舉幾項重要專案,在繪製前消除了算繪階段的架構缺陷。
Project Squad:融合風格階段
這項專案在風格階段解決了兩大缺陷,這導致無法清理管道:
樣式階段有兩個主要輸出:ComputedStyle
,包含在 DOM 樹狀結構上執行 CSS 階層演算法的結果;以及 LayoutObjects
的樹狀結構 (用於建立版面配置階段的作業順序)。從概念上來說,執行階層式演算法必須嚴格執行,再產生版面配置樹狀結構;之前,這兩個操作是交錯的。Project Squad 成功將這兩個專案分為不同的序列階段。
先前,ComputedStyle
不一定每次在重新計算樣式時取得最終值;在某些情況下,ComputedStyle
會在後續管道階段更新。Project Squad 成功重構這些程式碼路徑,因此在樣式階段之後,ComputedStyle
就一律不會修改。
LayoutNG:繪製版面配置階段
這項臨時專案是 RenderingNG 的基石之一,是版面配置轉譯階段的完整重寫。我們不會在這裡對整個專案做出正規,但整體 BlinkNG 專案有幾點值得注意:
- 先前,版面配置階段會接收由樣式階段建立的
LayoutObject
樹狀結構,並以大小和位置為樹狀結構加上註解。因此,輸出的輸入沒有乾淨的區隔。LayoutNG 導入了片段樹狀結構,這是版面配置的主要唯讀輸出內容,可做為後續轉譯階段的主要輸入。 - LayoutNG 將 Containment 屬性帶到版面配置中:在計算特定
LayoutObject
的大小和位置時,我們不會再查看根層級以該物件為主的子樹狀結構。更新特定物件版面配置所需的所有資訊都會事先計算,並以唯讀的方式提供給演算法使用。 - 先前在極端案例中,版面配置演算法並非完全正常運作:演算法的結果取決於最新的版面配置更新。因此 LayoutNG 已消除這些案例。
預先繪製階段
我們先前並未執行正式的前置算繪階段,而只是版面配置後作業需要的擷取袋。從辨識到的「預繪製」階段開始,我們發現有些相關函式在版面配置完成後,最適合做為版面配置樹狀結構的系統週遊遍佈;最重要的是:
- 發出繪製無效情況:在版面配置過程中,如果資訊不完整,就難以正確繪製無效繪製。如果它分成兩個不同的程序,這樣做會比較簡單,而且效率也非常高:在樣式和版面配置期間,內容能以簡單的布林值標記標示,例如「可能需要繪製無效」。在油漆前散步期間,我們會檢查這些旗標,並在必要時產生無效問題。
- 產生油漆屬性樹:以下程序會詳細說明。
- 計算及記錄像素相鄰的繪製位置:錄製結果可用於繪製階段,以及任何需要這些結果的下游程式碼,不必進行多餘運算。
屬性樹:一致的幾何圖形
我們在轉譯 NG 初期推出屬性樹狀結構,以處理捲動的複雜性,且網頁結構的結構與所有其他視覺效果不同。在屬性樹狀結構之前,Chromium 的合成器使用單一的「圖層」階層來表示複合內容的幾何關係,但此架構很快就完全離不開,十分複雜的功能,例如 position:fix 表示已變得明顯。圖層階層會產生額外的非本機指標,用於指出圖層的「捲動父項」或「clip 父項」,但長期下來,對程式碼難以理解。
屬性樹狀結構可分別代表內容溢位捲動及內容裁剪部分,與所有其他視覺效果分開修正。進而正確地模擬網站的視覺和捲動結構。接下來,我們「只需」在屬性樹狀結構上方實作演算法,例如合成圖層的螢幕空間轉換,或是決定要捲動哪些圖層。
事實上,我們很快就發現程式碼中還有許多其他地方有類似的幾何問題。(鍵資料結構發布則具有更完整的清單)。其中多個開發人員重複實作了合成器程式碼正在執行的相同作業,而且所有錯誤子集均不同,而且也沒有正確建立真正的網站結構。接著,解決方案變得很清楚:將所有幾何圖形演算法集中在同一處,然後重構所有程式碼即可使用。
這些演算法反過來依賴屬性樹狀結構,因此屬性樹狀結構是「索引鍵」資料結構,也就是轉譯 NG 管道各採用的結構。因此,為了實現集中式幾何程式碼的目標,我們需要在管道前,在管道中更早引入屬性樹狀結構的概念,並且變更所有目前依賴這些樹狀圖的 API 才能在執行前執行。
本故事是 BlinkNG 重構模式的另一個方面:識別鍵運算、進行重構以避免重複,以及建立定義明確的管線階段,建立提供資料結構的資料結構。等到所有必要資訊都可供使用時,我們就會計算屬性樹狀結構;並確保屬性樹狀結構在執行之後的算繪階段時無法變更。
油漆後合成物:管線繪製和合成
「圖層化」是指確認哪些 DOM 內容會歸入自己的複合式圖層 (然後代表 GPU 紋理)。在轉譯 NG 之前,圖層會在繪製之前執行,而不是之後 (請參閱這裡瞭解目前的管道 - 請注意順序的變更)。我們會先決定 DOM 的哪些部分進入合成圖層,然後只針對這些紋理繪製顯示清單。當然,做決定的因素取決於多項因素,例如哪些 DOM 元素是動畫元素或捲動畫面,或是由 3D 轉換所呈現,以及哪些元素繪製在哪些元素上。
這會造成重大問題,因為程式碼中存在循環依附元件的更多程度或更少,這對轉譯管道而言是一大問題。現在來看看為什麼。假設我們需要「撤銷」繪製結果 (這代表我們必須重新繪製顯示清單,然後再次光柵化)。invalidate之所以需要無效,可能是因為 DOM 的變更,或者樣式或版面配置有所變更。但當然,我們當然只會將已實際變更的部分撤銷。這意味著您得找出受影響的複合式圖層,並使這些圖層的部分或所有顯示清單失效。
這表示撤銷作業取決於 DOM、樣式、版面配置和過去的分層化決策 (過去:先前轉譯影格的意義)。但是目前的分層機制也取決於所有這些因素。由於我們沒有兩份分層資料的副本,因此很難判斷過去與未來層次化決策之間的差異。最終我們達到了大量的程式碼 採用循環原因這種情況有時會發生程式碼錯誤或不正確的問題,甚至是當機或安全性問題,
為因應這種情況,我們提早介紹 DisableCompositingQueryAsserts
物件的概念。大多數情況下,如果程式碼嘗試查詢過去的分層化決策,在偵錯模式下就會造成斷言或瀏覽器當機。這有助於我們避免引入新的錯誤。在任何情況下,如果程式碼對於查詢過往的層次決策合法,我們會放入程式碼,以配置 DisableCompositingQueryAsserts
物件來允許編寫程式碼。
我們計劃逐步淘汰所有呼叫網站 DisableCompositingQueryAssert
物件,然後宣告程式碼安全無虞且正確無誤。但我們發現,只要層次在繪製前發生分層化,對於這些呼叫基本上就無法移除。(我們終於最近才移除它!)這是我們針對「 Paint」專案首次發現的第一個原因。我們發現,即使您為作業定義了明確的管道階段,如果作業位於管道中的錯誤位置,您最終還是會遇到困難。
「 Paint After」(繪製後) 專案的第二個原因是基本合成錯誤。說明這項錯誤的方式之一,就是 DOM 元素並未以 1:1 的方式呈現網頁內容的有效率或完整的分層配置。此外,由於在繪製前才完成合成,因此它原本取決於 DOM 元素,而不是顯示清單或屬性樹狀結構。這與我們導入屬性樹狀結構的原因非常類似,而且就像屬性樹狀圖一樣,只要找出適當的管道階段、在適當的時機執行程式碼,並提供正確的鍵資料結構,解決方案就會直接發揮作用。與屬性樹狀結構相同,這是一個很好的好機會,可以確保繪製階段完成後,所有後續管道階段的輸出內容都無法變更。
優點
如您所見,定義明確的轉譯管道可帶來極大的長期效益。您或許還會想:
- 已大幅提升穩定性:這項功能相當容易理解。程式碼經過妥善定義且易於理解,更容易理解、撰寫和測試。這讓內容更可靠。此外,它還能讓程式碼更安全穩定,減少當機情形,並減少使用釋放後記憶體的錯誤。
- 擴大測試涵蓋範圍:在 BlinkNG 的過程中,我們在套件中新增了許多實用的測試。其中也包括對內部提供專注驗證的單元測試;迴歸測試可避免我們導入我們已修正的舊錯誤 (太多!);以及許多對外維護的網路平台測試套件 (所有瀏覽器皆用於評估是否符合網路標準)。
- 易於擴充:如果系統將各個系統細分成明確的元件,則不需要瞭解其他元件細節,就能推進目前的元件。這樣一來,每個人都能更輕鬆地為轉譯程式碼增添價值,不必精通專家,也能更輕鬆理解整個系統的行為。
- 效能:最佳化以精簡程式碼編寫的演算法已相當困難,但如果沒有這類管道,要實現更大規模的事情,例如通用執行緒捲動和動畫,或網站隔離的程序和執行緒幾乎是不可能的。平行處理量可協助我們大幅提高效能,但也難度過於複雜。
- 產出及遏制:BlinkNG 開發了多項新功能,能以全新及新方式推展管道。例如,如果只想在預算到期前執行轉譯管道,該怎麼辦?或者,您也可以針對目前與使用者無關的子樹略過轉譯程序。這就是 content-visibility CSS 屬性所啟用的功能。元件的樣式是否會取決於版面配置?也就是「容器查詢」。
個案研究:容器查詢
容器查詢是備受期待的新網路平台功能 (多年來一直是 CSS 開發人員最期待的功能)。如果很棒,為什麼還不存在?這是因為導入容器查詢時,需要非常謹慎地瞭解與控制樣式和版面配置程式碼之間的關係。一起來仔細看看。
容器查詢可讓套用至元素的樣式取決於祖系的配置大小。由於版面配置大小是在版面配置上計算,因此我們需要在版面配置後執行樣式重新計算,但樣式重新計算功能會在版面配置之前執行!這個雞蛋惡作劇是為何在 BlinkNG 之前無法實作容器查詢的完整原因。
我們該如何解決這個問題?它不是回溯管道依附元件,也就是專案解決後 (如 Composite after Paint) 已解決的問題嗎?更糟的是,如果新樣式改變祖系的大小,該怎麼辦?這有時會導致無限迴圈嗎?
原則上,只要使用包含 CSS 屬性,即可解決循環依附元件的問題,這樣不僅能讓元素外的轉譯作業「不依賴該元素子樹狀結構內的算繪情形」。也就是說,容器套用的新樣式不會影響容器的大小,因為容器查詢「需要包含」。
但事實上,這樣還不夠,機構才有必要導入較弱的隔離類型,而不僅僅是縮小規模。這是因為容器查詢容器只能根據內嵌尺寸,只向一個方向 (通常是區塊) 調整大小。新增內嵌大小納入的概念。但從那段很長的註解可看出,如果可以內嵌大小,一切都還不夠清楚。
使用抽象規格語言來描述遏制資訊是一回事,但以正確方式實作絕對是另一回事。請回想一下,BlinkNG 的其中一個目標,是要將遏制原則套用到構成轉譯主要邏輯的樹狀圖:當您週遊樹狀子目錄時,不需要從子樹狀結構外提供任何資訊。如果轉譯程式碼符合遏制原則,但發生這種情形 (儘管並非發生意外),我們會更簡潔,也更容易導入 CSS 遏制機制。
未來:從主執行緒合成...還有更多!
這裡顯示的轉譯管道其實比現行的 RenderingNG 實作還要久。其中分層化為非主執行緒,但目前還在主執行緒上。不過,這只是完成這項作業的一點,因為 Paint 已出貨並經過分層處理。
為了瞭解這麼做的重要性,以及這在哪些方面可能影響,我們必須從較高階的角度思考轉譯引擎的架構。改善 Chromium 效能時,最可靠的一項障礙之一,就是轉譯器的主執行緒會同時處理主要應用程式邏輯 (即執行指令碼) 和大量轉譯作業。因此,主要執行緒往往會隨工作量飽滿,而主執行緒壅塞通常是整個瀏覽器的瓶頸。
但好消息是,情況其實可以!這一方面,Chromium 的架構可追溯到 KHTML 天,當時單執行緒執行是主要的程式設計模型。隨著消費者級裝置普遍採用多核心處理器,單一執行緒假設已全面內建於 Blink (原為 WebKit) 中。我們一直想為轉譯引擎導入更多執行緒,但這在舊系統中是不可能的。轉譯 NG 的主要目標之一,就是從我們自己深入探究,並視情況將轉譯工作全部或全部移至其他執行緒 (或執行緒)。
BlinkNG 即將到來,我們就已經開始探索這個部分了;Non-Blocking Commit 是優先變更轉譯器執行緒模型的先驅。合成器修訂版本 (或簡稱「修訂」) 是主執行緒和合成器執行緒之間的同步處理步驟。在修訂期間,我們會建立主執行緒上產生的轉譯資料副本,供下游合成程式碼在合成器執行緒上執行的程式碼使用。進行這項同步處理時,主要執行緒會停止執行,同時複製程式碼會在合成器執行緒上執行。這麼做可確保主要執行緒在合成器執行緒複製資料時,不會修改其算繪資料。
若非封鎖修訂版本,則不需要主執行緒停止,並等待修訂階段結束。當修訂版本在合成器執行緒上並行執行時,主要執行緒會繼續執行工作。非封鎖修訂版本的淨效應會縮短在主執行緒上轉譯工作所需的時間,進而減少主執行緒的壅塞情形,並改善效能。截至本文撰寫時間 (2022 年 3 月),我們已擬定非封鎖承諾的原型,並準備深入分析此做法對成效的影響。
等待機翼屬於「離主執行緒合成」,目標是將分層從主執行緒移至背景工作執行緒,讓轉譯引擎與插圖保持一致。與非封鎖修訂版本相同,這麼做可以降低轉譯工作負載數量,藉此減少主執行緒上的壅塞情形。如果不對繪製後的複合模型進行架構改善,這樣的專案就永遠不可能實現。
管道中還有其他專案 (意外專案)!最後,我們終於有了一項基礎,可以讓您嘗試重新分配算繪工作,並一路見證可能的發展!