Passed
Push — master ( 57a23e...307e83 )
by Jan
02:53
created

Timer::show()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 0
cts 0
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 2
crap 6
1
<?php
2
namespace Xicrow\PhpDebug;
3
4
/**
5
 * Class Timer
6
 *
7
 * @package Xicrow\PhpDebug
8
 */
9
class Timer {
10
	/**
11
	 * @var array
12
	 */
13
	public static $collection = [];
14
15
	/**
16
	 * @var boolean|string
17
	 */
18
	public static $currentItem = false;
19
20
	/**
21
	 * @var array
22
	 */
23
	public static $runningItems = [];
24
25
	/**
26
	 * Force the unit to display elapsed times in (MS|S|M|H|D|W)
27
	 *
28
	 * @var null|string
29
	 */
30
	public static $forceDisplayUnit = null;
31
32
	/**
33
	 * Color threshold for output (5 => 'red', all items with values of 5 or higher will be red)
34
	 *
35
	 * @var array
36
	 */
37
	public static $colorThreshold = [];
38
39
	/**
40
	 *
41
	 */
42 6
	public static function reset() {
43 6
		self::$collection   = [];
44 6
		self::$currentItem  = false;
45 6
		self::$runningItems = [];
46 6
	}
47
48
	/**
49
	 * @param string|null $key
50
	 * @param array       $data
51
	 *
52
	 * @return string
53
	 */
54 6
	public static function add($key = null, $data = []) {
55
		// If no key is given
56 6
		if (is_null($key)) {
57
			// Set key to file and line
58
			$key = Debugger::getCalledFrom(2);
59
		}
60
61
		// If key is allready in use
62 6
		if (isset(self::$collection[$key])) {
63
			// Get original item
64 1
			$item = self::$collection[$key];
65
66
			// Set new item count
67 1
			$itemCount = (isset($item['count']) ? ($item['count'] + 1) : 2);
68
69
			// Set correct key for the original item
70 1
			if (strpos($item['key'], '#') === false) {
71 1
				self::$collection[$key] = array_merge($item, [
72 1
					'key'   => $key . ' #1',
73 1
					'count' => $itemCount,
74 1
				]);
75 1
			} else {
76
				self::$collection[$key] = array_merge($item, [
77
					'count' => $itemCount,
78
				]);
79
			}
80
81
			// Set new key
82 1
			$key = $key . ' #' . $itemCount;
83 1
		}
84
85
		// Make sure various options are set
86 6
		if (!isset($data['key'])) {
87 6
			$data['key'] = $key;
88 6
		}
89 6
		if (!isset($data['parent'])) {
90 6
			$data['parent'] = self::$currentItem;
91 6
		}
92 6
		if (!isset($data['level'])) {
93 6
			$data['level'] = 0;
94 6
			if (isset($data['parent']) && isset(self::$collection[$data['parent']])) {
95 1
				$data['level'] = (self::$collection[$data['parent']]['level'] + 1);
96 1
			}
97 6
		}
98
99
		// Add item to collection
100 6
		self::$collection[$key] = $data;
101
102 6
		return $key;
103
	}
104
105
	/**
106
	 * @param string|null $key
107
	 *
108
	 * @return string
109
	 */
110 3
	public static function start($key = null) {
111
		// Add new item
112 3
		$key = self::add($key, [
113 3
			'start' => microtime(true),
114 3
		]);
115
116
		// Set current item
117 3
		self::$currentItem = $key;
118
119
		// Add to running items
120 3
		self::$runningItems[$key] = true;
121
122 3
		return $key;
123
	}
124
125
	/**
126
	 * @param string|null $key
127
	 *
128
	 * @return string
129
	 */
130 2
	public static function stop($key = null) {
131
		// If no key is given
132 2
		if (is_null($key)) {
133
			// Get key of the last started item
134 1
			end(self::$runningItems);
135 1
			$key = key(self::$runningItems);
136 1
		}
137
138
		// Check for key duplicates, and find the last one not stopped
139 2
		if (isset(self::$collection[$key]) && isset(self::$collection[$key . ' #2'])) {
140 1
			$lastNotStopped = false;
141 1
			$currentKey     = $key;
142 1
			$currentIndex   = 1;
143 1
			while (isset(self::$collection[$currentKey])) {
144 1
				if (!isset(self::$collection[$currentKey]['stop'])) {
145 1
					$lastNotStopped = $currentKey;
146 1
				}
147
148 1
				$currentIndex++;
149 1
				$currentKey = $key . ' #' . $currentIndex;
150 1
			}
151
152 1
			if ($lastNotStopped) {
153 1
				$key = $lastNotStopped;
154 1
			}
155 1
		}
156
157
		// If item exists in collection
158 2
		if (isset(self::$collection[$key])) {
159
			// Update the item
160 2
			self::$collection[$key]['stop'] = microtime(true);
161
162 2
			self::$currentItem = self::$collection[$key]['parent'];
163 2
		}
164
165 2
		if (isset(self::$runningItems[$key])) {
166 2
			unset(self::$runningItems[$key]);
167 2
		}
168
169 2
		return $key;
170
	}
171
172
	/**
173
	 * @param string|null    $key
174
	 * @param int|float|null $start
175
	 * @param int|float|null $stop
176
	 *
177
	 * @return string
178
	 */
179 2
	public static function custom($key = null, $start = null, $stop = null) {
180
		// Add new item
181 2
		self::add($key, [
182 2
			'start' => $start,
183 2
			'stop'  => $stop,
184 2
		]);
185
186
		// If no stop value is given
187 2
		if (is_null($stop)) {
188
			// Set current item
189 1
			self::$currentItem = $key;
190
191
			// Add to running items
192 1
			self::$runningItems[$key] = true;
193 1
		}
194
195 2
		return $key;
196
	}
197
198
	/**
199
	 * @param string|null           $key
200
	 * @param string|array|\Closure $callback
201
	 *
202
	 * @return mixed
203
	 */
204 1
	public static function callback($key = null, $callback) {
205
		// Get parameters for callback
206 1
		$callbackParams = func_get_args();
207 1
		unset($callbackParams[0], $callbackParams[1]);
208 1
		$callbackParams = array_values($callbackParams);
209
210
		// Get key if no key is given
211 1
		if (is_null($key)) {
212 1
			if (is_string($callback)) {
213 1
				$key = $callback;
214 1
			} elseif (is_array($callback)) {
215 1
				$keyArr = [];
216 1
				foreach ($callback as $k => $v) {
217 1
					if (is_string($v)) {
218 1
						$keyArr[] = $v;
219 1
					} elseif (is_object($v)) {
220 1
						$keyArr[] = get_class($v);
221 1
					}
222 1
				}
223
224 1
				$key = implode('', $keyArr);
225 1
				if (count($keyArr) > 1) {
226 1
					$method = array_pop($keyArr);
227 1
					$key    = implode('/', $keyArr);
228 1
					$key    .= '::' . $method;
229 1
				}
230
231 1
				unset($keyArr, $method);
232 1
			} elseif (is_object($callback) && $callback instanceof \Closure) {
233 1
				$key = 'closure';
234 1
			}
235 1
			$key = 'callback: ' . $key;
236 1
		}
237
238
		// Set default return value
239 1
		$returnValue = true;
240
241
		// Set error handler, to convert errors to exceptions
242 1
		set_error_handler(function ($errno, $errstr, $errfile, $errline, array $errcontext) {
0 ignored issues
show
Unused Code introduced by
The parameter $errcontext is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
243 1
			throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
244 1
		});
245
246
		try {
247
			// Start output buffer to capture any output
248 1
			ob_start();
249
250
			// Start profiler
251 1
			self::start($key);
252
253
			// Execute callback, and get result
254 1
			$callbackResult = call_user_func_array($callback, $callbackParams);
255
256
			// Stop profiler
257 1
			self::stop($key);
258
259
			// Get and clean output buffer
260 1
			$callbackOutput = ob_get_clean();
261 1
		} catch (\ErrorException $callbackException) {
262
			// Stop and clean output buffer
263 1
			ob_end_clean();
264
265
			// Show error message
266 1
			Debugger::output('Invalid callback sent to Timer::callback: ' . str_replace('callback: ', '', $key));
267
268
			// Clear the item from the collection
269 1
			unset(self::$collection[$key]);
270
271
			// Clear callback result and output
272 1
			unset($callbackResult, $callbackOutput);
273
274
			// Set return value to false
275 1
			$returnValue = false;
276
		}
277
278
		// Restore error handler
279 1
		restore_error_handler();
280
281
		// Return result, output or true
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
282 1
		return (isset($callbackResult) ? $callbackResult : (!empty($callbackOutput) ? $callbackOutput : $returnValue));
283
	}
284
285
	/**
286
	 * @param string|null $key
287
	 * @param array       $options
288
	 *
289
	 * @codeCoverageIgnore
290
	 */
291
	public static function show($key = null, $options = []) {
292
		$output = self::getStats($key, $options);
293
294
		if (!empty($output)) {
295
			Debugger::output($output);
296
		}
297
	}
298
299
	/**
300
	 * @param array $options
301
	 *
302
	 * @codeCoverageIgnore
303
	 */
304
	public static function showAll($options = []) {
305
		// Stop started items
306
		if (count(self::$runningItems)) {
307
			foreach (self::$runningItems as $key => $value) {
308
				self::stop($key);
309
			}
310
		}
311
312
		// Output items
313
		$output    = '';
314
		$itemCount = 1;
315
		foreach (self::$collection as $key => $item) {
316
			$stats = self::getStats($key, $options);
317
318
			if (php_sapi_name() == 'cli') {
319
				$output .= (!empty($output) ? "\n" : '') . $stats;
320
			} else {
321
				$output .= '<div class="xicrow-php-debug-timer">';
322
				$output .= $stats;
323
				$output .= '</div>';
324
			}
325
326
			$itemCount++;
327
328
			unset($stats);
329
		}
330
		unset($itemCount);
331
332
		if (php_sapi_name() != 'cli') {
333
			$output .= '<style type="text/css">';
334
			$output .= 'pre.xicrow-php-debug-debugger div.xicrow-php-debug-timer{';
335
			$output .= 'cursor: pointer;';
336
			$output .= '}';
337
			$output .= 'pre.xicrow-php-debug-debugger div.xicrow-php-debug-timer:hover{';
338
			$output .= 'font-weight: bold;';
339
			$output .= 'background-color: #EEE;';
340
			$output .= '}';
341
			$output .= '</style>';
342
		}
343
344
		Debugger::output($output);
345
	}
346
347
	/**
348
	 * @param string|null $key
349
	 * @param array       $options
350
	 *
351
	 * @return string
352
	 */
353 1
	public static function getStats($key, $options = []) {
354
		// Merge options with default options
355 1
		$options = array_merge([
356
			// Show nested (boolean)
357 1
			'nested'         => true,
358
			// Prefix for nested items (string)
359 1
			'nested_prefix'  => '|-- ',
360
			// Max key length (int)
361 1
			'max_key_length' => 100,
362 1
		], $options);
363
364
		// If item does not exist
365 1
		if (!isset(self::$collection[$key])) {
366 1
			return 'Unknow item in with key: ' . $key;
367
		}
368
369
		// Get item
370 1
		$item = self::$collection[$key];
371
372
		// Get item result
373 1
		$itemResult          = 'N/A';
374 1
		$itemResultFormatted = 'N/A';
375 1
		if (isset($item['start']) && isset($item['stop'])) {
376 1
			$itemResult          = (($item['stop'] - $item['start']) * 1000);
377 1
			$itemResultFormatted = self::formatMiliseconds($itemResult, 4, self::$forceDisplayUnit);
378 1
		}
379
380
		// Variable for output
381 1
		$output = '';
382
383
		// Prep key for output
384 1
		$outputName = '';
385 1
		$outputName .= ($options['nested'] ? str_repeat($options['nested_prefix'], $item['level']) : '');
386 1
		$outputName .= $item['key'];
387 1
		if (mb_strlen($outputName) > $options['max_key_length']) {
388 1
			$outputName = '~' . mb_substr($item['key'], -($options['max_key_length'] - 1));
389 1
		}
390
391
		// Add item stats
392 1
		$output .= str_pad($outputName, ($options['max_key_length'] + (strlen($outputName) - mb_strlen($outputName))), ' ');
393 1
		$output .= ' | ';
394 1
		$output .= str_pad($itemResultFormatted, 20, ' ', ($itemResult == 'N/A' ? STR_PAD_RIGHT : STR_PAD_LEFT));
395
396 1
		if (php_sapi_name() != 'cli' && is_array(self::$colorThreshold) && count(self::$colorThreshold)) {
397
			krsort(self::$colorThreshold);
398
			foreach (self::$colorThreshold as $value => $color) {
399
				if (is_numeric($itemResult) && $itemResult >= $value) {
400
					$output = '<span style="color: ' . $color . ';">' . $output . '</span>';
401
				}
402
			}
403
		}
404
405 1
		return $output;
406
	}
407
408
	/**
409
	 * @param int|float   $number
410
	 * @param int         $precision
411
	 * @param null|string $forceUnit
412
	 *
413
	 * @return string
414
	 */
415 1
	public static function formatMiliseconds($number = 0, $precision = 2, $forceUnit = null) {
416
		$units = [
417 1
			'MS' => 1,
418 1
			'S'  => 1000,
419 1
			'M'  => 60,
420 1
			'H'  => 60,
421 1
			'D'  => 24,
422 1
			'W'  => 7,
423 1
		];
424
425 1
		if (is_null($forceUnit)) {
426 1
			$forceUnit = self::$forceDisplayUnit;
427 1
		}
428
429 1
		$value = $number;
430 1
		if (!empty($forceUnit) && array_key_exists($forceUnit, $units)) {
431 1
			$unit = $forceUnit;
432 1
			foreach ($units as $k => $v) {
433 1
				$value = ($value / $v);
434 1
				if ($k == $unit) {
435 1
					break;
436
				}
437 1
			}
438 1
		} else {
439 1
			$unit = '';
440 1
			foreach ($units as $k => $v) {
441 1
				if (empty($unit) || ($value / $v) > 1) {
442 1
					$value = ($value / $v);
443 1
					$unit  = $k;
444 1
				} else {
445 1
					break;
446
				}
447 1
			}
448
		}
449
450 1
		return sprintf('%0.' . $precision . 'f', $value) . ' ' . str_pad($unit, 2, ' ', STR_PAD_RIGHT);
451
	}
452
}
453