diff --git a/administrator/language/en-GB/en-GB.plg_system_debug.ini b/administrator/language/en-GB/en-GB.plg_system_debug.ini index 7b3353d082504..ab78291512957 100644 --- a/administrator/language/en-GB/en-GB.plg_system_debug.ini +++ b/administrator/language/en-GB/en-GB.plg_system_debug.ini @@ -3,7 +3,10 @@ ; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php ; Note : All ini files need to be saved as UTF-8 - No BOM +PLG_DEBUG_BYTES="Bytes" +PLG_DEBUG_CALL_STACK="Call Stack" PLG_DEBUG_ERRORS="Errors" +PLG_DEBUG_EXPLAIN="Explain" PLG_DEBUG_FIELD_ALLOWED_GROUPS_DESC="Optionally restrict users that can see debug information to those in the selected user groups. If none selected, all users see the debug information." PLG_DEBUG_FIELD_ALLOWED_GROUPS_LABEL="Allowed Groups" PLG_DEBUG_FIELD_LANGUAGE_ERRORFILES_DESC="Display a list of the language files that are in error according to the Joomla ini specification." @@ -12,6 +15,8 @@ PLG_DEBUG_FIELD_LANGUAGE_FILES_DESC="Display a list of the language files that J PLG_DEBUG_FIELD_LANGUAGE_FILES_LABEL="Show Language Files" PLG_DEBUG_FIELD_LANGUAGE_STRING_DESC="Display a list of the untranslated language strings." PLG_DEBUG_FIELD_LANGUAGE_STRING_LABEL="Show Language String" +PLG_DEBUG_FIELD_LOGS_DESC="Display a list of logged messages." +PLG_DEBUG_FIELD_LOGS_LABEL="Show Log Entries" PLG_DEBUG_FIELD_LOG_CATEGORIES_DESC="A comma separated list of log categories to include. Common log categories include but are not limited to: database, databasequery, deprecated, and jerror. If empty, all categories will be shown." PLG_DEBUG_FIELD_LOG_CATEGORIES_LABEL="Log Categories" PLG_DEBUG_FIELD_LOG_CATEGORY_MODE_DESC="Select whether the listed categories should be included or excluded." @@ -31,8 +36,6 @@ PLG_DEBUG_FIELD_LOG_PRIORITIES_INFO="Info" PLG_DEBUG_FIELD_LOG_PRIORITIES_LABEL="Log Priorities" PLG_DEBUG_FIELD_LOG_PRIORITIES_NOTICE="Notice" PLG_DEBUG_FIELD_LOG_PRIORITIES_WARNING="Warning" -PLG_DEBUG_FIELD_LOGS_DESC="Display a list of logged messages." -PLG_DEBUG_FIELD_LOGS_LABEL="Show Log Entries" PLG_DEBUG_FIELD_MEMORY_DESC="Display the total memory usage." PLG_DEBUG_FIELD_MEMORY_LABEL="Show Memory Usage" PLG_DEBUG_FIELD_PROFILING_DESC="Display the profiling waypoints." @@ -47,24 +50,43 @@ PLG_DEBUG_FIELD_STRIP_PREFIX_DESC="Strip words from the beginning of the string. PLG_DEBUG_FIELD_STRIP_PREFIX_LABEL="Strip From Start" PLG_DEBUG_FIELD_STRIP_SUFFIX_DESC="Strip words from the end of the string. For multiple words, use the format: (word1|word2)" PLG_DEBUG_FIELD_STRIP_SUFFIX_LABEL="Strip From End" -PLG_DEBUG_LANG_LOADED="Loaded" -PLG_DEBUG_LANG_NOT_LOADED="Not loaded" PLG_DEBUG_LANGUAGE_FIELDSET_LABEL="Language Options" PLG_DEBUG_LANGUAGE_FILES_IN_ERROR="Parsing errors in language files" PLG_DEBUG_LANGUAGE_FILES_LOADED="Language Files Loaded" +PLG_DEBUG_LANG_LOADED="Loaded" +PLG_DEBUG_LANG_NOT_LOADED="Not loaded" +PLG_DEBUG_LINK_FORMAT="Add xdebug.file_link_format directive to your php.ini file to have links for files" PLG_DEBUG_LOGGING_FIELDSET_LABEL="Logging" PLG_DEBUG_LOGS="Log Messages" +PLG_DEBUG_MEMORY="Memory" +PLG_DEBUG_MEMORY_USED_FOR_QUERY="Query memory: %s Memory before query: %s" PLG_DEBUG_MEMORY_USAGE="Memory Usage" +PLG_DEBUG_NO_PROFILE="No SHOW PROFILE (maybe because there are more than 100 queries)" PLG_DEBUG_OTHER_QUERIES="OTHER Tables:" +PLG_DEBUG_PROFILE="Profile" PLG_DEBUG_PROFILE_INFORMATION="Profile Information" PLG_DEBUG_QUERIES="Database Queries" PLG_DEBUG_QUERIES_LOGGED="%d Queries Logged" +PLG_DEBUG_QUERIES_TIME="Database queries total: %s" +PLG_DEBUG_QUERY_AFTER_LAST="After last query: %s" +PLG_DEBUG_QUERY_DUPLICATES="Duplicate queries" +PLG_DEBUG_QUERY_DUPLICATES_FOUND="Duplicate found!" +PLG_DEBUG_QUERY_DUPLICATES_NUMBER="%s duplicates" +PLG_DEBUG_QUERY_DUPLICATES_TOTAL_NUMBER="%s duplicate found!" +PLG_DEBUG_QUERY_EXPLAIN_NOT_POSSIBLE="EXPLAIN not possible on query: %s" +PLG_DEBUG_QUERY_TIME="Query Time: %s" PLG_DEBUG_QUERY_TYPES_LOGGED="%d Query Types Logged, Sorted by Occurrences." PLG_DEBUG_QUERY_TYPE_AND_OCCURRENCES="%2$d × %1$s" +PLG_DEBUG_ROWS_RETURNED_BY_QUERY="Rows returned: %s" PLG_DEBUG_SELECT_QUERIES="SELECT Tables:" PLG_DEBUG_SESSION="Session" +PLG_DEBUG_TIME="Time" PLG_DEBUG_TITLE="Joomla! Debug Console" PLG_DEBUG_UNKNOWN_FILE="Unknown file" PLG_DEBUG_UNTRANSLATED_STRINGS="Untranslated Strings" +PLG_DEBUG_WARNING_NO_INDEX="NO INDEX KEY COULD BE USED" +PLG_DEBUG_WARNING_NO_INDEX_DESC="This table has probably a missing index on WHERE equalities and/or JOIN ON column(s) or is written in a way that no index can be used, causing a time-consuming full table scan" +PLG_DEBUG_WARNING_USING_FILESORT="Using filesort" +PLG_DEBUG_WARNING_USING_FILESORT_DESC="This table has probably a missing index on WHERE/ON equality column(s) ending by the ORDER BY column(s) or is written in a way that no index can be used, causing a time-consuming filesort." PLG_DEBUG_XML_DESCRIPTION="This plugin provides a variety of system information as well as assistance for the creation of translation files." PLG_SYSTEM_DEBUG="System - Debug" diff --git a/libraries/joomla/database/driver.php b/libraries/joomla/database/driver.php index 1cbaa5ec917d1..dd3d0764eeb34 100644 --- a/libraries/joomla/database/driver.php +++ b/libraries/joomla/database/driver.php @@ -92,6 +92,19 @@ abstract class JDatabaseDriver extends JDatabase implements JDatabaseInterface */ protected $log = array(); + /** + * @var array The log of executed SQL statements timings (start and stop microtimes) by the database driver. + * @since CMS 3.1.2 + */ + protected $timings = array(); + + + /** + * @var array The log of executed SQL statements timings (start and stop microtimes) by the database driver. + * @since CMS 3.1.2 + */ + protected $callStacks = array(); + /** * @var string The character(s) used to quote SQL statement names such as table names or field names, * etc. The child classes should define this as necessary. If a single character string the @@ -170,6 +183,12 @@ abstract class JDatabaseDriver extends JDatabase implements JDatabaseInterface */ protected $transactionDepth = 0; + /** + * @var callable[] List of callables to call just before disconnecting database + * @since CMS 3.1.2 + */ + protected $disconnectHandlers = array(); + /** * Get a list of available database connectors. The list will only be populated with connectors that both * the class exists and the static test method returns true. This gives us the ability to have a multitude @@ -464,6 +483,19 @@ public function createDatabase($options, $utf = true) */ abstract public function disconnect(); + /** + * Adds a function callable just before disconnecting the database. Parameter of the callable is $this JDatabaseDriver + * + * @param callable $callable Function to call in disconnect() method just before disconnecting from database + * @return void + * + * @since CMS 3.1.2 + */ + public function addDisconnectHandler($callable) + { + $this->disconnectHandlers[] = $callable; + } + /** * Drops a table from the database. * @@ -648,6 +680,30 @@ public function getLog() return $this->log; } + /** + * Get the database driver SQL statement log. + * + * @return array SQL statements executed by the database driver. + * + * @since CMS 3.1.2 + */ + public function getTimings() + { + return $this->timings; + } + + /** + * Get the database driver SQL statement log. + * + * @return array SQL statements executed by the database driver. + * + * @since CMS 3.1.2 + */ + public function getCallStacks() + { + return $this->callStacks; + } + /** * Get the minimum supported database version. * diff --git a/libraries/joomla/database/driver/mysql.php b/libraries/joomla/database/driver/mysql.php index b6fe969672438..dc8800a5af0c0 100644 --- a/libraries/joomla/database/driver/mysql.php +++ b/libraries/joomla/database/driver/mysql.php @@ -54,10 +54,7 @@ public function __construct($options) */ public function __destruct() { - if (is_resource($this->connection)) - { - mysql_close($this->connection); - } + $this->disconnect(); } /** @@ -98,6 +95,12 @@ public function connect() // Set charactersets (needed for MySQL 4.1.2+). $this->setUTF(); + + // Turn MySQL profiling ON in debug mode: + if ($this->debug) + { + mysqli_query($this->connection, "SET profiling = 1;"); + } } /** @@ -110,7 +113,15 @@ public function connect() public function disconnect() { // Close the connection. - mysql_close($this->connection); + if (is_resource($this->connection)) + { + foreach ($this->disconnectHandlers as $h) + { + call_user_func_array($h, array( &$this)); + } + + mysql_close($this->connection); + } $this->connection = null; } @@ -254,6 +265,10 @@ public function execute() // Increment the query counter. $this->count++; + // Reset the error values. + $this->errorNum = 0; + $this->errorMsg = ''; + // If debugging is enabled then let's log the query. if ($this->debug) { @@ -261,15 +276,19 @@ public function execute() $this->log[] = $query; JLog::add($query, JLog::DEBUG, 'databasequery'); - } - // Reset the error values. - $this->errorNum = 0; - $this->errorMsg = ''; + $this->timings[] = microtime(true); + } // Execute the query. Error suppression is used here to prevent warnings/notices that the connection has been lost. $this->cursor = @mysql_query($query, $this->connection); + if ($this->debug) + { + $this->timings[] = microtime(true); + $this->callStacks[] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + } + // If an error occurred handle it. if (!$this->cursor) { diff --git a/libraries/joomla/database/driver/mysqli.php b/libraries/joomla/database/driver/mysqli.php index 9cf5181f70678..82a6207f5be4d 100644 --- a/libraries/joomla/database/driver/mysqli.php +++ b/libraries/joomla/database/driver/mysqli.php @@ -82,10 +82,7 @@ public function __construct($options) */ public function __destruct() { - if (is_callable(array($this->connection, 'close'))) - { - mysqli_close($this->connection); - } + $this->disconnect(); } /** @@ -157,6 +154,13 @@ public function connect() // Set charactersets (needed for MySQL 4.1.2+). $this->setUTF(); + + // Turn MySQL profiling ON in debug mode: + if ($this->debug) + { + mysqli_query($this->connection, "SET profiling_history_size = 100;"); + mysqli_query($this->connection, "SET profiling = 1;"); + } } /** @@ -169,8 +173,13 @@ public function connect() public function disconnect() { // Close the connection. - if (is_callable($this->connection, 'close')) + if ($this->connection) { + foreach ($this->disconnectHandlers as $h) + { + call_user_func_array($h, array( &$this)); + } + mysqli_close($this->connection); } @@ -496,6 +505,11 @@ public function execute() // Increment the query counter. $this->count++; + // Reset the error values. + $this->errorNum = 0; + $this->errorMsg = ''; + $memoryBefore = null; + // If debugging is enabled then let's log the query. if ($this->debug) { @@ -503,15 +517,26 @@ public function execute() $this->log[] = $query; JLog::add($query, JLog::DEBUG, 'databasequery'); - } - // Reset the error values. - $this->errorNum = 0; - $this->errorMsg = ''; + $this->timings[] = microtime(true); + + if (is_object($this->cursor)) + { + $this->freeResult(); + } + $memoryBefore = memory_get_usage(); + } // Execute the query. Error suppression is used here to prevent warnings/notices that the connection has been lost. $this->cursor = @mysqli_query($this->connection, $query); + if ($this->debug) + { + $this->timings[] = microtime(true); + $this->callStacks[] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + $this->callStacks[count($this->callStacks) - 1][0]['memory'] = array($memoryBefore, memory_get_usage(), is_object($this->cursor) ? $this->getNumRows() : null); + } + // If an error occurred handle it. if (!$this->cursor) { @@ -757,6 +782,10 @@ protected function fetchObject($cursor = null, $class = 'stdClass') protected function freeResult($cursor = null) { mysqli_free_result($cursor ? $cursor : $this->cursor); + if ((! $cursor) || ($cursor === $this->cursor)) + { + $this->cursor = null; + } } /** diff --git a/libraries/joomla/database/driver/pdo.php b/libraries/joomla/database/driver/pdo.php index 91245a009bc13..15a9d8cd941ac 100644 --- a/libraries/joomla/database/driver/pdo.php +++ b/libraries/joomla/database/driver/pdo.php @@ -90,8 +90,7 @@ public function __construct($options) */ public function __destruct() { - $this->freeResult(); - unset($this->connection); + $this->disconnect(); } /** @@ -307,6 +306,11 @@ public function connect() */ public function disconnect() { + foreach ($this->disconnectHandlers as $h) + { + call_user_func_array($h, array( &$this)); + } + $this->freeResult(); unset($this->connection); } @@ -374,6 +378,10 @@ public function execute() // Increment the query counter. $this->count++; + // Reset the error values. + $this->errorNum = 0; + $this->errorMsg = ''; + // If debugging is enabled then let's log the query. if ($this->debug) { @@ -381,11 +389,9 @@ public function execute() $this->log[] = $query; JLog::add($query, JLog::DEBUG, 'databasequery'); - } - // Reset the error values. - $this->errorNum = 0; - $this->errorMsg = ''; + $this->timings[] = microtime(true); + } // Execute the query. $this->executed = false; @@ -404,6 +410,12 @@ public function execute() $this->executed = $this->prepared->execute(); } + if ($this->debug) + { + $this->timings[] = microtime(true); + $this->callStacks[] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + } + // If an error occurred handle it. if (!$this->executed) { diff --git a/libraries/joomla/database/driver/postgresql.php b/libraries/joomla/database/driver/postgresql.php index cee7acd4ac3bf..3729e64dd5c75 100644 --- a/libraries/joomla/database/driver/postgresql.php +++ b/libraries/joomla/database/driver/postgresql.php @@ -84,10 +84,7 @@ public function __construct( $options ) */ public function __destruct() { - if (is_resource($this->connection)) - { - pg_close($this->connection); - } + $this->disconnect(); } /** @@ -136,6 +133,11 @@ public function disconnect() // Close the connection. if (is_resource($this->connection)) { + foreach ($this->disconnectHandlers as $h) + { + call_user_func_array($h, array( &$this)); + } + pg_close($this->connection); } @@ -532,7 +534,7 @@ public function getVersion() * * @example with insertid() call: * $query = $this->getQuery(true) - * insert('jos_dbtest') + * ->insert('jos_dbtest') * ->columns('title,start_date,description') * ->values("'testTitle2nd','1971-01-01','testDescription2nd'"); * $this->setQuery($query); @@ -628,6 +630,10 @@ public function execute() // Increment the query counter. $this->count++; + // Reset the error values. + $this->errorNum = 0; + $this->errorMsg = ''; + // If debugging is enabled then let's log the query. if ($this->debug) { @@ -635,15 +641,19 @@ public function execute() $this->log[] = $query; JLog::add($query, JLog::DEBUG, 'databasequery'); - } - // Reset the error values. - $this->errorNum = 0; - $this->errorMsg = ''; + $this->timings[] = microtime(true); + } // Execute the query. Error suppression is used here to prevent warnings/notices that the connection has been lost. $this->cursor = @pg_query($this->connection, $query); + if ($this->debug) + { + $this->timings[] = microtime(true); + $this->callStacks[] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + } + // If an error occurred handle it. if (!$this->cursor) { diff --git a/libraries/joomla/database/driver/sqlsrv.php b/libraries/joomla/database/driver/sqlsrv.php index 64ca5bd580d31..f1e4a407bbb67 100644 --- a/libraries/joomla/database/driver/sqlsrv.php +++ b/libraries/joomla/database/driver/sqlsrv.php @@ -92,10 +92,7 @@ public function __construct($options) */ public function __destruct() { - if (is_resource($this->connection)) - { - sqlsrv_close($this->connection); - } + $this->disconnect(); } /** @@ -155,6 +152,11 @@ public function disconnect() // Close the connection. if (is_resource($this->connection)) { + foreach ($this->disconnectHandlers as $h) + { + call_user_func_array($h, array( &$this)); + } + sqlsrv_close($this->connection); } @@ -581,6 +583,10 @@ public function execute() // Increment the query counter. $this->count++; + // Reset the error values. + $this->errorNum = 0; + $this->errorMsg = ''; + // If debugging is enabled then let's log the query. if ($this->debug) { @@ -588,11 +594,9 @@ public function execute() $this->log[] = $query; JLog::add($query, JLog::DEBUG, 'databasequery'); - } - // Reset the error values. - $this->errorNum = 0; - $this->errorMsg = ''; + $this->timings[] = microtime(true); + } // SQLSrv_num_rows requires a static or keyset cursor. if (strncmp(ltrim(strtoupper($query)), 'SELECT', strlen('SELECT')) == 0) @@ -607,6 +611,12 @@ public function execute() // Execute the query. Error suppression is used here to prevent warnings/notices that the connection has been lost. $this->cursor = @sqlsrv_query($this->connection, $query, array(), $array); + if ($this->debug) + { + $this->timings[] = microtime(true); + $this->callStacks[] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + } + // If an error occurred handle it. if (!$this->cursor) { diff --git a/libraries/joomla/profiler/profiler.php b/libraries/joomla/profiler/profiler.php index edd725dd718b6..c943272f8a335 100644 --- a/libraries/joomla/profiler/profiler.php +++ b/libraries/joomla/profiler/profiler.php @@ -37,6 +37,12 @@ class JProfiler */ protected $buffer = null; + /** + * @var array The profiling messages. + * @since 12.1 + */ + protected $marks = null; + /** * @var float * @since 12.1 @@ -64,8 +70,9 @@ class JProfiler */ public function __construct($prefix = '') { - $this->start = $this->getmicrotime(); + $this->start = microtime(1); $this->prefix = $prefix; + $this->marks = array(); $this->buffer = array(); } @@ -103,21 +110,32 @@ public static function getInstance($prefix = '') */ public function mark($label) { - $current = self::getmicrotime() - $this->start; + $current = microtime(1) - $this->start; $currentMem = memory_get_usage() / 1048576; + + $m = (object) array( + 'prefix' => $this->prefix, + 'time' => ($current > $this->previousTime ? '+' : '-') . (($current - $this->previousTime) * 1000), + 'totalTime' => ($current * 1000), + 'memory' => ($currentMem > $this->previousMem ? '+' : '-') . ($currentMem - $this->previousMem), + 'totalMemory' => $currentMem, + 'label' => $label + ); + $this->marks[] = $m; + $mark = sprintf( - '%s %.3f seconds (+%.3f); %0.2f MB (%s%0.3f) - %s', - $this->prefix, - $current, - $current - $this->previousTime, - $currentMem, - ($currentMem > $this->previousMem) ? '+' : '', $currentMem - $this->previousMem, - $label + '%s %.3f seconds (%.3f); %0.2f MB (%0.3f) - %s', + $m->prefix, + $m->totalTime / 1000, + $m->time / 1000, + $m->totalMemory, + $m->memory, + $m->label ); + $this->buffer[] = $mark; $this->previousTime = $current; $this->previousMem = $currentMem; - $this->buffer[] = $mark; return $mark; } @@ -128,6 +146,7 @@ public function mark($label) * @return float The current time * * @since 11.1 + * @deprecated 12.3 (Platform) & 4.0 (CMS) - Use PHP's microtime(1) */ public static function getmicrotime() { @@ -154,6 +173,19 @@ public function getMemory() * Get all profiler marks. * * Returns an array of all marks created since the Profiler object + * was instantiated. Marks are objects as per {@link JProfiler::mark()}. + * + * @return array Array of profiler marks + */ + public function getMarks() + { + return $this->marks; + } + + /** + * Get all profiler mark buffers. + * + * Returns an array of all mark buffers created since the Profiler object * was instantiated. Marks are strings as per {@link JProfiler::mark()}. * * @return array Array of profiler marks diff --git a/media/cms/css/debug.css b/media/cms/css/debug.css index dfbf4e0b4411b..627b99627fb7c 100644 --- a/media/cms/css/debug.css +++ b/media/cms/css/debug.css @@ -12,11 +12,10 @@ div#system-debug { background-color: #fff; color: #000; border: 1px dashed silver; - padding: 5px; - text-align: left; + padding: 10px; } -#system-debug div.dbgHeader { +#system-debug div.dbg-header { background-color: #ddd; border: 1px solid #eee; font-size: 16px; @@ -35,14 +34,8 @@ div#system-debug { margin: 0px; } -#system-debug a:hover, -#system-debug a:focus, -#system-debug a:active, -#system-debug a:link, -#system-debug a:visited { - background-color: #ddd; - color: #000; - text-decoration:none; +#system-debug .dbg-error a h3 { + background-color: red; } #system-debug a:hover h3, @@ -51,24 +44,24 @@ div#system-debug { color: #ddd; font-size: 14px; cursor: pointer; - text-decoration:none; + text-decoration: none; } -#system-debug div.dbgContainer { +#system-debug div.dbg-container { padding: 10px; } -#system-debug span.dbgCommand { +#system-debug span.dbg-command { color: blue; font-weight: bold; } -#system-debug span.dbgTable { +#system-debug span.dbg-table { color: green; font-weight: bold; } -#system-debug b.dbgOperator { +#system-debug b.dbg-operator { color: red; font-weight: bold; } @@ -84,13 +77,13 @@ div#system-debug { #system-debug h4 { font-size: 14px; - font-weight:bold; + font-weight: bold; margin: 5px 0 0 0; } #system-debug h5 { font-size: 13px; - font-weight:bold; + font-weight: bold; margin: 5px 0 0 0; } @@ -126,125 +119,67 @@ div#system-debug { font-size: 13px; } - -/* Common CSS for system debug */ - -#system-debug { - background-color: #fff; - color: #000; - border: 1px dashed silver; - padding: 10px; -} - -#system-debug div.dbgHeader { - background-color: #ddd; - border: 1px solid #eee; - font-size: 16px; -} - -#system-debug h3 { - margin: 0; -} - -#system-debug a h3 { - background-color: #ddd; - color: #000; - font-size: 14px; - padding: 5px; - text-decoration: none; - margin: 0px; -} - -#system-debug .dbgerror a h3 { +#system-debug div.dbg-header.dbg-error { background-color: red; } - -#system-debug a:hover, -#system-debug a:focus, -#system-debug a:active, -#system-debug a:link, -#system-debug a:visited { - background-color: #ddd; - color: #000; - text-decoration:none; -} - -#system-debug a:hover h3, -#system-debug a:focus h3 { - background-color: #4d4d4d; - color: #ddd; - font-size: 14px; - cursor: pointer; - text-decoration:none; -} - -#system-debug div.dbgContainer { - padding: 10px; -} - -#system-debug span.dbgCommand { - color: blue; - font-weight: bold; -} - -#system-debug span.dbgTable { - color: green; - font-weight: bold; -} - -#system-debug b.dbgOperator { +#system-debug .dbg-warning { color: red; font-weight: bold; + background-color: #ffffcc !important; } -#system-debug h1 { - background-color: #2c2c2c; - color: #fff; - padding: 10px; - margin: 0; - font-size: 16px; - line-height: 1em; +#system-debug .accordion { + margin-bottom: 0; } - -#system-debug h4 { - font-size: 14px; - font-weight:bold; +#system-debug .dbg-noprofile { + text-decoration: line-through; } -#system-debug h5 { - font-size: 13px; - font-weight:bold; - margin-top: 5px; +/* dbg-bars */ +#system-debug .alert, +#system-debug .dbg-bars { + margin-bottom: 10px; } - -div#system-debug { - margin: 5px; +#system-debug .dbg-bar-spacer { + float: left; + height: 100%; } - -#system-debug ol { - margin-left: 25px; - margin-right: 25px; +/* dbg-bars-query */ +#system-debug .dbg-bars-query .dbg-bar { + opacity: 0.3; + height: 12px; + margin-top: 3px; } - -#system-debug ul { - list-style: none; +#system-debug .dbg-bars-query:hover .dbg-bar { + opacity: 0.6; + height: 18px; + margin-top: 0; } - -#system-debug li { - font-size: 13px; - margin-bottom: 10px; +#system-debug .dbg-bars-query .dbg-bar:hover, +#system-debug .dbg-bars-query .dbg-bar-active, +#system-debug .dbg-bars-query:hover .dbg-bar-active { + opacity: 1; + height: 18px; + margin-top: 0; } -#system-debug code { - font-size: 13px; - text-align: left; - direction: ltr; +/* dbg-query-table */ +#system-debug table.dbg-query-table { + margin: 0px 0px 6px; } - -#system-debug p { - font-size: 13px; +#system-debug table.dbg-query-table th, +#system-debug table.dbg-query-table td { + padding: 3px 8px; } -#system-debug div.dbgHeader.dbgerror { - background-color: red; +#system-debug .dbg-profile-list .label { + display: inline-block; + min-width: 60px; + text-align: right; } + +#system-debug .dbg-query-memory, +#system-debug .dbg-query-rowsnumber +{ + margin-left: 50px; +} \ No newline at end of file diff --git a/plugins/system/debug/debug.php b/plugins/system/debug/debug.php index 1c22fd55a86bf..7873088edec98 100644 --- a/plugins/system/debug/debug.php +++ b/plugins/system/debug/debug.php @@ -36,6 +36,30 @@ class PlgSystemDebug extends JPlugin */ private $logEntries = array(); + /** + * Holds SHOW PROFILES of queries + * + * @var array + * @since CMS 3.1.2 + */ + private $sqlShowProfiles = array(); + + /** + * Holds all SHOW PROFILE FOR QUERY n, indexed by n-1 + * + * @var array + * @since CMS 3.1.2 + */ + private $sqlShowProfileEach = array(); + + /** + * Holds all EXPLAIN EXTENDED for all queries + * + * @var array + * @since CMS 3.1.2 + */ + private $explains = array(); + /** * Constructor. * @@ -72,7 +96,7 @@ public function __construct(&$subject, $config) foreach ($this->params->get('log_priorities', array()) as $p) { - $const = 'JLog::'.strtoupper($p); + $const = 'JLog::' . strtoupper($p); if (!defined($const)) { @@ -88,6 +112,10 @@ public function __construct(&$subject, $config) JLog::addLogger(array('logger' => 'callback', 'callback' => array($this, 'logger')), $priority, $categories, $mode); } + + // Prepare disconnect-handler for SQL profiling: + $db = JFactory::getDbo(); + $db->addDisconnectHandler(array($this, 'mysqlDisconnectHandler')); } /** @@ -105,6 +133,13 @@ public function onAfterDispatch() { JHtml::_('stylesheet', 'cms/debug.css', array(), true); } + + // Only if debugging is enabled for SQL queries popovers + if (JDEBUG && $this->isAuthorisedDisplayDebug()) + { + JHtml::_('bootstrap.tooltip'); + JHtml::_('bootstrap.popover', '.hasPopover', array('placement' => 'top')); + } } /** @@ -142,7 +177,8 @@ public function __destruct() // No debug for Safari and Chrome redirection if (strstr(strtolower($_SERVER['HTTP_USER_AGENT']), 'webkit') !== false - && substr($contents, 0, 50) == 'function toggleContainer(name) { var e = document.getElementById(name);// MooTools might not be available ;) e.style.display = (e.style.display == 'none') ? 'block' : 'none'; }"; - $html .= '
'; + $html[] = '
'; - $html .= '

