diff --git a/plugins/php/conf/php-fpm.conf b/plugins/php/conf/php-fpm.conf index 520a62edf..433896d70 100644 --- a/plugins/php/conf/php-fpm.conf +++ b/plugins/php/conf/php-fpm.conf @@ -1,5 +1,4 @@ [global] include={$SERVER_PATH}/php/{$PHP_VERSION}/etc/php-fpm.d/*.conf -;php_value[auto_prepend_file]={$SERVER_PATH}/md_start.php -;php_value[auto_append_file]={$SERVER_PATH}/md_end.php \ No newline at end of file +php_value[auto_prepend_file]={$SERVER_PATH}/php/app_start.php \ No newline at end of file diff --git a/plugins/php/index.html b/plugins/php/index.html index 2243a9543..e42be69d4 100755 --- a/plugins/php/index.html +++ b/plugins/php/index.html @@ -15,6 +15,7 @@

FPM日志

慢日志

PHPIFNO

+

预加载脚本

diff --git a/plugins/php/index.py b/plugins/php/index.py index d67ae2e59..a762fee5f 100755 --- a/plugins/php/index.py +++ b/plugins/php/index.py @@ -146,6 +146,15 @@ def makeOpenrestyConf(): mw.restartWeb() +def phpPrependFile(version): + app_start = getServerDir() + '/app_start.php' + if not os.path.exists(app_start): + tpl = getPluginDir() + '/conf/app_start.php' + content = mw.readFile(tpl) + content = contentReplace(content, version) + mw.writeFile(app_start, content) + + def phpFpmReplace(version): desc_php_fpm = getServerDir() + '/' + version + '/etc/php-fpm.conf' if not os.path.exists(desc_php_fpm): @@ -194,6 +203,7 @@ def initReplace(version): mw.writeFile(file_bin, content) mw.execShell('chmod +x ' + file_bin) + phpPrependFile(version) phpFpmWwwReplace(version) phpFpmReplace(version) @@ -619,6 +629,10 @@ def uninstallLib(version): return mw.returnJson(False, '卸载信息![通道0]:' + data[0] + "[通道0]:" + data[1]) +def getConfAppStart(): + pstart = mw.getServerDir() + '/php/app_start.php' + return pstart + if __name__ == "__main__": if len(sys.argv) < 3: @@ -650,6 +664,8 @@ if __name__ == "__main__": print fpmSlowLog(version) elif func == 'conf': print getConf(version) + elif func == 'app_start': + print getConfAppStart() elif func == 'get_php_conf': print getPhpConf(version) elif func == 'submit_php_conf': diff --git a/plugins/php/versions/71/xhprof.sh b/plugins/php/versions/71/xhprof.sh new file mode 100755 index 000000000..2e52e2971 --- /dev/null +++ b/plugins/php/versions/71/xhprof.sh @@ -0,0 +1,95 @@ +#!/bin/bash +PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin +export PATH + +curPath=`pwd` + +rootPath=$(dirname "$curPath") +rootPath=$(dirname "$rootPath") +rootPath=$(dirname "$rootPath") +rootPath=$(dirname "$rootPath") +serverPath=$(dirname "$rootPath") +sourcePath=${serverPath}/source/php + +LIBNAME=xhprof +LIBV=2.2.0 +sysName=`uname` +actionType=$1 +version=$2 +extFile=$serverPath/php/${version}/lib/php/extensions/no-debug-non-zts-20160303/${LIBNAME}.so + +if [ "$sysName" == "Darwin" ];then + BAK='_bak' +else + BAK='' +fi + + +Install_lib() +{ + isInstall=`cat $serverPath/php/$version/etc/php.ini|grep "${LIBNAME}.so"` + if [ "${isInstall}" != "" ];then + echo "php-$version 已安装${LIBNAME},请选择其它版本!" + return + fi + + if [ ! -f "$extFile" ];then + php_lib=$sourcePath/php_lib + mkdir -p $php_lib + if [ ! -d $php_lib/${LIBNAME}-${LIBV} ];then + wget -O $php_lib/${LIBNAME}-${LIBV}.tgz http://pecl.php.net/get/${LIBNAME}-${LIBV}.tgz + cd $php_lib && tar xvf ${LIBNAME}-${LIBV}.tgz + fi + cd $php_lib/${LIBNAME}-${LIBV}/extension + + $serverPath/php/$version/bin/phpize + ./configure --enable-xhprof \ + --with-php-config=$serverPath/php/$version/bin/php-config + make && make install && make clean + + fi + + if [ ! -f "$extFile" ];then + echo "ERROR!" + return + fi + + + echo "" >> $serverPath/php/$version/etc/php.ini + echo "[${LIBNAME}]" >> $serverPath/php/$version/etc/php.ini + echo "extension=${LIBNAME}.so" >> $serverPath/php/$version/etc/php.ini + + $serverPath/php/init.d/php$version reload + echo '===========================================================' + echo 'successful!' +} + + +Uninstall_lib() +{ + if [ ! -f "$serverPath/php/$version/bin/php-config" ];then + echo "php-$version 未安装,请选择其它版本!" + return + fi + + if [ ! -f "$extFile" ];then + echo "php-$version 未安装${LIBNAME},请选择其它版本!" + echo "php-$version not install ${LIBNAME}, Plese select other version!" + return + fi + + sed -i $BAK "/${LIBNAME}.so/d" $serverPath/php/$version/etc/php.ini + sed -i $BAK "/${LIBNAME}/d" $serverPath/php/$version/etc/php.ini + + rm -f $extFile + $serverPath/php/init.d/php$version reload + echo '===============================================' + echo 'successful!' +} + + +if [ "$actionType" == 'install' ];then + Install_lib +elif [ "$actionType" == 'uninstall' ];then + Uninstall_lib +fi \ No newline at end of file diff --git a/plugins/php/versions/phplib.conf b/plugins/php/versions/phplib.conf index d8580e433..e77b731b3 100755 --- a/plugins/php/versions/phplib.conf +++ b/plugins/php/versions/phplib.conf @@ -170,7 +170,12 @@ "53", "54", "55", - "56" + "56", + "70", + "71", + "72", + "73", + "74" ], "type": "性能分析", "msg": "不多说,不了解的不要安装!", diff --git a/plugins/xhprof/conf/xhprof.conf b/plugins/xhprof/conf/xhprof.conf new file mode 100755 index 000000000..bdc8095d5 --- /dev/null +++ b/plugins/xhprof/conf/xhprof.conf @@ -0,0 +1,27 @@ +server +{ + listen 5858; + server_name 127.0.0.1; + index index.html index.htm index.php; + root {$SERVER_PATH}/xhprof/xhprof_html; + + #error_page 404 /404.html; + include enable-php-{$PHP_VER}.conf; + + location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ + { + expires 30d; + } + + location ~ .*\.(js|css)?$ + { + expires 12h; + } + + location ~ /\. + { + deny all; + } + + access_log {$SERVER_PATH}/xhprof/access.log main; +} \ No newline at end of file diff --git a/plugins/xhprof/ico.png b/plugins/xhprof/ico.png new file mode 100644 index 000000000..c710c5861 Binary files /dev/null and b/plugins/xhprof/ico.png differ diff --git a/plugins/xhprof/index.html b/plugins/xhprof/index.html new file mode 100755 index 000000000..754cf36ef --- /dev/null +++ b/plugins/xhprof/index.html @@ -0,0 +1,20 @@ +
+
+
+

服务

+

重写模版

+

主页

+

预加载脚本

+

PHP版本

+

安全设置

+
+
+
+
+
+ +
+ \ No newline at end of file diff --git a/plugins/xhprof/index.py b/plugins/xhprof/index.py new file mode 100755 index 000000000..a6aa10fa3 --- /dev/null +++ b/plugins/xhprof/index.py @@ -0,0 +1,219 @@ +# coding:utf-8 + +import sys +import io +import os +import time +import re + +sys.path.append(os.getcwd() + "/class/core") +import mw +import site_api + +app_debug = False +if mw.isAppleSystem(): + app_debug = True + + +def getPluginName(): + return 'xhprof' + + +def getPluginDir(): + return mw.getPluginDir() + '/' + getPluginName() + + +def getServerDir(): + return mw.getServerDir() + '/' + getPluginName() + + +def getArgs(): + args = sys.argv[2:] + tmp = {} + args_len = len(args) + + if args_len == 1: + t = args[0].strip('{').strip('}') + t = t.split(':') + tmp[t[0]] = t[1] + elif args_len > 1: + for i in range(len(args)): + t = args[i].split(':') + tmp[t[0]] = t[1] + + return tmp + + +def getConf(): + return mw.getServerDir() + '/web_conf/nginx/vhost/xhprof.conf' + + +def getPort(): + file = getConf() + content = mw.readFile(file) + rep = 'listen\s*(.*);' + tmp = re.search(rep, content) + return tmp.groups()[0].strip() + + +def getHomePage(): + try: + port = getPort() + ip = '127.0.0.1' + if not mw.isAppleSystem(): + ip = mw.getLocalIp() + url = 'http://' + ip + ':' + port + '/index.php' + return mw.returnJson(True, 'OK', url) + except Exception as e: + return mw.returnJson(False, '插件未启动!') + + +def getPhpVer(expect=55): + import json + v = site_api.site_api().getPhpVersion() + v = json.loads(v) + for i in range(len(v)): + t = int(v[i]['version']) + if (t >= expect): + return str(t) + return str(expect) + + +def getCachePhpVer(): + cacheFile = getServerDir() + '/php.pl' + v = '' + if os.path.exists(cacheFile): + v = mw.readFile(cacheFile) + else: + v = getPhpVer() + mw.writeFile(cacheFile, v) + return v + + +def contentReplace(content): + service_path = mw.getServerDir() + php_ver = getCachePhpVer() + # print php_ver + content = content.replace('{$ROOT_PATH}', mw.getRootDir()) + content = content.replace('{$SERVER_PATH}', service_path) + content = content.replace('{$PHP_VER}', php_ver) + return content + + +def status(): + conf = getConf() + if os.path.exists(conf): + return 'start' + return 'stop' + + +def start(): + file_tpl = getPluginDir() + '/conf/xhprof.conf' + file_run = getConf() + + if not os.path.exists(file_run): + centent = mw.readFile(file_tpl) + centent = contentReplace(centent) + mw.writeFile(file_run, centent) + + mw.restartWeb() + return 'ok' + + +def stop(): + conf = getConf() + if os.path.exists(conf): + os.remove(conf) + mw.restartWeb() + return 'ok' + + +def restart(): + return start() + + +def reload(): + return start() + + +def setPhpVer(): + args = getArgs() + + if not 'phpver' in args: + return 'phpver missing' + + cacheFile = getServerDir() + '/php.pl' + mw.writeFile(cacheFile, args['phpver']) + restart() + + return 'ok' + + +def getSetPhpVer(): + cacheFile = getServerDir() + '/php.pl' + if os.path.exists(cacheFile): + return mw.readFile(cacheFile).strip() + return '' + + +def getXhPort(): + try: + port = getPort() + return mw.returnJson(True, 'OK', port) + except Exception as e: + print e + return mw.returnJson(False, '插件未启动!') + + +def setXhPort(): + args = getArgs() + if not 'port' in args: + return mw.returnJson(False, 'port missing!') + + port = args['port'] + if port == '80': + return mw.returnJson(False, '80端不能使用!') + + file = getConf() + if not os.path.exists(file): + return mw.returnJson(False, '插件未启动!') + content = mw.readFile(file) + rep = 'listen\s*(.*);' + content = re.sub(rep, "listen " + port + ';', content) + mw.writeFile(file, content) + mw.restartWeb() + return mw.returnJson(True, '修改成功!') + + +def getConfAppStart(): + pstart = mw.getServerDir() + '/php/app_start.php' + return pstart + +if __name__ == "__main__": + func = sys.argv[1] + if func == 'status': + print status() + elif func == 'start': + print start() + elif func == 'stop': + print stop() + elif func == 'restart': + print restart() + elif func == 'reload': + print reload() + elif func == 'conf': + print getConf() + elif func == 'get_home_page': + print getHomePage() + elif func == 'set_php_ver': + print setPhpVer() + elif func == 'get_set_php_ver': + print getSetPhpVer() + elif func == 'get_xhprof_port': + print getXhPort() + elif func == 'set_xhprof_port': + print setXhPort() + elif func == 'app_start': + print getConfAppStart() + else: + print 'error' diff --git a/plugins/xhprof/info.json b/plugins/xhprof/info.json new file mode 100755 index 000000000..414c73e01 --- /dev/null +++ b/plugins/xhprof/info.json @@ -0,0 +1,17 @@ +{ + "title":"xhprof", + "tip":"soft", + "name":"xhprof", + "type":"运行环境", + "ps":"PHP性能瓶颈查找工具", + "to_ver":["1.0"], + "versions":["1.0"], + "updates":["1.0"], + "shell":"install.sh", + "checks":"server/xhprof", + "path": "server/xhprof", + "author":"midoks", + "home":"http://pecl.php.net/package/xhprof", + "date":"2020-07-12", + "pid": "2" +} \ No newline at end of file diff --git a/plugins/xhprof/install.sh b/plugins/xhprof/install.sh new file mode 100755 index 000000000..ca8a2c188 --- /dev/null +++ b/plugins/xhprof/install.sh @@ -0,0 +1,36 @@ +#!/bin/bash +PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin +export PATH + +curPath=`pwd` +rootPath=$(dirname "$curPath") +rootPath=$(dirname "$rootPath") +serverPath=$(dirname "$rootPath") + +install_tmp=${rootPath}/tmp/mw_install.pl + +Install_xh() +{ + mkdir -p ${serverPath}/xhprof + + if [ ! -d ${serverPath}/xhprof/xhprof_lib ];then + cp -rf $curPath/lib/ ${serverPath}/xhprof + fi + + echo "${1}" > ${serverPath}/xhprof/version.pl + echo '安装完成' > $install_tmp + +} + +Uninstall_xh() +{ + rm -rf ${serverPath}/xhprof + echo '卸载完成' > $install_tmp +} + +action=$1 +if [ "${1}" == 'install' ];then + Install_xh $2 +else + Uninstall_xh $2 +fi diff --git a/plugins/xhprof/js/xhprof.js b/plugins/xhprof/js/xhprof.js new file mode 100755 index 000000000..dae22009b --- /dev/null +++ b/plugins/xhprof/js/xhprof.js @@ -0,0 +1,120 @@ +function str2Obj(str){ + var data = {}; + kv = str.split('&'); + for(i in kv){ + v = kv[i].split('='); + data[v[0]] = v[1]; + } + return data; +} + +function xhPost(method,args,callback){ + + var _args = null; + if (typeof(args) == 'string'){ + _args = JSON.stringify(str2Obj(args)); + } else { + _args = JSON.stringify(args); + } + + var loadT = layer.msg('正在获取...', { icon: 16, time: 0, shade: 0.3 }); + $.post('/plugins/run', {name:'xhprof', func:method, args:_args}, function(data) { + layer.close(loadT); + if (!data.status){ + layer.msg(data.msg,{icon:0,time:2000,shade: [0.3, '#000']}); + return; + } + + if(typeof(callback) == 'function'){ + callback(data); + } + },'json'); +} + + +function xhAsyncPost(method,args){ + + var _args = null; + if (typeof(args) == 'string'){ + _args = JSON.stringify(str2Obj(args)); + } else { + _args = JSON.stringify(args); + } + return syncPost('/plugins/run', {name:'xhprof', func:method, args:_args}); +} + +function homePage(){ + xhPost('get_home_page', '', function(data){ + var rdata = $.parseJSON(data.data); + if (!rdata.status){ + layer.msg(rdata.msg,{icon:0,time:2000,shade: [0.3, '#000']}); + return; + } + var con = ''; + $(".soft-man-con").html(con); + }); +} + +//phpmyadmin切换php版本 +function phpVer(version) { + + var _version = xhAsyncPost('get_set_php_ver','') + if (_version['data'] != ''){ + version = _version['data']; + } + + $.post('/site/get_php_version', function(rdata) { + // console.log(rdata); + var body = "
PHP版本
'; + $(".soft-man-con").html(body); + },'json'); +} + +function phpVerChange(type, msg) { + var phpver = $("#phpver").val(); + xhPost('set_php_ver', 'phpver='+phpver, function(data){ + if ( data.data == 'ok' ){ + layer.msg('设置成功!',{icon:1,time:2000,shade: [0.3, '#000']}); + } else { + layer.msg('设置失败!',{icon:2,time:2000,shade: [0.3, '#000']}); + } + }); +} + + +//xhprf 安全设置 +function safeConf() { + var data = xhAsyncPost('get_xhprof_port'); + var rdata = $.parseJSON(data.data); + if (!rdata.status){ + layer.msg(rdata.msg,{icon:2,time:2000,shade: [0.3, '#000']}); + return; + } + var con = '
\ + 访问端口\ + \ + \ +
'; + $(".soft-man-con").html(con); +} + +//修改 xhprf 端口 +function xhprofPort() { + var pmport = $("#pmport").val(); + if (pmport < 80 || pmport > 65535) { + layer.msg('端口范围不合法!', { icon: 2 }); + return; + } + var data = 'port=' + pmport; + + xhPost('set_xhprof_port',data, function(data){ + var rdata = $.parseJSON(data.data); + layer.msg(rdata.msg, { icon: rdata.status ? 1 : 2 }); + }); +} \ No newline at end of file diff --git a/plugins/xhprof/lib/xhprof_html/callgraph.php b/plugins/xhprof/lib/xhprof_html/callgraph.php new file mode 100755 index 000000000..46fc6a524 --- /dev/null +++ b/plugins/xhprof/lib/xhprof_html/callgraph.php @@ -0,0 +1,91 @@ + array(XHPROF_STRING_PARAM, ''), + + // source/namespace/type of run + 'source' => array(XHPROF_STRING_PARAM, 'xhprof'), + + // the focus function, if it is set, only directly + // parents/children functions of it will be shown. + 'func' => array(XHPROF_STRING_PARAM, ''), + + // image type, can be 'jpg', 'gif', 'ps', 'png' + 'type' => array(XHPROF_STRING_PARAM, 'png'), + + // only functions whose exclusive time over the total time + // is larger than this threshold will be shown. + // default is 0.01. + 'threshold' => array(XHPROF_FLOAT_PARAM, 0.01), + + // whether to show critical_path + 'critical' => array(XHPROF_BOOL_PARAM, true), + + // first run in diff mode. + 'run1' => array(XHPROF_STRING_PARAM, ''), + + // second run in diff mode. + 'run2' => array(XHPROF_STRING_PARAM, '') + ); + +// pull values of these params, and create named globals for each param +xhprof_param_init($params); + +// if invalid value specified for threshold, then use the default +if ($threshold < 0 || $threshold > 1) { + $threshold = $params['threshold'][1]; +} + +// if invalid value specified for type, use the default +if (!array_key_exists($type, $xhprof_legal_image_types)) { + $type = $params['type'][1]; // default image type. +} + +$xhprof_runs_impl = new XHProfRuns_Default(); + +if (!empty($run)) { + // single run call graph image generation + xhprof_render_image($xhprof_runs_impl, $run, $type, + $threshold, $func, $source, $critical); +} else { + // diff report call graph image generation + xhprof_render_diff_image($xhprof_runs_impl, $run1, $run2, + $type, $threshold, $source); +} diff --git a/plugins/xhprof/lib/xhprof_html/css/xhprof.css b/plugins/xhprof/lib/xhprof_html/css/xhprof.css new file mode 100755 index 000000000..c3abf8622 --- /dev/null +++ b/plugins/xhprof/lib/xhprof_html/css/xhprof.css @@ -0,0 +1,81 @@ +/* Copyright (c) 2009 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +td.sorted { + color:#0000FF; +} + +td.vbar, th.vbar { + text-align: right; + border-left: + solid 1px #bdc7d8; +} + +td.vbbar, th.vbar { + text-align: right; + border-left: + solid 1px #bdc7d8; + color:blue; +} + +/* diff reports: display regressions in red */ +td.vrbar { + text-align: right; + border-left:solid 1px #bdc7d8; + color:red; +} + +/* diff reports: display improvements in green */ +td.vgbar { + text-align: right; + border-left: solid 1px #bdc7d8; + color:green; +} + +td.vwbar, th.vwbar { + text-align: right; + border-left: solid 1px white; +} + +td.vwlbar, th.vwlbar { + text-align: left; + border-left: solid 1px white; +} + +p.blue { + color:blue +} + +.bubble { + background-color:#C3D9FF +} + +ul.xhprof_actions { + float: right; + padding-left: 16px; + list-style-image: none; + list-style-type: none; + margin:10px 10px 10px 3em; + position:relative; +} + +ul.xhprof_actions li { + border-bottom:1px solid #D8DFEA; +} + +ul.xhprof_actions li a:hover { + background:#3B5998 none repeat scroll 0 0; + color:#FFFFFF; +} diff --git a/plugins/xhprof/lib/xhprof_html/index.php b/plugins/xhprof/lib/xhprof_html/index.php new file mode 100755 index 000000000..f21d32fd5 --- /dev/null +++ b/plugins/xhprof/lib/xhprof_html/index.php @@ -0,0 +1,90 @@ + array(XHPROF_STRING_PARAM, ''), + 'wts' => array(XHPROF_STRING_PARAM, ''), + 'symbol' => array(XHPROF_STRING_PARAM, ''), + 'sort' => array(XHPROF_STRING_PARAM, 'wt'), // wall time + 'run1' => array(XHPROF_STRING_PARAM, ''), + 'run2' => array(XHPROF_STRING_PARAM, ''), + 'source' => array(XHPROF_STRING_PARAM, 'xhprof'), + 'all' => array(XHPROF_UINT_PARAM, 0), + ); + +// pull values of these params, and create named globals for each param +xhprof_param_init($params); + +/* reset params to be a array of variable names to values + by the end of this page, param should only contain values that need + to be preserved for the next page. unset all unwanted keys in $params. + */ +foreach ($params as $k => $v) { + $params[$k] = $$k; + + // unset key from params that are using default values. So URLs aren't + // ridiculously long. + if ($params[$k] == $v[1]) { + unset($params[$k]); + } +} + +echo ""; + +echo "XHProf: Hierarchical Profiler Report"; +xhprof_include_js_css(); +echo ""; + +echo ""; + +$vbar = ' class="vbar"'; +$vwbar = ' class="vwbar"'; +$vwlbar = ' class="vwlbar"'; +$vbbar = ' class="vbbar"'; +$vrbar = ' class="vrbar"'; +$vgbar = ' class="vgbar"'; + +$xhprof_runs_impl = new XHProfRuns_Default(); + +displayXHProfReport($xhprof_runs_impl, $params, $source, $run, $wts, + $symbol, $sort, $run1, $run2); + + +echo ""; +echo ""; diff --git a/plugins/xhprof/lib/xhprof_html/jquery/indicator.gif b/plugins/xhprof/lib/xhprof_html/jquery/indicator.gif new file mode 100755 index 000000000..9c26a717c Binary files /dev/null and b/plugins/xhprof/lib/xhprof_html/jquery/indicator.gif differ diff --git a/plugins/xhprof/lib/xhprof_html/jquery/jquery-1.2.6.js b/plugins/xhprof/lib/xhprof_html/jquery/jquery-1.2.6.js new file mode 100755 index 000000000..5b8bfc2dd --- /dev/null +++ b/plugins/xhprof/lib/xhprof_html/jquery/jquery-1.2.6.js @@ -0,0 +1,3549 @@ +(function(){ +/* + * jQuery 1.2.6 - New Wave Javascript + * + * Copyright (c) 2008 John Resig (jquery.com) + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * $Date: 2009-03-17 18:35:18 $ + * $Rev: 5685 $ + */ + +// Map over jQuery in case of overwrite +var _jQuery = window.jQuery, +// Map over the $ in case of overwrite + _$ = window.$; + +var jQuery = window.jQuery = window.$ = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context ); +}; + +// A simple way to check for HTML strings or ID strings +// (both of which we optimize for) +var quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#(\w+)$/, + +// Is it a simple selector + isSimple = /^.[^:#\[\.]*$/, + +// Will speed up references to undefined, and allows munging its name. + undefined; + +jQuery.fn = jQuery.prototype = { + init: function( selector, context ) { + // Make sure that a selection was provided + selector = selector || document; + + // Handle $(DOMElement) + if ( selector.nodeType ) { + this[0] = selector; + this.length = 1; + return this; + } + // Handle HTML strings + if ( typeof selector == "string" ) { + // Are we dealing with HTML string or an ID? + var match = quickExpr.exec( selector ); + + // Verify a match, and that no context was specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) + selector = jQuery.clean( [ match[1] ], context ); + + // HANDLE: $("#id") + else { + var elem = document.getElementById( match[3] ); + + // Make sure an element was located + if ( elem ){ + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id != match[3] ) + return jQuery().find( selector ); + + // Otherwise, we inject the element directly into the jQuery object + return jQuery( elem ); + } + selector = []; + } + + // HANDLE: $(expr, [context]) + // (which is just equivalent to: $(content).find(expr) + } else + return jQuery( context ).find( selector ); + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) + return jQuery( document )[ jQuery.fn.ready ? "ready" : "load" ]( selector ); + + return this.setArray(jQuery.makeArray(selector)); + }, + + // The current version of jQuery being used + jquery: "1.2.6", + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + // The number of elements contained in the matched element set + length: 0, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num == undefined ? + + // Return a 'clean' array + jQuery.makeArray( this ) : + + // Return just the object + this[ num ]; + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + // Build a new jQuery matched element set + var ret = jQuery( elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + // Return the newly-formed element set + return ret; + }, + + // Force the current matched set of elements to become + // the specified array of elements (destroying the stack in the process) + // You should use pushStack() in order to do this, but maintain the stack + setArray: function( elems ) { + // Resetting the length to 0, then using the native Array push + // is a super-fast way to populate an object with array-like properties + this.length = 0; + Array.prototype.push.apply( this, elems ); + + return this; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + var ret = -1; + + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem && elem.jquery ? elem[0] : elem + , this ); + }, + + attr: function( name, value, type ) { + var options = name; + + // Look for the case where we're accessing a style value + if ( name.constructor == String ) + if ( value === undefined ) + return this[0] && jQuery[ type || "attr" ]( this[0], name ); + + else { + options = {}; + options[ name ] = value; + } + + // Check to see if we're setting style values + return this.each(function(i){ + // Set all the styles + for ( name in options ) + jQuery.attr( + type ? + this.style : + this, + name, jQuery.prop( this, options[ name ], type, i, name ) + ); + }); + }, + + css: function( key, value ) { + // ignore negative width and height values + if ( (key == 'width' || key == 'height') && parseFloat(value) < 0 ) + value = undefined; + return this.attr( key, value, "curCSS" ); + }, + + text: function( text ) { + if ( typeof text != "object" && text != null ) + return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) ); + + var ret = ""; + + jQuery.each( text || this, function(){ + jQuery.each( this.childNodes, function(){ + if ( this.nodeType != 8 ) + ret += this.nodeType != 1 ? + this.nodeValue : + jQuery.fn.text( [ this ] ); + }); + }); + + return ret; + }, + + wrapAll: function( html ) { + if ( this[0] ) + // The elements to wrap the target around + jQuery( html, this[0].ownerDocument ) + .clone() + .insertBefore( this[0] ) + .map(function(){ + var elem = this; + + while ( elem.firstChild ) + elem = elem.firstChild; + + return elem; + }) + .append(this); + + return this; + }, + + wrapInner: function( html ) { + return this.each(function(){ + jQuery( this ).contents().wrapAll( html ); + }); + }, + + wrap: function( html ) { + return this.each(function(){ + jQuery( this ).wrapAll( html ); + }); + }, + + append: function() { + return this.domManip(arguments, true, false, function(elem){ + if (this.nodeType == 1) + this.appendChild( elem ); + }); + }, + + prepend: function() { + return this.domManip(arguments, true, true, function(elem){ + if (this.nodeType == 1) + this.insertBefore( elem, this.firstChild ); + }); + }, + + before: function() { + return this.domManip(arguments, false, false, function(elem){ + this.parentNode.insertBefore( elem, this ); + }); + }, + + after: function() { + return this.domManip(arguments, false, true, function(elem){ + this.parentNode.insertBefore( elem, this.nextSibling ); + }); + }, + + end: function() { + return this.prevObject || jQuery( [] ); + }, + + find: function( selector ) { + var elems = jQuery.map(this, function(elem){ + return jQuery.find( selector, elem ); + }); + + return this.pushStack( /[^+>] [^+>]/.test( selector ) || selector.indexOf("..") > -1 ? + jQuery.unique( elems ) : + elems ); + }, + + clone: function( events ) { + // Do the clone + var ret = this.map(function(){ + if ( jQuery.browser.msie && !jQuery.isXMLDoc(this) ) { + // IE copies events bound via attachEvent when + // using cloneNode. Calling detachEvent on the + // clone will also remove the events from the orignal + // In order to get around this, we use innerHTML. + // Unfortunately, this means some modifications to + // attributes in IE that are actually only stored + // as properties will not be copied (such as the + // the name attribute on an input). + var clone = this.cloneNode(true), + container = document.createElement("div"); + container.appendChild(clone); + return jQuery.clean([container.innerHTML])[0]; + } else + return this.cloneNode(true); + }); + + // Need to set the expando to null on the cloned set if it exists + // removeData doesn't work here, IE removes it from the original as well + // this is primarily for IE but the data expando shouldn't be copied over in any browser + var clone = ret.find("*").andSelf().each(function(){ + if ( this[ expando ] != undefined ) + this[ expando ] = null; + }); + + // Copy the events from the original to the clone + if ( events === true ) + this.find("*").andSelf().each(function(i){ + if (this.nodeType == 3) + return; + var events = jQuery.data( this, "events" ); + + for ( var type in events ) + for ( var handler in events[ type ] ) + jQuery.event.add( clone[ i ], type, events[ type ][ handler ], events[ type ][ handler ].data ); + }); + + // Return the cloned set + return ret; + }, + + filter: function( selector ) { + return this.pushStack( + jQuery.isFunction( selector ) && + jQuery.grep(this, function(elem, i){ + return selector.call( elem, i ); + }) || + + jQuery.multiFilter( selector, this ) ); + }, + + not: function( selector ) { + if ( selector.constructor == String ) + // test special case where just one selector is passed in + if ( isSimple.test( selector ) ) + return this.pushStack( jQuery.multiFilter( selector, this, true ) ); + else + selector = jQuery.multiFilter( selector, this ); + + var isArrayLike = selector.length && selector[selector.length - 1] !== undefined && !selector.nodeType; + return this.filter(function() { + return isArrayLike ? jQuery.inArray( this, selector ) < 0 : this != selector; + }); + }, + + add: function( selector ) { + return this.pushStack( jQuery.unique( jQuery.merge( + this.get(), + typeof selector == 'string' ? + jQuery( selector ) : + jQuery.makeArray( selector ) + ))); + }, + + is: function( selector ) { + return !!selector && jQuery.multiFilter( selector, this ).length > 0; + }, + + hasClass: function( selector ) { + return this.is( "." + selector ); + }, + + val: function( value ) { + if ( value == undefined ) { + + if ( this.length ) { + var elem = this[0]; + + // We need to handle select boxes special + if ( jQuery.nodeName( elem, "select" ) ) { + var index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type == "select-one"; + + // Nothing was selected + if ( index < 0 ) + return null; + + // Loop through all the selected options + for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { + var option = options[ i ]; + + if ( option.selected ) { + // Get the specifc value for the option + value = jQuery.browser.msie && !option.attributes.value.specified ? option.text : option.value; + + // We don't need an array for one selects + if ( one ) + return value; + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + + // Everything else, we just grab the value + } else + return (this[0].value || "").replace(/\r/g, ""); + + } + + return undefined; + } + + if( value.constructor == Number ) + value += ''; + + return this.each(function(){ + if ( this.nodeType != 1 ) + return; + + if ( value.constructor == Array && /radio|checkbox/.test( this.type ) ) + this.checked = (jQuery.inArray(this.value, value) >= 0 || + jQuery.inArray(this.name, value) >= 0); + + else if ( jQuery.nodeName( this, "select" ) ) { + var values = jQuery.makeArray(value); + + jQuery( "option", this ).each(function(){ + this.selected = (jQuery.inArray( this.value, values ) >= 0 || + jQuery.inArray( this.text, values ) >= 0); + }); + + if ( !values.length ) + this.selectedIndex = -1; + + } else + this.value = value; + }); + }, + + html: function( value ) { + return value == undefined ? + (this[0] ? + this[0].innerHTML : + null) : + this.empty().append( value ); + }, + + replaceWith: function( value ) { + return this.after( value ).remove(); + }, + + eq: function( i ) { + return this.slice( i, i + 1 ); + }, + + slice: function() { + return this.pushStack( Array.prototype.slice.apply( this, arguments ) ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function(elem, i){ + return callback.call( elem, i, elem ); + })); + }, + + andSelf: function() { + return this.add( this.prevObject ); + }, + + data: function( key, value ){ + var parts = key.split("."); + parts[1] = parts[1] ? "." + parts[1] : ""; + + if ( value === undefined ) { + var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); + + if ( data === undefined && this.length ) + data = jQuery.data( this[0], key ); + + return data === undefined && parts[1] ? + this.data( parts[0] ) : + data; + } else + return this.trigger("setData" + parts[1] + "!", [parts[0], value]).each(function(){ + jQuery.data( this, key, value ); + }); + }, + + removeData: function( key ){ + return this.each(function(){ + jQuery.removeData( this, key ); + }); + }, + + domManip: function( args, table, reverse, callback ) { + var clone = this.length > 1, elems; + + return this.each(function(){ + if ( !elems ) { + elems = jQuery.clean( args, this.ownerDocument ); + + if ( reverse ) + elems.reverse(); + } + + var obj = this; + + if ( table && jQuery.nodeName( this, "table" ) && jQuery.nodeName( elems[0], "tr" ) ) + obj = this.getElementsByTagName("tbody")[0] || this.appendChild( this.ownerDocument.createElement("tbody") ); + + var scripts = jQuery( [] ); + + jQuery.each(elems, function(){ + var elem = clone ? + jQuery( this ).clone( true )[0] : + this; + + // execute all scripts after the elements have been injected + if ( jQuery.nodeName( elem, "script" ) ) + scripts = scripts.add( elem ); + else { + // Remove any inner scripts for later evaluation + if ( elem.nodeType == 1 ) + scripts = scripts.add( jQuery( "script", elem ).remove() ); + + // Inject the elements into the document + callback.call( obj, elem ); + } + }); + + scripts.each( evalScript ); + }); + } +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +function evalScript( i, elem ) { + if ( elem.src ) + jQuery.ajax({ + url: elem.src, + async: false, + dataType: "script" + }); + + else + jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" ); + + if ( elem.parentNode ) + elem.parentNode.removeChild( elem ); +} + +function now(){ + return +new Date; +} + +jQuery.extend = jQuery.fn.extend = function() { + // copy reference to target object + var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options; + + // Handle a deep copy situation + if ( target.constructor == Boolean ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target != "object" && typeof target != "function" ) + target = {}; + + // extend jQuery itself if only one argument is passed + if ( length == i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) + // Extend the base object + for ( var name in options ) { + var src = target[ name ], copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) + continue; + + // Recurse if we're merging object values + if ( deep && copy && typeof copy == "object" && !copy.nodeType ) + target[ name ] = jQuery.extend( deep, + // Never move original objects, clone them + src || ( copy.length != null ? [ ] : { } ) + , copy ); + + // Don't bring in undefined values + else if ( copy !== undefined ) + target[ name ] = copy; + + } + + // Return the modified object + return target; +}; + +var expando = "jQuery" + now(), uuid = 0, windowData = {}, + // exclude the following css properties to add px + exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i, + // cache defaultView + defaultView = document.defaultView || {}; + +jQuery.extend({ + noConflict: function( deep ) { + window.$ = _$; + + if ( deep ) + window.jQuery = _jQuery; + + return jQuery; + }, + + // See test/unit/core.js for details concerning this function. + isFunction: function( fn ) { + return !!fn && typeof fn != "string" && !fn.nodeName && + fn.constructor != Array && /^[\s[]?function/.test( fn + "" ); + }, + + // check if an element is in a (or is an) XML document + isXMLDoc: function( elem ) { + return elem.documentElement && !elem.body || + elem.tagName && elem.ownerDocument && !elem.ownerDocument.body; + }, + + // Evalulates a script in a global context + globalEval: function( data ) { + data = jQuery.trim( data ); + + if ( data ) { + // Inspired by code by Andrea Giammarchi + // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html + var head = document.getElementsByTagName("head")[0] || document.documentElement, + script = document.createElement("script"); + + script.type = "text/javascript"; + if ( jQuery.browser.msie ) + script.text = data; + else + script.appendChild( document.createTextNode( data ) ); + + // Use insertBefore instead of appendChild to circumvent an IE6 bug. + // This arises when a base node is used (#2709). + head.insertBefore( script, head.firstChild ); + head.removeChild( script ); + } + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase(); + }, + + cache: {}, + + data: function( elem, name, data ) { + elem = elem == window ? + windowData : + elem; + + var id = elem[ expando ]; + + // Compute a unique ID for the element + if ( !id ) + id = elem[ expando ] = ++uuid; + + // Only generate the data cache if we're + // trying to access or manipulate it + if ( name && !jQuery.cache[ id ] ) + jQuery.cache[ id ] = {}; + + // Prevent overriding the named cache with undefined values + if ( data !== undefined ) + jQuery.cache[ id ][ name ] = data; + + // Return the named cache data, or the ID for the element + return name ? + jQuery.cache[ id ][ name ] : + id; + }, + + removeData: function( elem, name ) { + elem = elem == window ? + windowData : + elem; + + var id = elem[ expando ]; + + // If we want to remove a specific section of the element's data + if ( name ) { + if ( jQuery.cache[ id ] ) { + // Remove the section of cache data + delete jQuery.cache[ id ][ name ]; + + // If we've removed all the data, remove the element's cache + name = ""; + + for ( name in jQuery.cache[ id ] ) + break; + + if ( !name ) + jQuery.removeData( elem ); + } + + // Otherwise, we want to remove all of the element's data + } else { + // Clean up the element expando + try { + delete elem[ expando ]; + } catch(e){ + // IE has trouble directly removing the expando + // but it's ok with using removeAttribute + if ( elem.removeAttribute ) + elem.removeAttribute( expando ); + } + + // Completely remove the data cache + delete jQuery.cache[ id ]; + } + }, + + // args is for internal usage only + each: function( object, callback, args ) { + var name, i = 0, length = object.length; + + if ( args ) { + if ( length == undefined ) { + for ( name in object ) + if ( callback.apply( object[ name ], args ) === false ) + break; + } else + for ( ; i < length; ) + if ( callback.apply( object[ i++ ], args ) === false ) + break; + + // A special, fast, case for the most common use of each + } else { + if ( length == undefined ) { + for ( name in object ) + if ( callback.call( object[ name ], name, object[ name ] ) === false ) + break; + } else + for ( var value = object[0]; + i < length && callback.call( value, i, value ) !== false; value = object[++i] ){} + } + + return object; + }, + + prop: function( elem, value, type, i, name ) { + // Handle executable functions + if ( jQuery.isFunction( value ) ) + value = value.call( elem, i ); + + // Handle passing in a number to a CSS property + return value && value.constructor == Number && type == "curCSS" && !exclude.test( name ) ? + value + "px" : + value; + }, + + className: { + // internal only, use addClass("class") + add: function( elem, classNames ) { + jQuery.each((classNames || "").split(/\s+/), function(i, className){ + if ( elem.nodeType == 1 && !jQuery.className.has( elem.className, className ) ) + elem.className += (elem.className ? " " : "") + className; + }); + }, + + // internal only, use removeClass("class") + remove: function( elem, classNames ) { + if (elem.nodeType == 1) + elem.className = classNames != undefined ? + jQuery.grep(elem.className.split(/\s+/), function(className){ + return !jQuery.className.has( classNames, className ); + }).join(" ") : + ""; + }, + + // internal only, use hasClass("class") + has: function( elem, className ) { + return jQuery.inArray( className, (elem.className || elem).toString().split(/\s+/) ) > -1; + } + }, + + // A method for quickly swapping in/out CSS properties to get correct calculations + swap: function( elem, options, callback ) { + var old = {}; + // Remember the old values, and insert the new ones + for ( var name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + callback.call( elem ); + + // Revert the old values + for ( var name in options ) + elem.style[ name ] = old[ name ]; + }, + + css: function( elem, name, force ) { + if ( name == "width" || name == "height" ) { + var val, props = { position: "absolute", visibility: "hidden", display:"block" }, which = name == "width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ]; + + function getWH() { + val = name == "width" ? elem.offsetWidth : elem.offsetHeight; + var padding = 0, border = 0; + jQuery.each( which, function() { + padding += parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0; + border += parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0; + }); + val -= Math.round(padding + border); + } + + if ( jQuery(elem).is(":visible") ) + getWH(); + else + jQuery.swap( elem, props, getWH ); + + return Math.max(0, val); + } + + return jQuery.curCSS( elem, name, force ); + }, + + curCSS: function( elem, name, force ) { + var ret, style = elem.style; + + // A helper method for determining if an element's values are broken + function color( elem ) { + if ( !jQuery.browser.safari ) + return false; + + // defaultView is cached + var ret = defaultView.getComputedStyle( elem, null ); + return !ret || ret.getPropertyValue("color") == ""; + } + + // We need to handle opacity special in IE + if ( name == "opacity" && jQuery.browser.msie ) { + ret = jQuery.attr( style, "opacity" ); + + return ret == "" ? + "1" : + ret; + } + // Opera sometimes will give the wrong display answer, this fixes it, see #2037 + if ( jQuery.browser.opera && name == "display" ) { + var save = style.outline; + style.outline = "0 solid black"; + style.outline = save; + } + + // Make sure we're using the right name for getting the float value + if ( name.match( /float/i ) ) + name = styleFloat; + + if ( !force && style && style[ name ] ) + ret = style[ name ]; + + else if ( defaultView.getComputedStyle ) { + + // Only "float" is needed here + if ( name.match( /float/i ) ) + name = "float"; + + name = name.replace( /([A-Z])/g, "-$1" ).toLowerCase(); + + var computedStyle = defaultView.getComputedStyle( elem, null ); + + if ( computedStyle && !color( elem ) ) + ret = computedStyle.getPropertyValue( name ); + + // If the element isn't reporting its values properly in Safari + // then some display: none elements are involved + else { + var swap = [], stack = [], a = elem, i = 0; + + // Locate all of the parent display: none elements + for ( ; a && color(a); a = a.parentNode ) + stack.unshift(a); + + // Go through and make them visible, but in reverse + // (It would be better if we knew the exact display type that they had) + for ( ; i < stack.length; i++ ) + if ( color( stack[ i ] ) ) { + swap[ i ] = stack[ i ].style.display; + stack[ i ].style.display = "block"; + } + + // Since we flip the display style, we have to handle that + // one special, otherwise get the value + ret = name == "display" && swap[ stack.length - 1 ] != null ? + "none" : + ( computedStyle && computedStyle.getPropertyValue( name ) ) || ""; + + // Finally, revert the display styles back + for ( i = 0; i < swap.length; i++ ) + if ( swap[ i ] != null ) + stack[ i ].style.display = swap[ i ]; + } + + // We should always get a number back from opacity + if ( name == "opacity" && ret == "" ) + ret = "1"; + + } else if ( elem.currentStyle ) { + var camelCase = name.replace(/\-(\w)/g, function(all, letter){ + return letter.toUpperCase(); + }); + + ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ]; + + // From the awesome hack by Dean Edwards + // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + + // If we're not dealing with a regular pixel number + // but a number that has a weird ending, we need to convert it to pixels + if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) { + // Remember the original values + var left = style.left, rsLeft = elem.runtimeStyle.left; + + // Put in the new values to get a computed value out + elem.runtimeStyle.left = elem.currentStyle.left; + style.left = ret || 0; + ret = style.pixelLeft + "px"; + + // Revert the changed values + style.left = left; + elem.runtimeStyle.left = rsLeft; + } + } + + return ret; + }, + + clean: function( elems, context ) { + var ret = []; + context = context || document; + // !context.createElement fails in IE with an error but returns typeof 'object' + if (typeof context.createElement == 'undefined') + context = context.ownerDocument || context[0] && context[0].ownerDocument || document; + + jQuery.each(elems, function(i, elem){ + if ( !elem ) + return; + + if ( elem.constructor == Number ) + elem += ''; + + // Convert html string into DOM nodes + if ( typeof elem == "string" ) { + // Fix "XHTML"-style tags in all browsers + elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){ + return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ? + all : + front + ">"; + }); + + // Trim whitespace, otherwise indexOf won't work as expected + var tags = jQuery.trim( elem ).toLowerCase(), div = context.createElement("div"); + + var wrap = + // option or optgroup + !tags.indexOf("", "" ] || + + !tags.indexOf("", "" ] || + + tags.match(/^<(thead|tbody|tfoot|colg|cap)/) && + [ 1, "", "
" ] || + + !tags.indexOf("", "" ] || + + // matched above + (!tags.indexOf("", "" ] || + + !tags.indexOf("", "" ] || + + // IE can't serialize and "; + echo ""; + echo ""; + echo ""; +} + + +/* + * Formats call counts for XHProf reports. + * + * Description: + * Call counts in single-run reports are integer values. + * However, call counts for aggregated reports can be + * fractional. This function will print integer values + * without decimal point, but with commas etc. + * + * 4000 ==> 4,000 + * + * It'll round fractional values to decimal precision of 3 + * 4000.1212 ==> 4,000.121 + * 4000.0001 ==> 4,000 + * + */ +function xhprof_count_format($num) { + $num = round($num, 3); + if (round($num) == $num) { + return number_format($num); + } else { + return number_format($num, 3); + } +} + +function xhprof_percent_format($s, $precision = 1) { + return sprintf('%.'.$precision.'f%%', 100 * $s); +} + +/** + * Implodes the text for a bunch of actions (such as links, forms, + * into a HTML list and returns the text. + */ +function xhprof_render_actions($actions) { + $out = array(); + + if (count($actions)) { + $out[] = '
    '; + foreach ($actions as $action) { + $out[] = '
  • '.$action.'
  • '; + } + $out[] = '
'; + } + + return implode('', $out); +} + + +/** + * @param html-str $content the text/image/innerhtml/whatever for the link + * @param raw-str $href + * @param raw-str $class + * @param raw-str $id + * @param raw-str $title + * @param raw-str $target + * @param raw-str $onclick + * @param raw-str $style + * @param raw-str $access + * @param raw-str $onmouseover + * @param raw-str $onmouseout + * @param raw-str $onmousedown + * @param raw-str $dir + * @param raw-str $rel + */ +function xhprof_render_link($content, $href, $class='', $id='', $title='', + $target='', + $onclick='', $style='', $access='', $onmouseover='', + $onmouseout='', $onmousedown='') { + + if (!$content) { + return ''; + } + + if ($href) { + $link = ' 1, + "ct" => 1, + "wt" => 1, + "excl_wt" => 1, + "ut" => 1, + "excl_ut" => 1, + "st" => 1, + "excl_st" => 1, + "mu" => 1, + "excl_mu" => 1, + "pmu" => 1, + "excl_pmu" => 1, + "cpu" => 1, + "excl_cpu" => 1, + "samples" => 1, + "excl_samples" => 1 + ); + +// Textual descriptions for column headers in "single run" mode +$descriptions = array( + "fn" => "Function Name", + "ct" => "Calls", + "Calls%" => "Calls%", + + "wt" => "Incl. Wall Time
(microsec)", + "IWall%" => "IWall%", + "excl_wt" => "Excl. Wall Time
(microsec)", + "EWall%" => "EWall%", + + "ut" => "Incl. User
(microsecs)", + "IUser%" => "IUser%", + "excl_ut" => "Excl. User
(microsec)", + "EUser%" => "EUser%", + + "st" => "Incl. Sys
(microsec)", + "ISys%" => "ISys%", + "excl_st" => "Excl. Sys
(microsec)", + "ESys%" => "ESys%", + + "cpu" => "Incl. CPU
(microsecs)", + "ICpu%" => "ICpu%", + "excl_cpu" => "Excl. CPU
(microsec)", + "ECpu%" => "ECPU%", + + "mu" => "Incl.
MemUse
(bytes)", + "IMUse%" => "IMemUse%", + "excl_mu" => "Excl.
MemUse
(bytes)", + "EMUse%" => "EMemUse%", + + "pmu" => "Incl.
PeakMemUse
(bytes)", + "IPMUse%" => "IPeakMemUse%", + "excl_pmu" => "Excl.
PeakMemUse
(bytes)", + "EPMUse%" => "EPeakMemUse%", + + "samples" => "Incl. Samples", + "ISamples%" => "ISamples%", + "excl_samples" => "Excl. Samples", + "ESamples%" => "ESamples%", + ); + +// Formatting Callback Functions... +$format_cbk = array( + "fn" => "", + "ct" => "xhprof_count_format", + "Calls%" => "xhprof_percent_format", + + "wt" => "number_format", + "IWall%" => "xhprof_percent_format", + "excl_wt" => "number_format", + "EWall%" => "xhprof_percent_format", + + "ut" => "number_format", + "IUser%" => "xhprof_percent_format", + "excl_ut" => "number_format", + "EUser%" => "xhprof_percent_format", + + "st" => "number_format", + "ISys%" => "xhprof_percent_format", + "excl_st" => "number_format", + "ESys%" => "xhprof_percent_format", + + "cpu" => "number_format", + "ICpu%" => "xhprof_percent_format", + "excl_cpu" => "number_format", + "ECpu%" => "xhprof_percent_format", + + "mu" => "number_format", + "IMUse%" => "xhprof_percent_format", + "excl_mu" => "number_format", + "EMUse%" => "xhprof_percent_format", + + "pmu" => "number_format", + "IPMUse%" => "xhprof_percent_format", + "excl_pmu" => "number_format", + "EPMUse%" => "xhprof_percent_format", + + "samples" => "number_format", + "ISamples%" => "xhprof_percent_format", + "excl_samples" => "number_format", + "ESamples%" => "xhprof_percent_format", + ); + + +// Textual descriptions for column headers in "diff" mode +$diff_descriptions = array( + "fn" => "Function Name", + "ct" => "Calls Diff", + "Calls%" => "Calls
Diff%", + + "wt" => "Incl. Wall
Diff
(microsec)", + "IWall%" => "IWall
Diff%", + "excl_wt" => "Excl. Wall
Diff
(microsec)", + "EWall%" => "EWall
Diff%", + + "ut" => "Incl. User Diff
(microsec)", + "IUser%" => "IUser
Diff%", + "excl_ut" => "Excl. User
Diff
(microsec)", + "EUser%" => "EUser
Diff%", + + "cpu" => "Incl. CPU Diff
(microsec)", + "ICpu%" => "ICpu
Diff%", + "excl_cpu" => "Excl. CPU
Diff
(microsec)", + "ECpu%" => "ECpu
Diff%", + + "st" => "Incl. Sys Diff
(microsec)", + "ISys%" => "ISys
Diff%", + "excl_st" => "Excl. Sys Diff
(microsec)", + "ESys%" => "ESys
Diff%", + + "mu" => "Incl.
MemUse
Diff
(bytes)", + "IMUse%" => "IMemUse
Diff%", + "excl_mu" => "Excl.
MemUse
Diff
(bytes)", + "EMUse%" => "EMemUse
Diff%", + + "pmu" => "Incl.
PeakMemUse
Diff
(bytes)", + "IPMUse%" => "IPeakMemUse
Diff%", + "excl_pmu" => "Excl.
PeakMemUse
Diff
(bytes)", + "EPMUse%" => "EPeakMemUse
Diff%", + + "samples" => "Incl. Samples Diff", + "ISamples%" => "ISamples Diff%", + "excl_samples" => "Excl. Samples Diff", + "ESamples%" => "ESamples Diff%", + ); + +// columns that'll be displayed in a top-level report +$stats = array(); + +// columns that'll be displayed in a function's parent/child report +$pc_stats = array(); + +// Various total counts +$totals = 0; +$totals_1 = 0; +$totals_2 = 0; + +/* + * The subset of $possible_metrics that is present in the raw profile data. + */ +$metrics = null; + +/** + * Callback comparison operator (passed to usort() for sorting array of + * tuples) that compares array elements based on the sort column + * specified in $sort_col (global parameter). + * + * @author Kannan + */ +function sort_cbk($a, $b) { + global $sort_col; + global $diff_mode; + + if ($sort_col == "fn") { + + // case insensitive ascending sort for function names + $left = strtoupper($a["fn"]); + $right = strtoupper($b["fn"]); + + if ($left == $right) + return 0; + return ($left < $right) ? -1 : 1; + + } else { + + // descending sort for all others + $left = $a[$sort_col]; + $right = $b[$sort_col]; + + // if diff mode, sort by absolute value of regression/improvement + if ($diff_mode) { + $left = abs($left); + $right = abs($right); + } + + if ($left == $right) + return 0; + return ($left > $right) ? -1 : 1; + } +} + +/** + * Get the appropriate description for a statistic + * (depending upon whether we are in diff report mode + * or single run report mode). + * + * @author Kannan + */ +function stat_description($stat) { + global $descriptions; + global $diff_descriptions; + global $diff_mode; + + if ($diff_mode) { + return $diff_descriptions[$stat]; + } else { + return $descriptions[$stat]; + } +} + + +/** + * Analyze raw data & generate the profiler report + * (common for both single run mode and diff mode). + * + * @author: Kannan + */ +function profiler_report ($url_params, + $rep_symbol, + $sort, + $run1, + $run1_desc, + $run1_data, + $run2 = 0, + $run2_desc = "", + $run2_data = array()) { + global $totals; + global $totals_1; + global $totals_2; + global $stats; + global $pc_stats; + global $diff_mode; + global $base_path; + + // if we are reporting on a specific function, we can trim down + // the report(s) to just stuff that is relevant to this function. + // That way compute_flat_info()/compute_diff() etc. do not have + // to needlessly work hard on churning irrelevant data. + if (!empty($rep_symbol)) { + $run1_data = xhprof_trim_run($run1_data, array($rep_symbol)); + if ($diff_mode) { + $run2_data = xhprof_trim_run($run2_data, array($rep_symbol)); + } + } + + if ($diff_mode) { + $run_delta = xhprof_compute_diff($run1_data, $run2_data); + $symbol_tab = xhprof_compute_flat_info($run_delta, $totals); + $symbol_tab1 = xhprof_compute_flat_info($run1_data, $totals_1); + $symbol_tab2 = xhprof_compute_flat_info($run2_data, $totals_2); + } else { + $symbol_tab = xhprof_compute_flat_info($run1_data, $totals); + } + + $run1_txt = sprintf("Run #%s: %s", + $run1, $run1_desc); + + $base_url_params = xhprof_array_unset(xhprof_array_unset($url_params, + 'symbol'), + 'all'); + + $top_link_query_string = "$base_path/?" . http_build_query($base_url_params); + + if ($diff_mode) { + $diff_text = "Diff"; + $base_url_params = xhprof_array_unset($base_url_params, 'run1'); + $base_url_params = xhprof_array_unset($base_url_params, 'run2'); + $run1_link = xhprof_render_link('View Run #' . $run1, + "$base_path/?" . + http_build_query(xhprof_array_set($base_url_params, + 'run', + $run1))); + $run2_txt = sprintf("Run #%s: %s", + $run2, $run2_desc); + + $run2_link = xhprof_render_link('View Run #' . $run2, + "$base_path/?" . + http_build_query(xhprof_array_set($base_url_params, + 'run', + $run2))); + } else { + $diff_text = "Run"; + } + + // set up the action links for operations that can be done on this report + $links = array(); + $links [] = xhprof_render_link("View Top Level $diff_text Report", + $top_link_query_string); + + if ($diff_mode) { + $inverted_params = $url_params; + $inverted_params['run1'] = $url_params['run2']; + $inverted_params['run2'] = $url_params['run1']; + + // view the different runs or invert the current diff + $links [] = $run1_link; + $links [] = $run2_link; + $links [] = xhprof_render_link('Invert ' . $diff_text . ' Report', + "$base_path/?". + http_build_query($inverted_params)); + } + + // lookup function typeahead form + $links [] = ''; + + echo xhprof_render_actions($links); + + + echo + '
' . + '
' . $diff_text . ' Report
' . + '
' . ($diff_mode ? + $run1_txt . '
vs.
' . $run2_txt : + $run1_txt) . + '
' . + '
Tip
' . + '
Click a function name below to drill down.
' . + '
' . + '
'; + + // data tables + if (!empty($rep_symbol)) { + if (!isset($symbol_tab[$rep_symbol])) { + echo "
Symbol $rep_symbol not found in XHProf run
"; + return; + } + + /* single function report with parent/child information */ + if ($diff_mode) { + $info1 = isset($symbol_tab1[$rep_symbol]) ? + $symbol_tab1[$rep_symbol] : null; + $info2 = isset($symbol_tab2[$rep_symbol]) ? + $symbol_tab2[$rep_symbol] : null; + symbol_report($url_params, $run_delta, $symbol_tab[$rep_symbol], + $sort, $rep_symbol, + $run1, $info1, + $run2, $info2); + } else { + symbol_report($url_params, $run1_data, $symbol_tab[$rep_symbol], + $sort, $rep_symbol, $run1); + } + } else { + /* flat top-level report of all functions */ + full_report($url_params, $symbol_tab, $sort, $run1, $run2); + } +} + +/** + * Computes percentage for a pair of values, and returns it + * in string format. + */ +function pct($a, $b) { + if ($b == 0) { + return "N/A"; + } else { + $res = (round(($a * 1000 / $b)) / 10); + return $res; + } +} + +/** + * Given a number, returns the td class to use for display. + * + * For instance, negative numbers in diff reports comparing two runs (run1 & run2) + * represent improvement from run1 to run2. We use green to display those deltas, + * and red for regression deltas. + */ +function get_print_class($num, $bold) { + global $vbar; + global $vbbar; + global $vrbar; + global $vgbar; + global $diff_mode; + + if ($bold) { + if ($diff_mode) { + if ($num <= 0) { + $class = $vgbar; // green (improvement) + } else { + $class = $vrbar; // red (regression) + } + } else { + $class = $vbbar; // blue + } + } + else { + $class = $vbar; // default (black) + } + + return $class; +} + +/** + * Prints a element with a numeric value. + */ +function print_td_num($num, $fmt_func, $bold=false, $attributes=null) { + + $class = get_print_class($num, $bold); + + if (!empty($fmt_func) && is_numeric($num) ) { + $num = call_user_func($fmt_func, $num); + } + + print("$num\n"); +} + +/** + * Prints a element with a pecentage. + */ +function print_td_pct($numer, $denom, $bold=false, $attributes=null) { + global $vbar; + global $vbbar; + global $diff_mode; + + $class = get_print_class($numer, $bold); + + if ($denom == 0) { + $pct = "N/A%"; + } else { + $pct = xhprof_percent_format($numer / abs($denom)); + } + + print("$pct\n"); +} + +/** + * Print "flat" data corresponding to one function. + * + * @author Kannan + */ +function print_function_info($url_params, $info, $sort, $run1, $run2) { + static $odd_even = 0; + + global $totals; + global $sort_col; + global $metrics; + global $format_cbk; + global $display_calls; + global $base_path; + + // Toggle $odd_or_even + $odd_even = 1 - $odd_even; + + if ($odd_even) { + print(""); + } + else { + print(''); + } + + $href = "$base_path/?" . + http_build_query(xhprof_array_set($url_params, + 'symbol', $info["fn"])); + + print(''); + print(xhprof_render_link($info["fn"], $href)); + print_source_link($info); + print("\n"); + + if ($display_calls) { + // Call Count.. + print_td_num($info["ct"], $format_cbk["ct"], ($sort_col == "ct")); + print_td_pct($info["ct"], $totals["ct"], ($sort_col == "ct")); + } + + // Other metrics.. + foreach ($metrics as $metric) { + // Inclusive metric + print_td_num($info[$metric], $format_cbk[$metric], + ($sort_col == $metric)); + print_td_pct($info[$metric], $totals[$metric], + ($sort_col == $metric)); + + // Exclusive Metric + print_td_num($info["excl_" . $metric], + $format_cbk["excl_" . $metric], + ($sort_col == "excl_" . $metric)); + print_td_pct($info["excl_" . $metric], + $totals[$metric], + ($sort_col == "excl_" . $metric)); + } + + print("\n"); +} + +/** + * Print non-hierarchical (flat-view) of profiler data. + * + * @author Kannan + */ +function print_flat_data($url_params, $title, $flat_data, $sort, $run1, $run2, $limit) { + + global $stats; + global $sortable_columns; + global $vwbar; + global $base_path; + + $size = count($flat_data); + if (!$limit) { // no limit + $limit = $size; + $display_link = ""; + } else { + $display_link = xhprof_render_link(" [ display all ]", + "$base_path/?" . + http_build_query(xhprof_array_set($url_params, + 'all', 1))); + } + + print("

$title $display_link


"); + + print(''); + print(''); + + foreach ($stats as $stat) { + $desc = stat_description($stat); + if (array_key_exists($stat, $sortable_columns)) { + $href = "$base_path/?" + . http_build_query(xhprof_array_set($url_params, 'sort', $stat)); + $header = xhprof_render_link($desc, $href); + } else { + $header = $desc; + } + + if ($stat == "fn") + print(""); + else print(""); + } + print("\n"); + + if ($limit >= 0) { + $limit = min($size, $limit); + for ($i = 0; $i < $limit; $i++) { + print_function_info($url_params, $flat_data[$i], $sort, $run1, $run2); + } + } else { + // if $limit is negative, print abs($limit) items starting from the end + $limit = min($size, abs($limit)); + for ($i = 0; $i < $limit; $i++) { + print_function_info($url_params, $flat_data[$size - $i - 1], $sort, $run1, $run2); + } + } + print("
$header$header
"); + + // let's print the display all link at the bottom as well... + if ($display_link) { + echo '
' . $display_link . '
'; + } + +} + +/** + * Generates a tabular report for all functions. This is the top-level report. + * + * @author Kannan + */ +function full_report($url_params, $symbol_tab, $sort, $run1, $run2) { + global $vwbar; + global $vbar; + global $totals; + global $totals_1; + global $totals_2; + global $metrics; + global $diff_mode; + global $descriptions; + global $sort_col; + global $format_cbk; + global $display_calls; + global $base_path; + + $possible_metrics = xhprof_get_possible_metrics(); + + if ($diff_mode) { + + $base_url_params = xhprof_array_unset(xhprof_array_unset($url_params, + 'run1'), + 'run2'); + $href1 = "$base_path/?" . + http_build_query(xhprof_array_set($base_url_params, + 'run', $run1)); + $href2 = "$base_path/?" . + http_build_query(xhprof_array_set($base_url_params, + 'run', $run2)); + + print("

Overall Diff Summary

"); + print('' . "\n"); + print(''); + print(""); + print(""); + print(""); + print(""); + print(""); + print(''); + + if ($display_calls) { + print(''); + print(""); + print_td_num($totals_1["ct"], $format_cbk["ct"]); + print_td_num($totals_2["ct"], $format_cbk["ct"]); + print_td_num($totals_2["ct"] - $totals_1["ct"], $format_cbk["ct"], true); + print_td_pct($totals_2["ct"] - $totals_1["ct"], $totals_1["ct"], true); + print(''); + } + + foreach ($metrics as $metric) { + $m = $metric; + print(''); + print(""); + print_td_num($totals_1[$m], $format_cbk[$m]); + print_td_num($totals_2[$m], $format_cbk[$m]); + print_td_num($totals_2[$m] - $totals_1[$m], $format_cbk[$m], true); + print_td_pct($totals_2[$m] - $totals_1[$m], $totals_1[$m], true); + print(''); + } + print('
" . xhprof_render_link("Run #$run1", $href1) . "" . xhprof_render_link("Run #$run2", $href2) . "DiffDiff%
Number of Function Calls
" . str_replace("
", " ", $descriptions[$m]) . "
'); + + $callgraph_report_title = '[View Regressions/Improvements using Callgraph Diff]'; + + } else { + print("

\n"); + + print('' . "\n"); + echo ""; + echo ""; + echo ""; + echo ""; + + foreach ($metrics as $metric) { + echo ""; + echo ""; + echo ""; + echo ""; + } + + if ($display_calls) { + echo ""; + echo ""; + echo ""; + echo ""; + } + + echo "
Overall Summary
Total " + . str_replace("
", " ", stat_description($metric)) . ":
" . number_format($totals[$metric]) . " " + . $possible_metrics[$metric][1] . "
Number of Function Calls:" . number_format($totals['ct']) . "
"; + print("

\n"); + + $callgraph_report_title = '[View Full Callgraph]'; + } + + print("

" . + xhprof_render_link($callgraph_report_title, + "$base_path/callgraph.php" . "?" . http_build_query($url_params)) + . "

"); + + + $flat_data = array(); + foreach ($symbol_tab as $symbol => $info) { + $tmp = $info; + $tmp["fn"] = $symbol; + $flat_data[] = $tmp; + } + usort($flat_data, 'sort_cbk'); + + print("
"); + + if (!empty($url_params['all'])) { + $all = true; + $limit = 0; // display all rows + } else { + $all = false; + $limit = 100; // display only limited number of rows + } + + $desc = str_replace("
", " ", $descriptions[$sort_col]); + + if ($diff_mode) { + if ($all) { + $title = "Total Diff Report: ' + .'Sorted by absolute value of regression/improvement in $desc"; + } else { + $title = "Top 100 Regressions/" + . "Improvements: " + . "Sorted by $desc Diff"; + } + } else { + if ($all) { + $title = "Sorted by $desc"; + } else { + $title = "Displaying top $limit functions: Sorted by $desc"; + } + } + print_flat_data($url_params, $title, $flat_data, $sort, $run1, $run2, $limit); +} + + +/** + * Return attribute names and values to be used by javascript tooltip. + */ +function get_tooltip_attributes($type, $metric) { + return "type='$type' metric='$metric'"; +} + +/** + * Print info for a parent or child function in the + * parent & children report. + * + * @author Kannan + */ +function pc_info($info, $base_ct, $base_info, $parent) { + global $sort_col; + global $metrics; + global $format_cbk; + global $display_calls; + + if ($parent) + $type = "Parent"; + else $type = "Child"; + + if ($display_calls) { + $mouseoverct = get_tooltip_attributes($type, "ct"); + /* call count */ + print_td_num($info["ct"], $format_cbk["ct"], ($sort_col == "ct"), $mouseoverct); + print_td_pct($info["ct"], $base_ct, ($sort_col == "ct"), $mouseoverct); + } + + /* Inclusive metric values */ + foreach ($metrics as $metric) { + print_td_num($info[$metric], $format_cbk[$metric], + ($sort_col == $metric), + get_tooltip_attributes($type, $metric)); + print_td_pct($info[$metric], $base_info[$metric], ($sort_col == $metric), + get_tooltip_attributes($type, $metric)); + } +} + +function print_pc_array($url_params, $results, $base_ct, $base_info, $parent, + $run1, $run2) { + global $base_path; + + // Construct section title + if ($parent) { + $title = 'Parent function'; + } + else { + $title = 'Child function'; + } + if (count($results) > 1) { + $title .= 's'; + } + + print(""); + print("
" . $title . "
"); + print(""); + + $odd_even = 0; + foreach ($results as $info) { + $href = "$base_path/?" . + http_build_query(xhprof_array_set($url_params, + 'symbol', $info["fn"])); + + $odd_even = 1 - $odd_even; + + if ($odd_even) { + print(''); + } + else { + print(''); + } + + print("" . xhprof_render_link($info["fn"], $href)); + print_source_link($info); + print(""); + pc_info($info, $base_ct, $base_info, $parent); + print(""); + } +} + +function print_source_link($info) { + if (strncmp($info['fn'], 'run_init', 8) && $info['fn'] !== 'main()') { + if (defined('XHPROF_SYMBOL_LOOKUP_URL')) { + $link = xhprof_render_link( + 'source', + XHPROF_SYMBOL_LOOKUP_URL . '?symbol='.rawurlencode($info["fn"])); + print(' ('.$link.')'); + } + } +} + + +function print_symbol_summary($symbol_info, $stat, $base) { + + $val = $symbol_info[$stat]; + $desc = str_replace("
", " ", stat_description($stat)); + + print("$desc: "); + print(number_format($val)); + print(" (" . pct($val, $base) . "% of overall)"); + if (substr($stat, 0, 4) == "excl") { + $func_base = $symbol_info[str_replace("excl_", "", $stat)]; + print(" (" . pct($val, $func_base) . "% of this function)"); + } + print("
"); +} + +/** + * Generates a report for a single function/symbol. + * + * @author Kannan + */ +function symbol_report($url_params, + $run_data, $symbol_info, $sort, $rep_symbol, + $run1, + $symbol_info1 = null, + $run2 = 0, + $symbol_info2 = null) { + global $vwbar; + global $vbar; + global $totals; + global $pc_stats; + global $sortable_columns; + global $metrics; + global $diff_mode; + global $descriptions; + global $format_cbk; + global $sort_col; + global $display_calls; + global $base_path; + + $possible_metrics = xhprof_get_possible_metrics(); + + if ($diff_mode) { + $diff_text = "Diff"; + $regr_impr = "Regression/Improvement"; + } else { + $diff_text = ""; + $regr_impr = ""; + } + + if ($diff_mode) { + + $base_url_params = xhprof_array_unset(xhprof_array_unset($url_params, + 'run1'), + 'run2'); + $href1 = "$base_path?" + . http_build_query(xhprof_array_set($base_url_params, 'run', $run1)); + $href2 = "$base_path?" + . http_build_query(xhprof_array_set($base_url_params, 'run', $run2)); + + print("

$regr_impr summary for $rep_symbol

"); + print('' . "\n"); + print(''); + print(""); + print(""); + print(""); + print(""); + print(""); + print(''); + print(''); + + if ($display_calls) { + print(""); + print_td_num($symbol_info1["ct"], $format_cbk["ct"]); + print_td_num($symbol_info2["ct"], $format_cbk["ct"]); + print_td_num($symbol_info2["ct"] - $symbol_info1["ct"], + $format_cbk["ct"], true); + print_td_pct($symbol_info2["ct"] - $symbol_info1["ct"], + $symbol_info1["ct"], true); + print(''); + } + + + foreach ($metrics as $metric) { + $m = $metric; + + // Inclusive stat for metric + print(''); + print(""); + print_td_num($symbol_info1[$m], $format_cbk[$m]); + print_td_num($symbol_info2[$m], $format_cbk[$m]); + print_td_num($symbol_info2[$m] - $symbol_info1[$m], $format_cbk[$m], true); + print_td_pct($symbol_info2[$m] - $symbol_info1[$m], $symbol_info1[$m], true); + print(''); + + // AVG (per call) Inclusive stat for metric + print(''); + print(""); + $avg_info1 = 'N/A'; + $avg_info2 = 'N/A'; + if ($symbol_info1['ct'] > 0) { + $avg_info1 = ($symbol_info1[$m] / $symbol_info1['ct']); + } + if ($symbol_info2['ct'] > 0) { + $avg_info2 = ($symbol_info2[$m] / $symbol_info2['ct']); + } + print_td_num($avg_info1, $format_cbk[$m]); + print_td_num($avg_info2, $format_cbk[$m]); + print_td_num($avg_info2 - $avg_info1, $format_cbk[$m], true); + print_td_pct($avg_info2 - $avg_info1, $avg_info1, true); + print(''); + + // Exclusive stat for metric + $m = "excl_" . $metric; + print(''); + print(""); + print_td_num($symbol_info1[$m], $format_cbk[$m]); + print_td_num($symbol_info2[$m], $format_cbk[$m]); + print_td_num($symbol_info2[$m] - $symbol_info1[$m], $format_cbk[$m], true); + print_td_pct($symbol_info2[$m] - $symbol_info1[$m], $symbol_info1[$m], true); + print(''); + } + + print('
$rep_symbolRun #$run1Run #$run2DiffDiff%
Number of Function Calls
" . str_replace("
", " ", $descriptions[$m]) . "
" . str_replace("
", " ", $descriptions[$m]) . " per call
" . str_replace("
", " ", $descriptions[$m]) . "
'); + } + + print("

"); + print("Parent/Child $regr_impr report for $rep_symbol"); + + $callgraph_href = "$base_path/callgraph.php?" + . http_build_query(xhprof_array_set($url_params, 'func', $rep_symbol)); + + print(" [View Callgraph $diff_text]
"); + + print("


"); + + print('' . "\n"); + print(''); + + foreach ($pc_stats as $stat) { + $desc = stat_description($stat); + if (array_key_exists($stat, $sortable_columns)) { + + $href = "$base_path/?" . + http_build_query(xhprof_array_set($url_params, + 'sort', $stat)); + $header = xhprof_render_link($desc, $href); + } else { + $header = $desc; + } + + if ($stat == "fn") + print(""); + else print(""); + } + print(""); + + print(""); + + print(""); + // make this a self-reference to facilitate copy-pasting snippets to e-mails + print(""); + + if ($display_calls) { + // Call Count + print_td_num($symbol_info["ct"], $format_cbk["ct"]); + print_td_pct($symbol_info["ct"], $totals["ct"]); + } + + // Inclusive Metrics for current function + foreach ($metrics as $metric) { + print_td_num($symbol_info[$metric], $format_cbk[$metric], ($sort_col == $metric)); + print_td_pct($symbol_info[$metric], $totals[$metric], ($sort_col == $metric)); + } + print(""); + + print(""); + print(""); + + if ($display_calls) { + // Call Count + print(""); + print(""); + } + + // Exclusive Metrics for current function + foreach ($metrics as $metric) { + print_td_num($symbol_info["excl_" . $metric], $format_cbk["excl_" . $metric], + ($sort_col == $metric), + get_tooltip_attributes("Child", $metric)); + print_td_pct($symbol_info["excl_" . $metric], $symbol_info[$metric], + ($sort_col == $metric), + get_tooltip_attributes("Child", $metric)); + } + print(""); + + // list of callers/parent functions + $results = array(); + if ($display_calls) { + $base_ct = $symbol_info["ct"]; + } else { + $base_ct = 0; + } + foreach ($metrics as $metric) { + $base_info[$metric] = $symbol_info[$metric]; + } + foreach ($run_data as $parent_child => $info) { + list($parent, $child) = xhprof_parse_parent_child($parent_child); + if (($child == $rep_symbol) && ($parent)) { + $info_tmp = $info; + $info_tmp["fn"] = $parent; + $results[] = $info_tmp; + } + } + usort($results, 'sort_cbk'); + + if (count($results) > 0) { + print_pc_array($url_params, $results, $base_ct, $base_info, true, + $run1, $run2); + } + + // list of callees/child functions + $results = array(); + $base_ct = 0; + foreach ($run_data as $parent_child => $info) { + list($parent, $child) = xhprof_parse_parent_child($parent_child); + if ($parent == $rep_symbol) { + $info_tmp = $info; + $info_tmp["fn"] = $child; + $results[] = $info_tmp; + if ($display_calls) { + $base_ct += $info["ct"]; + } + } + } + usort($results, 'sort_cbk'); + + if (count($results)) { + print_pc_array($url_params, $results, $base_ct, $base_info, false, + $run1, $run2); + } + + print("
$header$header
"); + print("
Current Function
"); + print("
$rep_symbol"); + print_source_link(array('fn' => $rep_symbol)); + print("
" + ."Exclusive Metrics $diff_text for Current Function
"); + + // These will be used for pop-up tips/help. + // Related javascript code is in: xhprof_report.js + print("\n"); + print(''); + print("\n"); + +} + +/** + * Generate the profiler report for a single run. + * + * @author Kannan + */ +function profiler_single_run_report ($url_params, + $xhprof_data, + $run_desc, + $rep_symbol, + $sort, + $run) { + + init_metrics($xhprof_data, $rep_symbol, $sort, false); + + profiler_report($url_params, $rep_symbol, $sort, $run, $run_desc, + $xhprof_data); +} + + + +/** + * Generate the profiler report for diff mode (delta between two runs). + * + * @author Kannan + */ +function profiler_diff_report($url_params, + $xhprof_data1, + $run1_desc, + $xhprof_data2, + $run2_desc, + $rep_symbol, + $sort, + $run1, + $run2) { + + + // Initialize what metrics we'll display based on data in Run2 + init_metrics($xhprof_data2, $rep_symbol, $sort, true); + + profiler_report($url_params, + $rep_symbol, + $sort, + $run1, + $run1_desc, + $xhprof_data1, + $run2, + $run2_desc, + $xhprof_data2); +} + + +/** + * Generate a XHProf Display View given the various URL parameters + * as arguments. The first argument is an object that implements + * the iXHProfRuns interface. + * + * @param object $xhprof_runs_impl An object that implements + * the iXHProfRuns interface + *. + * @param array $url_params Array of non-default URL params. + * + * @param string $source Category/type of the run. The source in + * combination with the run id uniquely + * determines a profiler run. + * + * @param string $run run id, or comma separated sequence of + * run ids. The latter is used if an aggregate + * report of the runs is desired. + * + * @param string $wts Comma separate list of integers. + * Represents the weighted ratio in + * which which a set of runs will be + * aggregated. [Used only for aggregate + * reports.] + * + * @param string $symbol Function symbol. If non-empty then the + * parent/child view of this function is + * displayed. If empty, a flat-profile view + * of the functions is displayed. + * + * @param string $run1 Base run id (for diff reports) + * + * @param string $run2 New run id (for diff reports) + * + */ +function displayXHProfReport($xhprof_runs_impl, $url_params, $source, + $run, $wts, $symbol, $sort, $run1, $run2) { + + if ($run) { // specific run to display? + + // run may be a single run or a comma separate list of runs + // that'll be aggregated. If "wts" (a comma separated list + // of integral weights is specified), the runs will be + // aggregated in that ratio. + // + $runs_array = explode(",", $run); + + if (count($runs_array) == 1) { + $xhprof_data = $xhprof_runs_impl->get_run($runs_array[0], + $source, + $description); + } else { + if (!empty($wts)) { + $wts_array = explode(",", $wts); + } else { + $wts_array = null; + } + $data = xhprof_aggregate_runs($xhprof_runs_impl, + $runs_array, $wts_array, $source, false); + $xhprof_data = $data['raw']; + $description = $data['description']; + } + + + profiler_single_run_report($url_params, + $xhprof_data, + $description, + $symbol, + $sort, + $run); + + } else if ($run1 && $run2) { // diff report for two runs + + $xhprof_data1 = $xhprof_runs_impl->get_run($run1, $source, $description1); + $xhprof_data2 = $xhprof_runs_impl->get_run($run2, $source, $description2); + + profiler_diff_report($url_params, + $xhprof_data1, + $description1, + $xhprof_data2, + $description2, + $symbol, + $sort, + $run1, + $run2); + + } else { + echo "No XHProf runs specified in the URL."; + if (method_exists($xhprof_runs_impl, 'list_runs')) { + $xhprof_runs_impl->list_runs(); + } + } +} diff --git a/plugins/xhprof/lib/xhprof_lib/utils/callgraph_utils.php b/plugins/xhprof/lib/xhprof_lib/utils/callgraph_utils.php new file mode 100755 index 000000000..90f4c31a2 --- /dev/null +++ b/plugins/xhprof/lib/xhprof_lib/utils/callgraph_utils.php @@ -0,0 +1,486 @@ + 1, + "gif" => 1, + "png" => 1, + "svg" => 1, // support scalable vector graphic + "ps" => 1, + ); + +/** + * Send an HTTP header with the response. You MUST use this function instead + * of header() so that we can debug header issues because they're virtually + * impossible to debug otherwise. If you try to commit header(), SVN will + * reject your commit. + * + * @param string HTTP header name, like 'Location' + * @param string HTTP header value, like 'http://www.example.com/' + * + */ +function xhprof_http_header($name, $value) { + + if (!$name) { + xhprof_error('http_header usage'); + return null; + } + + if (!is_string($value)) { + xhprof_error('http_header value not a string'); + } + + header($name.': '.$value, true); +} + +/** + * Genearte and send MIME header for the output image to client browser. + * + * @author cjiang + */ +function xhprof_generate_mime_header($type, $length) { + switch ($type) { + case 'jpg': + $mime = 'image/jpeg'; + break; + case 'gif': + $mime = 'image/gif'; + break; + case 'png': + $mime = 'image/png'; + break; + case 'svg': + $mime = 'image/svg+xml'; // content type for scalable vector graphic + break; + case 'ps': + $mime = 'application/postscript'; + default: + $mime = false; + } + + if ($mime) { + xhprof_http_header('Content-type', $mime); + xhprof_http_header('Content-length', (string)$length); + } +} + +/** + * Generate image according to DOT script. This function will spawn a process + * with "dot" command and pipe the "dot_script" to it and pipe out the + * generated image content. + * + * @param dot_script, string, the script for DOT to generate the image. + * @param type, one of the supported image types, see + * $xhprof_legal_image_types. + * @returns, binary content of the generated image on success. empty string on + * failure. + * + * @author cjiang + */ +function xhprof_generate_image_by_dot($dot_script, $type) { + $descriptorspec = array( + // stdin is a pipe that the child will read from + 0 => array("pipe", "r"), + // stdout is a pipe that the child will write to + 1 => array("pipe", "w"), + // stderr is a pipe that the child will write to + 2 => array("pipe", "w") + ); + + $cmd = " dot -T".$type; + + $process = proc_open( $cmd, $descriptorspec, $pipes, sys_get_temp_dir(), array( 'PATH' => getenv( 'PATH' ) ) ); + if (is_resource($process)) { + fwrite($pipes[0], $dot_script); + fclose($pipes[0]); + + $output = stream_get_contents($pipes[1]); + + $err = stream_get_contents($pipes[2]); + if (!empty($err)) { + print "failed to execute cmd: \"$cmd\". stderr: `$err'\n"; + exit; + } + + fclose($pipes[2]); + fclose($pipes[1]); + proc_close($process); + return $output; + } + print "failed to execute cmd \"$cmd\""; + exit(); +} + +/* + * Get the children list of all nodes. + */ +function xhprof_get_children_table($raw_data) { + $children_table = array(); + foreach ($raw_data as $parent_child => $info) { + list($parent, $child) = xhprof_parse_parent_child($parent_child); + if (!isset($children_table[$parent])) { + $children_table[$parent] = array($child); + } else { + $children_table[$parent][] = $child; + } + } + return $children_table; +} + +/** + * Generate DOT script from the given raw phprof data. + * + * @param raw_data, phprof profile data. + * @param threshold, float, the threshold value [0,1). The functions in the + * raw_data whose exclusive wall times ratio are below the + * threshold will be filtered out and won't apprear in the + * generated image. + * @param page, string(optional), the root node name. This can be used to + * replace the 'main()' as the root node. + * @param func, string, the focus function. + * @param critical_path, bool, whether or not to display critical path with + * bold lines. + * @returns, string, the DOT script to generate image. + * + * @author cjiang + */ +function xhprof_generate_dot_script($raw_data, $threshold, $source, $page, + $func, $critical_path, $right=null, + $left=null) { + + $max_width = 5; + $max_height = 3.5; + $max_fontsize = 35; + $max_sizing_ratio = 20; + + $totals; + + if ($left === null) { + // init_metrics($raw_data, null, null); + } + $sym_table = xhprof_compute_flat_info($raw_data, $totals); + + if ($critical_path) { + $children_table = xhprof_get_children_table($raw_data); + $node = "main()"; + $path = array(); + $path_edges = array(); + $visited = array(); + while ($node) { + $visited[$node] = true; + if (isset($children_table[$node])) { + $max_child = null; + foreach ($children_table[$node] as $child) { + + if (isset($visited[$child])) { + continue; + } + if ($max_child === null || + abs($raw_data[xhprof_build_parent_child_key($node, + $child)]["wt"]) > + abs($raw_data[xhprof_build_parent_child_key($node, + $max_child)]["wt"])) { + $max_child = $child; + } + } + if ($max_child !== null) { + $path[$max_child] = true; + $path_edges[xhprof_build_parent_child_key($node, $max_child)] = true; + } + $node = $max_child; + } else { + $node = null; + } + } + } + + // if it is a benchmark callgraph, we make the benchmarked function the root. + if ($source == "bm" && array_key_exists("main()", $sym_table)) { + $total_times = $sym_table["main()"]["ct"]; + $remove_funcs = array("main()", + "hotprofiler_disable", + "call_user_func_array", + "xhprof_disable"); + + foreach ($remove_funcs as $cur_del_func) { + if (array_key_exists($cur_del_func, $sym_table) && + $sym_table[$cur_del_func]["ct"] == $total_times) { + unset($sym_table[$cur_del_func]); + } + } + } + + // use the function to filter out irrelevant functions. + if (!empty($func)) { + $interested_funcs = array(); + foreach ($raw_data as $parent_child => $info) { + list($parent, $child) = xhprof_parse_parent_child($parent_child); + if ($parent == $func || $child == $func) { + $interested_funcs[$parent] = 1; + $interested_funcs[$child] = 1; + } + } + foreach ($sym_table as $symbol => $info) { + if (!array_key_exists($symbol, $interested_funcs)) { + unset($sym_table[$symbol]); + } + } + } + + $result = "digraph call_graph {\n"; + + // Filter out functions whose exclusive time ratio is below threshold, and + // also assign a unique integer id for each function to be generated. In the + // meantime, find the function with the most exclusive time (potentially the + // performance bottleneck). + $cur_id = 0; $max_wt = 0; + foreach ($sym_table as $symbol => $info) { + if (empty($func) && abs($info["wt"] / $totals["wt"]) < $threshold) { + unset($sym_table[$symbol]); + continue; + } + if ($max_wt == 0 || $max_wt < abs($info["excl_wt"])) { + $max_wt = abs($info["excl_wt"]); + } + $sym_table[$symbol]["id"] = $cur_id; + $cur_id ++; + } + + // Generate all nodes' information. + foreach ($sym_table as $symbol => $info) { + if ($info["excl_wt"] == 0) { + $sizing_factor = $max_sizing_ratio; + } else { + $sizing_factor = $max_wt / abs($info["excl_wt"]) ; + if ($sizing_factor > $max_sizing_ratio) { + $sizing_factor = $max_sizing_ratio; + } + } + $fillcolor = (($sizing_factor < 1.5) ? + ", style=filled, fillcolor=red" : ""); + + if ($critical_path) { + // highlight nodes along critical path. + if (!$fillcolor && array_key_exists($symbol, $path)) { + $fillcolor = ", style=filled, fillcolor=yellow"; + } + } + + $fontsize = ", fontsize=" + .(int)($max_fontsize / (($sizing_factor - 1) / 10 + 1)); + + $width = ", width=".sprintf("%.1f", $max_width / $sizing_factor); + $height = ", height=".sprintf("%.1f", $max_height / $sizing_factor); + + if ($symbol == "main()") { + $shape = "octagon"; + $name = "Total: ".($totals["wt"] / 1000.0)." ms\\n"; + $name .= addslashes(isset($page) ? $page : $symbol); + } else { + $shape = "box"; + $name = addslashes($symbol)."\\nInc: ". sprintf("%.3f",$info["wt"] / 1000) . + " ms (" . sprintf("%.1f%%", 100 * $info["wt"] / $totals["wt"]).")"; + } + if ($left === null) { + $label = ", label=\"".$name."\\nExcl: " + .(sprintf("%.3f",$info["excl_wt"] / 1000.0))." ms (" + .sprintf("%.1f%%", 100 * $info["excl_wt"] / $totals["wt"]) + . ")\\n".$info["ct"]." total calls\""; + } else { + if (isset($left[$symbol]) && isset($right[$symbol])) { + $label = ", label=\"".addslashes($symbol). + "\\nInc: ".(sprintf("%.3f",$left[$symbol]["wt"] / 1000.0)) + ." ms - " + .(sprintf("%.3f",$right[$symbol]["wt"] / 1000.0))." ms = " + .(sprintf("%.3f",$info["wt"] / 1000.0))." ms". + "\\nExcl: " + .(sprintf("%.3f",$left[$symbol]["excl_wt"] / 1000.0)) + ." ms - ".(sprintf("%.3f",$right[$symbol]["excl_wt"] / 1000.0)) + ." ms = ".(sprintf("%.3f",$info["excl_wt"] / 1000.0))." ms". + "\\nCalls: ".(sprintf("%.3f",$left[$symbol]["ct"]))." - " + .(sprintf("%.3f",$right[$symbol]["ct"]))." = " + .(sprintf("%.3f",$info["ct"]))."\""; + } else if (isset($left[$symbol])) { + $label = ", label=\"".addslashes($symbol). + "\\nInc: ".(sprintf("%.3f",$left[$symbol]["wt"] / 1000.0)) + ." ms - 0 ms = ".(sprintf("%.3f",$info["wt"] / 1000.0)) + ." ms"."\\nExcl: " + .(sprintf("%.3f",$left[$symbol]["excl_wt"] / 1000.0)) + ." ms - 0 ms = " + .(sprintf("%.3f",$info["excl_wt"] / 1000.0))." ms". + "\\nCalls: ".(sprintf("%.3f",$left[$symbol]["ct"]))." - 0 = " + .(sprintf("%.3f",$info["ct"]))."\""; + } else { + $label = ", label=\"".addslashes($symbol). + "\\nInc: 0 ms - " + .(sprintf("%.3f",$right[$symbol]["wt"] / 1000.0)) + ." ms = ".(sprintf("%.3f",$info["wt"] / 1000.0))." ms". + "\\nExcl: 0 ms - " + .(sprintf("%.3f",$right[$symbol]["excl_wt"] / 1000.0)) + ." ms = ".(sprintf("%.3f",$info["excl_wt"] / 1000.0))." ms". + "\\nCalls: 0 - ".(sprintf("%.3f",$right[$symbol]["ct"])) + ." = ".(sprintf("%.3f",$info["ct"]))."\""; + } + } + $result .= "N" . $sym_table[$symbol]["id"]; + $result .= "[shape=$shape ".$label.$width + .$height.$fontsize.$fillcolor."];\n"; + } + + // Generate all the edges' information. + foreach ($raw_data as $parent_child => $info) { + list($parent, $child) = xhprof_parse_parent_child($parent_child); + + if (isset($sym_table[$parent]) && isset($sym_table[$child]) && + (empty($func) || + (!empty($func) && ($parent == $func || $child == $func)))) { + + $label = $info["ct"] == 1 ? $info["ct"]." call" : $info["ct"]." calls"; + + $headlabel = $sym_table[$child]["wt"] > 0 ? + sprintf("%.1f%%", 100 * $info["wt"] + / $sym_table[$child]["wt"]) + : "0.0%"; + + $taillabel = ($sym_table[$parent]["wt"] > 0) ? + sprintf("%.1f%%", + 100 * $info["wt"] / + ($sym_table[$parent]["wt"] - $sym_table["$parent"]["excl_wt"])) + : "0.0%"; + + $linewidth = 1; + $arrow_size = 1; + + if ($critical_path && + isset($path_edges[xhprof_build_parent_child_key($parent, $child)])) { + $linewidth = 10; $arrow_size = 2; + } + + $result .= "N" . $sym_table[$parent]["id"] . " -> N" + . $sym_table[$child]["id"]; + $result .= "[arrowsize=$arrow_size, color=grey, style=\"setlinewidth($linewidth)\"," + ." label=\"" + .$label."\", headlabel=\"".$headlabel + ."\", taillabel=\"".$taillabel."\" ]"; + $result .= ";\n"; + + } + } + $result = $result . "\n}"; + + return $result; +} + +function xhprof_render_diff_image($xhprof_runs_impl, $run1, $run2, + $type, $threshold, $source) { + $total1; + $total2; + + $raw_data1 = $xhprof_runs_impl->get_run($run1, $source, $desc_unused); + $raw_data2 = $xhprof_runs_impl->get_run($run2, $source, $desc_unused); + + // init_metrics($raw_data1, null, null); + $children_table1 = xhprof_get_children_table($raw_data1); + $children_table2 = xhprof_get_children_table($raw_data2); + $symbol_tab1 = xhprof_compute_flat_info($raw_data1, $total1); + $symbol_tab2 = xhprof_compute_flat_info($raw_data2, $total2); + $run_delta = xhprof_compute_diff($raw_data1, $raw_data2); + $script = xhprof_generate_dot_script($run_delta, $threshold, $source, + null, null, true, + $symbol_tab1, $symbol_tab2); + $content = xhprof_generate_image_by_dot($script, $type); + + xhprof_generate_mime_header($type, strlen($content)); + echo $content; +} + +/** + * Generate image content from phprof run id. + * + * @param object $xhprof_runs_impl An object that implements + * the iXHProfRuns interface + * @param run_id, integer, the unique id for the phprof run, this is the + * primary key for phprof database table. + * @param type, string, one of the supported image types. See also + * $xhprof_legal_image_types. + * @param threshold, float, the threshold value [0,1). The functions in the + * raw_data whose exclusive wall times ratio are below the + * threshold will be filtered out and won't apprear in the + * generated image. + * @param func, string, the focus function. + * @returns, string, the DOT script to generate image. + * + * @author cjiang + */ +function xhprof_get_content_by_run($xhprof_runs_impl, $run_id, $type, + $threshold, $func, $source, + $critical_path) { + if (!$run_id) + return ""; + + $raw_data = $xhprof_runs_impl->get_run($run_id, $source, $description); + if (!$raw_data) { + xhprof_error("Raw data is empty"); + return ""; + } + + $script = xhprof_generate_dot_script($raw_data, $threshold, $source, + $description, $func, $critical_path); + + $content = xhprof_generate_image_by_dot($script, $type); + return $content; +} + +/** + * Generate image from phprof run id and send it to client. + * + * @param object $xhprof_runs_impl An object that implements + * the iXHProfRuns interface + * @param run_id, integer, the unique id for the phprof run, this is the + * primary key for phprof database table. + * @param type, string, one of the supported image types. See also + * $xhprof_legal_image_types. + * @param threshold, float, the threshold value [0,1). The functions in the + * raw_data whose exclusive wall times ratio are below the + * threshold will be filtered out and won't apprear in the + * generated image. + * @param func, string, the focus function. + * @param bool, does this run correspond to a PHProfLive run or a dev run? + * @author cjiang + */ +function xhprof_render_image($xhprof_runs_impl, $run_id, $type, $threshold, + $func, $source, $critical_path) { + + $content = xhprof_get_content_by_run($xhprof_runs_impl, $run_id, $type, + $threshold, + $func, $source, $critical_path); + if (!$content) { + print "Error: either we can not find profile data for run_id ".$run_id + ." or the threshold ".$threshold." is too small or you do not" + ." have 'dot' image generation utility installed."; + exit(); + } + + xhprof_generate_mime_header($type, strlen($content)); + echo $content; +} diff --git a/plugins/xhprof/lib/xhprof_lib/utils/xhprof_lib.php b/plugins/xhprof/lib/xhprof_lib/utils/xhprof_lib.php new file mode 100755 index 000000000..4a07e900f --- /dev/null +++ b/plugins/xhprof/lib/xhprof_lib/utils/xhprof_lib.php @@ -0,0 +1,945 @@ + array("Wall", "microsecs", "walltime"), + "ut" => array("User", "microsecs", "user cpu time"), + "st" => array("Sys", "microsecs", "system cpu time"), + "cpu" => array("Cpu", "microsecs", "cpu time"), + "mu" => array("MUse", "bytes", "memory usage"), + "pmu" => array("PMUse", "bytes", "peak memory usage"), + "samples" => array("Samples", "samples", "cpu time")); + return $possible_metrics; +} + +/** + * Initialize the metrics we'll display based on the information + * in the raw data. + * + * @author Kannan + */ +function init_metrics($xhprof_data, $rep_symbol, $sort, $diff_report = false) { + global $stats; + global $pc_stats; + global $metrics; + global $diff_mode; + global $sortable_columns; + global $sort_col; + global $display_calls; + + $diff_mode = $diff_report; + + if (!empty($sort)) { + if (array_key_exists($sort, $sortable_columns)) { + $sort_col = $sort; + } else { + print("Invalid Sort Key $sort specified in URL"); + } + } + + // For C++ profiler runs, walltime attribute isn't present. + // In that case, use "samples" as the default sort column. + if (!isset($xhprof_data["main()"]["wt"])) { + + if ($sort_col == "wt") { + $sort_col = "samples"; + } + + // C++ profiler data doesn't have call counts. + // ideally we should check to see if "ct" metric + // is present for "main()". But currently "ct" + // metric is artificially set to 1. So, relying + // on absence of "wt" metric instead. + $display_calls = false; + } else { + $display_calls = true; + } + + // parent/child report doesn't support exclusive times yet. + // So, change sort hyperlinks to closest fit. + if (!empty($rep_symbol)) { + $sort_col = str_replace("excl_", "", $sort_col); + } + + if ($display_calls) { + $stats = array("fn", "ct", "Calls%"); + } else { + $stats = array("fn"); + } + + $pc_stats = $stats; + + $possible_metrics = xhprof_get_possible_metrics(); + foreach ($possible_metrics as $metric => $desc) { + if (isset($xhprof_data["main()"][$metric])) { + $metrics[] = $metric; + // flat (top-level reports): we can compute + // exclusive metrics reports as well. + $stats[] = $metric; + $stats[] = "I" . $desc[0] . "%"; + $stats[] = "excl_" . $metric; + $stats[] = "E" . $desc[0] . "%"; + + // parent/child report for a function: we can + // only breakdown inclusive times correctly. + $pc_stats[] = $metric; + $pc_stats[] = "I" . $desc[0] . "%"; + } + } +} + +/* + * Get the list of metrics present in $xhprof_data as an array. + * + * @author Kannan + */ +function xhprof_get_metrics($xhprof_data) { + + // get list of valid metrics + $possible_metrics = xhprof_get_possible_metrics(); + + // return those that are present in the raw data. + // We'll just look at the root of the subtree for this. + $metrics = array(); + foreach ($possible_metrics as $metric => $desc) { + if (isset($xhprof_data["main()"][$metric])) { + $metrics[] = $metric; + } + } + + return $metrics; +} + +/** + * Takes a parent/child function name encoded as + * "a==>b" and returns array("a", "b"). + * + * @author Kannan + */ +function xhprof_parse_parent_child($parent_child) { + $ret = explode("==>", $parent_child); + + // Return if both parent and child are set + if (isset($ret[1])) { + return $ret; + } + + return array(null, $ret[0]); +} + +/** + * Given parent & child function name, composes the key + * in the format present in the raw data. + * + * @author Kannan + */ +function xhprof_build_parent_child_key($parent, $child) { + if ($parent) { + return $parent . "==>" . $child; + } else { + return $child; + } +} + + +/** + * Checks if XHProf raw data appears to be valid and not corrupted. + * + * @param int $run_id Run id of run to be pruned. + * [Used only for reporting errors.] + * @param array $raw_data XHProf raw data to be pruned + * & validated. + * + * @return bool true on success, false on failure + * + * @author Kannan + */ +function xhprof_valid_run($run_id, $raw_data) { + + $main_info = $raw_data["main()"]; + if (empty($main_info)) { + xhprof_error("XHProf: main() missing in raw data for Run ID: $run_id"); + return false; + } + + // raw data should contain either wall time or samples information... + if (isset($main_info["wt"])) { + $metric = "wt"; + } else if (isset($main_info["samples"])) { + $metric = "samples"; + } else { + xhprof_error("XHProf: Wall Time information missing from Run ID: $run_id"); + return false; + } + + foreach ($raw_data as $info) { + $val = $info[$metric]; + + // basic sanity checks... + if ($val < 0) { + xhprof_error("XHProf: $metric should not be negative: Run ID $run_id" + . serialize($info)); + return false; + } + if ($val > (86400000000)) { + xhprof_error("XHProf: $metric > 1 day found in Run ID: $run_id " + . serialize($info)); + return false; + } + } + return true; +} + + +/** + * Return a trimmed version of the XHProf raw data. Note that the raw + * data contains one entry for each unique parent/child function + * combination.The trimmed version of raw data will only contain + * entries where either the parent or child function is in the list + * of $functions_to_keep. + * + * Note: Function main() is also always kept so that overall totals + * can still be obtained from the trimmed version. + * + * @param array XHProf raw data + * @param array array of function names + * + * @return array Trimmed XHProf Report + * + * @author Kannan + */ +function xhprof_trim_run($raw_data, $functions_to_keep) { + + // convert list of functions to a hash with function as the key + $function_map = array_fill_keys($functions_to_keep, 1); + + // always keep main() as well so that overall totals can still + // be computed if need be. + $function_map['main()'] = 1; + + $new_raw_data = array(); + foreach ($raw_data as $parent_child => $info) { + list($parent, $child) = xhprof_parse_parent_child($parent_child); + + if (isset($function_map[$parent]) || isset($function_map[$child])) { + $new_raw_data[$parent_child] = $info; + } + } + + return $new_raw_data; +} + +/** + * Takes raw XHProf data that was aggregated over "$num_runs" number + * of runs averages/nomalizes the data. Essentially the various metrics + * collected are divided by $num_runs. + * + * @author Kannan + */ +function xhprof_normalize_metrics($raw_data, $num_runs) { + + if (empty($raw_data) || ($num_runs == 0)) { + return $raw_data; + } + + $raw_data_total = array(); + + if (isset($raw_data["==>main()"]) && isset($raw_data["main()"])) { + xhprof_error("XHProf Error: both ==>main() and main() set in raw data..."); + } + + foreach ($raw_data as $parent_child => $info) { + foreach ($info as $metric => $value) { + $raw_data_total[$parent_child][$metric] = ($value / $num_runs); + } + } + + return $raw_data_total; +} + + +/** + * Get raw data corresponding to specified array of runs + * aggregated by certain weightage. + * + * Suppose you have run:5 corresponding to page1.php, + * run:6 corresponding to page2.php, + * and run:7 corresponding to page3.php + * + * and you want to accumulate these runs in a 2:4:1 ratio. You + * can do so by calling: + * + * xhprof_aggregate_runs(array(5, 6, 7), array(2, 4, 1)); + * + * The above will return raw data for the runs aggregated + * in 2:4:1 ratio. + * + * @param object $xhprof_runs_impl An object that implements + * the iXHProfRuns interface + * @param array $runs run ids of the XHProf runs.. + * @param array $wts integral (ideally) weights for $runs + * @param string $source source to fetch raw data for run from + * @param bool $use_script_name If true, a fake edge from main() to + * to __script:: is introduced + * in the raw data so that after aggregations + * the script name is still preserved. + * + * @return array Return aggregated raw data + * + * @author Kannan + */ +function xhprof_aggregate_runs($xhprof_runs_impl, $runs, + $wts, $source="phprof", + $use_script_name=false) { + + $raw_data_total = null; + $raw_data = null; + $metrics = array(); + + $run_count = count($runs); + $wts_count = count($wts); + + if (($run_count == 0) || + (($wts_count > 0) && ($run_count != $wts_count))) { + return array('description' => 'Invalid input..', + 'raw' => null); + } + + $bad_runs = array(); + foreach ($runs as $idx => $run_id) { + + $raw_data = $xhprof_runs_impl->get_run($run_id, $source, $description); + + // use the first run to derive what metrics to aggregate on. + if ($idx == 0) { + foreach ($raw_data["main()"] as $metric => $val) { + if ($metric != "pmu") { + // for now, just to keep data size small, skip "peak" memory usage + // data while aggregating. + // The "regular" memory usage data will still be tracked. + if (isset($val)) { + $metrics[] = $metric; + } + } + } + } + + if (!xhprof_valid_run($run_id, $raw_data)) { + $bad_runs[] = $run_id; + continue; + } + + if ($use_script_name) { + $page = $description; + + // create a fake function '__script::$page', and have and edge from + // main() to '__script::$page'. We will also need edges to transfer + // all edges originating from main() to now originate from + // '__script::$page' to all function called from main(). + // + // We also weight main() ever so slightly higher so that + // it shows up above the new entry in reports sorted by + // inclusive metrics or call counts. + if ($page) { + foreach ($raw_data["main()"] as $metric => $val) { + $fake_edge[$metric] = $val; + $new_main[$metric] = $val + 0.00001; + } + $raw_data["main()"] = $new_main; + $raw_data[xhprof_build_parent_child_key("main()", + "__script::$page")] + = $fake_edge; + } else { + $use_script_name = false; + } + } + + // if no weights specified, use 1 as the default weightage.. + $wt = ($wts_count == 0) ? 1 : $wts[$idx]; + + // aggregate $raw_data into $raw_data_total with appropriate weight ($wt) + foreach ($raw_data as $parent_child => $info) { + if ($use_script_name) { + // if this is an old edge originating from main(), it now + // needs to be from '__script::$page' + if (substr($parent_child, 0, 9) == "main()==>") { + $child = substr($parent_child, 9); + // ignore the newly added edge from main() + if (substr($child, 0, 10) != "__script::") { + $parent_child = xhprof_build_parent_child_key("__script::$page", + $child); + } + } + } + + if (!isset($raw_data_total[$parent_child])) { + foreach ($metrics as $metric) { + $raw_data_total[$parent_child][$metric] = ($wt * $info[$metric]); + } + } else { + foreach ($metrics as $metric) { + $raw_data_total[$parent_child][$metric] += ($wt * $info[$metric]); + } + } + } + } + + $runs_string = implode(",", $runs); + + if (isset($wts)) { + $wts_string = "in the ratio (" . implode(":", $wts) . ")"; + $normalization_count = array_sum($wts); + } else { + $wts_string = ""; + $normalization_count = $run_count; + } + + $run_count = $run_count - count($bad_runs); + + $data['description'] = "Aggregated Report for $run_count runs: ". + "$runs_string $wts_string\n"; + $data['raw'] = xhprof_normalize_metrics($raw_data_total, + $normalization_count); + $data['bad_runs'] = $bad_runs; + + return $data; +} + + +/** + * Analyze hierarchical raw data, and compute per-function (flat) + * inclusive and exclusive metrics. + * + * Also, store overall totals in the 2nd argument. + * + * @param array $raw_data XHProf format raw profiler data. + * @param array &$overall_totals OUT argument for returning + * overall totals for various + * metrics. + * @return array Returns a map from function name to its + * call count and inclusive & exclusive metrics + * (such as wall time, etc.). + * + * @author Kannan Muthukkaruppan + */ +function xhprof_compute_flat_info($raw_data, &$overall_totals) { + + global $display_calls; + + $metrics = xhprof_get_metrics($raw_data); + + $overall_totals = array("ct" => 0, + "wt" => 0, + "ut" => 0, + "st" => 0, + "cpu" => 0, + "mu" => 0, + "pmu" => 0, + "samples" => 0 + ); + + // compute inclusive times for each function + $symbol_tab = xhprof_compute_inclusive_times($raw_data); + + /* total metric value is the metric value for "main()" */ + foreach ($metrics as $metric) { + $overall_totals[$metric] = $symbol_tab["main()"][$metric]; + } + + /* + * initialize exclusive (self) metric value to inclusive metric value + * to start with. + * In the same pass, also add up the total number of function calls. + */ + foreach ($symbol_tab as $symbol => $info) { + foreach ($metrics as $metric) { + $symbol_tab[$symbol]["excl_" . $metric] = $symbol_tab[$symbol][$metric]; + } + if ($display_calls) { + /* keep track of total number of calls */ + $overall_totals["ct"] += $info["ct"]; + } + } + + /* adjust exclusive times by deducting inclusive time of children */ + foreach ($raw_data as $parent_child => $info) { + list($parent, $child) = xhprof_parse_parent_child($parent_child); + + if ($parent) { + foreach ($metrics as $metric) { + // make sure the parent exists hasn't been pruned. + if (isset($symbol_tab[$parent])) { + $symbol_tab[$parent]["excl_" . $metric] -= $info[$metric]; + } + } + } + } + + return $symbol_tab; +} + +/** + * Hierarchical diff: + * Compute and return difference of two call graphs: Run2 - Run1. + * + * @author Kannan + */ +function xhprof_compute_diff($xhprof_data1, $xhprof_data2) { + global $display_calls; + + // use the second run to decide what metrics we will do the diff on + $metrics = xhprof_get_metrics($xhprof_data2); + + $xhprof_delta = $xhprof_data2; + + foreach ($xhprof_data1 as $parent_child => $info) { + + if (!isset($xhprof_delta[$parent_child])) { + + // this pc combination was not present in run1; + // initialize all values to zero. + if ($display_calls) { + $xhprof_delta[$parent_child] = array("ct" => 0); + } else { + $xhprof_delta[$parent_child] = array(); + } + foreach ($metrics as $metric) { + $xhprof_delta[$parent_child][$metric] = 0; + } + } + + if ($display_calls) { + $xhprof_delta[$parent_child]["ct"] -= $info["ct"]; + } + + foreach ($metrics as $metric) { + $xhprof_delta[$parent_child][$metric] -= $info[$metric]; + } + } + + return $xhprof_delta; +} + + +/** + * Compute inclusive metrics for function. This code was factored out + * of xhprof_compute_flat_info(). + * + * The raw data contains inclusive metrics of a function for each + * unique parent function it is called from. The total inclusive metrics + * for a function is therefore the sum of inclusive metrics for the + * function across all parents. + * + * @return array Returns a map of function name to total (across all parents) + * inclusive metrics for the function. + * + * @author Kannan + */ +function xhprof_compute_inclusive_times($raw_data) { + global $display_calls; + + $metrics = xhprof_get_metrics($raw_data); + + $symbol_tab = array(); + + /* + * First compute inclusive time for each function and total + * call count for each function across all parents the + * function is called from. + */ + foreach ($raw_data as $parent_child => $info) { + + list($parent, $child) = xhprof_parse_parent_child($parent_child); + + if ($parent == $child) { + /* + * XHProf PHP extension should never trigger this situation any more. + * Recursion is handled in the XHProf PHP extension by giving nested + * calls a unique recursion-depth appended name (for example, foo@1). + */ + xhprof_error("Error in Raw Data: parent & child are both: $parent"); + return; + } + + if (!isset($symbol_tab[$child])) { + + if ($display_calls) { + $symbol_tab[$child] = array("ct" => $info["ct"]); + } else { + $symbol_tab[$child] = array(); + } + foreach ($metrics as $metric) { + $symbol_tab[$child][$metric] = $info[$metric]; + } + } else { + if ($display_calls) { + /* increment call count for this child */ + $symbol_tab[$child]["ct"] += $info["ct"]; + } + + /* update inclusive times/metric for this child */ + foreach ($metrics as $metric) { + $symbol_tab[$child][$metric] += $info[$metric]; + } + } + } + + return $symbol_tab; +} + + +/* + * Prunes XHProf raw data: + * + * Any node whose inclusive walltime accounts for less than $prune_percent + * of total walltime is pruned. [It is possible that a child function isn't + * pruned, but one or more of its parents get pruned. In such cases, when + * viewing the child function's hierarchical information, the cost due to + * the pruned parent(s) will be attributed to a special function/symbol + * "__pruned__()".] + * + * @param array $raw_data XHProf raw data to be pruned & validated. + * @param double $prune_percent Any edges that account for less than + * $prune_percent of time will be pruned + * from the raw data. + * + * @return array Returns the pruned raw data. + * + * @author Kannan + */ +function xhprof_prune_run($raw_data, $prune_percent) { + + $main_info = $raw_data["main()"]; + if (empty($main_info)) { + xhprof_error("XHProf: main() missing in raw data"); + return false; + } + + // raw data should contain either wall time or samples information... + if (isset($main_info["wt"])) { + $prune_metric = "wt"; + } else if (isset($main_info["samples"])) { + $prune_metric = "samples"; + } else { + xhprof_error("XHProf: for main() we must have either wt " + ."or samples attribute set"); + return false; + } + + // determine the metrics present in the raw data.. + $metrics = array(); + foreach ($main_info as $metric => $val) { + if (isset($val)) { + $metrics[] = $metric; + } + } + + $prune_threshold = (($main_info[$prune_metric] * $prune_percent) / 100.0); + + init_metrics($raw_data, null, null, false); + $flat_info = xhprof_compute_inclusive_times($raw_data); + + foreach ($raw_data as $parent_child => $info) { + + list($parent, $child) = xhprof_parse_parent_child($parent_child); + + // is this child's overall total from all parents less than threshold? + if ($flat_info[$child][$prune_metric] < $prune_threshold) { + unset($raw_data[$parent_child]); // prune the edge + } else if ($parent && + ($parent != "__pruned__()") && + ($flat_info[$parent][$prune_metric] < $prune_threshold)) { + + // Parent's overall inclusive metric is less than a threshold. + // All edges to the parent node will get nuked, and this child will + // be a dangling child. + // So instead change its parent to be a special function __pruned__(). + $pruned_edge = xhprof_build_parent_child_key("__pruned__()", $child); + + if (isset($raw_data[$pruned_edge])) { + foreach ($metrics as $metric) { + $raw_data[$pruned_edge][$metric]+=$raw_data[$parent_child][$metric]; + } + } else { + $raw_data[$pruned_edge] = $raw_data[$parent_child]; + } + + unset($raw_data[$parent_child]); // prune the edge + } + } + + return $raw_data; +} + + +/** + * Set one key in an array and return the array + * + * @author Kannan + */ +function xhprof_array_set($arr, $k, $v) { + $arr[$k] = $v; + return $arr; +} + +/** + * Removes/unsets one key in an array and return the array + * + * @author Kannan + */ +function xhprof_array_unset($arr, $k) { + unset($arr[$k]); + return $arr; +} + +/** + * Type definitions for URL params + */ +define('XHPROF_STRING_PARAM', 1); +define('XHPROF_UINT_PARAM', 2); +define('XHPROF_FLOAT_PARAM', 3); +define('XHPROF_BOOL_PARAM', 4); + + +/** + * Internal helper function used by various + * xhprof_get_param* flavors for various + * types of parameters. + * + * @param string name of the URL query string param + * + * @author Kannan + */ +function xhprof_get_param_helper($param) { + $val = null; + if (isset($_GET[$param])) + $val = $_GET[$param]; + else if (isset($_POST[$param])) { + $val = $_POST[$param]; + } + return $val; +} + +/** + * Extracts value for string param $param from query + * string. If param is not specified, return the + * $default value. + * + * @author Kannan + */ +function xhprof_get_string_param($param, $default = '') { + $val = xhprof_get_param_helper($param); + + if ($val === null) + return $default; + + return $val; +} + +/** + * Extracts value for unsigned integer param $param from + * query string. If param is not specified, return the + * $default value. + * + * If value is not a valid unsigned integer, logs error + * and returns null. + * + * @author Kannan + */ +function xhprof_get_uint_param($param, $default = 0) { + $val = xhprof_get_param_helper($param); + + if ($val === null) + $val = $default; + + // trim leading/trailing whitespace + $val = trim($val); + + // if it only contains digits, then ok.. + if (ctype_digit($val)) { + return $val; + } + + xhprof_error("$param is $val. It must be an unsigned integer."); + return null; +} + + +/** + * Extracts value for a float param $param from + * query string. If param is not specified, return + * the $default value. + * + * If value is not a valid unsigned integer, logs error + * and returns null. + * + * @author Kannan + */ +function xhprof_get_float_param($param, $default = 0) { + $val = xhprof_get_param_helper($param); + + if ($val === null) + $val = $default; + + // trim leading/trailing whitespace + $val = trim($val); + + // TBD: confirm the value is indeed a float. + if (true) // for now.. + return (float)$val; + + xhprof_error("$param is $val. It must be a float."); + return null; +} + +/** + * Extracts value for a boolean param $param from + * query string. If param is not specified, return + * the $default value. + * + * If value is not a valid unsigned integer, logs error + * and returns null. + * + * @author Kannan + */ +function xhprof_get_bool_param($param, $default = false) { + $val = xhprof_get_param_helper($param); + + if ($val === null) + $val = $default; + + // trim leading/trailing whitespace + $val = trim($val); + + switch (strtolower($val)) { + case '0': + case '1': + $val = (bool)$val; + break; + case 'true': + case 'on': + case 'yes': + $val = true; + break; + case 'false': + case 'off': + case 'no': + $val = false; + break; + default: + xhprof_error("$param is $val. It must be a valid boolean string."); + return null; + } + + return $val; + +} + +/** + * Initialize params from URL query string. The function + * creates globals variables for each of the params + * and if the URL query string doesn't specify a particular + * param initializes them with the corresponding default + * value specified in the input. + * + * @params array $params An array whose keys are the names + * of URL params who value needs to + * be retrieved from the URL query + * string. PHP globals are created + * with these names. The value is + * itself an array with 2-elems (the + * param type, and its default value). + * If a param is not specified in the + * query string the default value is + * used. + * @author Kannan + */ +function xhprof_param_init($params) { + /* Create variables specified in $params keys, init defaults */ + foreach ($params as $k => $v) { + switch ($v[0]) { + case XHPROF_STRING_PARAM: + $p = xhprof_get_string_param($k, $v[1]); + break; + case XHPROF_UINT_PARAM: + $p = xhprof_get_uint_param($k, $v[1]); + break; + case XHPROF_FLOAT_PARAM: + $p = xhprof_get_float_param($k, $v[1]); + break; + case XHPROF_BOOL_PARAM: + $p = xhprof_get_bool_param($k, $v[1]); + break; + default: + xhprof_error("Invalid param type passed to xhprof_param_init: " + . $v[0]); + exit(); + } + + if ($k === 'run') { + $p = implode(',', array_filter(explode(',', $p), 'ctype_xdigit')); + } + + // create a global variable using the parameter name. + $GLOBALS[$k] = $p; + } +} + + +/** + * Given a partial query string $q return matching function names in + * specified XHProf run. This is used for the type ahead function + * selector. + * + * @author Kannan + */ +function xhprof_get_matching_functions($q, $xhprof_data) { + + $matches = array(); + + foreach ($xhprof_data as $parent_child => $info) { + list($parent, $child) = xhprof_parse_parent_child($parent_child); + if (stripos($parent, $q) !== false) { + $matches[$parent] = 1; + } + if (stripos($child, $q) !== false) { + $matches[$child] = 1; + } + } + + $res = array_keys($matches); + + // sort it so the answers are in some reliable order... + asort($res); + + return ($res); +} diff --git a/plugins/xhprof/lib/xhprof_lib/utils/xhprof_runs.php b/plugins/xhprof/lib/xhprof_lib/utils/xhprof_runs.php new file mode 100755 index 000000000..01e9d069f --- /dev/null +++ b/plugins/xhprof/lib/xhprof_lib/utils/xhprof_runs.php @@ -0,0 +1,164 @@ +suffix; + + if (!empty($this->dir)) { + $file = $this->dir . "/" . $file; + } + return $file; + } + + public function __construct($dir = null) { + + // if user hasn't passed a directory location, + // we use the xhprof.output_dir ini setting + // if specified, else we default to the directory + // in which the error_log file resides. + + if (empty($dir)) { + $dir = ini_get("xhprof.output_dir"); + if (empty($dir)) { + + $dir = sys_get_temp_dir(); + + xhprof_error("Warning: Must specify directory location for XHProf runs. ". + "Trying {$dir} as default. You can either pass the " . + "directory location as an argument to the constructor ". + "for XHProfRuns_Default() or set xhprof.output_dir ". + "ini param."); + } + } + $this->dir = $dir; + } + + public function get_run($run_id, $type, &$run_desc) { + $file_name = $this->file_name($run_id, $type); + + if (!file_exists($file_name)) { + xhprof_error("Could not find file $file_name"); + $run_desc = "Invalid Run Id = $run_id"; + return null; + } + + $contents = file_get_contents($file_name); + $run_desc = "XHProf Run (Namespace=$type)"; + return unserialize($contents); + } + + public function save_run($xhprof_data, $type, $run_id = null) { + + // Use PHP serialize function to store the XHProf's + // raw profiler data. + $xhprof_data = serialize($xhprof_data); + + if ($run_id === null) { + $run_id = $this->gen_run_id($type); + } + + $file_name = $this->file_name($run_id, $type); + $file = fopen($file_name, 'w'); + + if ($file) { + fwrite($file, $xhprof_data); + fclose($file); + } else { + xhprof_error("Could not open $file_name\n"); + } + + // echo "Saved run in {$file_name}.\nRun id = {$run_id}.\n"; + return $run_id; + } + + function list_runs() { + if (is_dir($this->dir)) { + echo "
Existing runs:\n
    \n"; + $files = glob("{$this->dir}/*.{$this->suffix}"); + usort($files, function($a, $b) {return filemtime($b) - filemtime($a);}); + foreach ($files as $file) { + list($run,$source) = explode('.', basename($file)); + echo '
  • ' + . htmlentities(basename($file)) . " " + . date("Y-m-d H:i:s", filemtime($file)) . "
  • \n"; + } + echo "
\n"; + } + } +}