實戰 PerfDog 優化小遊戲效能

騰訊WeTest發表於2020-08-25

背景:
我們的引擎是Egret,使用的是原生的EUI,轉微信小遊戲;
工程第一版出來後使用PerfDog測試一波資料。結果發現很多問題,本文主要分兩部分

第一部分主要介紹通過PerfDog發現問題,
第二部分主要介紹通過PerfDog的資料定位並解決問題。
PerfDog具體操作可以看文件PerfDog使用說明

第一部分————資料分析
本次的案例多見於遊戲第一版時的情況,比較常見,所以拿出來做個分析。
這裡強調一點。分析問題需要整體資料聯動分析,單獨看某單一資訊是沒是意義的

第一次測試資料
FPS:

記憶體:

CPU:

結論:

1.我們發現在戰鬥時FPS波動較大
2.記憶體呈現持續上升的趨勢
3.CPU的APP Usage太小,僅佔1%左右

首先針對問題3的說明:
我之前選擇測試的是微信app,而小遊戲是作為子程式而存在的,所以應該選擇PerfDog的子程式進行測試,這樣得到的資料會更加的精準;下圖的深色程式表示正在執行的頂層程式

針對這種多程式的應用測試:

iOS平臺,APP多程式分為APP Extension和系統XPC Server。
比如:某電競直播軟體用到APP Extension擴充套件程式(擴充套件程式名LABroadcastUpload)。當然也可能用到系統XPC Server服務程式,如一般web瀏覽器會用到webkit。

Android平臺,一般大型APP,比如遊戲有時候是多程式協作執行(微信小遊戲,微視等APP及王者榮耀等遊戲多子程式),可選擇目標子程式進行鍼對性測試。預設是主程式。如圖王者榮耀

詳細的使用說明可以看這裡:PerfDog使用說明書

為了判斷是什麼導致的FPS波動較大,也為了判斷是否存在OOM,現在我們來選擇子程式進行第二次測試;

第二次測試資料
測試資料組成:
為了驗證我的一些猜想,也為了更細緻的定位問題,我們在測試過程中做了一些特殊操作:

1.戰鬥掛機 【為了判斷是否是戰鬥過程中觸發的記憶體洩露】
2.反覆開啟關閉UI 【為了判斷UI建立與銷燬是否存在記憶體洩露】
3.靜止在某一UI頁面 【為了與其他場景作區分】
4.息屏掛機 【為了判斷是否是由影像資源引起的記憶體洩露還是程式碼資源引起的洩露】
FPS資料:

CPU資料:

記憶體資料:

GPU壓力山大

FPS與GPU分析:

我們通過FPS資料發現在遊戲過程Jank十分嚴重,FPS波動過於劇烈,尤其是集中在UI開啟或者關閉的時候,遊戲來說,渲染畫面,相對來說GPU可能出現瓶頸,逐對GPU進行檢視,這個時候我們進行資料排查發現GPU的使用率也變得異常高,很明顯渲染的壓力很大,而我們遊戲UI開啟時實際上戰鬥也會被渲染,這和我們遊戲的設計有關,所以渲染的壓力很大。

記憶體分析:

我們通過PerfDog的資料發現記憶體是呈現一直上升的狀態,這樣下去最終的結果就是被System Kill掉。其實現在已經可以確定是發生了記憶體洩露,在72分鐘的時間裡記憶體從726M到了956M,而且還在不斷上升;

這裡額外說下,看是否存在OOM不能只看PSS(PerfDog預設的memory是PSS),同樣要注意VSS,有的遊戲可能會存在PSS一般大小,VSS不斷增大的情況,這也是不科學的。
簡單分享下常見記憶體指標關係

記憶體耗用
VSS - Virtual Set Size 虛擬耗用記憶體(包含共享庫佔用的記憶體)
RSS - Resident Set Size 實際使用實體記憶體(包含共享庫佔用的記憶體)
PSS - Proportional Set Size 實際使用的實體記憶體(比例分配共享庫佔用的記憶體)
USS - Unique Set Size 程式獨自佔用的實體記憶體(不包含共享庫佔用的記憶體)
一般來說記憶體佔用大小有如下規律:VSS >= RSS >= PSS >= USS

