如何自定义Profiler Counter统计Lua调用

解读

在国内 Unity 项目里,90% 以上手游的热更新脚本层是 Lua,而 Unity 2020+ 自带的 Profiler Counter API 只能采样 C# 层。面试官问“自定义 Profiler Counter 统计 Lua 调用”,本质想看三件事:

  1. 你是否理解 Lua 与 C# 的交互链路(xlua、tolua、slua、MoonSharp 等);
  2. 能否把 Lua 层的“次数/时间”无损地 透传到 C# Profiler 体系
  3. 是否知道 GPU/主线程/busy-wait 陷阱,保证数据不污染真机性能
    答不到“采样点注入、线程安全、Release 包无开销”这三点,基本会被判“只写过业务,没做过框架”。

知识点

  1. Unity ProfilerCounter 的 C# 注册范式
    ProfilerCounterValue<int>/long/float> 必须在主线程构造,且 CategoryScripts 或自定义 LuaStats
  2. Lua Hook 机制
    xlua/tolua 在 luaD_call 前后预留了 lua_pushcfunction(L, profile_hook) 的注入点;也可直接用 lua_sethook(L, hook, LUA_MASKCALL | LUA_MASKRET, 0)
  3. 线程安全与锁-free 队列
    Lua 虚拟机线程 ≠ Unity 主线程,需把采样数据写进 单生产者单消费者环形缓冲区,主线程每帧 ProfilerCounterValue.Sample() 一次性消费。
  4. il2cpp 裁剪与宏隔离
    #if ENABLE_PROFILER && !UNITY_RELEASE 包裹整个模块,防止 Release 包被打入;同时加 [Preserve] 防 il2cpp 裁剪。
  5. 真机验证
    安卓需 adb forward tcp:34999 localabstract:Unity-{bundle},同时打开 Development BuildAutoconnect Profiler,确认自定义 Counter 出现在 Unity Profiler Modules 面板。

答案

分五步落地,可直接写进公司框架:

  1. 定义 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
  1. Lua 层注入
    以 xlua 为例,在 xlua.cluaD_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()

  1. 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 以内。

  1. 主线程消费
#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
  1. 打包验证
    打出 Development 包,Unity Profiler 窗口勾选 Scripts 模块,即可看到两条自定义曲线:
    Lua.CallCount 每帧调用次数
    Lua.Time(ms) 每帧 Lua 总耗时
    与 Gfx.WaitForPresent 并列,方便定位“Lua 峰值导致帧率下降”。

拓展思考

  1. GPU 遮挡查询与 Lua 的关系
    如果 Lua 每帧构造大量 GameObject,会出现 Gfx.WaitForPresent 飙升;此时把 Lua 调用 Counter 与 GPU 时间轴对齐,可一眼看出“脚本压力”还是“渲染压力”。
  2. 采样率降级
    中低端机可动态把 LUA_MASKCALL 改成计数器每 10 次采样 1 次,用 ProfilerCounterOptions.ResetToZeroOnFlush 防止溢出。
  3. 与 Addressable 热更新联动
    Lua 代码走 Addressable 热更时,Profiler 宏也要热更:把 ENABLE_PROFILER 做成远程配置,热更后重启 Lua 虚拟机,实现“线上包也能远程采样”,方便运营期定位外挂脚本性能bug。