Lua Profiler机制的源码解析

作者: 陈码工 | 来源:发表于2017-07-12 16:41 被阅读395次

    一. Lua Profiler介绍和使用

    1. Lua Profiler介绍

    官网:
    http://luaprofiler.luaforge.net/
    使用手册:
    http://luaprofiler.luaforge.net/manual.html
    source code:
    https://github.com/luaforge/luaprofiler

    2. Lua Profiler的使用

    local function DoSomethingMore(x)
      x = x / 2
    end
    
    local function DoSomething(x)
      x = x + 1
      if x % 2 then DoSomethingMore(x) end
    end
    
    prf = require("profiler") -- 引入profiler模块
    prf.start()  -- start
    for outer=1,3 do
      for inner=1,2 do
        DoSomething(inner)
      end
    end
    prf.stop()  -- stop
    

    二. 基础数据结构

    1. clock_t

    clock_t ==> unsigned long (8byte);

    2. STACK_RECORD&sSTATE

    struct lprofP_sSTATE {
       int stack_level;
       lprofS_STACK stack_top;
    };
    

    其中, stack_top是一个指针, 其指向栈顶的Stack_Record;

    struct lprofS_sSTACK_RECORD {
        clock_t time_marker_function_local_time;
        clock_t time_marker_function_total_time;
        char *file_defined;
        char *function_name;
        char *source_code;        
        long line_defined;
        long current_line;
        float local_time;
        float total_time;
        lprofS_STACK_RECORD *next;
    };
    

    三. 操作

    1. 创建Profiler:lprofP_init_core_profiler >> lprofM_init

    /* opens the log file */
    /* returns true if the file could be opened */
    lprofP_STATE* lprofP_init_core_profiler(const char *_out_filename, int isto_printheader, float _function_call_time) {
      lprofP_STATE* S;
      char auxs[256];
      char *s;
      char *randstr;
      const char *out_filename;
    
      function_call_time = _function_call_time;
      out_filename = (_out_filename) ? (_out_filename):(OUT_FILENAME);
            
      /* the random string to build the logname is extracted */
      /* from 'tmpnam()' (the '/tmp/' part is deleted)     */
      randstr = tmpnam(NULL);
      for (s = strtok(randstr, "/\\"); s; s = strtok(NULL, "/\\")) {
        randstr = s;
      }
    
      if(randstr[strlen(randstr)-1]=='.')
        randstr[strlen(randstr)-1]='\0';
    
      sprintf(auxs, out_filename, randstr);
      outf = fopen(auxs, "a");  //open a file named auxs, appending mode
      if (!outf) {
        return 0;
      }
    
      if (isto_printheader) {
        output("stack_level\tfile_defined\tfunction_name\tline_defined\tcurrent_line\tlocal_time\ttotal_time\n");
      }
    
      /* initialize the 'function_meter' */
      S = lprofM_init();  //一个关键的函数, 初始化S指针, 让其指向sSTATE栈, 也就是功能上的Meter计时器
      if(!S) {
        fclose(outf);
        return 0;
      }
        
      return S;
    }
    

    我们接着往下看, Meter计时器的初始化lprofM_init函数

    /* init stack */
    lprofP_STATE* lprofM_init() {
      lprofP_STATE *S;
      S = (lprofP_STATE*)malloc(sizeof(lprofP_STATE));
      if(S) {
        S->stack_level = 0;
        S->stack_top = NULL;
        return S;
      } else return NULL;
    }
    

    M_init函数干的事很简单, 就是创建一个sSTATE,也就是创建了一个栈。

    2. call序列:lprofP_callhookIN >> lprofM_enter_function

    /* computes new stack and new timer */
    void lprofP_callhookIN(lprofP_STATE* S, char *func_name, char *file, int linedefined, int currentline) {    
      S->stack_level++;
      lprofM_enter_function(S, file, func_name, linedefined, currentline);
    }
    
    /* the local time for the parent function is paused  */
    /* and the local and total time markers are started */
    void lprofM_enter_function(lprofP_STATE* S, char *file_defined, char *fcn_name, long linedefined, long currentline) {
      char* prev_name;
      char* cur_name;
      /* the flow has changed to another function: */
      /* pause the parent's function timer timer   */
      if (S->stack_top) {
        lprofM_pause_local_time(S);  //传入指向STATE的指针S, 对其STATE.stack_top所指向的frame的timer进行暂停操作
        prev_name = S->stack_top->function_name;  //把前一个函数的名字记下来
      } else prev_name = "top level";
      /* measure new function */
      lprofC_start_timer(&(newf.time_marker_function_local_time));  //开始新的timer
      lprofC_start_timer(&(newf.time_marker_function_total_time)); 
      newf.file_defined = file_defined;  //对新的frame做初始化
      if(fcn_name != NULL) {
        newf.function_name = fcn_name;  
      } else if(strcmp(file_defined, "=[C]") == 0) {
        cur_name = (char*)malloc(sizeof(char)*(strlen("called from ")+strlen(prev_name)+1));
        sprintf(cur_name, "called from %s", prev_name);  //prev_name是上一个frame中记录的函数名
        newf.function_name = cur_name;
      } else {
        cur_name = (char*)malloc(sizeof(char)*(strlen(file_defined)+12));
        sprintf(cur_name, "%s:%li", file_defined, linedefined);
        newf.function_name = cur_name;
      }    
      newf.line_defined = linedefined;
      newf.current_line = currentline;
      newf.local_time = 0.0;
      newf.total_time = 0.0;
      lprofS_push(&(S->stack_top), newf); //往STATE.stack_top所对应的stack中push进去一个新的record(一个记录, 等同于一个stack frame)
    }
    

    3. return序列: lprofP_callhookOUT >> lprofM_leave_function

    /* pauses all timers to write a log line and computes the new stack */
    /* returns if there is another function in the stack */
    int lprofP_callhookOUT(lprofP_STATE* S) {
    
      S->stack_level--;
      /* 0: do not resume the parent function's timer yet... */
      info = lprofM_leave_function(S, 0);
      /* writing a log may take too long to be computed with the function's time ...*/
      lprofM_pause_total_time(S);
      info->local_time += function_call_time;
      info->total_time += function_call_time;
      
      char* source = info->file_defined;
      char* name = info->function_name;
      
    // begin output...  
      output("%d\t%s\t%s\t%d\t%d\t%f\t%f\n", S->stack_level, source, name, 
         info->line_defined, info->current_line,
         info->local_time, info->total_time);
      // output finished!!!
    
      /* ... now it's ok to resume the timer */
      if (S->stack_level != 0) {
        lprofM_resume_function(S);
      }
    
      return 1;
    
    }
    
    lprofS_STACK_RECORD *lprofM_leave_function(lprofP_STATE* S, int isto_resume) {
    
      ASSERT(S->stack_top, "leave_function: stack_top null");
      leave_ret = lprofS_pop(&(S->stack_top));
      compute_local_time(&leave_ret);  //更新stack_record.local_time
      compute_total_time(&leave_ret);  //更新stack_record.total_time
      /* resume the timer for the parent function ? */
      if (isto_resume)
        lprofM_resume_local_time(S);
      return &leave_ret;
    }
    

    思考: 最关键的问题来了, 当我们准备好这些profilerInit, callHookIN, callHookOut函数后, 是如何被与target函数挂上钩的?

    四. hook原理

    1. profiler模块函数的定义

    根据profiler module的注册代码, start函数和stop函数对应的是init和stop函数
    static const luaL_reg prof_funcs[] = {
    { "pause", profiler_pause },
    { "resume", profiler_resume },
    { "start", profiler_init },
    { "stop", profiler_stop },
    { NULL, NULL }
    };

    1)init函数

    static int profiler_init(lua_State *L) {
      lprofP_STATE* S;
      float function_call_time;
      lua_pop(L, 1);
      function_call_time = calcCallTime(L);
     //left out some code to set name of output file...
      lua_sethook(L, (lua_Hook)callhook, LUA_MASKCALL | LUA_MASKRET, 0);  //关键代码行
    
      lua_pushlightuserdata(L, &profstate_id);
      lua_pushlightuserdata(L, S);
      lua_settable(L, LUA_REGISTRYINDEX);
    
      /* use our own exit function instead */
      lua_getglobal(L, "os");
      lua_pushlightuserdata(L, &exit_id);
      lua_pushstring(L, "exit");
      lua_gettable(L, -3);
      lua_settable(L, LUA_REGISTRYINDEX);
      lua_pushstring(L, "exit");
      lua_pushcfunction(L, (lua_CFunction)exit_profiler);
      lua_settable(L, -3);
      return 1;
    }
    

    注意到这其中调用了lua_sethook函数。(下面马上要说到)

    2)stop函数

    static int profiler_stop(lua_State *L) {
      lprofP_STATE* S;
      lua_sethook(L, (lua_Hook)callhook, 0, 0); //关键代码行
      lua_pushlightuserdata(L, &profstate_id);
      lua_gettable(L, LUA_REGISTRYINDEX);
      if(!lua_isnil(L, -1)) {
        S = (lprofP_STATE*)lua_touserdata(L, -1);
        /* leave all functions under execution */
        while (lprofP_callhookOUT(S));
        lprofP_close_core_profiler(S);
        lua_pushlightuserdata(L, &profstate_id);
        lua_pushnil(L);
        lua_settable(L, LUA_REGISTRYINDEX);
        lua_pushboolean(L, 1);
      } else { lua_pushboolean(L, 0); }
      return 1;
    }
    

    2. 挂钩过程

    1) lua_sethook函数(关键函数)

    这个函数定义在了ldebug.c文件中, 是Lua语言debug相关功能的核心代码部分;

    /*
    ** this function can be called asynchronous (e.g. during a signal)
    */
    LUA_API int lua_sethook (lua_State *L, lua_Hook func, int mask, int count) {
      if (func == NULL || mask == 0) {  /* turn off hooks? */
        mask = 0;
        func = NULL;
      }
      L->hook = func;  // 最关键的一行代码, 把L->hook设置成了func
      L->basehookcount = count;
      resethookcount(L);
      L->hookmask = cast_byte(mask);
      return 1;
    }
    

    这个函数其实做的事比较简单, 就是把lua_State.hook设置成传入的callhook函数.
    接下来一起看看callhook函数做了什么.

    2) 被设置到L->hook中的callhook函数

    static void callhook(lua_State *L, lua_Debug *ar) {
      int currentline;
      lua_Debug previous_ar;
      lprofP_STATE* S;  //STATE类型指针S
      lua_pushlightuserdata(L, &profstate_id);
      lua_gettable(L, LUA_REGISTRYINDEX);
      S = (lprofP_STATE*)lua_touserdata(L, -1);
    
      if (lua_getstack(L, 1, &previous_ar) == 0) {
        currentline = -1;
      } else {
        lua_getinfo(L, "l", &previous_ar);
        currentline = previous_ar.currentline;
      }
          
      lua_getinfo(L, "nS", ar);
    
      if (!ar->event) {  //关键代码部分:做事件的分发, 要么是call, 要么是return.
        /* entering a function */
        lprofP_callhookIN(S, (char *)ar->name,
                  (char *)ar->source, ar->linedefined,
                  currentline);
      }
      else { /* ar->event == "return" */
        lprofP_callhookOUT(S);
      }
    }
    

    那么留给我们的问题又来了, 这个lua_Debug类型的ar指针, 到底是何方神圣, 为什么!(ar->event)就是进入函数, 而else就是return回去呢?
    lua_Debug类型定义如下, 其实就是一个存放了一些debug相关信息的数据结构, 其中有一个属性叫event。

    typedef struct lua_Debug {
      int event;  //根据上下文推荐, 0是call, 其他是return;
      const char *name;           /* (n) */
      const char *namewhat;       /* (n) */
      const char *what;           /* (S) */
      const char *source;         /* (S) */
      int currentline;            /* (l) */
      int nups;                   /* (u) number of upvalues */
      int linedefined;            /* (S) */
      int lastlinedefined;        /* (S) */
      char short_src[LUA_IDSIZE]; /* (S) */
      /* private part */
      other fields
    } lua_Debug;
    

    event的数值含义在lua.h中可以找到

    /*
    ** Event codes, 来自lua.h文件
    */
    #define LUA_HOOKCALL    0
    #define LUA_HOOKRET    1
    #define LUA_HOOKLINE    2
    #define LUA_HOOKCOUNT    3
    #define LUA_HOOKTAILRET 4
    

    稍微总结一下, 目前为止, 我们知道了lua_Debug.event = {0, 1, 2, ...} (见上面的代码段).

    我们可以利用ldebug.c文件中的lua_sethook函数传入一个callhook函数作为事件的分发器(被挂在L->hook字段上). callhook函数可以根据lua_Debug.event的值, 来判断要呼叫callhookIN 还是 callhookOUT函数.

    所以说, 在导入profiler模块后, 当执行了profiler.start()函数的时候,
    该函数的代码lua_sethook(lua_State *L = L, lua_Hook func = (lua_Hook)callhook, int mask = LUA_MASKCALL | LUA_MASKRET, int count = 0) 运行了, 就把callhook函数这个事件分发器给装配到了L上(有一行代码写lua_State.hook = callhook)

    所以我们接下来需要关心, 在底层的LuaVM中,L->hook函数什么时候被调用的呢?(底层调用L->hook的机制)

    3. LVM调用hook的机制

    1)luaV_execute

    作为LVM的虚拟机主要函数, 其的结构其实非常简单,就是一个很长很长的switch。

    switch(opcode):
      case OP_01:
          xxx;
          break;
      case OP_02:
          xxx;
          break;
      (...etc...)
    

    其中,OP_CALL(call操作)的代码如下

    case OP_CALL: {
            int b = GETARG_B(i);
            int nresults = GETARG_C(i) - 1;
            if (b != 0) L->top = ra+b;  /* else previous instruction set top */
            L->savedpc = pc;
            switch (luaD_precall(L, ra, nresults)) {  //关键行:此处调用precall函数
              case PCRLUA: {
                nexeccalls++;
                goto reentry;  /* restart luaV_execute over new Lua function */
              }
              case PCRC: {
                /* it was a C function (`precall' called it); adjust results */
                if (nresults >= 0) L->top = L->ci->top;
                base = L->base;
                continue;
              }
              default: {
                return;  /* yield */
              }
            }
          }
    

    原来在lvm.c中的void luaV_execute(lua_State *L)这个虚拟机的执行函数中, 对OP_CALL(call操作)的定义中, 就有luaD_precall的存在.

    2) luaD_precall (ldo.c)

    int luaD_precall (lua_State *L, StkId func, int nresults) {
    ... //省略一些代码
    if (L->hookmask & LUA_MASKCALL) {
          L->savedpc++;  /* hooks assume 'pc' is already incremented */
          luaD_callhook(L, LUA_HOOKCALL, -1);  //只要看到有这行代码就行了
          L->savedpc--;  /* correct 'pc' */
        }
    ... //省略一些代码
    }
    

    继续往下看这个luaD_callhook函数, 这里, 我们传入的参数是(L, 0, -1)

    void luaD_callhook (lua_State *L, int event, int line) {
      lua_Hook hook = L->hook;  //关键行!!!hook就是我们最开始所赋值的callhook事件分发函数
      if (hook && L->allowhook) {
        ptrdiff_t top = savestack(L, L->top);
        ptrdiff_t ci_top = savestack(L, L->ci->top);
        lua_Debug ar;
        ar.event = event;  //关键行!!!把0赋值给ar.event
        ar.currentline = line;
        if (event == LUA_HOOKTAILRET)
          ar.i_ci = 0;  /* tail call; no debug information about it */
        else
          ar.i_ci = cast_int(L->ci - L->base_ci);
        luaD_checkstack(L, LUA_MINSTACK);  /* ensure minimum stack size */
        L->ci->top = L->top + LUA_MINSTACK;
        lua_assert(L->ci->top <= L->stack_last);
        L->allowhook = 0;  /* cannot call hooks inside a hook */
        lua_unlock(L);
        (*hook)(L, &ar);  //关键代码!!! 调用hook函数, 传入L和&ar作为参数.
        lua_lock(L);
        lua_assert(!L->allowhook);
        L->allowhook = 1;
        L->ci->top = restorestack(L, ci_top);
        L->top = restorestack(L, top);
      }
    }
    

    至此, LVM中L->hook被调用的过程我们也看明白了, 其原本就存在于虚拟机主函数luaV_execute中.
    这个逻辑链是luaV_execute --> OP_CALL --> luaD_precall (属于ldo.c文件) --> luaD_callhook --> (*hook)(L, &ar)

    END.

    相关文章

      网友评论

      • Kare:Lua 可以调用OC的Lib文件里的方法吗?

      本文标题:Lua Profiler机制的源码解析

      本文链接:https://www.haomeiwen.com/subject/uymbhxtx.html