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