0

xhprof扩展实现的一些细节

 2 years ago
source link: http://yaoguais.github.io/article/xhprof/theory.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

xhprof扩展实现的一些细节

我们知道zend的一些内核调用都是函数指针,对函数指针进行一层包裹,就可以实现在调用前后做相关的记录,这个就是一些优化、缓存、诊断扩展实现的部分原理。

  1. xhprof_enable执行流程
  2. 回调函数系列
  3. hp_globals全局变量
  4. BEGIN_PROFILING 宏
  5. END_PROFILING 宏
  6. wall time 实现
  7. cpu usage 实现
  8. memory usage 实现

xhprof_enable执行流程

首选载入扩展时会调用MINIT函数,在此函数中注册了三个常量(用户xhprof_enable的第一个参数)、获取CPU的数量、设置CPU亲和力、初始化hp_globals的一些字段。

请求来了,会执行RINIT函数,在此函数并没有任何实质代码。

解析脚本,发现调用了xhprof_enable函数,在此函数中首先根据第二参数注册忽略的函数列表,然后调用hp_begin启动诊断。

hp_begin中,首先替换了zend_compile_filezend_compile_stringzend_execute等函数指针。 然后根据调用模式,设置了一些回调指针。添加"main()"到统计中去,等等。

接着执行用户的代码,回调我们设置的函数,进行信息统计。

接着执行脚本到xhprof_disable,调用hp_stop做一些收尾工作,然后返回hp_globals.stats_count这个数组。

脚本执行结束调用RSHUTDOWN,执行部分收尾工作,如重置某些变量。

最后执行MSHUTDOWN函数,释放申请的变量。

xhprof_sample_enable与上面的流程基本一致,只是函数回调时做的工作不同而已。

接下来分析我们设置的回调函数。

回调函数系列

设置的内核回调主要有个四个:

  • hp_compile_file主要以key(load::文件名)记录相关信息。
  • hp_compile_string主要以key(eval::文件名)记录eval函数执行的相关信息。
  • hp_executehp_execute_ex记录用户代码执行的相关信息。
  • hp_execute_internal记录C函数执行的相关信息。

hp_globals全局变量

该变量是一个hp_global_t结构体,其定义如下:

typedef struct hp_global_t {
  /*当前是否启动,默认是0,调用xhprof_enable等后会设置成1*/
  int              enabled;

  /*保证该全局变量只被初始化一次,类似于单例模式的实现*/
  int              ever_enabled;

  /*保存每个函数执行的信息,xhprof_disable等的返回值*/
  zval            *stats_count;

  /*采样模式还是一般模式*/
  int              profiler_level;

  /*被记录函数栈的栈顶指针*/
  hp_entry_t      *entries;

  /*空闲hp_entry_t变量的栈顶指针*/
  hp_entry_t      *entry_free_list;

  /*回调函数函数的集合,每一个函数的实现形式类似于C++中的虚函数*/
  hp_mode_cb       mode_cb;

  /*       ----------   Mode specific attributes:  -----------       */

  /*最后的采样时间*/
  struct timeval   last_sample_time;//使用gettimeofday获取
  uint64           last_sample_tsc;//使用cycle_timer获取
  /*采样的时间间隔,微妙,默认是0.1s*/
  uint64           sampling_interval_tsc;

  /*cpu的频率*/
  double *cpu_frequencies;

  /*cpu的个数*/
  uint32 cpu_num;

  /*cpu亲和力的掩码*/
  cpu_set_t prev_mask;

  /*当前cpu的ID*/
  uint32 cur_cpu_id;

  /*当前执行的模式,xhprof的第一个参数值,决定是否记录cpu、内存等*/
  uint32 xhprof_flags;

  /*被记录函数的映射哈希表*/
  uint8  func_hash_counters[256];

  /*忽略的函数列表,xhprof_enable的第二个参数*/
  char  **ignored_function_names;
  uint8   ignored_function_filter[XHPROF_IGNORED_FUNCTION_FILTER_SIZE];

} hp_global_t;

BEGIN_PROFILING 宏

在执行原始的内核回调前都是使用BEGIN_PROFILING宏进行相关的初始化操作。

