Lua Profiler——快速定位Lua性能问题

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,请在开始游戏前调用以下代码。

MikuLuaProfiler.HookLuaSetup.OnStartGame();

注意:不要在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配置表存储优化方案》

[Lua]字段,按照Lua内存的self进行排序,可以定位GC比较严重的Lua函数,进行针对性优化。也可以查看调用次数等。


在搜索框中搜索[Lua]

善用搜索功能,会揪出很多意想不到的性能问题,比如:老生常谈的Vertex3。

二、监控注册表
以SLua框架的示例Circle.cs 为例:

//Circle.cs
public 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>();
        }
        );
        ......
//Circle.lua
local 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 class
end
function class:update() -- gc alloc is zero0
    ......

C# 层创建的变量self、svr、update等,引用了Lua层的class、update等。

使用该工具得到的引用关系如图所示:

其中“@circle/circle&line:15"的函数为:

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

被添加到UI组件Slider的事件监听中。

“@circle/circle&line:65"的函数为:function class:update(),被一个LuaFunction类型对象update引用。

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

local class={}

(其余被引用的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。

function main()
    cube = GameObject.CreatePrimitive(PrimitiveType.Cube)
  ......

然后切换场景,是用工具检测得到被引用对象为:Cube,如图:

具体引用链为:

当切换场景时,虽然场景中没有了Cube对象,但对象池中还有,导致仍然有引用而无法GC。此时Cube对象是一个作为UnityEngine.Object为空,而作为System.Object不为空的对象,原因就是Lua对其的引用不为空,会导致泄漏。解决方案也较为简单,将Cube变量申明为local局部变量,解除引用即可:

function main()
  local cube = GameObject.CreatePrimitive(PrimitiveType.Cube)
  ......

更多实战例子可以阅读: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内存优化》
《Lua的CPU开销性能优化》

快用UWA Lab合辑Mark好项目!

请输入图片描述

今天的推荐就到这儿啦,或者它可直接使用,或者它需要您的润色,或者它启发了您的思路......

请不要吝啬您的点赞和转发,让我们知道我们在做对的事。当然如果您可以留言给出宝贵的意见,我们会越做越好。


【博物纳新】是UWA旨在为开发者推荐新颖、易用、有趣的开源项目,帮助大家在项目研发之余发现世界上的热门项目、前沿技术或者令人惊叹的视觉效果,并探索将其应用到自己项目的可行性。很多时候,我们并不知道自己想要什么,直到某一天我们遇到了它。

更多精彩内容请关注:lab.uwa4d.com