Completed
Pull Request — patch_1-1-7 (#3421)
by Spuds
13:53
created

Debug::toggleViewQueries()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 1
nc 2
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file contains a (static) class that will track some debug information
5
 * if debug is on.
6
 *
7
 * @name      ElkArte Forum
8
 * @copyright ElkArte Forum contributors
9
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause
10
 *
11
 * This file contains code covered by:
12
 * copyright:	2011 Simple Machines (http://www.simplemachines.org)
13
 * license:  	BSD, See included LICENSE.TXT for terms and conditions.
14
 *
15
 * @version 1.1
16
 *
17
 */
18
19
/**
20
 * Stored debugging information.
21
 */
22
class Debug
23
{
24
	/**
25
	 * This is used to remember if the debug is on or off
26
	 * @var bool
27
	 */
28
	private $_track = true;
29
30
	/**
31
	 * A list of known debug entities (here to preserve a kind of order)
32
	 * @var mixed[]
33
	 */
34
	private $_debugs = array(
35
		'templates' => array(),
36
		'sub_templates' => array(),
37
		'language_files' => array(),
38
		'sheets' => array(),
39
		'javascript' => array(),
40
		'hooks' => array(),
41
		'files_included' => array(),
42
		'tokens' => array(),
43
	);
44
45
	/**
46
	 * Holds the output ot the getrusage php function
47
	 * @var mixed[]
48
	 */
49
	private $_rusage = array();
50
51
	/**
52
	 * Holds All the cache hits for a page load
53
	 * @var string[]
54
	 */
55
	private $_cache_hits = array();
56
57
	/**
58
	 * Number of times the cache has been used
59
	 * @var int
60
	 */
61
	private $_cache_count = 0;
62
63
	/**
64
	 * All the queries executed
65
	 * @var mixed[]
66
	 */
67
	private $_db_cache = array();
68
69
	/**
70
	 * Number of queries
71
	 * @var int
72
	 */
73
	private $_db_count = 0;
74
75
	/**
76
	 * Some generic "system" debug info
77
	 * @var string[]
78
	 */
79
	private $_system = array();
80
81
	/**
82
	 * The instance of the class
83
	 * @var object
84
	 */
85
	private static $_instance = null;
86
87
88
	/**
89
	 * Adds a new generic debug entry
90
	 *
91
	 * @param string $type the kind of debug entry
92
	 * @param string|string[] string or array of the entry to show
93
	 */
94
	public function add($type, $value)
95
	{
96
		if (!$this->_track)
97
			return;
98
99
		if (is_array($value))
100
			$this->_debugs[$type] = array_merge($this->_debugs[$type], $value);
101
		else
102
			$this->_debugs[$type][] = $value;
103
	}
104
105
	/**
106
	 * Adds a new cache hits
107
	 *
108
	 * @param mixed[] $value contains the relevant cache info, in the form:
109
	 *         d => method: put or get
110
	 *         k => cache key
111
	 *         t => time taken to get/put the entry
112
	 *         s => length of the serialized value
113
	 */
114
	public function cache($value)
115
	{
116
		if (!$this->_track)
117
			return;
118
119
		$this->_cache_hits[] = $value;
120
		$this->_cache_count++;
121
	}
122
123
	/**
124
	 * Return the number of cache hits
125
	 *
126
	 * @return int
127
	 */
128
	public function cache_count()
129
	{
130
		return $this->_cache_count;
131
	}
132
133
	/**
134
	 * Adds a new database query
135
	 *
136
	 * @param mixed[] $value contains the relevant queries info, in the form:
137
	 *         q => the query string (only for the first 50 queries, after that only a "...")
138
	 *         f => the file in which the query has been executed
139
	 *         l => the line at which the query has been executed
140
	 *         s => seconds at which the query has been executed into the request
141
	 *         t => time taken by the query
142
	 */
143
	public function db_query($value)
144
	{
145
		if (!$this->_track)
146
			return;
147
148
		$this->_db_cache[] = $value;
149
		$this->_db_count++;
150
	}
151
152
	/**
153
	 * Merges the values passed with the current database entries
154
	 *
155
	 * @param mixed[] $value An array of queries info, see the db method for details
156
	 */
157
	public function merge_db($value)
158
	{
159
		if (!$this->_track)
160
			return;
161
162
		$this->_db_cache = array_merge($value, $this->_db_cache);
163
		$this->_db_count = count($this->_db_cache) + 1;
164
	}
165
166
	/**
167
	 * Return the current database entries
168
	 *
169
	 * @return array
170
	 */
171
	public function get_db()
172
	{
173
		if (!$this->_track)
174
			return array();
175
176
		return $this->_db_cache;
177
	}
178
179
	/**
180
	 * Adds a new getrusage value (by default two are added: one at the beginning
181
	 * of the script execution and one at the end)
182
	 *
183
	 * @param string $point can be end or start depending on when the function
184
	 *               is called
185
	 * @param string[]|null $rusage value of getrusage or null to let the method call it
186
	 */
187
	public function rusage($point, $rusage = null)
188
	{
189
		// getrusage is missing in php < 7 on Windows
190
		if ($this->_track === false || function_exists('getrusage') === false)
191
		{
192
			return;
193
		}
194
195
		if ($rusage === null)
196
			$this->_rusage[$point] = getrusage();
197
		else
198
			$this->_rusage[$point] = $rusage;
199
	}
200
201
	/**
202
	 * Enables tracking of debug entries
203
	 */
204
	public function on()
205
	{
206
		$this->_track = true;
207
	}
208
209
	/**
210
	 * Disables tracking of debug entries
211
	 */
212
	public function off()
213
	{
214
		$this->_track = false;
215
	}
216
217
	/**
218
	 * Toggles the visibility of the queries
219
	 */
220
	public function toggleViewQueries()
221
	{
222
		$_SESSION['view_queries'] = $_SESSION['view_queries'] == 1 ? 0 : 1;
223
	}
224
225
	/**
226
	 * Collects some other generic system information necessary for the
227
	 * debug screen
228
	 */
229
	private function _prepare_last_bits()
230
	{
231
		global $context;
232
233
		if (empty($_SESSION['view_queries']))
234
			$_SESSION['view_queries'] = 0;
235
236
		$files = get_included_files();
237
		$total_size = 0;
238
		for ($i = 0, $n = count($files); $i < $n; $i++)
239
		{
240
			if (file_exists($files[$i]))
241
				$total_size += filesize($files[$i]);
242
			$this->add('files_included', strtr($files[$i], array(BOARDDIR => '.')));
243
		}
244
245
		if (!empty($this->_db_cache))
246
			$_SESSION['debug'] = $this->_db_cache;
247
248
		// Compute some system info, if we can
249
		$this->_system['system_type'] = php_uname();
250
		require_once(SUBSDIR . '/Server.subs.php');
251
		$this->_system['server_load'] = detectServerLoad();
252
		$this->_system['script_mem_load'] = byte_format(memory_get_peak_usage());
253
254
		// getrusage() information is CPU time, not wall clock time like microtime, *nix only
255
		$this->rusage('end');
256
257
		if (!empty($this->_rusage))
258
		{
259
			$this->_system['script_cpu_load'] = ($this->_rusage['end']['ru_utime.tv_sec'] - $this->_rusage['start']['ru_utime.tv_sec'] + ($this->_rusage['end']['ru_utime.tv_usec'] / 1000000)) . ' / ' . ($this->_rusage['end']['ru_stime.tv_sec'] - $this->_rusage['start']['ru_stime.tv_sec'] + ($this->_rusage['end']['ru_stime.tv_usec'] / 1000000));
260
		}
261
262
		$this->_system['browser'] = $context['browser_body_id'] . ' <em>(' . implode('</em>, <em>', array_reverse(array_keys($context['browser'], true))) . ')</em>';
263
264
		// What tokens are active?
265
		if (isset($_SESSION['token']))
266
			$this->add('tokens', array_keys($_SESSION['token']));
267
	}
268
269
	/**
270
	 * This function shows the debug information tracked
271
	 */
272
	public function display()
273
	{
274
		global $scripturl, $txt;
275
276
		$this->_prepare_last_bits();
277
		$expand_id = array();
278
279
		// Gotta have valid HTML ;).
280
		$temp = ob_get_contents();
281
		ob_clean();
282
283
		echo preg_replace('~</body>\s*</html>~', '', $temp), '
284
		<div id="debug_logging_wrapper">
285
			<div id="debug_logging" class="smalltext">';
286
287
		foreach ($this->_system as $key => $value)
288
			if (!empty($value))
289
				echo '
290
				', $txt['debug_' . $key], $value, '<br />';
291
292
		$expandable = array('hooks', 'files_included');
293
294
		foreach ($this->_debugs as $key => $value)
295
		{
296
			$value = array_map('htmlentities', $value);
297
			if (in_array($key, $expandable))
298
			{
299
				$key = htmlentities($key, ENT_QUOTES);
300
				$expand_id[] = 'debug_' . $key;
301
				$pre = ' (<a id="debug_' . $key . '" href="#">' . $txt['debug_show'] . '</a><span class="hide">';
302
				$post = '</span>)';
303
			}
304
			else
305
			{
306
				$pre = '';
307
				$post = '';
308
			}
309
310
			echo '
311
				', $txt['debug_' . $key], count($value), ' - ' . $pre . '<em>', implode('</em>, <em>', $value), '</em>.' . $post . '<br />';
312
		}
313
314
		// If the cache is on, how successful was it?
315
		if (Cache::instance()->isEnabled() && !empty($this->_cache_hits))
316
		{
317
			$entries = array();
318
			$total_t = 0;
319
			$total_s = 0;
320
			foreach ($this->_cache_hits as $cache_hit)
321
			{
322
				$entries[] = $cache_hit['d'] . ' ' . $cache_hit['k'] . ': ' . sprintf($txt['debug_cache_seconds_bytes'], comma_format($cache_hit['t'], 5), $cache_hit['s']);
323
				$total_t += $cache_hit['t'];
324
				$total_s += $cache_hit['s'];
325
			}
326
			$expand_id[] = 'debug_cache_info';
327
328
			echo '
329
				', $txt['debug_cache_hits'], $this->cache_count(), ': ', sprintf($txt['debug_cache_seconds_bytes_total'], comma_format($total_t, 5), comma_format($total_s)), ' (<a id="debug_cache_info" href="#">', $txt['debug_show'], '</a><span class="hide"><em>', implode('</em>, <em>', $entries), '</em></span>)<br />';
330
		}
331
332
		// Want to see the querys in a new windows?
333
		echo '
334
				<a href="', $scripturl, '?action=viewquery" target="_blank" class="new_win">', sprintf($txt['debug_queries_used'], $this->_db_count), '</a><br />';
335
336
		if ($_SESSION['view_queries'] == 1 && !empty($this->_db_cache))
337
			$this->_show_queries();
338
339
		// Or show/hide the querys in line with all of this data
340
		echo '
341
				<a href="' . $scripturl . '?action=viewquery;sa=hide">', $txt['debug_' . (empty($_SESSION['view_queries']) ? 'show' : 'hide') . '_queries'], '</a>
342
			</div>
343
		</div>';
344
345
		if (!empty($expand_id))
346
		{
347
			echo '
348
			<script>
349
				$(function() {
350
					$(\'#', implode(', #', $expand_id), '\').click(function(ev) {
351
						ev.preventDefault();
352
						$(this).next().toggle();
353
						$(this).remove();
354
					});
355
				});
356
			</script>';
357
		}
358
359
		echo '
360
	</body></html>';
361
	}
362
363
	/**
364
	 * Displays a page with all the queries executed during the "current"
365
	 * page load and allows to EXPLAIN them
366
	 *
367
	 * @param integer $query_id the id of the query to EXPLAIN, if -1 no queries are explained
368
	 */
369
	public function viewQueries($query_id)
370
	{
371
		$queries_data = array();
372
373
		$query_analysis = new Query_Analysis();
374
375
		foreach ($_SESSION['debug'] as $q => $query_data)
376
		{
377
			$queries_data[$q] = $query_analysis->extractInfo($query_data);
378
379
			// Explain the query.
380
			if ($query_id == $q && $queries_data[$q]['is_select'])
381
			{
382
				$queries_data[$q]['explain'] = $query_analysis->doExplain();
383
			}
384
		}
385
386
		return $queries_data;
387
	}
388
389
	/**
390
	 * Displays a list of queries executed during the current
391
	 * page load
392
	 */
393
	private function _show_queries()
394
	{
395
		global $scripturl, $txt;
396
397
		foreach ($this->_db_cache as $q => $qq)
398
		{
399
			$is_select = strpos(trim($qq['q']), 'SELECT') === 0 || preg_match('~^INSERT(?: IGNORE)? INTO \w+(?:\s+\([^)]+\))?\s+SELECT .+$~s', trim($qq['q'])) != 0 || strpos(trim($qq['q']), 'WITH') === 0;
400
401
			// Temporary tables created in earlier queries are not explainable.
402
			if ($is_select)
403
			{
404
				foreach (array('log_topics_unread', 'topics_posted_in', 'tmp_log_search_topics', 'tmp_log_search_messages') as $tmp)
405
					if (strpos(trim($qq['q']), $tmp) !== false)
406
					{
407
						$is_select = false;
408
						break;
409
					}
410
			}
411
			// But actual creation of the temporary tables are.
412
			elseif (preg_match('~^CREATE TEMPORARY TABLE .+?SELECT .+$~s', trim($qq['q'])) != 0)
413
				$is_select = true;
414
415
			// Make the filenames look a bit better.
416
			if (isset($qq['f']))
417
				$qq['f'] = preg_replace('~^' . preg_quote(BOARDDIR, '~') . '~', '...', $qq['f']);
418
419
			echo '
420
		<strong>', $is_select ? '<a href="' . $scripturl . '?action=viewquery;qq=' . ($q + 1) . '#qq' . $q . '" target="_blank" class="new_win">' : '', nl2br(str_replace("\t", '&nbsp;&nbsp;&nbsp;', htmlspecialchars(ltrim($qq['q'], "\n\r"), ENT_COMPAT, 'UTF-8'))) . ($is_select ? '</a></strong>' : '</strong>') . '<br />
421
		&nbsp;&nbsp;&nbsp;';
422
			if (!empty($qq['f']) && !empty($qq['l']))
423
				echo sprintf($txt['debug_query_in_line'], $qq['f'], $qq['l']);
424
425
			if (isset($qq['s'], $qq['t']) && isset($txt['debug_query_which_took_at']))
426
				echo sprintf($txt['debug_query_which_took_at'], round($qq['t'], 8), round($qq['s'], 8)) . '<br />';
427
			elseif (isset($qq['t']))
428
				echo sprintf($txt['debug_query_which_took'], round($qq['t'], 8)) . '<br />';
429
			echo '
430
		<br />';
431
		}
432
	}
433
434
	/**
435
	 * Return the single instance of this class
436
	 * @return Debug
437
	 */
438
	public static function instance()
439
	{
440
		if (self::$_instance === null)
441
			self::$_instance = new Debug();
442
443
		return self::$_instance;
444
	}
445
}