Lua内存分析工具

最近给公司写了一个lua内存分析工具,可以方便的分析出Lua内存泄露问题(虽然还没正式使用,但我是这样想的,哈哈哈),有图形化界面操作,方便手机端上传快照等功能

内存分析我是在c语言端写的,也有人写过lua端的分析工具,也蛮好用的,不过lua分析工具本身也会影响到lua的内存占用(尽管用的是弱表缓存的),也会有些不准确。
Lua方案:https://github.com/yaukeywang/LuaMemorySnapshotDump

然后找到了云风大神写的C语言解决方案
https://blog.codingnow.com/2012/12/lua_snapshot.html
这个库功能颇为简单,简单到连对象引用链都没有,只打印出key名和内存地址

所以我还是决定自己造轮子改进一下云风大神的方案,也是更进一步的去学习一下lua的c api

C实现起来比Lua复杂一些

  1. 因为要操作Lua栈,稍微写错一个栈没对称弹出,就会导致溢出,调试起来非常麻烦
  2. 因为c语言就像一块空地,什么都要自己造,连一些最基本的数据结构,都没有...
  3. 你需要编译成各个平台的库,这个后面会讲到如何跟tolua c编译到一起
工具分为2个部分
  1. c库生成快照
  2. web端接收上传快照,快照分析
    web端图

Lua中哪些数据类型是需要GC的?

lua源码中定义了这些数据类型

/*
** basic types
*/
#define LUA_TNONE		(-1)

#define LUA_TNIL		0
#define LUA_TBOOLEAN		1
#define LUA_TLIGHTUSERDATA	2
#define LUA_TNUMBER		3
#define LUA_TSTRING		4
#define LUA_TTABLE		5
#define LUA_TFUNCTION		6
#define LUA_TUSERDATA		7
#define LUA_TTHREAD		8

使用GCObject的联合体将所有需要进行垃圾回收的数据囊括了进来。

/*
** Union of all collectable objects
*/
union GCObject {
  GCheader gch;
  union TString ts;
  union Udata u;
  union Closure cl;
  struct Table h;
  struct Proto p;
  struct UpVal uv;
  struct lua_State th;  /* thread */
};

但是还有一些不需要GC的数据类型,所以又定义了一个Value的联合体

/*
** Union of all Lua values
*/
typedef union {
  GCObject *gc;
  void *p;
  lua_Number n;
  int b;
} Value;

这样就可以将Lua中所有的数据类型表示出来了,Lua还使用了一个宏来判断哪些数据类型是需要GC的

#define iscollectable(o)	(ttype(o) >= LUA_TSTRING)

通过这个我们可以知道,定义在LUA_TSTRING后的数据类型,都需要GC。一共有:LUA_TSTRING、LUA_TTABLE、LUA_TFUNCTION、LUA_TUSERDATA、LUA_TTHREAD

通过这样的遍历方式,从根节点开始递归整颗GC树
遍历方式

如何遍历table?

void mark_object(lua_State *L, const char *desc, struct lua_gc_node *parent)
{
    luaL_checkstack(L, LUA_MINSTACK, NULL);
    int t = lua_type(L, -1);
    switch (t) {
    case LUA_TTABLE:
        mark_table(L, desc, parent);
        break;
    case LUA_TUSERDATA:
        mark_userdata(L, desc, parent);
        break;
    case LUA_TFUNCTION:
        mark_function(L, desc, parent);
        break;
    case LUA_TTHREAD:
        mark_thread(L, desc, parent);
        break;
    default:
        lua_pop(L,1);
        break;
    }
}

