基于 Cocos 引擎的 Lua 函数效率分析器
基本思路:
Lua 的 lua_hook 功能能在每个函数被调用时和返回时触发一个特定回调,通过计算调用和返
回的时间差即可知道一个函数的耗时
Lua 的 lua_getinfo 函数能取得当前 Lua 的完整调用栈信息
在 ios 和 android 平台可以使用 std::chrono::high_resolution_clock 获得高精度时间,windows
平 台 上 该 接 口 精 度 不 足 , 需 要 更 换 为 QueryPerformanceCounter 和
QueryPerformanceFrequency 两个接口
通过在 hook 函数开头和返回时分别计时来尽量排除计时器本身开销带来的误差
实现功能:
能统计 Lua 虚拟机中每个函数的独占开销占比、总开销占比、调用次数、平均调用时间,最
长调用时间和时间加权的平均调用时间
在调用结束后,生成一个 csv 格式的表格文件,方便进行分析
代码实现:
C++部分代码:
// lua profiler
static inline QWORD profileTimer()
{
#ifdef _MSC_VER
LARGE_INTEGER time;
QueryPerformanceCounter(&time);
return time.QuadPart;
#else
return std::chrono::high_resolution_clock::now().time_since_epoch().count();
#endif
}
static inline QWORD profileTicker()
{
#ifdef _MSC_VER
LARGE_INTEGER time;
QueryPerformanceFrequency(&time);
return time.QuadPart;
#else
return
std::chrono::high_resolution_clock::duration(std::chrono::seconds(1)).count();
#endif
}
struct ProfileStack
{
};
std::string name;
QWORD
QWORD
QWORD
startTime;
startOffset;
startCost;
struct ProfileStatistics
{
};
std::string name;
QWORD
QWORD
QWORD
QWORD
selfCost = 0;
totalCost = 0;
maxCost = 0;
callCount = 0;
double
sqrSum = 0;
static std::vector
profileStack;
static QWORD profileOffset = 0;
static QWORD profileCost = 0;
static std::unordered_map profileStatistics;
static void profileHook(lua_State* L, lua_Debug* d)
{
auto p1 = profileTimer();
if (d->event) // return
{
if (profileStack.empty()) return;
auto& ci = profileStack.back();
auto offset = profileOffset - ci.startOffset;
auto cost = p1 - ci.startTime - offset;
auto selfCost = cost - (profileCost - ci.startCost);
profileCost += selfCost;
auto& s = profileStatistics[ci.name];
s.selfCost += selfCost;
s.totalCost += cost;
s.callCount += 1;
s.sqrSum += (double)cost * (double)cost;
s.maxCost = std::max(s.maxCost, cost);
profileStack.pop_back();
auto p2 = profileTimer();
profileOffset += p2 - p1;
}
else // call
{
profileStack.push_back(ProfileStack());
auto& s = profileStack.back();
s.startCost = profileCost;
{
}
lua_getinfo(L, "Sn", d);
std::ostringstream oss;
oss << d->short_src;
if (d->linedefined > 0)
oss << "(" << d->linedefined << ")";
if (d->name) oss << ":[" << d->namewhat << "]" << d->name;
s.name = oss.str();
auto p2 = profileTimer();
auto offset = p2 - p1;
profileOffset += offset;
s.startTime = p2;
s.startOffset = profileOffset;
}
}
static void dumpProfile()
{
std::vector
list;
for (auto& i : profileStatistics)
{
}
list.push_back(i.second);
list.back().name = i.first;
auto fileUtils = cocos2d::FileUtils::getInstance();
std::ofstream fout(fileUtils->getSuitableFOpen(fileUtils->getWritablePath() +
"profile.csv"));
auto ticker = profileTicker() / 1000.0;
fout << "function name,total cost,self cost,call count,avg cost,max cost,avg^2 cost"
<< std::endl;
for (auto& i : list)
{
fout << i.name << ",";
fout << i.totalCost * 100.0 / profileCost << "%,";
fout << i.selfCost * 100.0 / profileCost << "%,";
fout << i.callCount << ",";
fout << (double)i.totalCost / i.callCount / ticker << ",";
fout << i.maxCost / ticker << ",";
fout << i.sqrSum / i.totalCost / ticker << std::endl;
}
}
static void beginProfile()
{
}
sealp::callLuaFunction("showNotify", "beginProfile");
profileOffset = 0;
profileCost = 0;
profileStack.clear();
profileStatistics.clear();
auto L = cocos2d::LuaEngine::getInstance()->getLuaStack()->getLuaState();
lua_sethook(L, profileHook, LUA_MASKCALL | LUA_MASKRET, 0);
void SealUtilToLua::beginProfile()
{
}
auto L = cocos2d::LuaEngine::getInstance()->getLuaStack()->getLuaState();
lua_Debug ar;
if (lua_getstack(L, 0, &ar))
{
}
auto scheduler = cocos2d::Director::getInstance()->getScheduler();
scheduler->performFunctionInCocosThread([]() { ::beginProfile(); });
else ::beginProfile();
static void endProfile()
{
}
auto L = cocos2d::LuaEngine::getInstance()->getLuaStack()->getLuaState();
lua_sethook(L, profileHook, 0, 0);
dumpProfile();
sealp::callLuaFunction("showNotify", "endProfile");
void SealUtilToLua::endProfile()
{
auto L = cocos2d::LuaEngine::getInstance()->getLuaStack()->getLuaState();
lua_Debug ar;
if (lua_getstack(L, 0, &ar))
{
}
auto scheduler = cocos2d::Director::getInstance()->getScheduler();
scheduler->performFunctionInCocosThread([]() { ::endProfile(); });
else ::endProfile();
}
将 SealUtilToLua::beginProfile()与 SealUtilToLua::beginProfile()两个接口导出到 Lua,在 Lua
中通过控制台指令调用即可
样例报表:
见 profile.csv,配合 excel 的筛选和排序功能能非常方便的定位开销过高的函数,通过数值间
的关联性还能分析出函数是否被重复调用等其他问题