Lua Profiler机制的源码解析

一. 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.

你可能感兴趣的:(Lua Profiler机制的源码解析)