天天看點

這款狗狗引擎這麼快?一定是考慮了這幾個特性

對,我就是 Servo 的官方 Logo

這款狗狗引擎這麼快?一定是考慮了這幾個特性

Servo 是一款現代化的高性能浏覽器引擎,既支援正常應用,也支援嵌入使用。官網 https://servo.org

她由 Mozilla 開發,由三星集團移植到 Android 系統和 ARM 處理器,旨在創造一個大規模并行計算的環境,Servo 也與 Rust 程式設計語言有着共生的關系。源自 https://zh.wikipedia.org/wiki/Servo

Servo 是一個新的 Web 浏覽器引擎。她的目标是建立一個多層級的高并發架構,同時在架構層面消除與錯誤的記憶體管理、資料競争相關的常見 bug 和安全漏洞。

因為 C++ 不适合處理這類問題,是以 Servo 是用 Rust 語言編寫的。Rust 在設計的時候充分考慮了 Servo 的需求,它提供了任務并行的基礎架構和強類型系統,進而保證了記憶體安全、避免了資料競争。

在設計的時候,Servo 的架構師們會優先考慮現代 Web 平台的以下特性:高性能、動态、富媒體應用,可能會犧牲一些無法優化的特性。他們想知道一個快速響應的 Web 平台是什麼樣子的,然後再實作它。

Servo 專注于實作一個功能完備的 Web 浏覽器引擎和可靠的嵌入式引擎,前者(Web 浏覽器引擎)使用了基于 HTML 的使用者界面 Browser.html。盡管 Servo 最初隻是一個研究型項目,但在開發它的時候就以提供可用于生産環境的代碼為目标。目前,Servo 的一些元件已經遷移到了 Firefox 浏覽器。

關于內建到 Firefox 中的 Servo 元件,可檢視 Jack Moffitt 的演講視訊 Web Engines Hackfest

并發是拆分任務以便交叉執行;并行是同時執行多個任務以提高速度。Servo 在以下環節中用到了并行和并發。

基于任務的架構:系統的主要元件應該有獨立的堆,以便有明确的失敗/恢複的邊界。這也讓整個系統的耦合度降低,以便可以輕松地替換掉某些元件,供我們實驗和研究。

并發渲染:将渲染和合成從布局中分離出來,以保證良好的響應性。渲染和合成都是單獨的線程;合成線程手動管理自己的記憶體,以避免垃圾回收暫停。

瓦片渲染:将螢幕劃分成瓦片網格,并行渲染每一個瓦片。暫且忽略由此帶來的收益,移動端渲染的時候是需要這種瓦片的。

層渲染:将顯示清單分成子樹,并行渲染子樹,并将其内容保留在 GPU 上。

選擇器比對:這是一個令人尴尬的并行問題。與 Gecko 不同,Servo 在流樹結構的單獨傳遞中進行選擇器比對,這樣會讓并行更容易。

并行布局:通過并行周遊 DOM 來建構流樹,這種周遊遵守由元素(比如浮動元素)生成的順序依賴關系。

文本形狀:作為内聯布局的關鍵部分,文本形狀的成本非常高,它很有并行的潛力。未實作。

解析:用 Rust 新寫了一個 HTML 解析器,專注于安全性和符合規範。尚未在解析中添加預測性和并行性。

圖像解碼:并行解碼多個圖像非常簡單。

其他資源的解碼:這可能不如圖像解碼重要,但頁面加載的所有内容都是可以并行處理的,比如解析整個樣式表、解碼視訊。

GC JS 和布局的并發:在大多數具有并發 JS 和布局的設計中,當查詢布局的時候,JS 有時需要等待,而且有可能是非常頻繁的。這将是運作 GC 的最佳時機。

GC,Garbage Collection,垃圾回收

庫不利于并行:用到的一些第三方庫在多線程環境下不能很好地運作;字型尤其困難;即使從技術角度講庫是線程安全的,但是,通常是通過庫的互斥鎖來實作線程安全的,而這不利于實作并行。

線程太多:如果在各個方面都抛到最大的并行量和并發量,那麼最終會因為線程太多而壓垮系統。