' . JText::_('PLG_DEBUG_TITLE') . '

'; + $html[] = '

' . JText::_('PLG_DEBUG_TITLE') . '

'; if (JDEBUG) { if (JError::getErrors()) { - $html .= $this->display('errors'); + $html[] = $this->display('errors'); } - $html .= $this->display('session'); + $html[] = $this->display('session'); if ($this->params->get('profile', 1)) { - $html .= $this->display('profile_information'); + $html[] = $this->display('profile_information'); } if ($this->params->get('memory', 1)) { - $html .= $this->display('memory_usage'); + $html[] = $this->display('memory_usage'); } if ($this->params->get('queries', 1)) { - $html .= $this->display('queries'); + $html[] = $this->display('queries'); } if ($this->params->get('logs', 1) && !empty($this->logEntries)) { - $html .= $this->display('logs'); + $html[] = $this->display('logs'); } } @@ -199,23 +235,23 @@ public function __destruct() if ($this->params->get('language_errorfiles', 1)) { $languageErrors = JFactory::getLanguage()->getErrorFiles(); - $html .= $this->display('language_files_in_error', $languageErrors); + $html[] = $this->display('language_files_in_error', $languageErrors); } if ($this->params->get('language_files', 1)) { - $html .= $this->display('language_files_loaded'); + $html[] = $this->display('language_files_loaded'); } if ($this->params->get('language_strings')) { - $html .= $this->display('untranslated_strings'); + $html[] = $this->display('untranslated_strings'); } } - $html .= '
'; + $html[] = '
'; - echo str_replace('', $html . '', $contents); + echo str_replace('', implode('', $html) . '', $contents); } /** @@ -270,7 +306,7 @@ protected function display($item, array $errors = array()) if (count($errors)) { - $status = ' dbgerror'; + $status = ' dbg-error'; } $fncName = 'display' . ucfirst(str_replace('_', '', $item)); @@ -282,20 +318,20 @@ protected function display($item, array $errors = array()) $html = ''; - $js = "toggleContainer('dbgContainer" . $item . "');"; + $js = "toggleContainer('dbg_container_" . $item . "');"; - $class = 'dbgHeader' . $status; + $class = 'dbg-header' . $status; - $html .= '

