我們是如何在CI流水線統(tǒng)計(jì)web前端FPS的?
1. 背景
1.1 FPS 統(tǒng)計(jì)意義
FPS(幀率)是圖像領(lǐng)域中的定義,是指畫面每秒渲染幀數(shù),F(xiàn)PS 一般在 0-60 之間,低于 30 時(shí)人眼能明顯感覺到卡頓。頁面交互過程中頁面展示是否流暢,頁面中的動(dòng)畫是否存在卡頓等,都需要通過 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,則會(huì)在頁面左上角顯示實(shí)時(shí) Frame Rate(FPS)和 GPU 內(nèi)存使用情況的小窗。


缺點(diǎn) :生產(chǎn)環(huán)境數(shù)據(jù)無法收集上報(bào),需要人工實(shí)時(shí)觀測;比較適合在開發(fā)階段進(jìn)行自測
1.2.2 requestAnimationFrame API
window.requestAnimationFrame() 告訴瀏覽器你希望執(zhí)行一個(gè)動(dòng)畫,并且要求瀏覽器在下次重繪之前調(diào)用指定的回調(diào)函數(shù)更新動(dòng)畫。該方法需要傳入一個(gè)回調(diào)函數(shù)作為參數(shù),該回調(diào)函數(shù)會(huì)在瀏覽器下一次重繪之前執(zhí)行回調(diào)?;卣{(diào)函數(shù)執(zhí)行次數(shù)通常與瀏覽器屏幕刷新次數(shù)相匹配,一般是每秒 60 次。
那么正好可以利用 requestAnimationFrame API 的特性來計(jì)算統(tǒng)計(jì) FPS ,原理如下:
假設(shè)動(dòng)畫在時(shí)間 A 開始執(zhí)行,在時(shí)間 B 結(jié)束,耗時(shí) (B-A) s,這期間 requestAnimationFrame 一共執(zhí)行了 n 次,則此段動(dòng)畫的 FPS = n / (B-A)。
requestAnimationFrame 在不掉幀的情況下一秒內(nèi)會(huì)執(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í)行的時(shí)間間隔當(dāng)成一幀,而非主線程加合成線程所消耗的時(shí)間為一幀。js 執(zhí)行屬于主線程,主線程很容易遭到阻塞(例如:js 執(zhí)行耗時(shí)較長),而此時(shí)合成器線程基本上是空閑的,合成器能夠自己運(yùn)行某些動(dòng)畫(合成滾動(dòng)和加速 CSS 動(dòng)畫),它可以在不等待 JS 的情況下運(yùn)行這些動(dòng)畫。例如這個(gè) demo 頁面:https://xdevilj136.github.io//main-thread-block.html,主線程被 js 執(zhí)行完全阻塞,requestAnimationFrame 無法正常統(tǒng)計(jì) FPS,這種情況下實(shí)際頁面還是可以正常滾動(dòng)的。
1.3 痛點(diǎn)
現(xiàn)有的前端 FPS 統(tǒng)計(jì)方式存在一些痛點(diǎn),解決痛點(diǎn)希望滿足以下方面:
不侵入業(yè)務(wù)代碼,對 web 頁面進(jìn)行 FPS 統(tǒng)計(jì)
具有一定的通用性,適用于前端大部分動(dòng)畫、交互場景
統(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 流水線定時(shí)統(tǒng)計(jì)騰訊文檔頁面 FPS 數(shù)據(jù)并定時(shí)生成性能報(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ū)動(dòng) chrome 瀏覽器打開測試頁面,并通過 API 模擬頁面交互操作,以測試頁面不同的交互場景;
chromnium 內(nèi)部的 Chrome tracing,記錄了 chrome 瀏覽器打開、展示頁面整個(gè)過程中各個(gè)進(jìn)程不同階段的 tracing 記錄,通過獲取并分析 Chrome tracing 的記錄 logs, 即可計(jì)算統(tǒng)計(jì)出頁面對應(yīng)測試階段的 FPS 指標(biāo)。

2.2 Selenium WebDriver 介紹
Selenium 是 ThoughtWorks 提供的一個(gè)強(qiáng)大的基于瀏覽器的開源自動(dòng)化測試工具集,Selenium WebDriver 是工具集其中一個(gè)子工具,主要用于在各種瀏覽器上自動(dòng)化測試 web 應(yīng)用。
它對瀏覽器提供的原生 API 進(jìn)行了封裝,使其成為一套更加面向?qū)ο蟮?Selenium WebDriver API,使用這套 API 可以操控瀏覽器的開啟、關(guān)閉,打開網(wǎng)頁,操作界面元素,還可以操作瀏覽器 devtools 等,由于使用的原生 API,其速度與穩(wěn)定性都會(huì)好很多。
Selenium WebDriver 通過 JsonWireProtocol 協(xié)議與各瀏覽器的 driver 進(jìn)行通信(例如:ChromeDriver 即為 Chromium 實(shí)現(xiàn)了 JsonWireProtocol 協(xié)議),Selenium 對不同廠商的各個(gè) driver 進(jìn)行了封裝,如:selenium-chrome-driver、selenium-edge-driver、selenium-firefox-driver 等,可支持各種主流瀏覽器的自動(dòng)化測試。
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 無法滿足需求時(shí),可使用此工具來進(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é)果如上圖,時(shí)序按從左到右排列,通過左側(cè)的 Processes 和 Threads 進(jìn)行細(xì)分,右側(cè)每一個(gè)小色塊對應(yīng)一個(gè) TRACEEVENT(即 Chromium 內(nèi)部 tracing 庫生成的單個(gè)記錄事件點(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 合成器)。
同時(shí),Trace viewer 結(jié)果展示圖中,還可以通過菜單選擇對應(yīng)的 flow 展示某個(gè) event 流的軌跡走向,例如單幀在渲染進(jìn)程中的 flow 大致是經(jīng)歷如下階段:
輸入事件來自于瀏覽器進(jìn)程,并被傳遞給合成器線程,對應(yīng)的 TRACE_EVENT 為 "InputEventFilter::ForwardToHandler"
輸入事件從合成器線程到主線程,啟動(dòng)了 Blink 的輸入事件處理
Blink 生成一個(gè)新的動(dòng)畫幀,并在 "WebViewImpl::animate "中調(diào)用 requestAnimationFrame 回調(diào)
如果在 RAF 回調(diào)或輸入事件處理程序中 JavaScript 修改了頁面,觸發(fā)了一個(gè)重新布局,首先是樣式的重新計(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 可以識(shí)別四種不同格式的 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 事件的追蹤時(shí)時(shí)間戳,以微秒為單位
通過以上得出結(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)過幾個(gè)不同的進(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í)行耗時(shí)較長),而此時(shí)合成器線程基本上是空閑的,合成器能夠自己運(yùn)行某些動(dòng)畫(合成滾動(dòng)和加速 CSS 動(dòng)畫),它可以在不等待 JS 的情況下運(yùn)行這些動(dòng)畫,所以不能選擇主線程 TRACE_EVENT
雖然按照 flow 流向,最終走向的 TRACEEVENT 在 gpu 進(jìn)程,但通過實(shí)際測試和 chromnium 源碼的進(jìn)一步分析,發(fā)現(xiàn) chromnium 在跨平臺(tái)處理時(shí)針對 linux 在 gpu 進(jìn)程做了特殊處理,導(dǎo)致 linux 平臺(tái)下 data flow 的 TRACEEVENT 不一定在每一幀都確定走到 gpu
Commit 是一種從主線程推送數(shù)據(jù)到合成器線程的方式,并且保證了該過程中的數(shù)據(jù)完整性。Commit 不是通過發(fā)送 ipc,而是通過阻塞主線程并復(fù)制數(shù)據(jù)的方式來完成提交。收到主線程請求后的某個(gè)時(shí)刻,調(diào)度器將調(diào)用 ScheduledActionBeginMainFrame 對請求進(jìn)行響應(yīng),合成器線程會(huì)發(fā)送一個(gè) BeginFrameArgs 到主線程啟動(dòng) 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 和簡單配置,不會(huì)侵入業(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)化支持更多平臺(tái)和場景的測試,流水線接入更多的應(yīng)用品類。
關(guān)于AlloyTeam
AlloyTeam 是國內(nèi)影響力最大的前端團(tuán)隊(duì)之一,核心成員來自前 WebQQ 前端團(tuán)隊(duì)。 AlloyTeam負(fù)責(zé)過WebQQ、QQ群、興趣部落、騰訊文檔等大型Web項(xiàng)目,積累了許多豐富寶貴的Web開發(fā)經(jīng)驗(yàn)。 這里技術(shù)氛圍好,領(lǐng)導(dǎo)nice、錢景好,無論你是身經(jīng)百戰(zhàn)的資深工程師,還是即將從學(xué)校步入社會(huì)的新人,只要你熱愛挑戰(zhàn),希望前端技術(shù)和我們飛速提高,這里將是最適合你的地方。 加入我們,請將簡歷發(fā)送至 [email protected],或直接在公眾號(hào)留言~ 期待您的回復(fù)??
最后
面試交流群持續(xù)開放,分享了近 許多 個(gè)面經(jīng)。
加我微信: DayDay2021,備注面試,拉你進(jìn)群。
我是 小弋,我們下篇見~

2021-06-08
2021-06-03

