我們是如何在CI流水線統(tǒng)計web前端FPS的?
1. 背景
1.1 FPS 統(tǒng)計意義
FPS(幀率)是圖像領域中的定義,是指畫面每秒渲染幀數(shù),F(xiàn)PS 一般在 0-60 之間,低于 30 時人眼能明顯感覺到卡頓。頁面交互過程中頁面展示是否流暢,頁面中的動畫是否存在卡頓等,都需要通過 FPS 的統(tǒng)計指標作為頁面性能的參考依據(jù)。

1.2 現(xiàn)有 web 前端 FPS 統(tǒng)計方式
1.2.1 Chrome devtools
如下圖,通過 Chrome devtools 右側(cè)菜單 -> more tools -> Rendering -> 勾選 Frame Rendering Stats,則會在頁面左上角顯示實時 Frame Rate(FPS)和 GPU 內(nèi)存使用情況的小窗。


缺點 :生產(chǎn)環(huán)境數(shù)據(jù)無法收集上報,需要人工實時觀測;比較適合在開發(fā)階段進行自測
1.2.2 requestAnimationFrame API
window.requestAnimationFrame() 告訴瀏覽器你希望執(zhí)行一個動畫,并且要求瀏覽器在下次重繪之前調(diào)用指定的回調(diào)函數(shù)更新動畫。該方法需要傳入一個回調(diào)函數(shù)作為參數(shù),該回調(diào)函數(shù)會在瀏覽器下一次重繪之前執(zhí)行回調(diào)?;卣{(diào)函數(shù)執(zhí)行次數(shù)通常與瀏覽器屏幕刷新次數(shù)相匹配,一般是每秒 60 次。
那么正好可以利用 requestAnimationFrame API 的特性來計算統(tǒng)計 FPS ,原理如下:
假設動畫在時間 A 開始執(zhí)行,在時間 B 結(jié)束,耗時 (B-A) s,這期間 requestAnimationFrame 一共執(zhí)行了 n 次,則此段動畫的 FPS = n / (B-A)。
requestAnimationFrame 在不掉幀的情況下一秒內(nèi)會執(zhí)行 60 次,即 FPS = 60 / 1。
統(tǒng)計 FPS 核心代碼如下:
let lastTime = performance.now();let frames = 0;const loop = () => {const currentTime = performance.now();frames += 1;if (currentTime > 1000 + lastTime) {fps = Math.round((frames * 1000) / (currentTime - lastTime));frames = 0;lastTime = currentTime;console.log(`fps:${fps}`);}window.requestAnimationFrame(loop);}loop();
在生產(chǎn)環(huán)境,只需要通過 requestAnimationFrame 統(tǒng)計出監(jiān)控階段的回調(diào)調(diào)用次數(shù),即可計算出對應 FPS,對 FPS 也比較方便進行收集和上報,是目前使用最多的 FPS 統(tǒng)計方式。
缺點:
對業(yè)務代碼 侵入性較強 ,需要引入腳本且實現(xiàn)代碼指定統(tǒng)計階段
統(tǒng)計的 FPS** 結(jié)果不夠準確**,因為它是將每兩次主線程執(zhí)行的時間間隔當成一幀,而非主線程加合成線程所消耗的時間為一幀。js 執(zhí)行屬于主線程,主線程很容易遭到阻塞(例如:js 執(zhí)行耗時較長),而此時合成器線程基本上是空閑的,合成器能夠自己運行某些動畫(合成滾動和加速 CSS 動畫),它可以在不等待 JS 的情況下運行這些動畫。例如這個 demo 頁面:https://xdevilj136.github.io//main-thread-block.html,主線程被 js 執(zhí)行完全阻塞,requestAnimationFrame 無法正常統(tǒng)計 FPS,這種情況下實際頁面還是可以正常滾動的。
1.3 痛點
現(xiàn)有的前端 FPS 統(tǒng)計方式存在一些痛點,解決痛點希望滿足以下方面:
不侵入業(yè)務代碼,對 web 頁面進行 FPS 統(tǒng)計
具有一定的通用性,適用于前端大部分動畫、交互場景
統(tǒng)計 FPS 結(jié)果數(shù)據(jù)相對準確
可以在 CI 階段進行 FPS 統(tǒng)計,生成性能報告
目前 alloyperf 的 FPS 統(tǒng)計工具模塊,已經(jīng)實現(xiàn)并滿足以上要求,在 CI 流水線定時統(tǒng)計騰訊文檔頁面 FPS 數(shù)據(jù)并定時生成性能報告。后面章節(jié),將介紹 alloyperf FPS 統(tǒng)計的實現(xiàn)原理。
2. alloyperf FPS 統(tǒng)計工具介紹
2.1 alloyperf FPS 統(tǒng)計工具
alloyperf FPS 統(tǒng)計工具實現(xiàn)主要利用 Selenium WebDriver 和 chrominum:
Selenium WebDriver 驅(qū)動 chrome 瀏覽器打開測試頁面,并通過 API 模擬頁面交互操作,以測試頁面不同的交互場景;
chromnium 內(nèi)部的 Chrome tracing,記錄了 chrome 瀏覽器打開、展示頁面整個過程中各個進程不同階段的 tracing 記錄,通過獲取并分析 Chrome tracing 的記錄 logs, 即可計算統(tǒng)計出頁面對應測試階段的 FPS 指標。

2.2 Selenium WebDriver 介紹
Selenium 是 ThoughtWorks 提供的一個強大的基于瀏覽器的開源自動化測試工具集,Selenium WebDriver 是工具集其中一個子工具,主要用于在各種瀏覽器上自動化測試 web 應用。
它對瀏覽器提供的原生 API 進行了封裝,使其成為一套更加面向?qū)ο蟮?Selenium WebDriver API,使用這套 API 可以操控瀏覽器的開啟、關閉,打開網(wǎng)頁,操作界面元素,還可以操作瀏覽器 devtools 等,由于使用的原生 API,其速度與穩(wěn)定性都會好很多。
Selenium WebDriver 通過 JsonWireProtocol 協(xié)議與各瀏覽器的 driver 進行通信(例如:ChromeDriver 即為 Chromium 實現(xiàn)了 JsonWireProtocol 協(xié)議),Selenium 對不同廠商的各個 driver 進行了封裝,如:selenium-chrome-driver、selenium-edge-driver、selenium-firefox-driver 等,可支持各種主流瀏覽器的自動化測試。
Selenium WebDriver 架構(gòu)如下圖所示:

