stat是虚幻引擎提供的性能统计和优化工具,比较类似于Unity的Profiler,通过在代码中埋指定的函数或宏,就可以将需要的信息监控起来。前面会简单介绍在C 中怎么用,以及stat实现原理,后面会介绍怎样在Lua中使用。如果对stat很了解了,只想看Lua使用方法,可以翻到最后。顺便说一下,Lua的使用方法是我自己实现的,目前网上都没有类似教程或做法,各种第三方Lua插件或其他语言的支持插件都没有对应的支持,所以我觉得比较有参考价值。
stat下面就简单讲一点点。网上的资料,包括知乎上都有非常多教程,也可以参考官方文档:
UE本身已经在引擎中埋好了非常多的点,什么都不加,在游戏控制台中只要输入stat startfile,stat stopfile,就可以记录下来游戏在start和stop之间的性能数据,包括每个监控代码块的耗时,内存,各种计数等信息。
记录信息保存在Saved/Profiling文件夹下,以ue4stats作为后缀的文件(UE5是uestats,很睿智的做法,毕竟大版本升级了,这里去掉了4,后缀不同所以不兼容老版本,但其实本质没什么变化)
通过SessionFrontend打开
如果只想看统计信息,不想开引擎,也可以到引擎目录下找UnrealFrontend.exe独立程序打开看。
我们项目基本上都是测试同学抓stat数据,开发进行性能分析。因为平时我也在写代码,写一半了测试发来stat文件要分析,这时因为刚写一半的引擎或游戏代码还没编译或编译不过,肯定开不了引擎,那么就没法从引擎里面打开SessionFrontend了。但这时直接用这个独立程序看,就可以不重新编译,写代码和分析性能两件事都不耽误,还是很方便的。
比如这里能看到游戏线程以及内部每个加了统计的代码块平均耗时,或者单帧耗时等。具体看官方文档,这里不细说。
在自己的代码中使用
在自己的代码中使用,UE也提供了教程,写的很详细
比如PlayerController Tick
在代码中用宏来声明:
使用的时候也有对应的宏
具体原理
这里是CycleCounter,也就是统计每帧被这个宏包含的代码块耗时的。可以看到用起来非常简单。我们查看源码:
可以看到,就是构造的时候Start,析构的时候Stop,因此在函数局部代码块里定义一个这个类的对象,当出了作用域就会析构,自然的就记录的开始和结束的时间。
Start和Stop都是调用FThreadStats的AddMessage函数,发了两个事件。
进去看,里面构造Message的时候,就从PlatformTime上取了Cycles()得到开始和结束的时间,证明了猜测的正确。
当然除了耗时外,也可以统计其他信息,比如自定义整数,浮点数,字符串等。他们都是通过FThreadStats::AddMessage发送给stat线程的,stat线程收到后就会展示到屏幕或者写文件。这里的stat信息,其实最终都构造成了一个很长的FName字符串,每一段信息都有固定前缀,拼接到一起成为一个LongName,大概如下面这样(这里只说明原理,可以自行查看源码)
所以,stat文件里,记录的就是很多条这样的信息。
至于stat创建,必须在代码里某个位置,用宏来声明。可以看到,其实就是定义了一个static变量,一般都是在全局范围的。
如果你比较细心,就会发现stat全部都是用宏来定义的,依赖于C 的静态编译,把需要统计的stat定义以及对应代码,通过编译推导,导出给引擎,那么对于lua这样的动态语言,比如想统计lua中某个函数的耗时,因为lua是解释执行的,并不能在编译期知道lua代码中定义的stat信息。因此C 提供的这些宏,在Lua中就完全用不了了,即使硬着头皮先定义好,但在lua中统计性能还得让C 不停的编译,就变成了一个非常麻烦的事情,也失去了使用Lua来高效开发的意义。所以我接下来做的事情,就是封装一套接口,可以让stat在运行时定义以及统计,这样Lua就可以方便的使用stat来统计性能了。
在运行时定义和使用stat
首先,做这件事,我们就要清楚Stat到底做了什么。前面也说了,统计时本质就是AddMessage到stat线程,但是AddMessage需要StatId这样的参数,需要先构造。C 是通过全局对象构造的,而运行时构造StatId的是要解决的问题,接下来会说。但总之,只要搞定了在运行时构造stat id和调用stat统计这两件事,就达到了目的。
构造stat
只要你一层一层的扒开stat中封装的宏,最终你会看到,这个statid是通过DoSetup这个函数,如下图这样构造出来的,显然这个函数即使运行时调用也没什么不可以。至于怎么调用到这里的具体过程就不细说了,可以自行看源码。
所以,这段代码就是我要封装的构造stat函数,把各个参数都暴露出来
这里简单讲一下具体每个参数的意义:
StatName,就是统计的Stat程序内部用的名字,StatDesc是一个友好可读的名字。
就像上图,我们在宏里写的一样,对应这两个参数
然后下面的Group,因为我这里只是统计Lua,所以就直接在C 预定义好了,所有Lua都使用同一个Group。这里就不通过参数传进来了
接下来是比较重要的4个参数:
bool bShouldClearEveryFrame:是否每帧都清除,如果为true,这里面的值每帧就清理成0了
EStatDataType::Type InStatType:stat数据类型,有int64,double,FName几种
bool bCycleStat:这个是CycleCounter专用,CycleCounter这个要打开
FPlatformMemory::EMemoryCounterRegion MemRegion:这个是内存专用
直接用这个函数非常丑,所以我就包装了几个和stat宏差不多的:
对于统计来说,lua不像CycleCounter有构造函数和析构函数,那么我们就只能给lua中封装两个函数Start和Stop,让lua手动调用(也很方便,比较像Unity的Profiler用法),当然除了CycleCounter外,还有其他的stat,比如加值,减值,设值等,本质上都是AddMessage给stat线程,这个封装就很简单了,我也照着stat宏封装了一些方便的函数
最后,lua要使用,肯定要包装成lua的格式导出到Lua
这样,就可以在lua中愉快的使用stat了
最后,附上具体源码,我是用的UnLua,其他Lua做法类似,可以自行修改使用。
github:
补充一点,对于Lua5.4,可以使用close语法来做到和析构函数一样得效果,有需要可以自行实现