' . $title . '

'; + $html[] = '

' . $title . '

'; // @todo set with js.. ? $style = ' style="display: none;"'; - $html .= '
'; - $html .= $this->$fncName(); - $html .= '
'; + $html[] = '
'; + $html[] = $this->$fncName(); + $html[] = '
'; - return $html; + return implode('', $html); } /** @@ -318,12 +354,12 @@ protected function displaySession($key = '', $session = null, $id = 0) $session = $_SESSION; } - static $html = ''; + $html = array(); static $id; if (!is_array($session)) { - $html .= $key . ' ⇒' . $session . PHP_EOL; + $html[] = $key . ' ⇒' . $session . PHP_EOL; } else { @@ -349,20 +385,20 @@ protected function displaySession($key = '', $session = null, $id = 0) if (!$display) { - $js = "toggleContainer('dbgContainer_session" . $id . "');"; + $js = "toggleContainer('dbg_container_session" . $id . '_' . $sKey . "');"; - $html .= '

' . $sKey . '

'; + $html[] = '

' . $sKey . '

'; // @todo set with js.. ? $style = ' style="display: none;"'; - $html .= '
'; - $id ++; + $html[] = '
'; + $id++; // Recurse... $this->displaySession($sKey, $entries, $id); - $html .= '
'; + $html[] = '
'; continue; } @@ -374,14 +410,14 @@ protected function displaySession($key = '', $session = null, $id = 0) if (is_string($entries)) { - $html .= ''; - $html .= $sKey . ' ⇒ ' . $entries . '
'; - $html .= '
'; + $html[] = ''; + $html[] = $sKey . ' ⇒ ' . $entries . '
'; + $html[] = '
'; } } } - return $html; + return implode('', $html); } /** @@ -393,31 +429,31 @@ protected function displaySession($key = '', $session = null, $id = 0) */ protected function displayErrors() { - $html = ''; + $html = array(); - $html .= '
    '; + $html[] = '
      '; while ($error = JError::getError(true)) { $col = (E_WARNING == $error->get('level')) ? 'red' : 'orange'; - $html .= '
    1. '; - $html .= '' . $error->getMessage() . '
      '; + $html[] = '
    2. '; + $html[] = '' . $error->getMessage() . '
      '; $info = $error->get('info'); if ($info) { - $html .= '
      ' . print_r($info, true) . '

      '; + $html[] = '
      ' . print_r($info, true) . '

      '; } - $html .= $this->renderBacktrace($error); - $html .= '
    3. '; + $html[] = $this->renderBacktrace($error); + $html[] = ''; } - $html .= '
    '; + $html[] = '
