You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
397 lines
8.7 KiB
397 lines
8.7 KiB
1 year ago
|
<?php
|
||
|
/*
|
||
|
** Zabbix
|
||
|
** Copyright (C) 2001-2023 Zabbix SIA
|
||
|
**
|
||
|
** This program is free software; you can redistribute it and/or modify
|
||
|
** it under the terms of the GNU General Public License as published by
|
||
|
** the Free Software Foundation; either version 2 of the License, or
|
||
|
** (at your option) any later version.
|
||
|
**
|
||
|
** This program is distributed in the hope that it will be useful,
|
||
|
** but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
** GNU General Public License for more details.
|
||
|
**
|
||
|
** You should have received a copy of the GNU General Public License
|
||
|
** along with this program; if not, write to the Free Software
|
||
|
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||
|
**/
|
||
|
|
||
|
|
||
|
class CProfiler {
|
||
|
|
||
|
/**
|
||
|
* Determines time for single sql query to be considered slow.
|
||
|
*
|
||
|
* @var float
|
||
|
*/
|
||
|
protected $slowSqlQueryTime = 0.01;
|
||
|
|
||
|
/**
|
||
|
* Determines time for single Elasticsearch query to be considered slow.
|
||
|
*
|
||
|
* @var float
|
||
|
*/
|
||
|
protected $slowElasticQueryTime = 0.01;
|
||
|
|
||
|
/**
|
||
|
* Contains all api requests info.
|
||
|
*
|
||
|
* @var array
|
||
|
*/
|
||
|
protected $apiLog = [];
|
||
|
|
||
|
/**
|
||
|
* Contains SQL queries info.
|
||
|
*
|
||
|
* @var array
|
||
|
*/
|
||
|
protected $sqlQueryLog = [];
|
||
|
|
||
|
/**
|
||
|
* Contains Elasticsearch queries info.
|
||
|
*
|
||
|
* @var array
|
||
|
*/
|
||
|
protected $elasticQueryLog = [];
|
||
|
|
||
|
/**
|
||
|
* Total time of all performed sql queries.
|
||
|
*
|
||
|
* @var float
|
||
|
*/
|
||
|
protected $sqlTotalTime = 0.0;
|
||
|
|
||
|
/**
|
||
|
* Total time of all performed Elasticsearch queries.
|
||
|
*
|
||
|
* @var float
|
||
|
*/
|
||
|
protected $elasticTotalTime = 0.0;
|
||
|
|
||
|
/**
|
||
|
* Timestamp of profiling start.
|
||
|
*
|
||
|
* @var float
|
||
|
*/
|
||
|
private $startTime;
|
||
|
|
||
|
/**
|
||
|
* Timestamp of profiling stop.
|
||
|
*
|
||
|
* @var float
|
||
|
*/
|
||
|
private $stopTime;
|
||
|
|
||
|
/**
|
||
|
* Instance of this class object.
|
||
|
*
|
||
|
* @var CProfiler
|
||
|
*/
|
||
|
private static $instance;
|
||
|
|
||
|
/**
|
||
|
* Root directory path
|
||
|
*
|
||
|
* @var string
|
||
|
*/
|
||
|
private $root_dir;
|
||
|
|
||
|
/**
|
||
|
* @static
|
||
|
*
|
||
|
* @return CProfiler
|
||
|
*/
|
||
|
public static function getInstance() {
|
||
|
if (self::$instance === null) {
|
||
|
self::$instance = new self;
|
||
|
}
|
||
|
|
||
|
return self::$instance;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Private constructor.
|
||
|
*/
|
||
|
private function __construct() {
|
||
|
$this->root_dir = realpath(dirname(__FILE__).'/../../..');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Start script profiling.
|
||
|
*/
|
||
|
public function start() {
|
||
|
$this->startTime = microtime(true);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Stop script profiling.
|
||
|
*/
|
||
|
public function stop() {
|
||
|
$this->stopTime = microtime(true);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Make profiling data.
|
||
|
*
|
||
|
* @return CPre
|
||
|
*/
|
||
|
public function make() {
|
||
|
global $DB;
|
||
|
|
||
|
$debug = [];
|
||
|
$debug[] = (new CLink())->setAttribute('name', 'debug');
|
||
|
$debug[] = '******************** '._('Script profiler').' ********************';
|
||
|
$debug[] = BR();
|
||
|
$debug[] = _s('Total time: %1$s', round($this->stopTime - $this->startTime, 6));
|
||
|
$debug[] = BR();
|
||
|
$debug[] = _s('Total SQL time: %1$s', $this->sqlTotalTime);
|
||
|
$debug[] = BR();
|
||
|
|
||
|
if ($this->elasticQueryLog) {
|
||
|
$debug[] = _s('Total Elasticsearch time: %1$s', $this->elasticTotalTime);
|
||
|
$debug[] = BR();
|
||
|
}
|
||
|
|
||
|
if (isset($DB) && isset($DB['SELECT_COUNT'])) {
|
||
|
$debug[] = _s('SQL count: %1$s (selects: %2$s | executes: %3$s)',
|
||
|
count($this->sqlQueryLog), $DB['SELECT_COUNT'], $DB['EXECUTE_COUNT']);
|
||
|
$debug[] = BR();
|
||
|
}
|
||
|
|
||
|
$debug[] = _s('Peak memory usage: %1$s', mem2str($this->getMemoryPeak()));
|
||
|
$debug[] = BR();
|
||
|
$debug[] = _s('Memory limit: %1$s', ini_get('memory_limit'));
|
||
|
$debug[] = BR();
|
||
|
$debug[] = BR();
|
||
|
|
||
|
foreach ($this->apiLog as $i => $apiCall) {
|
||
|
list($class, $method, $params, $result, $file, $line) = $apiCall;
|
||
|
|
||
|
// api method
|
||
|
$debug[] = ($i + 1).'. ';
|
||
|
$debug[] = bold($class.'.'.$method);
|
||
|
$debug[] = ($file !== null ? ' ['.$file.':'.$line.']' : null);
|
||
|
$debug[] = BR();
|
||
|
$debug[] = BR();
|
||
|
|
||
|
// parameters, result
|
||
|
$debug[] = (new CTable())
|
||
|
->addRow([
|
||
|
[_('Parameters').':', BR(), print_r($params, true)],
|
||
|
[_('Result').':', BR(), print_r($result, true)]
|
||
|
]);
|
||
|
|
||
|
$debug[] = BR();
|
||
|
}
|
||
|
|
||
|
$debug[] = BR();
|
||
|
|
||
|
foreach ($this->sqlQueryLog as $query) {
|
||
|
$time = $query[0];
|
||
|
|
||
|
$sql = [
|
||
|
'SQL ('.$time.'): ',
|
||
|
(new CSpan($query[1]))
|
||
|
->addClass(substr($query[1], 0, 6) === 'SELECT' ? ZBX_STYLE_GREEN : ZBX_STYLE_BLUE),
|
||
|
BR()
|
||
|
];
|
||
|
|
||
|
if ($time > $this->slowSqlQueryTime) {
|
||
|
$sql = bold($sql);
|
||
|
}
|
||
|
$debug[] = $sql;
|
||
|
|
||
|
$debug[] = $this->formatCallStack($query[2]);
|
||
|
$debug[] = BR();
|
||
|
$debug[] = BR();
|
||
|
}
|
||
|
|
||
|
$debug[] = BR();
|
||
|
|
||
|
foreach ($this->elasticQueryLog as $query) {
|
||
|
$time = $query[0];
|
||
|
|
||
|
$record = [
|
||
|
'Elasticsearch ('.$time.'): ',
|
||
|
$query[1].' ',
|
||
|
(new CSpan($query[2]))->addClass(ZBX_STYLE_BLUE),
|
||
|
BR(),
|
||
|
'Request: ',
|
||
|
(new CSpan($query[3]))->addClass(ZBX_STYLE_GREEN),
|
||
|
BR()
|
||
|
];
|
||
|
|
||
|
if ($time > $this->slowElasticQueryTime) {
|
||
|
$sql = bold($record);
|
||
|
}
|
||
|
$debug[] = $record;
|
||
|
|
||
|
$debug[] = $this->formatCallStack($query[4]);
|
||
|
$debug[] = BR();
|
||
|
$debug[] = BR();
|
||
|
}
|
||
|
|
||
|
return (new CPre())
|
||
|
->addClass(ZBX_STYLE_DEBUG_OUTPUT)
|
||
|
->addItem($debug);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Output profiling data.
|
||
|
*/
|
||
|
public function show() {
|
||
|
return $this->make()->show();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Store sql query data.
|
||
|
*
|
||
|
* @param float $time
|
||
|
* @param string $sql
|
||
|
*/
|
||
|
public function profileSql($time, $sql) {
|
||
|
$time = round($time, 6);
|
||
|
|
||
|
$this->sqlTotalTime += $time;
|
||
|
$this->sqlQueryLog[] = [
|
||
|
$time,
|
||
|
$sql,
|
||
|
array_slice(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), 1)
|
||
|
];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Store api call data.
|
||
|
*
|
||
|
* @param string $class
|
||
|
* @param string $method
|
||
|
* @param array $params
|
||
|
* @param array $result
|
||
|
*/
|
||
|
public function profileApiCall($class, $method, array $params, $result) {
|
||
|
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
|
||
|
|
||
|
// Use the file name and line number from the first call to the API wrapper object.
|
||
|
// Due to a bug earlier versions of PHP 5.3 did not provide the file name and line number
|
||
|
// of calls to magic methods.
|
||
|
if (isset($backtrace[2]['file'])) {
|
||
|
$file = basename($backtrace[2]['file']);
|
||
|
$line = basename($backtrace[2]['line']);
|
||
|
}
|
||
|
else {
|
||
|
$file = null;
|
||
|
$line = null;
|
||
|
}
|
||
|
|
||
|
$this->apiLog[] = [
|
||
|
$class,
|
||
|
$method,
|
||
|
$params,
|
||
|
$result,
|
||
|
$file,
|
||
|
$line
|
||
|
];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Store Elasticsearch query data.
|
||
|
*
|
||
|
* @param float $time
|
||
|
* @param string $method
|
||
|
* @param string $endpoint
|
||
|
* @param string $query
|
||
|
*/
|
||
|
public function profileElasticsearch($time, $method, $endpoint, $query) {
|
||
|
if (!is_null(CWebUser::$data) && isset(CWebUser::$data['debug_mode'])
|
||
|
&& CWebUser::$data['debug_mode'] == GROUP_DEBUG_MODE_DISABLED) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
$time = round($time, 6);
|
||
|
|
||
|
$this->elasticTotalTime += $time;
|
||
|
$this->elasticQueryLog[] = [
|
||
|
$time,
|
||
|
$method,
|
||
|
$endpoint,
|
||
|
$query,
|
||
|
array_slice(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), 1)
|
||
|
];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return memory used by PHP.
|
||
|
*
|
||
|
* @return int
|
||
|
*/
|
||
|
private function getMemoryPeak() {
|
||
|
return function_exists('memory_get_peak_usage') ? memory_get_peak_usage(true) : memory_get_usage(true);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Formats the function call stack and returns it as a string.
|
||
|
*
|
||
|
* The call stack can be obtained from Exception::getTrace() or from an API result debug stack trace. If no call
|
||
|
* stack is given, it will be taken from debug_backtrace().
|
||
|
*
|
||
|
* @param array $callStack
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
public function formatCallStack(array $callStack = null) {
|
||
|
if (!$callStack) {
|
||
|
$callStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
|
||
|
|
||
|
// never show the call to this method
|
||
|
array_shift($callStack);
|
||
|
}
|
||
|
|
||
|
$functions = [];
|
||
|
$callWithFile = [];
|
||
|
|
||
|
$callStack = array_reverse($callStack);
|
||
|
$firstCall = reset($callStack);
|
||
|
|
||
|
foreach ($callStack as $call) {
|
||
|
// do not show the call to the error handler function
|
||
|
if ($call['function'] != 'zbx_err_handler') {
|
||
|
if (array_key_exists('class', $call)) {
|
||
|
$functions[] = $call['class'].$call['type'].$call['function'].'()';
|
||
|
}
|
||
|
else {
|
||
|
$functions[] = $call['function'].'()';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// if the error is caused by an incorrect function call - the location of that call is contained in
|
||
|
// the call of that function
|
||
|
// if it's caused by something else (like an undefined index) - the location of the call is contained in the
|
||
|
// call to the error handler function
|
||
|
// to display the location we use the last call where this information is present
|
||
|
if (array_key_exists('file', $call)) {
|
||
|
$callWithFile = $call;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$callStackString = '';
|
||
|
|
||
|
if ($functions) {
|
||
|
$callStackString .= pathinfo($firstCall['file'], PATHINFO_BASENAME).':'.$firstCall['line'].' → '.
|
||
|
implode(' → ', $functions);
|
||
|
}
|
||
|
|
||
|
if ($callWithFile) {
|
||
|
$file_name = $callWithFile['file'];
|
||
|
|
||
|
if (substr_compare($file_name, $this->root_dir, 0, strlen($this->root_dir)) === 0) {
|
||
|
$file_name = substr($file_name, strlen($this->root_dir) + 1);
|
||
|
}
|
||
|
$callStackString .= ' in '.$file_name.':'.$callWithFile['line'];
|
||
|
}
|
||
|
|
||
|
return $callStackString;
|
||
|
}
|
||
|
}
|