2.3 Chrome tracing 介紹
對于 FPS 的統(tǒng)計,Chrome tracing 是核心也是本文的重點,下面重點介紹。
2.3.1 Tracing ecosystem
Tracing ecosystem 即 tracing 的生態(tài)系統(tǒng),tracing 即跟蹤應用運行過程并生成記錄的行為。Tracing ecosystem 的運行基于"trace 文件",trace 文件包含所有的跟蹤記錄數(shù)據(jù),Tracing ecosystem 包含兩種工具:
記錄并生成 trace 文件的工具
解析展示 trace 文件的工具
記錄并生成 trace 文件的工具有很多,比如:Android 的 systrace 命令行工具、開源的 adb_trace 等,web 前端常用的有 chrome devtools 中 performance record 功能、chrome tracing 的 record 功能。
解析展示 trace 文件的工具,web 前端常用的 chrome devtools performance、chrome tracing 同樣具有這樣的強大能力,chrome tracing 相對展示的信息更加詳細。


2.3.2 Trace viewer
chrome tracing 是內(nèi)置在 chrome 中的工具,可用來收集和解析展示非常詳細的性能跟蹤數(shù)據(jù),在 devtools 無法滿足需求時,可使用此工具來進行更加復雜或具體的性能分析。
通過 chrome tracing 的 record 按鈕進行記錄后即可生成對應的跟蹤數(shù)據(jù),chrome tracing 內(nèi)部通過 trace viewer 可直接對產(chǎn)生的數(shù)據(jù)進行解析和展示:

Trace viewer 可以對 record 產(chǎn)生的 trace 數(shù)據(jù)直接進行展示,也可以 load 對應的 trace json 文件并進行解析展示。展示結(jié)果如上圖,時序按從左到右排列,通過左側(cè)的 Processes 和 Threads 進行細分,右側(cè)每一個小色塊對應一個 TRACEEVENT(即 Chromium 內(nèi)部 tracing 庫生成的單個記錄事件點)。
在 trace viewer 中點選對應的 TRACEEVENT 色塊,甚至可以直接點擊下方的詳情跳轉(zhuǎn)到相關的 Chromnium 源碼:


