friendsofphp/uprofiler

此包已废弃,不再维护。未建议替代包。

uprofiler:PHP轻量级性能分析器

dev-master 2018-06-01 09:34 UTC

This package is not auto-updated.

Last update: 2019-02-20 19:29:30 UTC


README

警告:此项目不再维护,并且不支持PHP 7+。如果您正在寻找PHP性能分析器,可以查看https://blackfire.io/(免费版本比此项目的功能更全面,界面也更好)。

uprofiler

简介

uprofiler是PHP的分层性能分析器。它报告函数级别的调用次数和包含/排除指标,如墙(耗时)时间、CPU时间和内存使用情况。函数的配置文件可以根据调用者或被调用者进行分解。原始数据收集组件是用C语言实现的,作为PHP Zend扩展的uprofiler。uprofiler有一个简单的基于HTML的用户界面(用PHP编写)。基于浏览器的UI用于查看性能分析结果,这使得查看结果或与他人共享结果变得容易。也支持调用图图像视图。

uprofiler报告通常有助于理解正在执行的代码结构。报告的分层性质可用于确定,例如,导致特定函数被调用的调用链。

uprofiler支持比较两次运行(即“差异”报告)或从多次运行中聚合数据。差异和聚合报告与单次运行报告一样,提供“平面”和“分层”的配置文件视图。

uprofiler是一个基于轻量级检测的性能分析器。在数据收集阶段,它跟踪程序动态调用图中的弧的调用次数和包含指标。在报告/后处理阶段计算排除指标。uprofiler通过在数据收集时检测调用图中的循环来处理递归函数,并通过为递归调用提供独特的深度限定名称来避免循环。

uprofiler的轻量级特性和聚合能力使其非常适合从生产环境中收集“函数级别”的性能统计数据。

uprofiler最初在Facebook开发,于2009年3月开源。

uprofiler概述

uprofiler提供

  • 平面配置文件

    函数级别的总结信息,如调用次数、包含/排除墙时间、内存使用情况和CPU时间。

    http://get.uprofiler.io/sample-flat-view.jpg
  • 分层配置文件(父/子视图)

    对于每个函数,它提供对调用者和被调用者的调用和时间的分解,例如

    • 哪些函数调用了特定的函数以及调用了多少次?
    • 哪些函数被特定的函数调用?
    • 从特定的父调用中调用函数时,在函数下花费的总时间。
    http://get.uprofiler.io/sample-parent-child-view.jpg
  • 差异报告

    您可能想比较两次uprofiler运行的数据,出于各种原因——为了找出代码库版本之间的回归原因,评估您所做的代码更改的性能改进,等等。

    差异报告接收两次运行的结果作为输入,并为每个函数提供函数级差异的平铺信息以及层次信息(差异按父/子函数分解)。

    差异报告中“平铺”视图突出显示主要的回归和改进。

    http://get.uprofiler.io/sample-diff-report-flat-view.jpg

    在差异报告的“平铺”视图中点击函数,将进入该函数的“层次”或(父/子)差异视图。我们可以按父/子函数分解差异。

    http://get.uprofiler.io/sample-diff-report-parent-child-view.jpg
  • 调用图视图

    配置文件数据也可以以调用图的形式查看。调用图视图突出显示程序的临界路径。

    http://get.uprofiler.io/sample-callgraph-image.jpg
  • 内存配置文件

    uprofiler的内存配置文件模式有助于跟踪分配大量内存的函数。

    需要明确指出,uprofiler并不严格跟踪每个分配/释放操作。它使用更简单的方案。它跟踪每个函数的进入和退出之间PHP分配内存的增减。它还跟踪每个函数分配的PHP“峰值”内存的增减。

  • uprofiler将include, include_once, require和require_once操作视为函数。被包含的文件名用于生成这些“虚假”函数的名称(见下文)。

术语

  • 包含时间(或子树时间):包括函数及其从给定函数调用的后代函数中花费的时间。
  • 排除时间/自时间:衡量函数本身花费的时间。不包括后代函数中的时间。
  • 墙时间:也称为经过时间或墙钟时间。
  • CPU时间:用户空间的CPU时间 + 内核空间的CPU时间。

特殊函数的命名约定

  • main():调用图的根处的虚构函数。

  • load::<filename>run_init::<filename>

    uprofiler将PHP的include/require操作视为函数调用。

    例如,一个include "lib/common.php";操作将导致两个uprofiler函数条目

    • load::lib/common.php:这表示解释器编译/加载文件所做的工作。[注意:如果您正在使用APC等PHP操作码缓存,那么编译仅在APC中发生缓存缺失时进行。]
    • run_init::lib/common.php:这表示由于包含操作而在文件作用域执行的初始化代码。
  • foo@<n>:表示这是对foo()的递归调用,其中<>表示递归深度。递归可以是直接的(例如,由于foo() --> foo()),也可以是间接的(例如,由于foo() --> goo() --> foo())。

限制

真正的层次分析程序在每个数据收集点跟踪完整的调用栈,并且后来能够回答像:第3次调用foo()的成本是多少?或者当调用栈看起来像a()->b()->bar()时,bar()的成本是多少?等问题。

uprofiler只跟踪1级的调用上下文,因此只能回答关于函数向上或向下1级的问题。实际上,这已经足够应对大多数用例。

为了更具体地说明这一点,假设以下例子。比如说你有

1 call from a() --> c()
1 call from b() --> c()
50 calls from c() --> d()

虽然uprofiler可以告诉你d()c()调用了50次,但它不能告诉你其中有多少次调用是由于a()还是b()触发的。[我们可能会推测其中25次是由于a(),25次是由于b(),但这并不一定正确。]

然而,在实践中,这并不是一个很大的限制。

安装uprofiler扩展

扩展位于“extension/”子目录中。

注意

尚未实现Windows端口。到目前为止,我们已经在Linux/FreeBSD和Mac OS上测试了uprofiler。

注意

uprofiler使用RDTSC指令(时间戳计数器)实现一个低开销的计时器来计算经过的时间。因此,目前uprofiler仅适用于x86架构。此外,由于RDTSC值可能在CPU之间不同步,uprofiler在分析期间将程序绑定到单个CPU。

如果启用SpeedStep技术,uprofiler的基于RDTSC的计时器功能将无法正确工作。这项技术在一些Intel处理器上可用。[注意:Mac桌面和笔记本电脑通常默认启用SpeedStep。要使用uprofiler,您需要禁用SpeedStep。]

以下步骤适用于Linux/Unix环境

$ cd extension/
$ phpize
$ ./configure --with-php-config=path-to-php-config
$ make
$ make install

php.ini文件:您可以将您的php.ini文件更新为自动加载您的扩展。将以下内容添加到您的php.ini文件中

[uprofiler]
extension=uprofiler.so
;
; directory used by default implementation of the iuprofilerRuns
; interface (namely, the uprofilerRuns_Default class) for storing
; uprofiler runs.
;
uprofiler.output_dir=<directory_for_storing_uprofiler_runs>

使用uprofiler进行性能分析

使用示例测试程序生成原始性能分析数据,例如

<?php

// foo.php

function bar($x) {
  if ($x > 0) {
    bar($x - 1);
  }
}

function foo() {
  for ($idx = 0; $idx < 2; $idx++) {
    bar($idx);
    $x = strlen("abc");
  }
}

// start profiling
uprofiler_enable();

// run program
foo();

// stop profiler
$uprofiler_data = uprofiler_disable();

// display raw uprofiler data for the profiler run
print_r($uprofiler_data);

运行上述测试程序

$ php -dextension=uprofiler.so foo.php

您应该得到类似以下输出

Array
(
    [foo==>bar] => Array
        (
            [ct] => 2         # 2 calls to bar() from foo()
            [wt] => 27        # inclusive time in bar() when called from foo()
        )

    [foo==>strlen] => Array
        (
            [ct] => 2
            [wt] => 2
        )

    [bar==>bar@1] => Array    # a recursive call to bar()
        (
            [ct] => 1
            [wt] => 2
        )

    [main()==>foo] => Array
        (
            [ct] => 1
            [wt] => 74
        )

    [main()==>uprofiler_disable] => Array
        (
            [ct] => 1
            [wt] => 0
        )

    [main()] => Array         # fake symbol representing root
        (
            [ct] => 1
            [wt] => 83
        )

)

注意

原始数据仅包含“包含”指标。例如,原始数据中的wall time指标表示包含时间的微秒。任何函数的独占时间都是在分析/报告阶段计算的。

注意

默认情况下,只分析调用次数和经过的时间。您还可以选择性地分析CPU时间和/或内存使用情况。将上述程序中的uprofiler_enable();替换为,例如uprofiler_enable(UPROFILER_FLAGS_CPU + UPROFILER_FLAGS_MEMORY)

现在,您应该得到类似以下输出

Array
(
    [foo==>bar] => Array
        (
            [ct] => 2        # number of calls to bar() from foo()
            [wt] => 37       # time in bar() when called from foo()
            [cpu] => 0       # cpu time in bar() when called from foo()
            [mu] => 2208     # change in PHP memory usage in bar() when called from foo()
            [pmu] => 0       # change in PHP peak memory usage in bar() when called from foo()
        )

    [foo==>strlen] => Array
        (
            [ct] => 2
            [wt] => 3
            [cpu] => 0
            [mu] => 624
            [pmu] => 0
        )

    [bar==>bar@1] => Array
        (
            [ct] => 1
            [wt] => 2
            [cpu] => 0
            [mu] => 856
            [pmu] => 0
        )

    [main()==>foo] => Array
        (
            [ct] => 1
            [wt] => 104
            [cpu] => 0
            [mu] => 4168
            [pmu] => 0
        )

    [main()==>uprofiler_disable] => Array
        (
            [ct] => 1
            [wt] => 1
            [cpu] => 0
            [mu] => 344
            [pmu] => 0
        )

    [main()] => Array
        (
            [ct] => 1
            [wt] => 139
            [cpu] => 0
            [mu] => 5936
            [pmu] => 0
        )

)

在性能分析期间跳过内置函数

默认情况下,PHP内置函数(如strlen)会被分析。如果您不想分析内置函数(以进一步减少分析开销或生成原始数据的尺寸),您可以使用UPROFILER_FLAGS_NO_BUILTINS标志,例如uprofiler_enable(UPROFILER_FLAGS_NO_BUILTINS)

在性能分析期间忽略特定函数

您可以让uprofiler在性能分析期间忽略指定的函数列表。这允许您忽略,例如,用于间接函数调用(如call_user_funccall_user_func_array)的函数。这些中间函数不必要地复杂化了调用层次结构,并使uprofiler报告更难以解释,因为它们混淆了间接调用的函数的父子关系。

要指定要忽略的性能分析期间的函数列表,请使用uprofiler_enable的第二个(可选)参数。例如

// elapsed time profiling; ignore call_user_func* during profiling
uprofiler_enable(0,
             array('ignored_functions' =>  array('call_user_func',
                                                 'call_user_func_array')));

// elapsed time + memory profiling; ignore call_user_func* during profiling
uprofiler_enable(UPROFILER_FLAGS_MEMORY,
              array('ignored_functions' =>  array('call_user_func',
                                                  'call_user_func_array')));

设置uprofiler UI

PHP源结构

uprofiler UI是用PHP实现的。代码位于两个子目录中,uprofiler_html/uprofiler_lib/

uprofiler_html目录包含3个顶级PHP页面。

  • index.php:用于查看单个运行或diff报告。
  • callgraph.php:用于查看uprofiler运行的调用图作为图像。
  • typeahead.php:在uprofiler报告中的typeahead表单中隐式使用。

uprofiler_lib/目录包含用于显示和分析(计算扁平配置文件信息、计算差异、聚合多次运行的数据等)的支持代码。

Web服务器配置

您需要确保uprofiler_html/目录可以从您的Web服务器访问,并且您的Web服务器已配置为提供PHP脚本。

管理uprofiler运行

客户在保存从uprofiler运行中获取的uprofiler原始数据方面具有灵活性。uprofiler UI层暴露了一个名为iuprofilerRuns的接口(见uprofiler_lib/utils/uprofiler_runs.php),客户可以实现该接口。这允许客户告诉UI层如何获取与uprofiler运行相对应的数据。

uprofiler UI库包含iuprofilerRuns接口的默认基于文件的实现,即"uprofilerRuns_Default"(也位于uprofiler_lib/utils/uprofiler_runs.php)。此默认实现将运行存储在由INI参数uprofiler.output_dir指定的目录中。

uprofiler运行必须由命名空间和运行ID唯一标识。

a) 持久保存uprofiler数据:

假设您正在使用iuprofilerRuns接口的默认实现uprofilerRuns_Default,一个典型的uprofiler运行随后保存步骤可能如下所示

// start profiling
uprofiler_enable();

// run program
....

// stop profiler
$uprofiler_data = uprofiler_disable();

//
// Saving the uprofiler run
// using the default implementation of iuprofilerRuns.
//
include_once $uprofiler_ROOT . "/uprofiler_lib/utils/uprofiler_lib.php";
include_once $uprofiler_ROOT . "/uprofiler_lib/utils/uprofiler_runs.php";

$uprofiler_runs = new uprofilerRuns_Default();

// Save the run under a namespace "uprofiler_foo".
//
// **NOTE**:
// By default save_run() will automatically generate a unique
// run id for you. [You can override that behavior by passing
// a run id (optional arg) to the save_run() method instead.]
//
$run_id = $uprofiler_runs->save_run($uprofiler_data, "uprofiler_foo");

echo "---------------\n".
     "Assuming you have set up the http based UI for \n".
     "uprofiler at some address, you can view run at \n".
     "http://<uprofiler-ui-address>/index.php?run=$run_id&source=uprofiler_foo\n".
     "---------------\n";

上述操作应将运行保存为INI参数uprofiler.output_dir指定的目录中的文件。文件名可能是类似49bafaa3a3f66.uprofiler_foo的东西;两个部分是运行ID("49bafaa3a3f66")和命名空间("uprofiler_foo")。[如果您想自己创建/分配运行ID(例如数据库序列号或时间戳),您可以将运行ID显式传递给save_run方法。

b) 使用自己的iuprofilerRuns实现

如果您决定您想以不同的方式存储uprofiler运行(例如压缩格式、在DB等备用位置),则需要实现一个实现了iuprofilerRuns接口的类。

您还需要修改"uprofiler_html/"目录中的3个主要PHP入口页面(index.php、callgraph.php、typeahead.php),以使用新类而不是默认类uprofilerRuns_Default。更改以下3个文件中的这一行。

$uprofiler_runs_impl = new uprofilerRuns_Default();

您还需要在上面的文件中"包含"实现您类的文件。

从UI访问运行

a) 查看单个运行报告

要查看ID为和命名空间为的运行报告,请使用以下格式的URL

http://<uprofiler-ui-address>/index.php?run=<run_id>&source=<namespace>

例如,http://<uprofiler-ui-address>/index.php?run=49bafaa3a3f66&source=uprofiler_foo

b) 查看差异报告

要查看ID为,在命名空间中的报告,请使用以下格式的URL

http://<uprofiler-ui-address>/index.php?run1=<run_id1>&run2=<run_id2>&source=<namespace>

c) 聚合报告

您也可以指定一组运行ID,您想要对这些运行进行聚合视图/报告。

例如,您有三个在命名空间"benchmark"中ID为1、2和3的uprofiler运行。要查看这些运行的聚合报告

http://<uprofiler-ui-address>/index.php?run=1,2,3&source=benchmark

加权聚合:

进一步假设上述三次运行对应于三种类型的程序 p1.php、p2.php 和 p3.php,它们在混合中通常分别占 20%、30%、50%。要查看与这些运行的加权平均值相对应的汇总报告,请使用

http://<uprofiler-ui-address>/index.php?run=1,2,3&wts=20,30,50&source=benchmark

在生产中使用 uprofiler 的注意事项

一些观察结果/指南。您的结果可能会有所不同

  • Linux 上的 CPU 计时器(getrusage)具有很高的开销。它也是粗粒度的(以毫秒精度而不是微秒级别)以在函数级别上有用。因此,使用 UPROFILER_FLAGS_CPU 模式时报告的数字偏差通常较高。

    我们建议在生产中使用经过时间加内存分析的模式。[注意:内存分析模式的额外开销非常低。]

    // elapsed time profiling (default) + memory profiling
    uprofiler_enable(UPROFILER_FLAGS_MEMORY);
  • 分析随机样本的页面/请求可以很好地捕获代表您生产工作负载的数据。

    要分析例如 1/10000 的请求,您可以在请求处理开始时使用类似以下的方法进行仪器化

    if (mt_rand(1, 10000) == 1) {
      uprofiler_enable(UPROFILER_FLAGS_MEMORY);
      $uprofiler_on = true;
    }

    在请求结束时(或在请求关闭函数中),您可能进行如下操作

    if ($uprofiler_on) {
      // stop profiler
      $uprofiler_data = uprofiler_disable();
    
      // save $uprofiler_data somewhere (say a central DB)
      ...
    }

    然后,您可以使用 uprofiler_aggregate_runs() 通过时间(例如,每 5 分钟/每小时/每天的基础)、页面/请求类型或其他维度对这些单个配置文件进行汇总/聚合。

轻量级采样模式

uprofiler 扩展还提供了一种非常轻量级的 采样模式。采样间隔为 0.1 秒。样本记录完整的函数调用堆栈。如果需要一种具有极低开销的性能监控和诊断方法,采样模式可能很有用。

扩展公开的相关函数以使用采样模式为 uprofiler_sample_enable()uprofiler_sample_disable()

附加功能

uprofiler_lib/utils/uprofiler_lib.php 文件包含可用于操作/聚合 uprofiler 运行的附加库函数。

例如

  • uprofiler_aggregate_runs():可以将多个 uprofiler 运行聚合为单个运行。这可以帮助使用 uprofiler 构建一个系统级的“函数级”性能监控工具。[例如,您可能定期将生产中抽取的上 profiler 运行汇总,以生成每小时、每天的报表。]
  • uprofiler_prune_run():聚合大量 uprofiler 运行(特别是如果它们对应于不同类型的程序)可能会导致调用图大小过大。您可以使用 uprofiler_prune_run 函数通过编辑掉只占总时间极小部分的子树来修剪调用图数据。

依赖项

  • JQuery Javascript:为了提示和函数名自动完成,我们使用了 JQuery 的 JavaScript 库。JQuery 在 MIT 和 GPL 许可证 下可用。uprofiler 使用的相关 JQuery 代码位于 uprofiler_html/jquery 子目录中。
  • dot(图像生成工具):调用图图像可视化([查看调用图])功能依赖于您的路径中存在 Graphviz "dot" 工具。"dot" 是用于绘制/生成有向图的图像的工具。

致谢

基于HTML的浏览分析结果导航界面受到了与Oracle存储过程语言PL/SQL中存在的一个类似工具的启发。但相似之处就此结束;分析器本身的内部结构相当不同。