'; - return $html; + return implode('', $html); } /** @@ -429,14 +465,133 @@ protected function displayErrors() */ protected function displayProfileInformation() { - $html = ''; + $html = array(); + + $htmlMarks = array(); - foreach (JProfiler::getInstance('Application')->getBuffer() as $mark) + $totalTime = 0; + $totalMem = 0; + $marks = array(); + foreach (JProfiler::getInstance('Application')->getMarks() as $mark) { - $html .= '
' . $mark . '
'; + $totalTime += $mark->time; + $totalMem += $mark->memory; + $htmlMark = sprintf( + JText::_('PLG_DEBUG_TIME') . ': %.1f ms / %.1f ms' + . ' ' . JText::_('PLG_DEBUG_MEMORY') . ': %0.3f MB / %0.2f MB' + . ' %s: %s', + $mark->time, + $mark->totalTime, + $mark->memory, + $mark->totalMemory, + $mark->prefix, + $mark->label + ); + $marks[] = (object) array( + 'time' => $mark->time, + 'memory' => $mark->memory, + 'html' => $htmlMark, + 'tip' => $mark->label + ); } + $avgTime = $totalTime / count($marks); + $avgMem = $totalMem / count($marks); - return $html; + foreach ($marks as $mark) + { + if ($mark->time > $avgTime * 1.5) + { + $barClass = 'bar-danger'; + $labelClass = 'label-important'; + } + else if ($mark->time < $avgTime / 1.5) + { + $barClass = 'bar-success'; + $labelClass = 'label-success'; + } + else + { + $barClass = 'bar-warning'; + $labelClass = 'label-warning'; + } + + if ($mark->memory > $avgMem * 1.5) + { + $barClassMem = 'bar-danger'; + $labelClassMem = 'label-important'; + } + else if ($mark->memory < $avgMem / 1.5) + { + $barClassMem = 'bar-success'; + $labelClassMem = 'label-success'; + } + else + { + $barClassMem = 'bar-warning'; + $labelClassMem = 'label-warning'; + } + $bars[] = (object) array( + 'width' => round($mark->time / ($totalTime / 100), 4), + 'class' => $barClass, + 'tip' => $mark->tip + ); + $barsMem[] = (object) array( + 'width' => round($mark->memory / ($totalMem / 100), 4), + 'class' => $barClassMem, + 'tip' => $mark->tip + ); + $htmlMarks[] = '
' . str_replace('label-time', $labelClass, str_replace('label-memory', $labelClassMem, $mark->html)) . '
'; + } + $html[] = '

