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

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


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

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

2.3 Chrome tracing 介紹
對于 FPS 的統(tǒng)計(jì),Chrome tracing 是核心也是本文的重點(diǎn),下面重點(diǎn)介紹。
2.3.1 Tracing ecosystem
Tracing ecosystem 即 tracing 的生態(tài)系統(tǒng),tracing 即跟蹤應(yīng)用運(yùn)行過程并生成記錄的行為。Tracing ecosystem 的運(yùn)行基于"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 同樣具有這樣的強(qiáng)大能力,chrome tracing 相對展示的信息更加詳細(xì)。


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

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


Chromnium 通過 TRACE_EVENT0 函數(shù)將對應(yīng)的 EVENT 記錄到對應(yīng)的 category,例如上圖將 ProxyImpl::NotifyReadyToCommitOnImpl 記錄到 cc(即 Chrome Compositor 合成器)。
同時,Trace viewer 結(jié)果展示圖中,還可以通過菜單選擇對應(yīng)的 flow 展示某個 event 流的軌跡走向,例如單幀在渲染進(jìn)程中的 flow 大致是經(jīng)歷如下階段:
輸入事件來自于瀏覽器進(jìn)程,并被傳遞給合成器線程,對應(yīng)的 TRACE_EVENT 為 "InputEventFilter::ForwardToHandler"
輸入事件從合成器線程到主線程,啟動了 Blink 的輸入事件處理
Blink 生成一個新的動畫幀,并在 "WebViewImpl::animate "中調(diào)用 requestAnimationFrame 回調(diào)
如果在 RAF 回調(diào)或輸入事件處理程序中 JavaScript 修改了頁面,觸發(fā)了一個重新布局,首先是樣式的重新計(jì)算,對應(yīng)于"Document::updateStyle"
Blink 重新繪制覆蓋失效區(qū)域,對應(yīng) TRACE_EVENT "Picture::Record",layer 屬性(如 transform、opacity)也在 Blink 的 layer tree 副本中被更新
通過"ThreadProxy::BeginMainFrame::Commit",新的記錄和更新后的 layer tree 從 Blink 線程傳遞到合成器線程,在這期間主線程被合成器線程阻塞
之后合成器進(jìn)行柵格化處理,然后傳遞給瀏覽器合成器并交換幀緩存"DelegatingRenderer:SwapBuffers",最終完成繪制
所以通過 TRACE_EVENT 的 flow 軌跡,即可以非常精細(xì)地看到頁面每一幀的具體渲染流程。
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 對應(yīng)的 args 字段基本一致,本文只需關(guān)注:
name: TRACE_EVENT 名稱
cat: TRACE_EVENT 類別
ts: TRACE_EVENT 事件的追蹤時時間戳,以微秒為單位
通過以上得出結(jié)論:通過 flow 確認(rèn)每一幀渲染必定經(jīng)過哪些關(guān)鍵 TRACE_EVENT ,然后分析對應(yīng)的 trace 文件,即可計(jì)算得到 FPS 數(shù)據(jù)。
2.4 統(tǒng)計(jì) FPS
2.4.1 FPS 統(tǒng)計(jì)關(guān)鍵 Trace Event
下圖為幀繪制內(nèi)容數(shù)據(jù)的 flow 流向示意圖,與 Chrome tracing 的 flow 軌跡對應(yīng):

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

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

3. 總結(jié)
針對 1.3 中提到的目前現(xiàn)有 web 前端 FPS 統(tǒng)計(jì)方式的痛點(diǎn),alloyperf fps 模塊都已經(jīng)實(shí)現(xiàn)了相應(yīng)的解決。
對于測試頁面,只需要提供頁面 url 和簡單配置,不會侵入業(yè)務(wù)代碼
通過 webdriver 模擬頁面交互操作,具有一定的通用性
通過 Chromnium 底層 TRACE_EVENT 分析統(tǒng)計(jì) FPS,結(jié)果數(shù)據(jù)相對準(zhǔn)確
可以在 CI 流水線引入進(jìn)行 FPS 統(tǒng)計(jì),生成性能報(bào)告
目前 alloyperf fps 模塊已經(jīng)在騰訊文檔 CI 流水線運(yùn)行,日常輸出 FPS 性能報(bào)告。
alloyperf 其他模塊(首屏統(tǒng)計(jì)、內(nèi)存監(jiān)測等)正在陸續(xù)開發(fā)中,后續(xù) FPS 模塊也將持續(xù)優(yōu)化支持更多平臺和場景的測試,流水線接入更多的應(yīng)用品類。
內(nèi)推社群
我組建了一個氛圍特別好的騰訊內(nèi)推社群,如果你對加入騰訊感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行面試相關(guān)的答疑、聊聊面試的故事、并且在你準(zhǔn)備好的時候隨時幫你內(nèi)推。下方加 winty 好友回復(fù)「面試」即可。