這裡再稍微介紹下安卓的LMK(Low memory killer),詳細資訊就不多贅述了。

1.Android系統 會定時執行一次檢查,記憶體達到某個值後,就會殺死相應的程式,釋放掉記憶體。
2.每個程式都會有一個oom_adj值,這個值越小,程式越重要,被殺的可能性越低
3.Low memory killer 主要是通過程式的oom_adj 來判定程式的重要程度。oom_adj的大小和程式的型別以及程式被排程的次序有關
4.閾值表可以通過/sys/module/lowmemorykiller/parameters/adj和/sys/module/lowmemorykiller/parameters/minfree進行配置

現在綜合兩次測試資料得出結論

結論:
1.FPS波動過於劇烈,很不穩定,尤其是在uI建立與關閉時候;
2.存在記憶體洩露,由於不管什麼操作記憶體都一直漲,大概率是公共元件部分引起的
3.其實還有一些其他小問題,不過優先解決這兩個

第二部分————問題定位
記憶體洩露問題分析
有了PerfDog以上的資料,接下來我們就要開始定位排查問題啦,

專案區域性架構:

1.我們的專案的基礎架構是所有的基礎功能都呼叫的同一份基礎class(祖傳程式碼),例如通訊類等等;
2.我們發現記憶體在一直上升,無論是角色在什麼環境下,甚至是在息屏的時候記憶體也在上升,那麼我們其實可以大概率定位是專案內部的基礎class內部出了問題;

接下來開始細細排查;

記憶體洩露排查
首先要先了解一些JS的記憶體管理機制

回收機制
JS中記憶體的分配和回收都是VM自動完成的,不需要像C/C++為每一個new/malloc操作去寫配對的delete/free程式碼,JS引擎中對變數的儲存主要是在棧記憶體,堆記憶體。記憶體洩漏的實質是一些物件出現意外而沒有被回收,而是常駐記憶體。
GC原理
JavaScript虛擬機器有一個特點,就是物件建立的開銷遠遠大於物件計算的開銷,並且物件建立會導致垃圾回收,而垃圾回收會導致遊戲不定期卡頓。
在堆中檢視無用的物件,把這些物件佔用的記憶體空間進行回收。瀏覽器上的GC(Gabage Collection垃圾回收)實現,大多是採用可達性演算法,關於可達性的物件,便是能與GC Roots構成連通圖的物件。當一個物件到GC Roots沒有任何引用鏈時,則會成為垃圾回收器的目標,系統會在合適的時候回收它所佔的記憶體。

我這裡使用的谷歌瀏覽器的Head Profiling,或者你也可以使用白鷺引擎的profiler:
使用很簡單:

1.開啟Google瀏覽器,開啟要監控的網頁,win下按F12彈出開發者工具
2.切換到Memory,選擇堆型別,選中Take Heap SnapShot開始進行快照
3.右邊的檢視列出了heap裡的物件列表,點選物件可以看到物件的引用層級關係
4.進入遊戲後拍下快照,開啟某個介面,關閉介面,拍下快照
5.將新的快照轉換到Comparsion對比檢視,進行記憶體對比分析
需要額外注意的是:
每次拍快照前,都會先自動執行一次GC,保證檢視裡的物件都是root可及的。GC的觸發是依賴瀏覽器的,所以不能通過時時觀察記憶體峰值而判斷是否有記憶體洩漏。

我們可以每隔一段時間來拍一次快照(由於公司專案原因,我就不展示真實專案了,此處僅作為教學):

我們可以開啟谷歌瀏覽器的記憶體分析工具後有三個選項,我們可以根據自己的除錯方式交替使用;

1.Heap snapshot - 用以列印堆快照,堆快照檔案顯示頁面的 javascript 物件和相關 DOM 節點之間的記憶體分配
2.Allocation instrumentation on timeline - 在時間軸上記錄記憶體資訊,隨著時間變化記錄記憶體資訊。
3.Allocation sampling - 記憶體資訊取樣,使用取樣的方法記錄記憶體分配。此配置檔案型別具有最小的效能開銷,可用於長時間執行的操作。它提供了由 javascript 執行堆疊細分的良好近似值分配。

這裡舉例使用堆快照分析,

右側檢視詳細資訊