' . JText::_('PLG_DEBUG_TIME') . '

'; + $html[] = $this->renderBars($bars, 'profile'); + $html[] = '

' . JText::_('PLG_DEBUG_MEMORY') . '

'; + $html[] = $this->renderBars($barsMem, 'profile'); + + $html[] = '
' . implode('', $htmlMarks) . '
'; + + $db = JFactory::getDbo(); + + $log = $db->getLog(); + if ($log) + { + $timings = $db->getTimings(); + if ($timings) + { + $totalQueryTime = 0.0; + $lastStart = null; + foreach ($timings as $k => $v) + { + if (!($k % 2)) + { + $lastStart = $v; + } + else + { + $totalQueryTime += $v - $lastStart; + } + } + $totalQueryTime = $totalQueryTime * 1000; + + if ($totalQueryTime > ($totalTime * 0.25)) + { + $labelClass = 'label-important'; + } + else if ($totalQueryTime < ($totalTime * 0.15)) + { + $labelClass = 'label-success'; + } + else + { + $labelClass = 'label-warning'; + } + + $html[] = '
' . JText::sprintf( + 'PLG_DEBUG_QUERIES_TIME', + sprintf('%.1f ms', $totalQueryTime) + ) . '
'; + } + } + return implode('', $html); } /** @@ -450,12 +605,8 @@ protected function displayMemoryUsage() { $bytes = memory_get_usage(); - $html = ''; - $html .= JHtml::_('number.bytes', $bytes); - $html .= ' (' . number_format($bytes) . ' Bytes)'; - $html .= ''; - - return $html; + return '' . JHtml::_('number.bytes', $bytes) . '' + . ' (' . number_format($bytes) . ' ' . JText::_('PLG_DEBUG_BYTES') . ')'; } /** @@ -467,25 +618,176 @@ protected function displayMemoryUsage() */ protected function displayQueries() { - $db = JFactory::getDbo(); + $db = JFactory::getDbo(); $log = $db->getLog(); - if ( ! $log) + if (!$log) { - return; + return null; } - $html = ''; - - $html .= '

' . JText::sprintf('PLG_DEBUG_QUERIES_LOGGED', $db->getCount()) . '

