Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
94.49% |
120 / 127 |
|
50.00% |
5 / 10 |
CRAP | |
0.00% |
0 / 1 |
XhprofData | |
94.49% |
120 / 127 |
|
50.00% |
5 / 10 |
51.44 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
getRawData | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
splitKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
pruneData | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
5 | |||
getInclusiveMetrics | |
94.12% |
48 / 51 |
|
0.00% |
0 / 1 |
16.05 | |||
getCompleteMetrics | |
96.00% |
24 / 25 |
|
0.00% |
0 / 1 |
11 | |||
getCallers | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
getCallees | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
getCriticalPath | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
6 | |||
makeSortFunction | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
6.10 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | use Wikimedia\RunningStat; |
22 | |
23 | /** |
24 | * Convenience class for working with XHProf profiling data |
25 | * <https://github.com/phacility/xhprof>. XHProf can be installed via PECL. |
26 | * |
27 | * @copyright © 2014 Wikimedia Foundation and contributors |
28 | * @since 1.28 |
29 | */ |
30 | class XhprofData { |
31 | |
32 | /** |
33 | * @var array |
34 | */ |
35 | protected $config; |
36 | |
37 | /** |
38 | * Hierarchical profiling data returned by xhprof. |
39 | * @var array[] |
40 | */ |
41 | protected $hieraData; |
42 | |
43 | /** |
44 | * Per-function inclusive data. |
45 | * @var array[][] |
46 | */ |
47 | protected $inclusive; |
48 | |
49 | /** |
50 | * Per-function inclusive and exclusive data. |
51 | * @var array[] |
52 | */ |
53 | protected $complete; |
54 | |
55 | /** |
56 | * Configuration data can contain: |
57 | * - include: Array of function names to include in profiling. |
58 | * - sort: Key to sort per-function reports on. |
59 | * |
60 | * @param array $data Xhprof profiling data, as returned by xhprof_disable() |
61 | * @param array $config |
62 | */ |
63 | public function __construct( array $data, array $config = [] ) { |
64 | $this->config = array_merge( [ |
65 | 'include' => null, |
66 | 'sort' => 'wt', |
67 | ], $config ); |
68 | |
69 | $this->hieraData = $this->pruneData( $data ); |
70 | } |
71 | |
72 | /** |
73 | * Get raw data collected by xhprof. |
74 | * |
75 | * Each key in the returned array is an edge label for the call graph in |
76 | * the form "caller==>callee". There is once special case edge labeled |
77 | * simply "main()" which represents the global scope entry point of the |
78 | * application. |
79 | * |
80 | * XHProf will collect different data depending on the flags that are used: |
81 | * - ct: Number of matching events seen. |
82 | * - wt: Inclusive elapsed wall time for this event in microseconds. |
83 | * - cpu: Inclusive elapsed cpu time for this event in microseconds. |
84 | * (XHPROF_FLAGS_CPU) |
85 | * - mu: Delta of memory usage from start to end of callee in bytes. |
86 | * (XHPROF_FLAGS_MEMORY) |
87 | * - pmu: Delta of peak memory usage from start to end of callee in |
88 | * bytes. (XHPROF_FLAGS_MEMORY) |
89 | * - alloc: Delta of amount memory requested from malloc() by the callee, |
90 | * in bytes. (XHPROF_FLAGS_MALLOC) |
91 | * - free: Delta of amount of memory passed to free() by the callee, in |
92 | * bytes. (XHPROF_FLAGS_MALLOC) |
93 | * |
94 | * @return array |
95 | * @see getInclusiveMetrics() |
96 | * @see getCompleteMetrics() |
97 | */ |
98 | public function getRawData() { |
99 | return $this->hieraData; |
100 | } |
101 | |
102 | /** |
103 | * Convert an xhprof data key into an array of ['parent', 'child'] |
104 | * function names. |
105 | * |
106 | * The resulting array is left padded with nulls, so a key |
107 | * with no parent (eg 'main()') will return [null, 'function']. |
108 | * |
109 | * @param string $key |
110 | * @return array |
111 | */ |
112 | public static function splitKey( $key ) { |
113 | return array_pad( explode( '==>', $key, 2 ), -2, null ); |
114 | } |
115 | |
116 | /** |
117 | * Remove data for functions that are not included in the 'include' |
118 | * configuration array. |
119 | * |
120 | * @param array[] $data Raw xhprof data |
121 | * @return array[] |
122 | */ |
123 | protected function pruneData( $data ) { |
124 | if ( !$this->config['include'] ) { |
125 | return $data; |
126 | } |
127 | |
128 | $want = array_fill_keys( $this->config['include'], true ); |
129 | $want['main()'] = true; |
130 | |
131 | $keep = []; |
132 | foreach ( $data as $key => $stats ) { |
133 | [ $parent, $child ] = self::splitKey( $key ); |
134 | if ( isset( $want[$parent] ) || isset( $want[$child] ) ) { |
135 | $keep[$key] = $stats; |
136 | } |
137 | } |
138 | return $keep; |
139 | } |
140 | |
141 | /** |
142 | * Get the inclusive metrics for each function call. Inclusive metrics |
143 | * for given function include the metrics for all functions that were |
144 | * called from that function during the measurement period. |
145 | * |
146 | * See getRawData() for a description of the metric that are returned for |
147 | * each function call. The values for the wt, cpu, mu and pmu metrics are |
148 | * arrays with these values: |
149 | * - total: Cumulative value |
150 | * - min: Minimum value |
151 | * - mean: Mean (average) value |
152 | * - max: Maximum value |
153 | * - variance: Variance (spread) of the values |
154 | * |
155 | * @return array[][] |
156 | * @see getRawData() |
157 | * @see getCompleteMetrics() |
158 | */ |
159 | public function getInclusiveMetrics() { |
160 | if ( $this->inclusive === null ) { |
161 | $main = $this->hieraData['main()']; |
162 | $hasCpu = isset( $main['cpu'] ); |
163 | $hasMu = isset( $main['mu'] ); |
164 | $hasAlloc = isset( $main['alloc'] ); |
165 | |
166 | $inclusive = []; |
167 | foreach ( $this->hieraData as $key => $stats ) { |
168 | [ , $child ] = self::splitKey( $key ); |
169 | if ( !isset( $inclusive[$child] ) ) { |
170 | $inclusive[$child] = [ |
171 | 'ct' => 0, |
172 | 'wt' => new RunningStat(), |
173 | ]; |
174 | if ( $hasCpu ) { |
175 | $inclusive[$child]['cpu'] = new RunningStat(); |
176 | } |
177 | if ( $hasMu ) { |
178 | $inclusive[$child]['mu'] = new RunningStat(); |
179 | $inclusive[$child]['pmu'] = new RunningStat(); |
180 | } |
181 | if ( $hasAlloc ) { |
182 | $inclusive[$child]['alloc'] = new RunningStat(); |
183 | $inclusive[$child]['free'] = new RunningStat(); |
184 | } |
185 | } |
186 | |
187 | $inclusive[$child]['ct'] += $stats['ct']; |
188 | foreach ( $stats as $stat => $value ) { |
189 | if ( $stat === 'ct' ) { |
190 | continue; |
191 | } |
192 | |
193 | if ( !isset( $inclusive[$child][$stat] ) ) { |
194 | // Ignore unknown stats |
195 | continue; |
196 | } |
197 | |
198 | for ( $i = 0; $i < $stats['ct']; $i++ ) { |
199 | $inclusive[$child][$stat]->addObservation( |
200 | $value / $stats['ct'] |
201 | ); |
202 | } |
203 | } |
204 | } |
205 | |
206 | // Convert RunningStat instances to static arrays and add |
207 | // percentage stats. |
208 | foreach ( $inclusive as $func => $stats ) { |
209 | foreach ( $stats as $name => $value ) { |
210 | if ( $value instanceof RunningStat ) { |
211 | $total = $value->getMean() * $value->getCount(); |
212 | $percent = ( isset( $main[$name] ) && $main[$name] ) |
213 | ? 100 * $total / $main[$name] |
214 | : 0; |
215 | $inclusive[$func][$name] = [ |
216 | 'total' => $total, |
217 | 'min' => $value->min, |
218 | 'mean' => $value->getMean(), |
219 | 'max' => $value->max, |
220 | 'variance' => $value->m2, |
221 | 'percent' => $percent, |
222 | ]; |
223 | } |
224 | } |
225 | } |
226 | |
227 | uasort( $inclusive, self::makeSortFunction( |
228 | $this->config['sort'], 'total' |
229 | ) ); |
230 | $this->inclusive = $inclusive; |
231 | } |
232 | return $this->inclusive; |
233 | } |
234 | |
235 | /** |
236 | * Get the inclusive and exclusive metrics for each function call. |
237 | * |
238 | * In addition to the normal data contained in the inclusive metrics, the |
239 | * metrics have an additional 'exclusive' measurement which is the total |
240 | * minus the totals of all child function calls. |
241 | * |
242 | * @return array[] |
243 | * @see getRawData() |
244 | * @see getInclusiveMetrics() |
245 | */ |
246 | public function getCompleteMetrics() { |
247 | if ( $this->complete === null ) { |
248 | // Start with inclusive data |
249 | $this->complete = $this->getInclusiveMetrics(); |
250 | |
251 | foreach ( $this->complete as $func => $stats ) { |
252 | foreach ( $stats as $stat => $value ) { |
253 | if ( $stat === 'ct' ) { |
254 | continue; |
255 | } |
256 | // Initialize exclusive data with inclusive totals |
257 | $this->complete[$func][$stat]['exclusive'] = $value['total']; |
258 | } |
259 | // Add space for call tree information to be filled in later |
260 | $this->complete[$func]['calls'] = []; |
261 | $this->complete[$func]['subcalls'] = []; |
262 | } |
263 | |
264 | foreach ( $this->hieraData as $key => $stats ) { |
265 | [ $parent, $child ] = self::splitKey( $key ); |
266 | if ( $parent !== null ) { |
267 | // Track call tree information |
268 | $this->complete[$child]['calls'][$parent] = $stats; |
269 | $this->complete[$parent]['subcalls'][$child] = $stats; |
270 | } |
271 | |
272 | if ( isset( $this->complete[$parent] ) ) { |
273 | // Deduct child inclusive data from exclusive data |
274 | foreach ( $stats as $stat => $value ) { |
275 | if ( $stat === 'ct' ) { |
276 | continue; |
277 | } |
278 | |
279 | if ( !isset( $this->complete[$parent][$stat] ) ) { |
280 | // Ignore unknown stats |
281 | continue; |
282 | } |
283 | |
284 | $this->complete[$parent][$stat]['exclusive'] -= $value; |
285 | } |
286 | } |
287 | } |
288 | |
289 | uasort( $this->complete, self::makeSortFunction( |
290 | $this->config['sort'], 'exclusive' |
291 | ) ); |
292 | } |
293 | return $this->complete; |
294 | } |
295 | |
296 | /** |
297 | * Get a list of all callers of a given function. |
298 | * |
299 | * @param string $function Function name |
300 | * @return array |
301 | * @see getEdges() |
302 | */ |
303 | public function getCallers( $function ) { |
304 | $edges = $this->getCompleteMetrics(); |
305 | if ( isset( $edges[$function]['calls'] ) ) { |
306 | return array_keys( $edges[$function]['calls'] ); |
307 | } else { |
308 | return []; |
309 | } |
310 | } |
311 | |
312 | /** |
313 | * Get a list of all callees from a given function. |
314 | * |
315 | * @param string $function Function name |
316 | * @return array |
317 | * @see getEdges() |
318 | */ |
319 | public function getCallees( $function ) { |
320 | $edges = $this->getCompleteMetrics(); |
321 | if ( isset( $edges[$function]['subcalls'] ) ) { |
322 | return array_keys( $edges[$function]['subcalls'] ); |
323 | } else { |
324 | return []; |
325 | } |
326 | } |
327 | |
328 | /** |
329 | * Find the critical path for the given metric. |
330 | * |
331 | * @param string $metric Metric to find critical path for |
332 | * @return array |
333 | */ |
334 | public function getCriticalPath( $metric = 'wt' ) { |
335 | $func = 'main()'; |
336 | $path = [ |
337 | $func => $this->hieraData[$func], |
338 | ]; |
339 | while ( $func ) { |
340 | $callees = $this->getCallees( $func ); |
341 | $maxCallee = null; |
342 | $maxCall = null; |
343 | foreach ( $callees as $callee ) { |
344 | $call = "{$func}==>{$callee}"; |
345 | if ( $maxCall === null || |
346 | $this->hieraData[$call][$metric] > |
347 | $this->hieraData[$maxCall][$metric] |
348 | ) { |
349 | $maxCallee = $callee; |
350 | $maxCall = $call; |
351 | } |
352 | } |
353 | if ( $maxCall !== null ) { |
354 | $path[$maxCall] = $this->hieraData[$maxCall]; |
355 | } |
356 | $func = $maxCallee; |
357 | } |
358 | return $path; |
359 | } |
360 | |
361 | /** |
362 | * Make a closure to use as a sort function. The resulting function will |
363 | * sort by descending numeric values (largest value first). |
364 | * |
365 | * @param string $key Data key to sort on |
366 | * @param string $sub Sub key to sort array values on |
367 | * @return Closure |
368 | */ |
369 | public static function makeSortFunction( $key, $sub ) { |
370 | return static function ( $a, $b ) use ( $key, $sub ) { |
371 | if ( isset( $a[$key] ) && isset( $b[$key] ) ) { |
372 | // Descending sort: larger values will be first in result. |
373 | // Values for 'main()' will not have sub keys |
374 | $valA = is_array( $a[$key] ) ? $a[$key][$sub] : $a[$key]; |
375 | $valB = is_array( $b[$key] ) ? $b[$key][$sub] : $b[$key]; |
376 | return $valB <=> $valA; |
377 | } else { |
378 | // Sort datum with the key before those without |
379 | return isset( $a[$key] ) ? -1 : 1; |
380 | } |
381 | }; |
382 | } |
383 | } |