Chromnium 通過 TRACE_EVENT0 函數(shù)將對應的 EVENT 記錄到對應的 category,例如上圖將 ProxyImpl::NotifyReadyToCommitOnImpl 記錄到 cc(即 Chrome Compositor 合成器)。
同時,Trace viewer 結(jié)果展示圖中,還可以通過菜單選擇對應的 flow 展示某個 event 流的軌跡走向,例如單幀在渲染進程中的 flow 大致是經(jīng)歷如下階段:
輸入事件來自于瀏覽器進程,并被傳遞給合成器線程,對應的 TRACE_EVENT 為 "InputEventFilter::ForwardToHandler"
輸入事件從合成器線程到主線程,啟動了 Blink 的輸入事件處理
Blink 生成一個新的動畫幀,并在 "WebViewImpl::animate "中調(diào)用 requestAnimationFrame 回調(diào)
如果在 RAF 回調(diào)或輸入事件處理程序中 JavaScript 修改了頁面,觸發(fā)了一個重新布局,首先是樣式的重新計算,對應于"Document::updateStyle"
Blink 重新繪制覆蓋失效區(qū)域,對應 TRACE_EVENT "Picture::Record",layer 屬性(如 transform、opacity)也在 Blink 的 layer tree 副本中被更新
通過"ThreadProxy::BeginMainFrame::Commit",新的記錄和更新后的 layer tree 從 Blink 線程傳遞到合成器線程,在這期間主線程被合成器線程阻塞
之后合成器進行柵格化處理,然后傳遞給瀏覽器合成器并交換幀緩存"DelegatingRenderer:SwapBuffers",最終完成繪制
所以通過 TRACE_EVENT 的 flow 軌跡,即可以非常精細地看到頁面每一幀的具體渲染流程。
2.3.3 trace 文件格式
Trace Viewer 可以識別四種不同格式的 trace 文件,JSON 類型格式包括 JSON 數(shù)組和 JSON 對象,另外兩種是 Linux ftrace 數(shù)據(jù)類型。比較通用的是 JSON 格式,也是 chrome tracing 使用的格式,Linux ftrace 類型本文不做贅述。
JSON 數(shù)組(chrome devtools performance 生成格式):
[{"args":{"name":"swapper"},"cat":"__metadata","name":"thread_name","ph":"M","pid":337,"tid":0,"ts":0},{"args":{"name":"CrBrowserMain"},"cat":"__metadata","name":"thread_name","ph":"M","pid":337,"tid":775,"ts":0}]
JSON 對象(chrome tracing 生成格式):
{"traceEvents":[{"args":{"name":"swapper"},"cat":"__metadata","name":"thread_name","ph":"M","pid":337,"tid":0,"ts":0},{"args":{"name":"Compositor"},"cat":"__metadata","name":"thread_name","ph":"M","pid":7546,"tid":42243,"ts":0}],"displayTimeUnit": "ns","systemTraceEvents": "SystemTraceData","otherData": {"version": "My Application v1.0" },"stackFrames": {...}"samples": [...],}
兩種格式結(jié)構(gòu)略有不同,但每條 TRACE_EVENT 對應的 args 字段基本一致,本文只需關注:
name: TRACE_EVENT 名稱
cat: TRACE_EVENT 類別
ts: TRACE_EVENT 事件的追蹤時時間戳,以微秒為單位
通過以上得出結(jié)論:通過 flow 確認每一幀渲染必定經(jīng)過哪些關鍵 TRACE_EVENT ,然后分析對應的 trace 文件,即可計算得到 FPS 數(shù)據(jù)。
2.4 統(tǒng)計 FPS
2.4.1 FPS 統(tǒng)計關鍵 Trace Event
下圖為幀繪制內(nèi)容數(shù)據(jù)的 flow 流向示意圖,與 Chrome tracing 的 flow 軌跡對應:

