[!!!][TASK] Make TimeTracker a singleton
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / TimeTracker / TimeTracker.php
1 <?php
2 namespace TYPO3\CMS\Core\TimeTracker;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16 use TYPO3\CMS\Core\Imaging\Icon;
17 use TYPO3\CMS\Core\Imaging\IconFactory;
18 use TYPO3\CMS\Core\SingletonInterface;
19 use TYPO3\CMS\Core\Utility\GeneralUtility;
20
21 /**
22 * Frontend Timetracking functions
23 *
24 * Is used to register how much time is used with operations in TypoScript
25 */
26 class TimeTracker implements SingletonInterface
27 {
28
29 /**
30 * If set to true (see constructor) then then the timetracking is enabled
31 * @var bool
32 */
33 protected $isEnabled = false;
34
35 /**
36 * Is loaded with the millisecond time when this object is created
37 *
38 * @var int
39 */
40 public $starttime = 0;
41
42 /**
43 * Log Rendering flag. If set, ->push() and ->pull() is called from the cObj->cObjGetSingle().
44 * This determines whether or not the TypoScript parsing activity is logged. But it also slows down the rendering
45 *
46 * @var bool
47 */
48 public $LR = 1;
49
50 /**
51 * @var array
52 */
53 public $printConf = array(
54 'showParentKeys' => 1,
55 'contentLength' => 10000,
56 // Determines max length of displayed content before it gets cropped.
57 'contentLength_FILE' => 400,
58 // Determines max length of displayed content FROM FILE cObjects before it gets cropped. Reason is that most FILE cObjects are huge and often used as template-code.
59 'flag_tree' => 1,
60 'flag_messages' => 1,
61 'flag_queries' => 0,
62 'flag_content' => 0,
63 'allTime' => 0,
64 'keyLgd' => 40
65 );
66
67 /**
68 * @var array
69 */
70 public $wrapError = [
71 0 => ['', ''],
72 1 => ['<strong>', '</strong>'],
73 2 => ['<strong style="color:#ff6600;">', '</strong>'],
74 3 => ['<strong style="color:#ff0000;">', '</strong>']
75 ];
76
77 /**
78 * @var array
79 */
80 public $wrapIcon = [
81 0 => '',
82 1 => 'actions-document-info',
83 2 => 'status-dialog-warning',
84 3 => 'status-dialog-error'
85 ];
86
87 /**
88 * @var int
89 */
90 public $uniqueCounter = 0;
91
92 /**
93 * @var array
94 */
95 public $tsStack = array(array());
96
97 /**
98 * @var int
99 */
100 public $tsStackLevel = 0;
101
102 /**
103 * @var array
104 */
105 public $tsStackLevelMax = array();
106
107 /**
108 * @var array
109 */
110 public $tsStackLog = array();
111
112 /**
113 * @var int
114 */
115 public $tsStackPointer = 0;
116
117 /**
118 * @var array
119 */
120 public $currentHashPointer = array();
121
122 /**
123 * Log entries that take than this number of milliseconds (own time) will be highlighted during log display. Set 0 to disable highlighting.
124 *
125 * @var int
126 */
127 public $highlightLongerThan = 0;
128
129 /*******************************************
130 *
131 * Logging parsing times in the scripts
132 *
133 *******************************************/
134
135 /**
136 * TimeTracker constructor.
137 *
138 * @param bool $isEnabled
139 */
140 public function __construct($isEnabled = true)
141 {
142 $this->isEnabled = $isEnabled;
143 }
144
145 /**
146 * Sets the starting time
147 *
148 * @return void
149 */
150 public function start()
151 {
152 if (!$this->isEnabled) {
153 return;
154 }
155 $this->starttime = $this->getMilliseconds();
156 }
157
158 /**
159 * Pushes an element to the TypoScript tracking array
160 *
161 * @param string $tslabel Label string for the entry, eg. TypoScript property name
162 * @param string $value Additional value(?)
163 * @return void
164 * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::cObjGetSingle(), pull()
165 */
166 public function push($tslabel, $value = '')
167 {
168 if (!$this->isEnabled) {
169 return;
170 }
171 array_push($this->tsStack[$this->tsStackPointer], $tslabel);
172 array_push($this->currentHashPointer, 'timetracker_' . $this->uniqueCounter++);
173 $this->tsStackLevel++;
174 $this->tsStackLevelMax[] = $this->tsStackLevel;
175 // setTSlog
176 $k = end($this->currentHashPointer);
177 $this->tsStackLog[$k] = array(
178 'level' => $this->tsStackLevel,
179 'tsStack' => $this->tsStack,
180 'value' => $value,
181 'starttime' => microtime(true),
182 'stackPointer' => $this->tsStackPointer
183 );
184 }
185
186 /**
187 * Pulls an element from the TypoScript tracking array
188 *
189 * @param string $content The content string generated within the push/pull part.
190 * @return void
191 * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::cObjGetSingle(), push()
192 */
193 public function pull($content = '')
194 {
195 if (!$this->isEnabled) {
196 return;
197 }
198 $k = end($this->currentHashPointer);
199 $this->tsStackLog[$k]['endtime'] = microtime(true);
200 $this->tsStackLog[$k]['content'] = $content;
201 $this->tsStackLevel--;
202 array_pop($this->tsStack[$this->tsStackPointer]);
203 array_pop($this->currentHashPointer);
204 }
205
206 /**
207 * Logs the TypoScript entry
208 *
209 * @param string $content The message string
210 * @param int $num Message type: 0: information, 1: message, 2: warning, 3: error
211 * @return void
212 * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::CONTENT()
213 */
214 public function setTSlogMessage($content, $num = 0)
215 {
216 if (!$this->isEnabled) {
217 return;
218 }
219 end($this->currentHashPointer);
220 $k = current($this->currentHashPointer);
221 // Enlarge the "details" column by adding a span
222 if (strlen($content) > 30) {
223 $placeholder = '<br /><span style="width: 300px; height: 1px; display: inline-block;"></span>';
224 }
225 $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
226 $this->tsStackLog[$k]['message'][] = $iconFactory->getIcon($this->wrapIcon[$num], Icon::SIZE_SMALL)->render() . $this->wrapError[$num][0] . htmlspecialchars($content) . $this->wrapError[$num][1] . $placeholder;
227 }
228
229 /**
230 * Set TSselectQuery - for messages in TypoScript debugger.
231 *
232 * @param array $data Query array
233 * @param string $msg Message/Label to attach
234 * @return void
235 */
236 public function setTSselectQuery(array $data, $msg = '')
237 {
238 if (!$this->isEnabled) {
239 return;
240 }
241 end($this->currentHashPointer);
242 $k = current($this->currentHashPointer);
243 if ($msg !== '') {
244 $data['msg'] = $msg;
245 }
246 $this->tsStackLog[$k]['selectQuery'][] = $data;
247 }
248
249 /**
250 * Increases the stack pointer
251 *
252 * @return void
253 * @see decStackPointer(), \TYPO3\CMS\Frontend\Page\PageGenerator::renderContent(), \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::cObjGetSingle()
254 */
255 public function incStackPointer()
256 {
257 if (!$this->isEnabled) {
258 return;
259 }
260 $this->tsStackPointer++;
261 $this->tsStack[$this->tsStackPointer] = array();
262 }
263
264 /**
265 * Decreases the stack pointer
266 *
267 * @return void
268 * @see incStackPointer(), \TYPO3\CMS\Frontend\Page\PageGenerator::renderContent(), \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::cObjGetSingle()
269 */
270 public function decStackPointer()
271 {
272 if (!$this->isEnabled) {
273 return;
274 }
275 unset($this->tsStack[$this->tsStackPointer]);
276 $this->tsStackPointer--;
277 }
278
279 /**
280 * Gets a microtime value as milliseconds value.
281 *
282 * @param float $microtime The microtime value - if not set the current time is used
283 * @return int The microtime value as milliseconds value
284 */
285 public function getMilliseconds($microtime = null)
286 {
287 if (!$this->isEnabled) {
288 return 0;
289 }
290 if (!isset($microtime)) {
291 $microtime = microtime(true);
292 }
293 return round($microtime * 1000);
294 }
295
296 /**
297 * Gets the difference between a given microtime value and the starting time as milliseconds.
298 *
299 * @param float $microtime The microtime value - if not set the current time is used
300 * @return int The difference between a given microtime value and starting time as milliseconds
301 */
302 public function getDifferenceToStarttime($microtime = null)
303 {
304 return $this->getMilliseconds($microtime) - $this->starttime;
305 }
306
307 /*******************************************
308 *
309 * Printing the parsing time information (for Admin Panel)
310 *
311 *******************************************/
312 /**
313 * Print TypoScript parsing log
314 *
315 * @return string HTML table with the information about parsing times.
316 */
317 public function printTSlog()
318 {
319 if (!$this->isEnabled) {
320 return '';
321 }
322 // Calculate times and keys for the tsStackLog
323 foreach ($this->tsStackLog as $uniqueId => &$data) {
324 $data['endtime'] = $this->getDifferenceToStarttime($data['endtime']);
325 $data['starttime'] = $this->getDifferenceToStarttime($data['starttime']);
326 $data['deltatime'] = $data['endtime'] - $data['starttime'];
327 if (is_array($data['tsStack'])) {
328 $data['key'] = implode($data['stackPointer'] ? '.' : '/', end($data['tsStack']));
329 }
330 }
331 unset($data);
332 // Create hierarchical array of keys pointing to the stack
333 $arr = array();
334 foreach ($this->tsStackLog as $uniqueId => $data) {
335 $this->createHierarchyArray($arr, $data['level'], $uniqueId);
336 }
337 // Parsing the registeret content and create icon-html for the tree
338 $this->tsStackLog[$arr['0.'][0]]['content'] = $this->fixContent($arr['0.'], $this->tsStackLog[$arr['0.'][0]]['content'], '', 0, $arr['0.'][0]);
339 // Displaying the tree:
340 $outputArr = array();
341 $outputArr[] = $this->fw('TypoScript Key');
342 $outputArr[] = $this->fw('Value');
343 if ($this->printConf['allTime']) {
344 $outputArr[] = $this->fw('Time');
345 $outputArr[] = $this->fw('Own');
346 $outputArr[] = $this->fw('Sub');
347 $outputArr[] = $this->fw('Total');
348 } else {
349 $outputArr[] = $this->fw('Own');
350 }
351 $outputArr[] = $this->fw('Details');
352 $out = '';
353 foreach ($outputArr as $row) {
354 $out .= '
355 <th><strong>' . $row . '</strong></th>';
356 }
357 $out = '<tr class="typo3-adminPanel-itemRow">' . $out . '</tr>';
358 $flag_tree = $this->printConf['flag_tree'];
359 $flag_messages = $this->printConf['flag_messages'];
360 $flag_content = $this->printConf['flag_content'];
361 $flag_queries = $this->printConf['flag_queries'];
362 $keyLgd = $this->printConf['keyLgd'];
363 $c = 0;
364 foreach ($this->tsStackLog as $uniqueId => $data) {
365 if ($this->highlightLongerThan && (int)$data['owntime'] > (int)$this->highlightLongerThan) {
366 $logRowClass = 'typo3-adminPanel-logRow-highlight';
367 } else {
368 $logRowClass = $c % 2 ? 'line-odd' : 'line-even';
369 }
370 $logRowClass .= ' typo3-adminPanel-section-content-title';
371 $item = '';
372 // If first...
373 if (!$c) {
374 $data['icons'] = '';
375 $data['key'] = 'Script Start';
376 $data['value'] = '';
377 }
378 // Key label:
379 $keyLabel = '';
380 if (!$flag_tree && $data['stackPointer']) {
381 $temp = array();
382 foreach ($data['tsStack'] as $k => $v) {
383 $temp[] = GeneralUtility::fixed_lgd_cs(implode($v, $k ? '.' : '/'), -$keyLgd);
384 }
385 array_pop($temp);
386 $temp = array_reverse($temp);
387 array_pop($temp);
388 if (!empty($temp)) {
389 $keyLabel = '<br /><span style="color:#999999;">' . implode($temp, '<br />') . '</span>';
390 }
391 }
392 if ($flag_tree) {
393 $tmp = GeneralUtility::trimExplode('.', $data['key'], true);
394 $theLabel = end($tmp);
395 } else {
396 $theLabel = $data['key'];
397 }
398 $theLabel = GeneralUtility::fixed_lgd_cs($theLabel, -$keyLgd);
399 $theLabel = $data['stackPointer'] ? '<span class="stackPointer">' . $theLabel . '</span>' : $theLabel;
400 $keyLabel = $theLabel . $keyLabel;
401 $item .= '<td class="' . $logRowClass . '">' . ($flag_tree ? $data['icons'] : '') . $this->fw($keyLabel) . '</td>';
402 // Key value:
403 $keyValue = $data['value'];
404 $item .= '<td class="' . $logRowClass . ' typo3-adminPanel-tsLogTime">' . $this->fw(htmlspecialchars($keyValue)) . '</td>';
405 if ($this->printConf['allTime']) {
406 $item .= '<td class="' . $logRowClass . ' typo3-adminPanel-tsLogTime"> ' . $this->fw($data['starttime']) . '</td>';
407 $item .= '<td class="' . $logRowClass . ' typo3-adminPanel-tsLogTime"> ' . $this->fw($data['owntime']) . '</td>';
408 $item .= '<td class="' . $logRowClass . ' typo3-adminPanel-tsLogTime"> ' . $this->fw(($data['subtime'] ? '+' . $data['subtime'] : '')) . '</td>';
409 $item .= '<td class="' . $logRowClass . ' typo3-adminPanel-tsLogTime"> ' . $this->fw(($data['subtime'] ? '=' . $data['deltatime'] : '')) . '</td>';
410 } else {
411 $item .= '<td class="' . $logRowClass . ' typo3-adminPanel-tsLogTime"> ' . $this->fw($data['owntime']) . '</td>';
412 }
413 // Messages:
414 $msgArr = array();
415 $msg = '';
416 if ($flag_messages && is_array($data['message'])) {
417 foreach ($data['message'] as $v) {
418 $msgArr[] = nl2br($v);
419 }
420 }
421 if ($flag_queries && is_array($data['selectQuery'])) {
422 $msgArr[] = \TYPO3\CMS\Core\Utility\DebugUtility::viewArray($data['selectQuery']);
423 }
424 if ($flag_content && (string)$data['content'] !== '') {
425 $maxlen = 120;
426 // Break lines which are too longer than $maxlen chars (can happen if content contains long paths...)
427 if (preg_match_all('/(\\S{' . $maxlen . ',})/', $data['content'], $reg)) {
428 foreach ($reg[1] as $key => $match) {
429 $match = preg_replace('/(.{' . $maxlen . '})/', '$1 ', $match);
430 $data['content'] = str_replace($reg[0][$key], $match, $data['content']);
431 }
432 }
433 $msgArr[] = '<span style="color:#000066;">' . nl2br($data['content']) . '</span>';
434 }
435 if (!empty($msgArr)) {
436 $msg = implode($msgArr, '<hr />');
437 }
438 $item .= '<td valign="top" class="' . $logRowClass . '" style="text-align:left;">' . $this->fw($msg) . '</td>';
439 $out .= '<tr class="typo3-adminPanel-itemRow">' . $item . '</tr>';
440 $c++;
441 }
442 $out = '<table class="typo3-adminPanel-table typo3-adminPanel-tsLog">' . $out . '</table>';
443 return $out;
444 }
445
446 /**
447 * Recursively generates the content to display
448 *
449 * @param array $arr Array which is modified with content. Reference
450 * @param string $content Current content string for the level
451 * @param string $depthData Prefixed icons for new PM icons
452 * @param bool $first Set this for the first call from outside.
453 * @param string $vKey Seems to be the previous tsStackLog key
454 * @return string Returns the $content string generated/modified. Also the $arr array is modified!
455 */
456 protected function fixContent(&$arr, $content, $depthData = '', $first = 0, $vKey = '')
457 {
458 $ac = 0;
459 $c = 0;
460 // First, find number of entries
461 foreach ($arr as $k => $v) {
462 if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($k)) {
463 $ac++;
464 }
465 }
466 // Traverse through entries
467 $subtime = 0;
468 foreach ($arr as $k => $v) {
469 if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($k)) {
470 $c++;
471 $deeper = is_array($arr[$k . '.']) ? 1 : 0;
472 $LN = $ac == $c ? 'blank' : 'line';
473
474 $BTM = $ac == $c ? 'bottom' : '';
475 $PM = is_array($arr[$k . '.']) ? '<i class="fa fa-' . ($deeper ? 'minus' : 'plus') . '-square-o"></i>' : '<span class="treeline-icon treeline-icon-join' . ($BTM ? 'bottom' : '') . '"></span>';
476
477 $this->tsStackLog[$v]['icons'] = $depthData . ($first ? '' : $PM);
478 if ($this->tsStackLog[$v]['content'] !== '') {
479 $content = str_replace($this->tsStackLog[$v]['content'], $v, $content);
480 }
481 if (is_array($arr[$k . '.'])) {
482 $this->tsStackLog[$v]['content'] = $this->fixContent($arr[$k . '.'], $this->tsStackLog[$v]['content'], $depthData . ($first ? '' : '<span class="treeline-icon treeline-icon-' . $LN . '"></span>'), 0, $v);
483 } else {
484 $this->tsStackLog[$v]['content'] = $this->fixCLen($this->tsStackLog[$v]['content'], $this->tsStackLog[$v]['value']);
485 $this->tsStackLog[$v]['subtime'] = '';
486 $this->tsStackLog[$v]['owntime'] = $this->tsStackLog[$v]['deltatime'];
487 }
488 $subtime += $this->tsStackLog[$v]['deltatime'];
489 }
490 }
491 // Set content with special chars
492 if (isset($this->tsStackLog[$vKey])) {
493 $this->tsStackLog[$vKey]['subtime'] = $subtime;
494 $this->tsStackLog[$vKey]['owntime'] = $this->tsStackLog[$vKey]['deltatime'] - $subtime;
495 }
496 $content = $this->fixCLen($content, $this->tsStackLog[$vKey]['value']);
497 // Traverse array again, this time substitute the unique hash with the red key
498 foreach ($arr as $k => $v) {
499 if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($k)) {
500 if ($this->tsStackLog[$v]['content'] !== '') {
501 $content = str_replace($v, '<strong style="color:red;">[' . $this->tsStackLog[$v]['key'] . ']</strong>', $content);
502 }
503 }
504 }
505 // Return the content
506 return $content;
507 }
508
509 /**
510 * Wraps the input content string in green colored span-tags IF the length o fthe input string exceeds $this->printConf['contentLength'] (or $this->printConf['contentLength_FILE'] if $v == "FILE"
511 *
512 * @param string $c The content string
513 * @param string $v Command: If "FILE" then $this->printConf['contentLength_FILE'] is used for content length comparison, otherwise $this->printConf['contentLength']
514 * @return string
515 */
516 protected function fixCLen($c, $v)
517 {
518 $len = $v == 'FILE' ? $this->printConf['contentLength_FILE'] : $this->printConf['contentLength'];
519 if (strlen($c) > $len) {
520 $c = '<span style="color:green;">' . htmlspecialchars(GeneralUtility::fixed_lgd_cs($c, $len)) . '</span>';
521 } else {
522 $c = htmlspecialchars($c);
523 }
524 return $c;
525 }
526
527 /**
528 * Wraps input string in a <span> tag
529 *
530 * @param string $str The string to be wrapped
531 * @return string
532 */
533 protected function fw($str)
534 {
535 return '<span>' . $str . '</span>';
536 }
537
538 /**
539 * Helper function for internal data manipulation
540 *
541 * @param array $arr Array (passed by reference) and modified
542 * @param int $pointer Pointer value
543 * @param string $uniqueId Unique ID string
544 * @return void
545 * @access private
546 * @see printTSlog()
547 */
548 protected function createHierarchyArray(&$arr, $pointer, $uniqueId)
549 {
550 if (!is_array($arr)) {
551 $arr = array();
552 }
553 if ($pointer > 0) {
554 end($arr);
555 $k = key($arr);
556 $this->createHierarchyArray($arr[(int)$k . '.'], $pointer - 1, $uniqueId);
557 } else {
558 $arr[] = $uniqueId;
559 }
560 }
561 }