Lua Profiler——快速定位Lua性能問題

導讀

隨著Lua在項目中的大量使用,它所帶來的性能問題也逐步成為了項目運行時的重大性能瓶頸之一。特別是內存相關的性能問題,無論是內存分配過大還是內存洩露無法回收,目前都已經在不少研發項目中集中爆發。

UWA推出的GOT Online中的Lua模式已經慢慢成為研發團隊對Lua進行日常性能監控的有效手段。因此,也有越來越多的團隊反饋,在監控到table數持續上漲,引用Mono對象持續增多等等問題時,應該如何快速地解決?

本次博物納新推薦的開源庫項目:LuaProfiler-For-Unity,相信可以幫助到大家。
Lua Profiler For Unity支持XLua、SLua、ToLua,該工具是基於遠程Socket的Profiler工具,因此它支持Android,iOS的真機Profiler。

小編將結合實際項目中遇到的問題,為大家介紹這款開源庫的用法。

開源庫鏈接:( https://lab.uwa4d.com/lab/5bf38db072745c25a80c1276 )

作者Blog:(https://www.zhihu.com/people/ElPsyConGree/activities)

操作流程

一、部署和安裝
參考項目中的Readme文檔闡述的詳細流程:
1、打開兩個Unity項目,一個放進遊戲客戶端,一個用於展示數據
2、打開LuaProfiler文件夾

LuaProfiler文件目錄

3、將 LuaProfilerClient 文件夾複製到遊戲項目內,如果您的C#Lua腳本位於Plugins文件夾中,則將 LuaProfilerClient 複製到插件。此工具必須確保該代碼必須位於具有C#Lua代碼的同一DLL中。
4、使用 Unity5.6 or newer version Unity版本將 LuaProfilerServer 作為Unity項目打開
5、如果Unity版本低於5,請在開始遊戲前調用以下代碼。

<code>MikuLuaProfiler.HookLuaSetup.OnStartGame();/<code>

注意:不要在static變量聲明裡面啟動Lua流程(比如XLua的Demo),請在Awake或者Start裡面進行調用。


二、使用教程
(該小節內容全部來自於項目Readme文檔,想要閱讀更詳細教程的讀者可訪問項目主頁:(https://lab.uwa4d.com/lab/5bf38db072745c25a80c1276))

1、配置客戶端

Lua Profiler Client界面

LuaProfilerClient插件所在的遊戲項目工程,通過Editor界面的Window->Lua Profiler Window打開客戶端設置界面。選擇想要分析器的代碼類型,C#代碼顏色為綠色,Lua代碼顏色為藍色。

2、配置服務器端
LuaProfilerServer插件所在的項目工程,通過Editor界面的Window->Lua Profiler Window打開服務器數據顯示界面。

Lua Profiler Server界面

單擊OpenService,等待客戶端連接。

操作流程示意圖

3、相關功能操作

使用示意圖

3.1 相關基礎數據統計
折線走勢圖展示了PSS、Mono內存、Lua內存、FPS的走勢:

折線走勢圖

數據列表

數據列表中列舉了當前幀下圖所示的相關數據:

3.2 監控註冊表

註冊表引用的Lua對象統計

此處區域會顯示當前被註冊表引用的類型為Function和table的Lua對象。

3.3 Diff兩個不同時期的Lua變量
選取一個適當的時機,比如配置表加載完後,準備打開一個新的UI的時候點擊MarkLuaRecord按鈕。

操作步驟

打開UI然後關閉並卸載掉UI資源,點擊DiffRecord,工具將會對Mark時候的Lua變量與DiffRecord時候的變量進行差異比較。

數據顯示界面

點擊ShowLog按鈕,將會把文件存盤打開之後將把對於變量的類型以及引用路徑打印出來。注意_G表示全局表,_R表示被C#引用的對象。

引用鏈

3.4 Destroy null values統計
該模塊展示了Unity已經將資源釋放,而Lua仍然引用的變量。

引用鏈


功能原理詳解

一、相關基礎數據統計
查看這部分數據時,除了關注排序中開銷較大、內存佔用較高的函數外還可以關注一些重要的字段,例如:

require字段,比較普遍的性能問題在於:加載配置表時產生一個內存佔用較大的table,可以通過搜索require字段並進行排序查找內存佔用較大的配置表,並對它進行針對性優化。

關於配置表的優化方案可以閱讀《Lua配置表存儲優化方案》(https://blog.uwa4d.com/archives/1490.html)。

[Lua]字段,按照Lua內存的self進行排序,可以定位GC比較嚴重的Lua函數,進行針對性優化。也可以查看調用次數等。

在搜索框中搜索[Lua]

善用搜索功能,會揪出很多意想不到的性能問題,比如:老生常談的Vertex3。

二、監控註冊表
以SLua框架的示例Circle.cs 為例:

<code>//Circle.cspublic class Circle : MonoBehaviour { LuaSvr svr; LuaSvr s2; LuaTable self; LuaFunction update; [CustomLuaClass] public delegate void UpdateDelegate(object self); UpdateDelegate ud; void Start () { svr = new LuaSvr(); svr.init(null, () => { self = (LuaTable)svr.start("circle/circle"); update = (LuaFunction)self["update"]; ud = update.cast<updatedelegate>(); } ); ....../<updatedelegate>/<code>

<code>//Circle.lualocal class={}function main() local slider = GameObject.Find("Canvas/Slider"):GetComponent(UI.Slider) local counttxt = GameObject.Find("Canvas/Count"):GetComponent(UI.Text) slider.onValueChanged:AddListener( function(v) class:init(v) counttxt.text=string.format("cube:%d",v) end ) class.root = GameObject("root") class.ftext = GameObject.Find("Canvas/Text"):GetComponent(UI.Text) class.r=10 class.cubes={} class.t=0 class.f=0 class.framet=0 class.max=0 class:init() return classendfunction class:update() -- gc alloc is zero0 ....../<code>

C# 層創建的變量self、svr、update等,引用了Lua層的class、update等。

使用該工具得到的引用關係如圖所示:

其中“@circle/circle&line:15"的函數為:

<code>function(v) class:init(v) counttxt.text=string.format("cube:%d",v)end/<code>

被添加到UI組件Slider的事件監聽中。

“@circle/circle&line:65"的函數為:function class:update(),被一個LuaFunction類型對象update引用。

其中具有key為:bgCurrent、init、root、t等值的table,為Lua代碼中的:

<code>local class={}/<code>

(其餘被引用的table和function是框架初始化時產生的)

C#層是一個ref,內存佔用較小,但是Lua層會是一個較為複雜的table或者函數調用等,內存佔用較大。當不再使用的C#對象沒有被完全釋放時,由於C#層內存佔用較小,並不會及時進行GC,使得Lua層仍然存在引用,無法進行GC,導致大量內存滯留。

三、Destroy null values統計
在任意一種Lua插件中,都存在類似的機制:在C#層維護一個Cache來引用那些被Lua訪問過的C#層對象,防止出現以下的問題:當Lua中再次訪問該C#對象時,該對象可能已經被C#層的GC回收掉了,從而導致邏輯錯誤。所以,在Lua中始終保留某個C#層對象的引用,將會導致其無法被釋放,當這樣的引用越來越多,就會導致C#層的內存洩漏。

其中比較常見的例子便是:應該被申明為local的對象忘記寫local。

<code>function main() cube = GameObject.CreatePrimitive(PrimitiveType.Cube) ....../<code>

然後切換場景,是用工具檢測得到被引用對象為:Cube,如圖:

具體引用鏈為:

當切換場景時,雖然場景中沒有了Cube對象,但對象池中還有,導致仍然有引用而無法GC。此時Cube對象是一個作為UnityEngine.Object為空,而作為System.Object不為空的對象,原因就是Lua對其的引用不為空,會導致洩漏。解決方案也較為簡單,將Cube變量申明為local局部變量,解除引用即可:

<code>function main() local cube = GameObject.CreatePrimitive(PrimitiveType.Cube) ....../<code>

更多實戰例子可以閱讀:https://zhuanlan.zhihu.com/p/89912209

四、Diff兩個不同時期的Lua變量
對兩個時期的Lua State做兩次完整的快照,通過比較兩次快照的數據,可以得到相關增加與減少的變量。得到疑似洩露的地方。通過快照獲取到的引用鏈定位洩漏的變量。

該工具得到引用鏈中:_G表示全局表,_R表示被C#引用的對象

引用鏈

想要更加深入瞭解該工具、深入瞭解Lua性能優化方案的讀者,推薦閱讀:

1、作者書寫的工具介紹與性能檢測思路的文章,其中詳細解釋了Lua、Mono雙GC系統、以及Mono對象、Lua對象、Unity對象三者的釋放流程以及Lua、C#、C++整個調用流程結構:(https://www.zhihu.com/question/307064711)

2、UWA Blog中對於Lua性能優化的文章:
《Lua性能優化—Lua內存優化》

(https://blog.uwa4d.com/archives/usparkle_luaperformance.html)

《Lua的CPU開銷性能優化》

(https://blog.uwa4d.com/archives/2037.html)


快用UWA Lab合輯Mark好項目!

今天的推薦就到這兒啦,或者它可直接使用,或者它需要您的潤色,或者它啟發了您的思路......

請不要吝嗇您的點贊和轉發,讓我們知道我們在做對的事。當然如果您可以留言給出寶貴的意見,我們會越做越好。

【博物納新】是UWA旨在為開發者推薦新穎、易用、有趣的開源項目,幫助大家在項目研發之餘發現世界上的熱門項目、前沿技術或者令人驚歎的視覺效果,並探索將其應用到自己項目的可行性。很多時候,我們並不知道自己想要什麼,直到某一天我們遇到了它。

更多精彩內容請關注:lab.uwa4d.com