#define BEGIN_PROFILING(entries, symbol, profile_curr)                  \
  do {                                                                  \
    /* Use a hash code to filter most of the string comparisons. */     \
    uint8 hash_code  = hp_inline_hash(symbol);                          \
    profile_curr = !hp_ignore_entry(hash_code, symbol);                 \
    if (profile_curr) {                                                 \
      hp_entry_t *cur_entry = hp_fast_alloc_hprof_entry();              \
      (cur_entry)->hash_code = hash_code;                               \
      (cur_entry)->name_hprof = symbol;                                 \
      (cur_entry)->prev_hprof = (*(entries));                           \
      /* Call the universal callback */                                 \
      hp_mode_common_beginfn((entries), (cur_entry) TSRMLS_CC);         \
      /* Call the mode's beginfn callback */                            \
      hp_globals.mode_cb.begin_fn_cb((entries), (cur_entry) TSRMLS_CC); \
      /* Update entries linked list */                                  \
      (*(entries)) = (cur_entry);                                       \
    }                                                                   \
  } while (0)

这个宏,首先在hash表中检测本次记录的函数是否是被设置的忽略的函数,如果是的话,就跳过下面的步骤。

然后快速分配一个hp_entry_t结构体,这个分配过程有点特别,并不是每次都emalloc。 其中hp_globals.entries是一个栈顶指针,而hp_globals.entry_free_list也是一个栈顶指针。当hp_globals.entry_free_list没有元素时,才分配内存。而当其有元素时,该栈就会出栈一个元素,而hp_globals.entries则入栈该元素。从而达到了快速分配的效果。

但是这样也有坏处,就是可能缓存栈上元素过多,占用更多的内存。这也是空间换时间的做法。

hp_mode_common_beginfn函数主要是添加本次函数调用的递归深度,其实现原理是从栈顶向栈底查找该函数已经被调用的次数,如果name_hprof相同的话,就说明已经调用了一次。最后还是利用了hash表的快速查找。

然后执行begin_fn_cb这个回调函数,在xhprof_enable模式下,这个指针指向的是hp_mode_hier_beginfn_cb这个函数,在此函数中,根据调试的需求,相应的记录函数的开始时间,内存占用,内存峰值等。

当被调试的函数执行完毕后,会调用END_PROFILING宏,进行计算,得出相应的结果,记录在hp_globals.stats_count中。

END_PROFILING 宏

END_PROFILING宏的定义如下:

#define END_PROFILING(entries, profile_curr)                            \
  do {                                                                  \
    if (profile_curr) {                                                 \
      hp_entry_t *cur_entry;                                            \
      /* Call the mode's endfn callback. */                             \
      /* NOTE(cjiang): we want to call this 'end_fn_cb' before */       \
      /* 'hp_mode_common_endfn' to avoid including the time in */       \
      /* 'hp_mode_common_endfn' in the profiling results.      */       \
      hp_globals.mode_cb.end_fn_cb((entries) TSRMLS_CC);                \
      cur_entry = (*(entries));                                         \
      /* Call the universal callback */                                 \
      hp_mode_common_endfn((entries), (cur_entry) TSRMLS_CC);           \
      /* Free top entry and update entries linked list */               \
      (*(entries)) = (*(entries))->prev_hprof;                          \
      hp_fast_free_hprof_entry(cur_entry);                              \
    }                                                                   \
  } while (0)

xhprof_enable下,end_fn_cb指向hp_mode_hier_endfn_cb函数,该函数调用hp_mode_shared_endfn_cb获取本次被记录函数的统计zval,并记录call count、wall time这两项内置指标。

然后根据配置,计算当前的CPU、内存等。对应的减去先前的CPU、内存,就得到了本次记录函数的执行相关信息。

所以,本扩展最关键的地方,就是这两个宏。

下面我们看看wall time、cpu、memory记录函数的实现。

wall time 实现

获取当前挂钟时间是通过xhprof的cycle_timer实现的,我们看看这个函数。

inline uint64 cycle_timer() {
  uint32 __a,__d;
  uint64 val;
  asm volatile("rdtsc" : "=a" (__a), "=d" (__d));
  (val) = ((uint64)__a) | (((uint64)__d)<<32);
  return val;
}

通过文章, 我们可以知道这是C与汇编的结合,通过rdtsc指令获取当前挂钟时间。

rdtsc指令返回的是自开机始CPU的周期数,返回的是一个64位的值EDX:EAX(高32在EDX,低32位在EAX)。

cpu usage 实现

cpu的信息是通过getrusage函数实现的,这里有篇讲解该函数的文章

该函数用来获取用户开销时间,系统开销时间,接收的信号量等等。

其用法如下:

#include <sys/types.h>
#include <sys/time.h>
#include <sys/resource.h>

#define   RUSAGE_SELF     0
#define   RUSAGE_CHILDREN     -1

int getrusage(int who, struct rusage *usage); 
//当调用成功后,返回0,否则-1;