'; + $timings = $db->getTimings(); + $callStacks = $db->getCallStacks(); - $html .= '
    '; + $db->setDebug(false); $selectQueryTypeTicker = array(); $otherQueryTypeTicker = array(); - foreach ($log as $query) + $timing = array(); + $maxtime = 0; + + if (isset($timings[0])) + { + $startTime = $timings[0]; + $endTime = $timings[count($timings) - 1]; + $totalBargraphTime = $endTime - $startTime; + if ($totalBargraphTime > 0) + { + + foreach ($log as $id => $query) + { + if (isset($timings[$id * 2 + 1])) + { + // Compute the query time: $timing[$k] = array( queryTime, timeBetweenQueries ): + $timing[$id] = array(($timings[$id * 2 + 1] - $timings[$id * 2]) * 1000, $id > 0 ? ($timings[$id * 2] - $timings[$id * 2 - 1]) * 1000 : 0); + $maxtime = max($maxtime, $timing[$id]['0']); + } + } + } + } + else + { + $startTime = null; + $totalBargraphTime = 1; + } + + $bars = array(); + $info = array(); + $totalQueryTime = 0; + $duplicates = array(); + + foreach ($log as $id => $query) + { + $did = md5($query); + if (!isset($duplicates[$did])) + { + $duplicates[$did] = array(); + } + $duplicates[$did][] = $id; + + if ($timings && isset($timings[$id * 2 + 1])) + { + // Compute the query time: + $queryTime = ($timings[$id * 2 + 1] - $timings[$id * 2]) * 1000; + $totalQueryTime += $queryTime; + + // Run an EXPLAIN EXTENDED query on the SQL query if possible: + $hasWarnings = false; + $hasWarningsInProfile = false; + + if (isset($this->explains[$id])) + { + $explain = $this->tableToHtml($this->explains[$id], $hasWarnings); + } + else + { + $explain = JText::sprintf('PLG_DEBUG_QUERY_EXPLAIN_NOT_POSSIBLE', htmlspecialchars($query)); + } + + // Run a SHOW PROFILE query: + $profile = ''; + if (in_array($db->name, array('mysqli', 'mysql'))) + { + if (isset($this->sqlShowProfileEach[$id])) + { + $profileTable = $this->sqlShowProfileEach[$id]; + $profile = $this->tableToHtml($profileTable, $hasWarningsInProfile); + } + } + + $ratio = 0.5; // how heavy should the string length count: 0 - 1 + $timeScore = $queryTime / (strlen($query) * $ratio) * 200; + + // Determine color of bargraph depending on query speed and presence of warnings in EXPLAIN: + if ($timeScore > 10) + { + $barClass = 'bar-danger'; + $labelClass = 'label-important'; + } + else if ($hasWarnings || $timeScore > 5) + { + $barClass = 'bar-warning'; + $labelClass = 'label-warning'; + } + else + { + $barClass = 'bar-success'; + $labelClass = 'label-success'; + } + + // Computes bargraph as follows: Position begin and end of the bar relatively to whole execution time: + $prevBar = ($id && isset($bars[$id - 1])) ? $bars[$id - 1] : 0; + + $barPre = round($timing[$id][1] / ($totalBargraphTime * 10), 4); + $barWidth = round($timing[$id][0] / ($totalBargraphTime * 10), 4); + $minWidth = 0.3; + if ($barWidth < $minWidth) + { + $barPre -= ($minWidth - $barWidth); + if ($barPre < 0) + { + $minWidth += $barPre; + $barPre = 0; + } + $barWidth = $minWidth; + } + + $bars[$id] = (object) array( + 'class' => $barClass, + 'width' => $barWidth, + 'pre' => $barPre, + 'tip' => sprintf('%.2f ms', $queryTime) + ); + $info[$id] = (object) array( + 'class' => $labelClass, + 'explain' => $explain, + 'profile' => $profile, + 'hasWarnings' => $hasWarnings + ); + } + } + // remove single queries from $duplicates + $total_duplicates = 0; + foreach ($duplicates as $did => $dups) + { + if (count($dups) < 2) + { + unset($duplicates[$did]); + } + else + { + $total_duplicates += count($dups); + } + } + + // Fix first bar width + $minWidth = 0.3; + if ($bars[0]->width < $minWidth && isset($bars[1])) + { + $bars[1]->pre -= ($minWidth - $bars[0]->width); + if ($bars[1]->pre < 0) + { + $minWidth += $bars[1]->pre; + $bars[1]->pre = 0; + } + $bars[0]->width = $minWidth; + } + + $memoryUsageNow = memory_get_usage(); + $list = array(); + foreach ($log as $id => $query) { // Start Query Type Ticker Additions $fromStart = stripos($query, 'from'); @@ -531,14 +833,199 @@ protected function displayQueries() $text = $this->highlightQuery($query); - $html .= '
  1. ' . $text . '
  2. '; + if ($timings && isset($timings[$id * 2 + 1])) + { + // Compute the query time: + $queryTime = ($timings[$id * 2 + 1] - $timings[$id * 2]) * 1000; + + // Timing + // Formats the output for the query time with EXPLAIN query results as tooltip: + $htmlTiming = '
    ' . JText::sprintf('PLG_DEBUG_QUERY_TIME', sprintf('%.2f ms', $timing[$id]['0'])); + + if ($timing[$id]['1']) + { + $htmlTiming .= ' ' . JText::sprintf('PLG_DEBUG_QUERY_AFTER_LAST', sprintf('%.2f ms', $timing[$id]['1'])); + } + + if (isset($callStacks[$id][0]['memory'])) + { + $memoryUsed = $callStacks[$id][0]['memory'][1] - $callStacks[$id][0]['memory'][0]; + $memoryBeforeQuery = $callStacks[$id][0]['memory'][0]; + + // Determine color of query memory usage: + if ($memoryUsed > 0.1 * $memoryUsageNow) + { + $labelClass = 'label-important'; + } + elseif ($memoryUsed > 0.05 * $memoryUsageNow) + { + $labelClass = 'label-warning'; + } + else + { + $labelClass = 'label-success'; + } + $htmlTiming .= ' ' . '' . JText::sprintf('PLG_DEBUG_MEMORY_USED_FOR_QUERY', + sprintf('%.3f MB', $memoryUsed / 1048576), + sprintf('%.3f MB', $memoryBeforeQuery / 1048576)) + . ''; + + if ($callStacks[$id][0]['memory'][2] !== null) + { + // Determine color of number or results: + $resultsReturned = $callStacks[$id][0]['memory'][2]; + if ($resultsReturned > 3000) + { + $labelClass = 'label-important'; + } + elseif ($resultsReturned > 1000) + { + $labelClass = 'label-warning'; + } + elseif ($resultsReturned == 0) + { + $labelClass = ''; + } + else + { + $labelClass = 'label-success'; + } + $htmlResultsReturned = '' . (int) $resultsReturned . ''; + $htmlTiming .= ' ' . '' . JText::sprintf('PLG_DEBUG_ROWS_RETURNED_BY_QUERY', $htmlResultsReturned) . ''; + } + } + + $htmlTiming .= '
    '; + + // Bar + $htmlBar = $this->renderBars($bars, 'query', $id); + + // Profile Query + $title = JText::_('PLG_DEBUG_PROFILE'); + if (!$info[$id]->profile) + { + $title = '' . $title . ''; + } + $htmlProfile = ($info[$id]->profile ? $info[$id]->profile : JText::_('PLG_DEBUG_NO_PROFILE')); + + // Backtrace/Called + $htmlCallStack = ''; + if (isset($callStacks[$id])) + { + $htmlCallStackElements = array(); + foreach ($callStacks[$id] as $functionCall) + { + if (isset($functionCall['file']) && isset($functionCall['line']) && (strpos($functionCall['file'], '/libraries/joomla/database/') === false)) + { + $htmlFile = htmlspecialchars($functionCall['file']); + $htmlLine = htmlspecialchars($functionCall['line']); + $htmlCallStackElements[] = '' . $this->formatLink($htmlFile, $htmlLine) . ''; + } + } + $htmlCallStack = '
    ' . implode('
    ', $htmlCallStackElements) . '
    '; + if (!$this->linkFormat) + { + $htmlCallStack .= '
    [' . JText::_('PLG_DEBUG_LINK_FORMAT') . ']
    '; + } + } + + $htmlAccordions = JHtml::_( + 'bootstrap.startAccordion', 'dbg_query_' . $id, array( + 'active' => ($info[$id]->hasWarnings ? ('dbg_query_explain_' . $id) : '') + ) + ); + + $htmlAccordions .= JHtml::_('bootstrap.addSlide', 'dbg_query_' . $id, JText::_('PLG_DEBUG_EXPLAIN'), 'dbg_query_explain_' . $id) + . $info[$id]->explain + . JHtml::_('bootstrap.endSlide'); + + $htmlAccordions .= JHtml::_('bootstrap.addSlide', 'dbg_query_' . $id, $title, 'dbg_query_profile_' . $id) + . $htmlProfile + . JHtml::_('bootstrap.endSlide'); + + if ($htmlCallStack) + { + $htmlAccordions .= JHtml::_('bootstrap.addSlide', 'dbg_query_' . $id, JText::_('PLG_DEBUG_CALL_STACK'), 'dbg_query_callstack_' . $id) + . $htmlCallStack + . JHtml::_('bootstrap.endSlide'); + } + $htmlAccordions .= JHtml::_('bootstrap.endAccordion'); + + $did = md5($query); + if (isset($duplicates[$did])) + { + $dups = array(); + foreach ($duplicates[$did] as $dup) + { + if ($dup != $id) + { + $dups[] = '#' . ($dup + 1) . ''; + } + } + $htmlQuery = '
    ' . JText::_('PLG_DEBUG_QUERY_DUPLICATES') . ': ' . implode('  ', $dups) . '
    ' + . '
    ' . $text . '
    '; + } + else + { + $htmlQuery = '
    ' . $text . '
    '; + } + + $list[] = '' + . $htmlTiming + . $htmlBar + . $htmlQuery + . $htmlAccordions; + } + else + { + $list[] = '
    ' . $text . '
    '; + } + } + + $totalTime = 0; + foreach (JProfiler::getInstance('Application')->getMarks() as $mark) + { + $totalTime += $mark->time; + } + if ($totalQueryTime > ($totalTime * 0.25)) + { + $labelClass = 'label-important'; + } + else if ($totalQueryTime < ($totalTime * 0.15)) + { + $labelClass = 'label-success'; + } + else + { + $labelClass = 'label-warning'; + } + + $html = array(); + + $html[] = '

    ' . JText::sprintf('PLG_DEBUG_QUERIES_LOGGED', $db->getCount()) + . sprintf(' %.1f ms', ($totalQueryTime)) . '


    '; + + if ($total_duplicates) + { + $html[] = '
    ' + . '

    ' . JText::sprintf('PLG_DEBUG_QUERY_DUPLICATES_TOTAL_NUMBER', $total_duplicates) . '

    '; + foreach ($duplicates as $dups) + { + $links = array(); + foreach ($dups as $dup) + { + $links[] = '#' . ($dup + 1) . ''; + } + $html[] = '
    ' . JText::sprintf('PLG_DEBUG_QUERY_DUPLICATES_NUMBER', count($links)) . ': ' . implode('  ', $links) . '
    '; + } + $html[] = '
    '; } - $html .= '
