1. 程式人生 > >Lua內存分析工具

Lua內存分析工具

pes header hash 官方文檔 html img 詳細 min 上傳

最近給公司寫了一個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端接收上傳快照,快照分析
    技術分享圖片

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類裏加上一個方法引入我們的庫函數
技術分享圖片

然後在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 (作者)

Lua內存分析工具