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; } }