可見rect物件一直在增高,那麼我們可以檢視一下導致rect物件未被釋放的原因:

是由於Rect物件中存在一個屬性rect一直被引用導致記憶體無法釋放,那麼我們到程式碼對應的位置去找,就可以較快的定位原因;最終我們發現是因為在自定義的一個全域性事件監聽器中例項化了一個物件,但是這個物件的一些屬性會持續被這個事件監聽器所引用而不會被回收

當然為了更快的定位哪個函式,我們也可以使用

一般結果是這個樣子

Overview的HEAP(堆)曲線圖表示JS堆。
Call Stack通常來說,垂直方向並沒有太大的意義,僅僅表示函式巢狀比較深而已,但是橫向表示呼叫時間,如果呼叫時間太長,那麼就需要優化優化了。錄製結果的呼叫堆疊,橫向表示時會出現帶有更多詳情的浮窗間,垂直方向表示呼叫棧,從上往下表示函式呼叫。滑動滑鼠滾輪可以檢視某段時間的呼叫棧資訊。把滑鼠放到Call Stacks呼叫棧的某個函式上面可以檢視函式詳細資訊。這個一般是效能優化時關注,對於記憶體洩漏,主要用於幫助定位進行了什麼操作。
Counter(計數器)窗格。在這裡你可以看到記憶體使用情況(與Overview(概述)窗格中的HEAP(堆)曲線圖相同),分別顯示以下內容:JS heap(JS堆),documents(文件),DOM nodes(DOM節點),listeners(偵聽器)和GPU memory(GPU記憶體)。勾選或取消勾選核取方塊可以將其從圖表中顯示或隱藏。

主要關注第三個的JS堆記憶體、節點數量、監聽器數量。滑鼠移到曲線上,可以在左下角顯示具體資料。這些資料若有一個在持續上漲,沒有下降趨勢,都有可能是洩漏。
由於篇幅原因,這裡不過多介紹這些工具的使用,網上有很多相關教程;

卡頓優化
我們通過PerfDog的資料發現GPU壓力很大,遊戲來說,渲染畫面久一般是drawcall過多,或者每次draw的時間較長。

而我們的遊戲在檢視在drawcall後確定是由於遊戲執行時drawcall過多,導致每幀的渲染耗時比較長,所以會呈現一種卡頓的現象;
關於檢視drawcall等可以通過白鷺自身的FPS皮膚檢視 白鷺debug文件
在優化前首先要了解egret在渲染的一幀裡做了什麼工作內容

細分的話又可以分成

每一幀的工作內容:

1.執行一次EnterFrame,此時,引擎會執行遊戲中的邏輯。並且丟擲EnterFrame事件
2.引擎會執行一個clear。將上一幀的畫面全部擦除
3.Egret核心會遍歷遊戲場景中的所有DisplayObject,並重新計算所有顯示物件的transform
4.所有的影像全部draw到畫布

現在來優化一下:
首先要降低drawcall:

1.把小圖全都換成圖集
2.實現文字合批,通過自定義字型,使用圖片字型的方式代替原生的字型
3.動靜分離,將需要變化的和不變的分別放在不同的層級下,比如背景層、圖示層和動態變化層
4.動畫儘量使用dragon bones幀動畫而不是spine 動畫
5.使用cacheAsBitmap,把向量圖在執行時以點陣圖形式進行計算

降低幀事件的開銷:

1.不要的DisplayObject,直接removeChild 而不是設定他的visible屬性為false,否則在第三步還會參與計算
2.不在主迴圈裡建立任何物件,遊戲中的人物、怪物、技能特效統統做成物件池
3.不在EnterFrame事件中做過多的操作,非要用可以自定義一些事件

我們可以用以下的函式統計建立的gameobject的數量

它是顯示了每一秒鐘去拿一個hashCount跟上一個hashCount作對比,這個hashCount是由白鷺引擎內部 API,用於統計引擎物件的建立數量。如果遊戲靜止放置不動,理論上hashCount diff的結果應該是0,實際上要儘可能控制在120以下,如果超標,只需要在引擎的 HashObject 的建構函式這裡新增一個斷點,在執行時去檢查呼叫堆疊就排查就可以了。

檢視PerfDog詳情:https://perfdog.qq.com/?ADTAG=media.dev_website

相關文章