who:可能选择有
    RUSAGE_SELF:获取当前进程的资源使用信息。以当前进程的相关信息来填充rusage(数据)结构
    RUSAGE_CHILDREN:获取子进程的资源使用信息。rusage结构中的数据都将是当前进程的子进程的信息
    usage:指向存放资源使用信息的结构指针。

struct rusage {
        struct timeval ru_utime; // user time used 
        struct timeval ru_stime; // system time used 
        long ru_maxrss; // maximum resident set size 
        long ru_ixrss; // integral shared memory size
        long ru_idrss; // integral unshared data size 
        long ru_isrss; // integral unshared stack size 
        long ru_minflt; // page reclaims 
        long ru_majflt; // page faults 
        long ru_nswap;// swaps
        long ru_inblock; // block input operations 
        long ru_oublock; // block output operations 
        long ru_msgsnd; // messages sent 
        long ru_msgrcv; //messages received 
        long ru_nsignals; // signals received 
        long ru_nvcsw; // voluntary context switches 
        long ru_nivcsw; // involuntary context switches 
};

struct timeval
{
__time_t tv_sec;        /* Seconds. */
__suseconds_t tv_usec;  /* Microseconds. */
};

在xhprof中,有如下的代码:

struct rusage    ru_end;
getrusage(RUSAGE_SELF, &ru_end);
hp_inc_count(counts, "cpu", (get_us_interval(&(top->ru_start_hprof.ru_utime),
                                          &(ru_end.ru_utime)) +
                          get_us_interval(&(top->ru_start_hprof.ru_stime),
                                          &(ru_end.ru_stime)))
static long get_us_interval(struct timeval *start, struct timeval *end) {
  return (((end->tv_sec - start->tv_sec) * 1000000)
          + (end->tv_usec - start->tv_usec));
}

从这里也可以看出xhprof中记录的是微妙(microsecond)。

memory usage 实现

内存信息分为占用内存与内存峰值两部分。

其中获取当前内存占用值使用的是zend_memory_usage函数。这个函数的实现如下:

long int mu_end  = zend_memory_usage(0 TSRMLS_CC);

ZEND_API size_t zend_memory_usage(int real_usage TSRMLS_DC)
{
    if (real_usage) {
        return AG(mm_heap)->real_size;
    } else {
        size_t usage = AG(mm_heap)->size;
#if ZEND_MM_CACHE
        usage -= AG(mm_heap)->cached;
#endif
        return usage;
    }
}

# define AG(v) (alloc_globals.v)
static zend_alloc_globals alloc_globals;

struct _zend_mm_heap {
    int                 use_zend_alloc;
    void               *(*_malloc)(size_t);
    void                (*_free)(void*);
    void               *(*_realloc)(void*, size_t);
    size_t              free_bitmap;
    size_t              large_free_bitmap;
    size_t              block_size;
    size_t              compact_size;
    zend_mm_segment    *segments_list;
    zend_mm_storage    *storage;
    size_t              real_size;
    size_t              real_peak;
    size_t              limit;
    size_t              size;
    size_t              peak;
    size_t              reserve_size;
    void               *reserve;
    int                 overflow;
    int                 internal;
#if ZEND_MM_CACHE
    unsigned int        cached;
    zend_mm_free_block *cache[ZEND_MM_NUM_BUCKETS];
#endif
    zend_mm_free_block *free_buckets[ZEND_MM_NUM_BUCKETS*2];
    zend_mm_free_block *large_free_buckets[ZEND_MM_NUM_BUCKETS];
    zend_mm_free_block *rest_buckets[2];
    int                 rest_count;
#if ZEND_MM_CACHE_STAT
    struct {
        int count;
        int max_count;
        int hit;
        int miss;
    } cache_stat[ZEND_MM_NUM_BUCKETS+1];
#endif
};

而获取峰值是通过zend_memory_peak_usage函数实现的,该函数的定义如下:

long int pmu_end = zend_memory_peak_usage(0 TSRMLS_CC);

ZEND_API size_t zend_memory_peak_usage(int real_usage TSRMLS_DC)
{
    if (real_usage) {
        return AG(mm_heap)->real_peak;
    } else {
        return AG(mm_heap)->peak;
    }
}

关于内存管理,我们可以参考鸟哥的几篇文章 PHP原理之内存管理中难懂的几个点深入理解PHP内存管理之谁动了我的内存

从实现可以简单的猜测,real_size、size、real_peak,peak会在特定的情况下进行更新,比如内存分配、内存释放等。

关于内存分配,主要还是参考上面两篇文章,能力有限,就不妄加解读了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK