friendsofphp/ uprofiler
uprofiler:PHP轻量级性能分析器
Requires
- php: >=5.2.0
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时间。
分层配置文件(父/子视图)
对于每个函数,它提供对调用者和被调用者的调用和时间的分解,例如
- 哪些函数调用了特定的函数以及调用了多少次?
- 哪些函数被特定的函数调用?
- 从特定的父调用中调用函数时,在函数下花费的总时间。
差异报告
您可能想比较两次uprofiler运行的数据,出于各种原因——为了找出代码库版本之间的回归原因,评估您所做的代码更改的性能改进,等等。
差异报告接收两次运行的结果作为输入,并为每个函数提供函数级差异的平铺信息以及层次信息(差异按父/子函数分解)。
差异报告中“平铺”视图突出显示主要的回归和改进。
在差异报告的“平铺”视图中点击函数,将进入该函数的“层次”或(父/子)差异视图。我们可以按父/子函数分解差异。
调用图视图
配置文件数据也可以以调用图的形式查看。调用图视图突出显示程序的临界路径。
内存配置文件
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_func
和call_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为
http://<uprofiler-ui-address>/index.php?run=<run_id>&source=<namespace>
例如,http://<uprofiler-ui-address>/index.php?run=49bafaa3a3f66&source=uprofiler_foo
b) 查看差异报告
要查看ID为
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中存在的一个类似工具的启发。但相似之处就此结束;分析器本身的内部结构相当不同。