void mark_table(lua_State *L, const char *desc, struct lua_gc_node *parent)
{
    const void *p = lua_topointer(L, -1);
    if(p == NULL)
    {
        return;
    }
    if(isMark(p))
    {
        lua_pop(L, 1);
        return;
    }

    struct lua_gc_node *currNode = gen_node(L, p, desc, parent);

    bool weakk = false;
    bool weakv = false;

    if(lua_getmetatable(L, -1))
    {
        lua_pushliteral(L, "__mode");
        lua_rawget(L, -2);
        if (lua_isstring(L,-1)) 
        {
            const char *mode = lua_tostring(L, -1);
            if (strchr(mode, 'k')) 
            {
                weakk = true;
            }
            if (strchr(mode, 'v')) 
            {
                weakv = true;
            }
        }
        lua_pop(L,1);

        luaL_checkstack(L, LUA_MINSTACK, NULL);
        mark_table(L, ".[metatable]", currNode);
    }
 
    lua_pushnil(L);
    while (lua_next(L, -2) != 0) 
    {
        if(weakv)
        {
            lua_pop(L, 1);
        }
        else
        {
            char temp[128];
            const char * _key = keystring(L, -2, temp);
            mark_object(L, _key, currNode);
        }
        if(!weakk)
        {
            lua_pushvalue(L,-1);
            mark_object(L, ".[key]", currNode);
        }
    }
    lua_pop(L, 1);
}

const void *p = lua_topointer(L, -1);
取出栈顶的指针,下面用到指针做key存入一个哈希表里,来标记是否被遍历过

从metatable中取出__mode,来判断key,value是否为弱引用。如果是弱引用就不需要继续递归了,否则就继续调用mark_object递归

通过lua_next方法可以取出table中的key,value压入栈中

这里一定要严谨使用lua_pop(L, 1)管理虚拟栈的平衡,否则栈很快就溢出了

其他的函数可以多查找Lua手册,里面说的很详细,我就不一一列举啦。

另外在c语言中自己创建的内存,需要手动释放,否则也会有内存溢出问题

s = malloc(sizeof(struct lua_gc_node)); 通过malloc开辟内存
free(current_node); 对应使用free释放

递归完毕后,输出成Json格式的快照文件,方便Web端操作。

Web端功能

  1. 手机上文件传到PC上不太方便,所以弄了个web端直接接收上传的快照文件
  2. 取补集(可以取出2个快照之间,新创建了哪些东西没释放掉),比如战斗前快照,跟战斗后快照进行取补集,就可以知道战斗内有哪些是没释放的,立马就能查出泄露
  3. 取交集(可以查询常住内存)

上传文件的php代码

<?php
  if ($_FILES["file"]["error"] > 0)
  {
      echo "错误:" . $_FILES["file"]["error"] . "<br>";
  }
  else
  {
      if (file_exists("upload/" . $_FILES["file"]["name"]))
      {
          echo $_FILES["file"]["name"] . " 文件已经存在。 ";
      }
      else
      {
          // 如果 upload 目录不存在该文件则将文件上传到 upload 目录下
          move_uploaded_file($_FILES["file"]["tmp_name"], "upload/" . $_FILES["file"]["name"]);
          echo "文件存储在: " . "upload/" . $_FILES["file"]["name"];
      }
  }
?>

如何把我们的代码编译到toluac中?

在网上搜这方面的资料,找到了之前同事(外号姐夫)写的博客,哈哈
https://www.jianshu.com/p/5a35602adef8

他讲的很清楚,我这里就不写了,可以看这篇文章把环境搭好。

另外还需要在tolua#中的LuaDLL.cs类里加上一个方法引入我们的库函数
image.png

然后在LuaManager.cs中把函数注册进去给lua使用

lua.OpenLibs (LuaDLL.luaopen_snapshot37);
lua.LuaSetField(-2, "snapshot37");

这样在lua代码里,我们就可以通过

local snapLib = require "snapshot37"

来引入我们的函数了

总结

  1. Lua内存分析工具的一些解决方案
  2. Lua中各种数据类型是怎么表示的
  3. 遍历GCObject的步骤
  4. 具体的一些LUA C API有不明白的可以多查看Lua官方文档

本文主要介绍了以上内容,欢迎找我一起交流讨论

参考

https://blog.codingnow.com/2012/12/lua_snapshot.html
http://www.cnblogs.com/yaukey/p/unity_lua_memory_leak_trace.html
https://www.jianshu.com/p/5a35602adef8
https://troydhanson.github.io/uthash
《Lua设计与实现》—— codedump (作者)

posted @ 2018-02-25 00:03  李嘉的博客  阅读(8226)  评论(2编辑  收藏  举报