如何自定义Profiler Counter统计Lua调用
解读
在国内 Unity 项目里,90% 以上手游的热更新脚本层是 Lua,而 Unity 2020+ 自带的 Profiler Counter API 只能采样 C# 层。面试官问“自定义 Profiler Counter 统计 Lua 调用”,本质想看三件事:
- 你是否理解 Lua 与 C# 的交互链路(xlua、tolua、slua、MoonSharp 等);
- 能否把 Lua 层的“次数/时间”无损地 透传到 C# Profiler 体系;
- 是否知道 GPU/主线程/busy-wait 陷阱,保证数据不污染真机性能。
答不到“采样点注入、线程安全、Release 包无开销”这三点,基本会被判“只写过业务,没做过框架”。
知识点
- Unity ProfilerCounter 的 C# 注册范式
ProfilerCounterValue<int>/long/float>必须在主线程构造,且Category选Scripts或自定义LuaStats。 - Lua Hook 机制
xlua/tolua 在luaD_call前后预留了lua_pushcfunction(L, profile_hook)的注入点;也可直接用lua_sethook(L, hook, LUA_MASKCALL | LUA_MASKRET, 0)。 - 线程安全与锁-free 队列
Lua 虚拟机线程 ≠ Unity 主线程,需把采样数据写进 单生产者单消费者环形缓冲区,主线程每帧ProfilerCounterValue.Sample()一次性消费。 - il2cpp 裁剪与宏隔离
用#if ENABLE_PROFILER && !UNITY_RELEASE包裹整个模块,防止 Release 包被打入;同时加[Preserve]防 il2cpp 裁剪。 - 真机验证
安卓需adb forward tcp:34999 localabstract:Unity-{bundle},同时打开Development Build与Autoconnect Profiler,确认自定义 Counter 出现在 Unity Profiler Modules 面板。
答案
分五步落地,可直接写进公司框架:
- 定义 Counter
#if ENABLE_PROFILER && !UNITY_RELEASE
using Unity.Profiling;
static readonly ProfilerCategory LuaCategory = ProfilerCategory.Scripts;
static readonly ProfilerCounterValue<int> LuaCallCounter =
new(LuaCategory, "Lua.CallCount", ProfilerMarkerDataUnit.Count,
ProfilerCounterOptions.FlushOnEndOfFrame);
static readonly ProfilerCounterValue<double> LuaTimeCounter =
new(LuaCategory, "Lua.Time(ms)", ProfilerMarkerDataUnit.TimeNanoseconds,
ProfilerCounterOptions.FlushOnEndOfFrame);
#endif
- Lua 层注入
以 xlua 为例,在xlua.c的luaD_call前后加
#if ENABLE_PROFILER && !UNITY_RELEASE
extern void LuaProfiler_Begin(const char* name);
extern void LuaProfiler_End();
#endif
在 luaD_call 前调用 LuaProfiler_Begin(funcname),返回后调用 LuaProfiler_End()。
- C 层到 C# 层零拷贝
typedef struct { uint64_t time; const char* name; } LuaSample;
#define LUA_PROFILER_BUFFER_SIZE (1 << 14)
static LuaSample ring[LUA_PROFILER_BUFFER_SIZE];
static int wpos = 0;
void LuaProfiler_Begin(const char* name){
ring[wpos].name = name;
ring[wpos].time = getTimeNanoseconds();
}
void LuaProfiler_End(){
uint64_t delta = getTimeNanoseconds() - ring[wpos].time;
ring[wpos].time = delta;
wpos = (wpos + 1) & (LUA_PROFILER_BUFFER_SIZE - 1);
}
getTimeNanoseconds() 用 clock_gettime(CLOCK_MONOTONIC),安卓精度 20ns 以内。
- 主线程消费
#if ENABLE_PROFILER && !UNITY_RELEASE
void Update(){
int r = LuaProfiler_PeekReadPos(); // native 暴露读指针
while(lastRead != r){
var s = LuaProfiler_GetSample(lastRead);
LuaCallCounter += 1;
LuaTimeCounter += s.time * 1e-6; // ns -> ms
lastRead = (lastRead + 1) & (BUFFER_SIZE - 1);
}
}
#endif
- 打包验证
打出 Development 包,Unity Profiler 窗口勾选 Scripts 模块,即可看到两条自定义曲线:
Lua.CallCount 每帧调用次数
Lua.Time(ms) 每帧 Lua 总耗时
与 Gfx.WaitForPresent 并列,方便定位“Lua 峰值导致帧率下降”。
拓展思考
- GPU 遮挡查询与 Lua 的关系
如果 Lua 每帧构造大量 GameObject,会出现 Gfx.WaitForPresent 飙升;此时把 Lua 调用 Counter 与 GPU 时间轴对齐,可一眼看出“脚本压力”还是“渲染压力”。 - 采样率降级
中低端机可动态把LUA_MASKCALL改成计数器每 10 次采样 1 次,用ProfilerCounterOptions.ResetToZeroOnFlush防止溢出。 - 与 Addressable 热更新联动
Lua 代码走 Addressable 热更时,Profiler 宏也要热更:把ENABLE_PROFILER做成远程配置,热更后重启 Lua 虚拟机,实现“线上包也能远程采样”,方便运营期定位外挂脚本性能bug。