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