無線效能優化:FPS測試

xiaoqb發表於2016-01-28

時間回到幾周前,這天,女神突然來找我,“我這裡有幾個頁面想測量下頁面滾動的順暢性,你有啥辦法不?”。Are you kidding me?這麼簡單,簡直是道送分題啊,於是當著女神面,開啟 Chrome 開發者工具,勾選上 Show FPS meter,醒目的 FPS 監控皮膚就出來了,滑動頁面時 FPS 的曲線就基本反映出了頁面滾動的順暢性,坐等女神的誇獎~~

“這個我知道啊,但是我有好多頁面,難道要一個一個人工看嗎?而且這個也沒有一個記錄的匯出,如果能有個方法幫我自動的測量,有問題再通知我,我再仔細排查就好了”。

這個需求開始有點技術含量了,不過應該也難不倒我,頁面都接入了 UITest,在頁面做 UI 測試的時候,跑一下測量 FPS 的測試用例就 ok 了,那如何測量呢?女神等我~~

mozPaintCount

mozPaintCount 變數是 Mozilla 提供的方法,其返回的是當前文件 paint 到螢幕上的數量,通過計算單位時間 paint 數量變化,即可計算出頁面的 FPS,so easy。

等等,這個變數目前好像只有 Firefox 支援,Chrome 上並沒有一個 webkitPaintCount 或者 paintCount 變數,而我們的 UITest 是跑在 ChromeDriver 或者 PhantomJS 上,並沒有 Firefox 環境,好吧,這個可以做備選方案,依賴於 UITest 支援 Firefox 環境。為了完成女神的需求,我們還要考慮其他方案了。

requestAnimationFrame

在頁面重繪前,瀏覽器會執行傳入 requestAnimationFrame 的入參函式,一般多用來實現連貫的逐幀動畫。那我們基於 requestAnimationFrame 不就可以獲得頁面的繪製頻率,計算出 FPS,而且瀏覽器支援情況也不錯,說幹就幹,示例程式碼如下(簡單示例,沒做相容等處理):

var lastTime = performance.now();
var frame = 0;
var lastFameTime = performance.now();

var loop = function(time) {
var now = performance.now();
var fs = (now - lastFameTime);
lastFameTime = now;
var fps = Math.round(1000/fs);
frame++;
if (now > 1000 + lastTime) {
var fps = Math.round( ( frame * 1000 ) / ( now - lastTime ) );
frame = 0;
lastTime = now;
};
window.requestAnimFrame(loop);
}

用例結果如下:

requestAnimationFrame 測試 FPS 結果

大功告成,可以去找女神答覆了,等等,這樣是不是太簡單了點,無法讓女神刮目相看啊(在女神面前裝逼),我們是不是再深入點。

Chrome 瀏覽器渲染頁面時,涉及了兩個執行緒,Render 主執行緒和 Compositor 合成執行緒,且兩個執行緒通過名為 Commit 的訊息來保持同步,而每一幀消耗時間應該是包含兩部分,Render 主執行緒消耗的時間和 Compositor 執行緒消耗的時間。典型的狀態如下(以下三張圖片來自 frame-timing-polyfill):

典型狀態

主執行緒 Commit 訊息提交給合成執行緒,任務都在 16.66ms 內完成。當然,對於一些輸入事件,比如滾動,是先轉移給合成執行緒進行處理,然後通知給主執行緒,這樣可以保證對使用者的輸入操作做及時的響應,同時,對於一些頁面更新,如 CSS 動畫和 CSS 濾鏡,只需合成執行緒處理,而無需請求主執行緒,如下所示:

只需要 Compositor 執行緒

當然,也有可能,主執行緒處理耗時較多,導致提交給合成執行緒的時間推遲到了下一幀,如下所示:

主執行緒超時

針對以上三種情況,那流暢性如何定義呢?就需要因地制宜,不同環境分別分析了。

  • 對於滾動和 CSS 動畫,由於不涉及主執行緒的影響,我們會更關心合成執行緒的繪製頻率,而合成執行緒的繪製頻率也反映了滾動和 CSS 動畫的流程性。

  • 對於 JS 幀動畫而言,我們期望主執行緒和合成執行緒的消耗加起來到能在 16.66ms 內,且不丟幀。因此我們需要同時關注主執行緒的 Commit 頻率和合成執行緒的繪製頻率,並且我們期望每個主執行緒的 Commit 都對應唯一一個合成執行緒的繪製(保證不丟幀)。

