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