PerfDog 助力微信小遊戲優化-----實踐

不二發表於2020-07-28

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

第一部分主要介紹通過PerfDog發現問題,
第二部分主要介紹通過PerfDog的資料定位並解決問題。
第三部分介紹原生小遊戲常見優化的地方

PerfDog具體操作方法不再贅述,這裡可以看文件PerfDog使用說明

第一部分————資料分析

第一次測試資料

FPS感人(我們限幀60)
在這裡插入圖片描述
Cpu勉強還過得去
【圖一】
在這裡插入圖片描述
【圖二】
在這裡插入圖片描述
記憶體堪憂

在這裡插入圖片描述


有的同學可能發現 App的CPUusage比total cpuusage低很多(圖一),是因為我選擇測試的是微信app,小遊戲 是作為子程式而存在的,所以後來選擇PerfDog的子程式進行測試,得到的資料會更加的精準
==深色表示正在執行的頂層程式==
在這裡插入圖片描述
再次測試就是正常的資料啦(圖二),於是我們切換到子執行緒開始進行第二次測試

第二次測試資料

FPS資料:
在這裡插入圖片描述
CPU資料:
在這裡插入圖片描述
記憶體資料:
在這裡插入圖片描述
這時我們在測試過程中發現記憶體不斷上升,沒有呈現一個正常的記憶體趨勢,所以調整了一下測試策略

測試資料組成:
我們在測試過程中做了一些特殊操作:

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

在這裡插入圖片描述
GPU壓力山大
在這裡插入圖片描述
我們通過FPS資料發現在遊戲過程Jank十分嚴重,FPS波動過於劇烈,尤其是集中在UI開啟或者關閉的時候,這個時候我們進行資料排查發現GPU的使用率也變得異常高,基本上已經爆表,很明顯渲染的壓力很大,而我們遊戲UI開啟時實際上戰鬥也會被渲染,這和我們遊戲的設計有關,所以渲染的壓力很大。

再來看看記憶體:
記憶體資料:
在這裡插入圖片描述
我們通過PerfDog的資料發現記憶體是呈現一直上升的狀態,尤其是VSS(VirtualMemory),如脫韁的野馬一發不可收拾,這樣下去最終的結果就是被System Kill掉。
其實現在已經可以確定是發生了記憶體洩露,在72分鐘的時間裡記憶體從726M到了956M,而且還在不斷上升;

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

結論:

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

卡頓優化

我們發現在遊戲執行時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 的建構函式這裡新增一個斷點,在執行時去檢查呼叫堆疊就排查就可以了。

第三部分————原生小遊戲優化

小小遊戲中避免頻繁使用setData,這裡是最容易出問題的地方
setData原理:
每一次setData, 邏輯層向渲染層的發起一次通訊,這個通訊還不是直接傳給webView, 而是通過走了native層,渲染層收到通訊後,還需要重新渲染出來,
一次setData會帶來兩次開銷:通訊的開銷 + webview更新的開銷。
在這裡插入圖片描述
儘量避免頻繁使用setData
在這裡插入圖片描述

小程式和原生小遊戲有很多相同的地方:在這裡插入圖片描述
補充:
微信小程式公開課

相關文章