1<?php 2 3namespace Elgg; 4 5use Elgg\Project\Paths; 6 7/** 8 * Analyzes duration of functions, queries, and processes 9 * 10 * @internal 11 */ 12class Profiler { 13 14 public $percentage_format = "%01.2f"; 15 public $duration_format = "%01.6f"; 16 public $minimum_percentage = 0.2; 17 18 /** 19 * @var float Total time 20 */ 21 private $total; 22 23 /** 24 * Return a tree of time periods from a Timer 25 * 26 * @param Timer $timer Timer object 27 * @return false|array 28 */ 29 public function buildTree(Timer $timer) { 30 $times = $timer->getTimes(); 31 32 if (!isset($times[Timer::MARKER_END])) { 33 $times[Timer::MARKER_END] = microtime(true); 34 } 35 36 $begin = $this->findBeginTime($times); 37 $end = $this->findEndTime($times); 38 $this->total = $this->diffMicrotime($begin, $end); 39 40 return $this->analyzePeriod('', $times); 41 } 42 43 /** 44 * Turn the tree of times into a sorted list 45 * 46 * @param array $list Output list of times to populate 47 * @param array $tree Result of buildTree() 48 * @param string $prefix Prefix of period string. Leave empty. 49 * @return void 50 */ 51 public function flattenTree(array &$list = [], array $tree = [], $prefix = '') { 52 $is_root = empty($list); 53 54 if (isset($tree['periods'])) { 55 foreach ($tree['periods'] as $period) { 56 $this->flattenTree($list, $period, "{$prefix} {$period['name']}"); 57 } 58 unset($tree['periods']); 59 } 60 $tree['name'] = trim($prefix); 61 $list[] = $tree; 62 63 if ($is_root) { 64 usort($list, function ($a, $b) { 65 if ($a['duration'] == $b['duration']) { 66 return 0; 67 } 68 return ($a['duration'] > $b['duration']) ? -1 : 1; 69 }); 70 } 71 } 72 73 /** 74 * Nicely format the elapsed time values 75 * 76 * @param array $tree Result of buildTree() 77 * @return array 78 */ 79 public function formatTree(array $tree) { 80 $tree['duration'] = sprintf($this->duration_format, $tree['duration']); 81 if (isset($tree['percentage'])) { 82 $tree['percentage'] = sprintf($this->percentage_format, $tree['percentage']); 83 } 84 if (isset($tree['periods'])) { 85 $tree['periods'] = array_map([$this, 'formatTree'], $tree['periods']); 86 } 87 return $tree; 88 } 89 90 /** 91 * Append a SCRIPT element to the page output 92 * 93 * @param \Elgg\Hook $hook "output", "page" 94 * 95 * @return string 96 */ 97 public static function handlePageOutput(\Elgg\Hook $hook) { 98 $profiler = new self(); 99 $min_percentage = _elgg_config()->profiling_minimum_percentage; 100 if ($min_percentage !== null) { 101 $profiler->minimum_percentage = $min_percentage; 102 } 103 104 $tree = $profiler->buildTree(_elgg_services()->timer); 105 $tree = $profiler->formatTree($tree); 106 $data = [ 107 'tree' => $tree, 108 'total' => $tree['duration'] . " seconds", 109 ]; 110 111 $list = []; 112 $profiler->flattenTree($list, $tree); 113 114 $root = Paths::project(); 115 $list = array_map(function ($period) use ($root) { 116 $period['name'] = str_replace("Closure $root", "Closure ", $period['name']); 117 return "{$period['percentage']}% ({$period['duration']}) {$period['name']}"; 118 }, $list); 119 120 $data['list'] = $list; 121 122 $html = $hook->getValue(); 123 $html .= "<script>console.log(" . json_encode($data) . ");</script>"; 124 125 return $html; 126 } 127 128 /** 129 * Analyze a time period 130 * 131 * @param string $name Period name 132 * @param array $times Times 133 * 134 * @return false|array False if missing begin/end time 135 */ 136 private function analyzePeriod($name, array $times) { 137 $begin = $this->findBeginTime($times); 138 $end = $this->findEndTime($times); 139 if ($begin === false || $end === false) { 140 return false; 141 } 142 $has_own_markers = isset($times[Timer::MARKER_BEGIN]) && isset($times[Timer::MARKER_BEGIN]); 143 unset($times[Timer::MARKER_BEGIN], $times[Timer::MARKER_END]); 144 145 $total = $this->diffMicrotime($begin, $end); 146 $ret = [ 147 'name' => $name, 148 'percentage' => 100, // may be overwritten by parent 149 'duration' => $total, 150 ]; 151 152 foreach ($times as $times_key => $period) { 153 $period = $this->analyzePeriod($times_key, $period); 154 if ($period === false) { 155 continue; 156 } 157 $period['percentage'] = 100 * $period['duration'] / $this->total; 158 if ($period['percentage'] < $this->minimum_percentage) { 159 continue; 160 } 161 $ret['periods'][] = $period; 162 } 163 164 if (isset($ret['periods'])) { 165 if (!$has_own_markers) { 166 // this is an aggregation of different non sequential timers (eg. SQL queries) 167 $ret['duration'] = 0; 168 foreach ($ret['periods'] as $period) { 169 $ret['duration'] += $period['duration']; 170 } 171 $ret['percentage'] = 100 * $ret['duration'] / $this->total; 172 } 173 174 usort($ret['periods'], function ($a, $b) { 175 if ($a['duration'] == $b['duration']) { 176 return 0; 177 } 178 return ($a['duration'] > $b['duration']) ? -1 : 1; 179 }); 180 } 181 182 return $ret; 183 } 184 185 /** 186 * Get the microtime start time 187 * 188 * @param array $times Time periods 189 * @return float|false 190 */ 191 private function findBeginTime(array $times) { 192 if (isset($times[Timer::MARKER_BEGIN])) { 193 return $times[Timer::MARKER_BEGIN]; 194 } 195 unset($times[Timer::MARKER_BEGIN], $times[Timer::MARKER_END]); 196 $first = reset($times); 197 if (is_array($first)) { 198 return $this->findBeginTime($first); 199 } 200 return false; 201 } 202 203 /** 204 * Get the microtime end time 205 * 206 * @param array $times Time periods 207 * @return float|false 208 */ 209 private function findEndTime(array $times) { 210 if (isset($times[Timer::MARKER_END])) { 211 return $times[Timer::MARKER_END]; 212 } 213 unset($times[Timer::MARKER_BEGIN], $times[Timer::MARKER_END]); 214 $last = end($times); 215 if (is_array($last)) { 216 return $this->findEndTime($last); 217 } 218 return false; 219 } 220 221 /** 222 * Calculate a precise time difference. 223 * 224 * @param float $start result of microtime(true) 225 * @param float $end result of microtime(true) 226 * 227 * @return float difference in seconds, calculated with minimum precision loss 228 */ 229 private function diffMicrotime($start, $end) { 230 return (float) $end - $start; 231 } 232} 233