'; + $html[] = '
  1. ' . implode('
  2. ', $list) . '
'; if (!$this->params->get('query_types', 1)) { - return $html; + return implode('', $html); } // Get the totals for the query types: @@ -546,44 +1033,222 @@ protected function displayQueries() $totalOtherQueryTypes = count($otherQueryTypeTicker); $totalQueryTypes = $totalSelectQueryTypes + $totalOtherQueryTypes; - $html .= '

' . JText::sprintf('PLG_DEBUG_QUERY_TYPES_LOGGED', $totalQueryTypes) . '

'; + $html[] = '

' . JText::sprintf('PLG_DEBUG_QUERY_TYPES_LOGGED', $totalQueryTypes) . '

'; if ($totalSelectQueryTypes) { - $html .= '
' . JText::sprintf('PLG_DEBUG_SELECT_QUERIES') . '
'; + $html[] = '
' . JText::_('PLG_DEBUG_SELECT_QUERIES') . '
'; arsort($selectQueryTypeTicker); - $html .= '
    '; - + $list = array(); foreach ($selectQueryTypeTicker as $query => $occurrences) { - $html .= '
  1. ' - . JText::sprintf('PLG_DEBUG_QUERY_TYPE_AND_OCCURRENCES', $this->highlightQuery($query), $occurrences) - . '
  2. '; + $list[] = '
    '
    +					. JText::sprintf('PLG_DEBUG_QUERY_TYPE_AND_OCCURRENCES', $this->highlightQuery($query), $occurrences)
    +					. '
    '; } - $html .= '
'; + $html[] = '
  1. ' . implode('
  2. ', $list) . '
'; } if ($totalOtherQueryTypes) { - $html .= '
' . JText::sprintf('PLG_DEBUG_OTHER_QUERIES') . '
'; + $html[] = '
' . JText::_('PLG_DEBUG_OTHER_QUERIES') . '
'; arsort($otherQueryTypeTicker); - $html .= '
    '; - + $list = array(); foreach ($otherQueryTypeTicker as $query => $occurrences) { - $html .= '
  1. ' - . JText::sprintf('PLG_DEBUG_QUERY_TYPE_AND_OCCURRENCES', $this->highlightQuery($query), $occurrences) - . '
  2. '; + $list[] = '
    '
    +					. JText::sprintf('PLG_DEBUG_QUERY_TYPE_AND_OCCURRENCES', $this->highlightQuery($query), $occurrences)
    +					. '
    '; } - $html .= '
'; + $html[] = '
  1. ' . implode('
  2. ', $list) . '