那是否有方法可以讓我們分別取到 Render 主執行緒和 Compositor 合成執行緒的資料呢?答案是 Frame Timing

Frame Timing

Frame Timing API 目前還只是草案,暫時還沒發現有瀏覽器支援,不過我們可以先實現,萬一瀏覽器支援了呢~~,目前的 API 如下:

var rendererEvents = window.performance.getEntriesByType("renderer");
var compositeEvents = window.performance.getEntriesByType("composite");

獲取 Render 主執行緒和合成執行緒的記錄,每條記錄包含的資訊基本如下:

{
sourceFrameNumber: 120,
startTime: 1342.549374253,
duration: 10.654313323
}

每個記錄都包括唯一的 Frame Number、Frame 開始時間以及持續時間。根據 duration 就可以知道該幀是否達到 16.66ms 的標準,同時根據單位時間記錄數(Frame)的個數就能算出主執行緒或者合成執行緒每秒的幀率。

同時,對於主執行緒 Commit 給合成執行緒繪製的情況,可以根據唯一的 sourceFrameNumber 將 renderEvents 的記錄和 compositeEvents 的記錄做關聯,得出每個主執行緒 Commit 所對應的合成執行緒繪製的次數,如前所說,這也是判斷 JS 動畫流程性的一個可檢測指標~~(具體實現程式碼較多且比較簡單,就不貼了,小夥伴們動動腦筋,分分鐘就寫出來了~)

至此,終於可以向女神交差了,想想女神崇拜的目光,還有點小激動呢~~

再等等,既然 Chrome 能開啟 FPS meter,而且我們的 UI 測試也是跑在 ChromDriver 中的,那是不是可以通過配置開啟 Chrome 的 FPS meter 獲取到 FPS 呢,女神,再等我下~

Show FPS Counter or Performance Log

果不其然,查詢 ChromeDriver 的配置設定

–show-fps-counter: Draws a heads-up-display showing Frames Per Second as well as GPU memory usage. If you also use –vmodule=”head*=1” then FPS will also be output to the console log.

只要能輸出到 console log 裡,我們就能方便的取到了,於是果斷在 UITest 的 ChromeDriver 配置里加上裡這兩項:

chromeOptions["args"] = [
    // 其他配置省略
    `--show-fps-counter`,
    `--vmodule="head*=1"`
];

滿心歡喜的一試,FPS meter 是出來的,但是說好的 console log 並沒有,具體的討論可以參見這個 issue

難道沒有別的辦法了嗎?既然 Chrome 的 Timeline 那麼強大,其中也包含了每一幀的耗時,那是不是可以取到 Timeline 的資料?當然沒問題,我們使用 selenium-webdriver就能方便的獲取到頁面的 Performance Log,示例如下所示:

var webdriver = require(`selenium-webdriver`);
var chrome = require(`selenium-webdriver/chrome`);

// ...

// 配置需要跟蹤記錄的資料
var options = new chrome.Options();
var traceCategories = [
`blink.console`,
`devtools.timeline`,
`toplevel`,
`disabled-by-default-devtools.timeline`,
`disabled-by-default-d.evtools.timeline.frame`
];
options.setLoggingPrefs({ performance: `ALL` });
options.setPerfLoggingPrefs({
`traceCategories`: traceCategories.join(`,`)
});

// ...

// 傳入 chromedriver 例項
function getTrace(browser) {
return (new webdriver.WebDriver.Logs(browser))
.get(`performance`)
.then(function(logs) {
// performance log
});
}

其中,traceCategories 的配置和 chrome://tracing/ 的配置一樣,開啟 Chrome,位址列輸入 chrome://tracing/,點選 record 按鈕,會出現如下所示的配置項:

Chrome Tracing

其中的配置就是我們可以獲取的。獲取到 Performance Log 後匯出成 JSON 檔案,匯入到 Chrome 的 Timeline 裡,你會驚奇的發現,這和直接用 Timeline 效果是一樣一樣的,如下所示:

Timeline

但是接下來,我發現真正頭疼的問題來了,Performance Log 是一堆密密麻麻的資料,而且還沒找到相關文件,目前只是可以取出平均的 FPS,計算方法如下,首先解析資料,取出型別為 DrawFrame 的記錄個數,然後除以整個統計的持續時間,即可大體得出整體平均的 FPS,如何解析出更具體的資料,還在持續研究中,希望這下可以讓女神滿意,嘿嘿嘿~

參考文件

文章轉自:http://taobaofed.org/blog/2016/01/13/measuring-fps/
作者:冬萌


相關文章