圖1. 任務監管圖,源自 servo/wiki/Design

圖2. 任務通信圖,源自 servo/wiki/Design

每一個框代表一個 Rust 任務 (注:一個任務就是一個線程)

藍色框是浏覽器管道裡的主要任務

灰色框是浏覽器管道的輔助任務

白色框是 worker 任務,它表示會有多個任務,具體的任務數要根據工作量來确定

虛線表示主管關系

實線表示通信信道

我們可以把每個 constellation(見“附錄.術語”小節)執行個體看做是浏覽器的單個頁簽或者視窗,它管理着接收輸入的任務管道,針對 DOM 運作 JavaScript,執行布局,建構顯示清單,将顯示清單渲染到瓦片上,最後把最終圖像合成到螢幕上。

這個管道由四個主要任務組成:

腳本(Script):建立和擁有 DOM,執行 JavaScript 引擎。它接收來自多個源的事件,包括導航事件。當内容任務(Content)需要查詢布局相關的資訊時,腳本任務必須向布局任務發送一個請求。每個内容任務都有自己的 JavaScript 運作時。

布局(Layout):擷取 DOM 快照,計算樣式,構造主要的布局資料結構-流樹(flow tree)。流樹用于計算節點的布局,從它那可以建構顯示清單,顯示清單會被發送到渲染任務。

渲染(Renderer):接收顯示清單,并将可見部分渲染到一個或多個瓦片上,盡可能并行。

合成(Compositor):合成渲染的瓦片,并将它們發送到螢幕上進行顯示。作為 UI 線程,合成任務也是 UI 事件的第一個接收器,UI 事件通常會被立即發送到内容任務以供處理(盡管一些事件,比如滾動事件,首先由合成任務處理并響應)。

管道中的多任務通信涉及到兩種複雜的資料結構:DOM 和顯示清單。DOM 從内容傳到布局,顯示清單從布局傳到渲染。找出一種有效且類型安全的方式來表示、共享和傳遞這兩種資料結構是該項目的諸多挑戰之一。

Servo 的 DOM 樹節點是有版本控制的,它們可以在單個 writer 和多個 reader 之間共享。DOM 使用寫時複制(copy-on-write)的政策允許當有多個 reader 時 writer 也能修改 DOM。writer 總是内容任務,reader 總是布局任務或其子任務。

DOM 節點是 Rust 值(Rust value),而 Rust 值的生命周期由 JavaScript 垃圾收集器管理。JavaScript 直接通路 DOM 節點,而沒有依賴 XPCOM 或其它類似的基礎設施。

DOM 接口目前不是類型安全的,這可能會導緻不正确的節點管理。消除這類不安全是該項目的一個必要的高優先級目标;由于 DOM 節點具有複雜的生命周期,這将會帶來一些挑戰。

Servo 的渲染完全由顯示清單驅動,顯示清單是由布局任務建立的一系列進階繪圖指令。Servo 的顯示清單是完全不可變的,是以它可以被同時運作的多個渲染任務所共享。這與 Webkit 和 Gecko 的渲染器不同:WebKit 的渲染器沒有使用顯示清單;Gecko 的渲染器使用了顯示清單,但它在渲染期間還會查詢額外的資訊。

目前,Servo 使用的腳本引擎是 SpiderMonkey(可插拔引擎是一個長期的、低優先級的目标)。每個内容任務都有自己的 JavaScript 運作時。DOM 綁定使用原生的 JavaScript 引擎 API 而不是 XPCOM。從 WebIDL 自動生成綁定是一個高優任務。

與 Chromium 和 WebKit2 類似,Servo 的架構師們打算做一個可信任的應用程式程序和多個不太可信的引擎程序。進階 API 實際上是基于 IPC 的,非 IPC 實作可能用于測試和單程序用例(雖然預計最糟糕的時候也會用于多程序)。引擎程序将使用作業系統沙箱工具來限制對系統資源的通路。