'; } - return $html; + return implode('', $html); + } + + /** + * @param int $id Id if the bar to highlight + * @param array $bars Array of bar data + * + * @return string + * + * @since CMS 3.1.2 + */ + protected function renderBars(&$bars, $class = '', $id = '') + { + $html = array(); + foreach ($bars as $i => $bar) + { + if (isset($bar->pre) && $bar->pre) + { + $html[] = '
'; + } + $barClass = trim('bar dbg-bar ' . (isset($bar->class) ? $bar->class : '')); + if ($id != '' && $i == $id) + { + $barClass .= ' dbg-bar-active'; + } + $tip = ''; + if (isset($bar->tip) && $bar->tip) + { + $barClass .= ' hasTooltip'; + $tip = JHtml::tooltipText($bar->tip, '', 0); + } + + $html[] = ''; + } + return '
' . implode('', $html) . '
'; + } + + /** + * @param array $table + * @param boolean $hasWarnings Changes value to true if warnings are displayed, otherwise untouched + * + * @return string + * + * @since CMS 3.1.2 + */ + protected function tableToHtml($table, &$hasWarnings) + { + + if (!$table) + { + return null; + } + + $html = array(); + + $html[] = ''; + foreach (array_keys($table[0]) as $k) + { + $html[] = ''; + } + $html[] = ''; + + $durations = array(); + foreach ($table as $tr) + { + if (isset($tr['Duration'])) + { + $durations[] = $tr['Duration']; + } + } + rsort($durations, SORT_NUMERIC); + + foreach ($table as $tr) + { + $html[] = ''; + foreach ($tr as $k => $td) + { + if ($td === null) + { + // Display null's as 'NULL': + $td = 'NULL'; + } + + // Treat special columns: + if ($k == 'Duration') + { + if ($td >= 0.001 && ($td == $durations[0] || (isset($durations[1]) && $td == $durations[1]))) + { + // Duration column with duration value of more than 1 ms and within 2 top duration in SQL engine: Highlight warning: + $html[] = ''; + } + $html[] = ''; + } + $html[] = '
' . htmlspecialchars($k) . '
'; + $hasWarnings = true; + } + else + { + $html[] = ''; + } + // Display duration in ms with the unit instead of seconds: + $html[] = sprintf('%.1f ms', $td * 1000); + } + elseif ($k == 'key') + { + if ($td === 'NULL') + { + // Displays query parts which don't use a key with warning: + $html[] = '' . '' . JText::_('PLG_DEBUG_WARNING_NO_INDEX') . '' . ''; + $hasWarnings = true; + } + else + { + $html[] = '' . htmlspecialchars($td) . ''; + } + } + elseif ($k == 'Extra') + { + $htmlTd = htmlspecialchars($td); + // Replace spaces with nbsp for less tall tables displayed: + $htmlTd = preg_replace('/([^;]) /', '\1 ', $htmlTd); + // Displays warnings for "Using filesort": + $htmlTdWithWarnings = str_replace('Using filesort', '' . JText::_('PLG_DEBUG_WARNING_USING_FILESORT') . '', $htmlTd); + if ($htmlTdWithWarnings !== $htmlTd) + { + $hasWarnings = true; + } + + $html[] = '' . $htmlTdWithWarnings; + } + else + { + $html[] = '' . htmlspecialchars($td); + } + $html[] = '
'; + + return implode('', $html); + } + + /** + * Disconnect-handler for database to collect profiling and explain information + * + * @since CMS 3.1.2 + * + * @param JDatabaseDriver $db + * + * @return void + */ + public function mysqlDisconnectHandler(&$db) + { + $db->setDebug(false); + + $dbVersion5037 = (strncmp($db->name, 'mysql', 5) == 0) && version_compare($db->getVersion(), '5.0.37', '>='); + if ($dbVersion5037) + { + // Run a SHOW PROFILE query: + $db->setQuery('SHOW PROFILES'); //SHOW PROFILE ALL FOR QUERY ' . (int) ($k+1)); + $this->sqlShowProfiles = $db->loadAssocList(); + if ($this->sqlShowProfiles) + { + foreach ($this->sqlShowProfiles as $qn) + { + $db->setQuery('SHOW PROFILE FOR QUERY ' . (int) ($qn['Query_ID'])); + $this->sqlShowProfileEach[(int) ($qn['Query_ID'] - 1)] = $db->loadAssocList(); + } + } + } + + if (in_array($db->name, array('mysqli', 'mysql', 'postgresql'))) + { + $log = $db->getLog(); + foreach ($log as $k => $query) + { + $dbVersion56 = (strncmp($db->name, 'mysql', 5) == 0) && version_compare($db->getVersion(), '5.6', '>='); + if ((stripos($query, 'select') === 0) || ($dbVersion56 && ((stripos($query, 'delete') === 0) || (stripos($query, 'update') === 0)))) + { + $db->setQuery('EXPLAIN ' . ($dbVersion56 ? 'EXTENDED ' : '') . $query); + $this->explains[$k] = $db->loadAssocList(); + } + } + } } /** @@ -595,27 +1260,25 @@ protected function displayQueries() */ protected function displayLanguageFilesInError() { - $html = ''; - $errorfiles = JFactory::getLanguage()->getErrorFiles(); if (!count($errorfiles)) { - $html .= '

' . JText::_('JNONE') . '

'; - - return $html; + return '

' . JText::_('JNONE') . '

'; } - $html .= ''; - return $html; + return implode('', $html); } /** @@ -627,29 +1290,29 @@ protected function displayLanguageFilesInError() */ protected function displayLanguageFilesLoaded() { - $html = ''; + $html = array(); - $html .= ''; - return $html; + return implode('', $html); } /** @@ -661,19 +1324,15 @@ protected function displayLanguageFilesLoaded() */ protected function displayUntranslatedStrings() { - $stripFirst = $this->params->get('strip-first'); - $stripPref = $this->params->get('strip-prefix'); - $stripSuff = $this->params->get('strip-suffix'); + $stripFirst = $this->params->get('strip-first'); + $stripPref = $this->params->get('strip-prefix'); + $stripSuff = $this->params->get('strip-suffix'); $orphans = JFactory::getLanguage()->getOrphans(); - $html = ''; - - if ( ! count($orphans)) + if (!count($orphans)) { - $html .= '

' . JText::_('JNONE') . '

'; - - return $html; + return '

' . JText::_('JNONE') . '

'; } ksort($orphans, SORT_STRING); @@ -696,9 +1355,9 @@ protected function displayUntranslatedStrings() if (($pos = strpos($info['string'], '=')) > 0) { - $parts = explode('=', $info['string']); - $key = $parts[0]; - $guess = $parts[1]; + $parts = explode('=', $info['string']); + $key = $parts[0]; + $guess = $parts[1]; } else { @@ -736,13 +1395,14 @@ protected function displayUntranslatedStrings() } } + $html = array(); foreach ($guesses as $file => $keys) { - $html .= "\n\n# " . ($file ? $this->formatLink($file) : JText::_('PLG_DEBUG_UNKNOWN_FILE')) . "\n\n"; - $html .= implode("\n", $keys); + $html[] = "\n\n# " . ($file ? $this->formatLink($file) : JText::_('PLG_DEBUG_UNKNOWN_FILE')) . "\n\n"; + $html[] = implode("\n", $keys); } - return '
' . $html . '
'; + return '
' . implode('', $html) . '
'; } /** @@ -764,17 +1424,17 @@ protected function highlightQuery($query) $regex = array( - // Tables are identified by the prefix - '/(=)/' - => '$1', + // Tables are identified by the prefix + '/(=)/' + => '$1', - // All uppercase words have a special meaning - '/(?)([A-Z_]{2,})(?!\w)/x' - => '$1', + // All uppercase words have a special meaning + '/(?)([A-Z_]{2,})(?!\w)/x' + => '$1', - // Tables are identified by the prefix - '/(' . JFactory::getDbo()->getPrefix() . '[a-z_0-9]+)/' - => '$1' + // Tables are identified by the prefix + '/(' . JFactory::getDbo()->getPrefix() . '[a-z_0-9]+)/' + => '$1' ); @@ -790,9 +1450,9 @@ protected function highlightQuery($query) * * Stolen from JError to prevent it's removal. * - * @param integer $error The error + * @param Exception $error The error * - * @return string Contents of the backtrace + * @return string Contents of the backtrace * * @since 2.5 */ @@ -800,23 +1460,23 @@ protected function renderBacktrace($error) { $backtrace = $error->getTrace(); - $html = ''; + $html = array(); if (is_array($backtrace)) { $j = 1; - $html .= ''; + $html[] = '
'; - $html .= ''; - $html .= ''; - $html .= ''; + $html[] = ''; + $html[] = ''; + $html[] = ''; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; + $html[] = ''; + $html[] = ''; + $html[] = ''; + $html[] = ''; + $html[] = ''; for ($i = count($backtrace) - 1; $i >= 0; $i--) { @@ -827,28 +1487,28 @@ protected function renderBacktrace($error) $link = $this->formatLink($backtrace[$i]['file'], $backtrace[$i]['line']); } - $html .= ''; - $html .= ''; + $html[] = ''; + $html[] = ''; if (isset($backtrace[$i]['class'])) { - $html .= ''; + $html[] = ''; } else { - $html .= ''; + $html[] = ''; } - $html .= ''; + $html[] = ''; - $html .= ''; + $html[] = ''; $j++; } - $html .= '
Call stack
Call stack
#FunctionLocation
#FunctionLocation
' . $j . '
' . $j . '' . $backtrace[$i]['class'] . $backtrace[$i]['type'] . $backtrace[$i]['function'] . '()' . $backtrace[$i]['class'] . $backtrace[$i]['type'] . $backtrace[$i]['function'] . '()' . $backtrace[$i]['function'] . '()' . $backtrace[$i]['function'] . '()' . $link . '' . $link . '
'; + $html[] = ''; } - return $html; + return implode('', $html); } /** @@ -914,7 +1574,8 @@ protected function displayLogs() JLog::WARNING => 'WARNING', JLog::NOTICE => 'NOTICE', JLog::INFO => 'INFO', - JLog::DEBUG => 'DEBUG'); + JLog::DEBUG => 'DEBUG' + ); $out = array(); @@ -925,5 +1586,4 @@ protected function displayLogs() return implode('

', $out); } - }