Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.49% covered (success)
94.49%
120 / 127
50.00% covered (danger)
50.00%
5 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
XhprofData
94.49% covered (success)
94.49%
120 / 127
50.00% covered (danger)
50.00%
5 / 10
51.44
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
5
 getInclusiveMetrics
94.12% covered (success)
94.12%
48 / 51
0.00% covered (danger)
0.00%
0 / 1
16.05
 getCompleteMetrics
96.00% covered (success)
96.00%
24 / 25
0.00% covered (danger)
0.00%
0 / 1
11
 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 * 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
21use 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 */
30class 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}