如圖所示,繪制內(nèi)容的數(shù)據(jù)流向要經(jīng)過幾個不同的進程和線程,不同的線程的任務由 Chromnium 中不同模塊(對應 category)負責,blink 主要負責主線程、cc 主要負責合成器線程、viz 主要負責 gpu 相關。
在通過 Chrome tracing 跟蹤 flow 和跟蹤 chromnium 相關源碼過程中,主要發(fā)現(xiàn)以下關鍵點:
主線程很容易遭到阻塞(例如:js 執(zhí)行耗時較長),而此時合成器線程基本上是空閑的,合成器能夠自己運行某些動畫(合成滾動和加速 CSS 動畫),它可以在不等待 JS 的情況下運行這些動畫,所以不能選擇主線程 TRACE_EVENT
雖然按照 flow 流向,最終走向的 TRACEEVENT 在 gpu 進程,但通過實際測試和 chromnium 源碼的進一步分析,發(fā)現(xiàn) chromnium 在跨平臺處理時針對 linux 在 gpu 進程做了特殊處理,導致 linux 平臺下 data flow 的 TRACEEVENT 不一定在每一幀都確定走到 gpu
Commit 是一種從主線程推送數(shù)據(jù)到合成器線程的方式,并且保證了該過程中的數(shù)據(jù)完整性。Commit 不是通過發(fā)送 ipc,而是通過阻塞主線程并復制數(shù)據(jù)的方式來完成提交。收到主線程請求后的某個時刻,調(diào)度器將調(diào)用 ScheduledActionBeginMainFrame 對請求進行響應,合成器線程會發(fā)送一個 BeginFrameArgs 到主線程啟動 BeginMainFrame。完成此操作后,cc 再進行后續(xù)柵格化等一系列流程。Commit 流程如下圖所示:

最終確定每一幀必定走到的 TRACEEVENT 有合成器線程 ScheduledActionBeginMainFrame 階段,因此選取 cat="cc"、name="Scheduler::NotifyBeginMainFrameStarted"的 event 作為 FPS 統(tǒng)計的關鍵 TRACEEVENT。
2.4.2 統(tǒng)計流程
確定 FPS 統(tǒng)計關鍵 Trace Event 后,核心問題就得到了解決,計算 FPS 大體流程如下:

3. 總結(jié)
針對 1.3 中提到的目前現(xiàn)有 web 前端 FPS 統(tǒng)計方式的痛點,alloyperf fps 模塊都已經(jīng)實現(xiàn)了相應的解決。
對于測試頁面,只需要提供頁面 url 和簡單配置,不會侵入業(yè)務代碼
通過 webdriver 模擬頁面交互操作,具有一定的通用性
通過 Chromnium 底層 TRACE_EVENT 分析統(tǒng)計 FPS,結(jié)果數(shù)據(jù)相對準確
可以在 CI 流水線引入進行 FPS 統(tǒng)計,生成性能報告
目前 alloyperf fps 模塊已經(jīng)在騰訊文檔 CI 流水線運行,日常輸出 FPS 性能報告。
alloyperf 其他模塊(首屏統(tǒng)計、內(nèi)存監(jiān)測等)正在陸續(xù)開發(fā)中,后續(xù) FPS 模塊也將持續(xù)優(yōu)化支持更多平臺和場景的測試,流水線接入更多的應用品類。
關于AlloyTeam
AlloyTeam 是國內(nèi)影響力最大的前端團隊之一,核心成員來自前 WebQQ 前端團隊。 AlloyTeam負責過WebQQ、QQ群、興趣部落、騰訊文檔等大型Web項目,積累了許多豐富寶貴的Web開發(fā)經(jīng)驗。 這里技術氛圍好,領導nice、錢景好,無論你是身經(jīng)百戰(zhàn)的資深工程師,還是即將從學校步入社會的新人,只要你熱愛挑戰(zhàn),希望前端技術和我們飛速提高,這里將是最適合你的地方。 加入我們,請將簡歷發(fā)送至 [email protected],或直接在公眾號留言~ 期待您的回復??
最后
面試交流群持續(xù)開放,分享了近 許多 個面經(jīng)。
加我微信: DayDay2021,備注面試,拉你進群。
我是 小弋,我們下篇見~

2021-06-08
2021-06-03