目前,Servo 并不打算像 Chromium 那樣采用極端沙箱(extreme sandboxing),主要是因為鎖定沙箱會導緻大量的開發工作(特别是在 Windows XP 和舊版 Linux 等低優先級的平台上),并且該項目的其它方面的優先級更高一點。Rust 的類型系統還為記憶體安全漏洞增加了一層重要的防禦功能,雖然僅憑這一點并不能使沙箱在防禦不安全代碼、類型系統中的錯誤以及第三方/主機庫等方面變得不那麼緊迫,但相對于其他浏覽器引擎它确實能顯著減少 Servo 的攻擊面。此外,Servo 的架構師們對某些沙箱技術有性能方面的顧慮(例如,将所有 OpenGL 調用代理到單獨的程序)。

網頁依賴于各種各樣的外部資源,而這些資源具有很多的檢索和解碼機制。這些資源會被緩存在多處,比如磁盤、記憶體。在并行浏覽器的設定中,這些資源一定會在并發的多個 worker 之間排程。

通常,浏覽器是單線程的,會在“主線程”上執行 I/O,而“主線程”同時又擔負着大部分的計算任務,這就會導緻延遲問題。而 Servo 中沒有“主線程”,所有外部資源的加載都由一個資源管理任務來處理。

浏覽器有很多緩存,而 Servo 的基于任務的架構意味着它可能會擁有比現有浏覽器引擎還多的緩存(例如,我們在擁有全局任務緩存的同時,也擁有着一個本地任務緩存,它存儲着來自全局緩存的結果,以通過排程程式來儲存往返記錄)。Servo 應該有一個統一的緩存機制,以便在低記憶體的環境中也運作良好。

constellation:該線程控制相關網頁内容。在支援多頁簽的浏覽器中,可以把它當做單個頁簽的擁有者;它封裝了會話曆史記錄,知道 frame 樹中的所有 frame,是每個 frame 管道的擁有者。

管道(pipeline):為特定文檔封裝了腳本線程、布局線程和渲染線程之間的通信。每個管道都有一個全局唯一的 id,可以從 constellation 裡通路到它。

腳本線程/腳本任務(script thread/script task):這個線程執行 JavaScript,并存儲同源下所有文檔的 DOM 表示。它可以把從 constellation 接收到的輸入事件轉換為規範裡定義的 DOM 事件,也可以在收到新頁面的時候調用 HTML 解析,也可以為事件評估 JS。

布局線程(layout thread):這個線程負責将 DOM 樹布局到特定文檔的層(layer)上。它會收到來自腳本線程的指令,要麼是為渲染線程生成一個新的顯示清單,要麼是為腳本線程傳回頁面的布局結果。

顯示清單(display list):一個具體的渲染說明(進階繪圖指令)清單。顯示清單是發生在布局之後的,是以所有的項都有相對堆疊上下文的像素位置,并且已經應用了 z-index,是以後加入顯示清單的項将始終在其它項的上面。

渲染線程/繪制線程(renderer thread/paint thread):這個線程負責将顯示清單轉換成一系列的繪圖指令。該繪圖指令會将關聯文檔的内容渲染在一個緩沖區裡,之後會被發送到合成器。

合成/合成器(Compositor):負責 Web 内容的合成渲染,并将它們盡可能快地顯示在螢幕上。也負責從作業系統接收輸入事件,并将它們轉發到 constellation 線程。

本文主要介紹了 Servo 的設計概況,重點介紹了它基于任務的整體架構及其四個主要任務(也稱“線程”,在 Servo 的這個上下文裡),即腳本任務、布局任務、渲染任務、合成任務。下圖便是對上述内容的一個總結,希望對大家有所幫助和啟發。

這款狗狗引擎這麼快?一定是考慮了這幾個特性

圖3. Servo 概況

目前,已出爐兩篇文章。後續,我會繼續探索更多詳細内容,敬請期待。

Quantum 初探:介紹了 Quantum 項目的由來和概況,也順便介紹了 Servo 的小曆史(https://mp.weixin.qq.com/s/5sROBD__VIysOp-ISuQjfA)

Servo 的設計架構:介紹了 Servo 的基于任務的設計架構,重點介紹了它的并行并發政策(https://github.com/anjia/blog/issues/3)

關于 Quantum 和 Servo,如果您有其它更想知道的,歡迎留言。