Completed
Branch master (939199)
by
unknown
39:35
created

includes/libs/XhprofData.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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 RunningStat\RunningStat;
0 ignored issues
show
This use statement conflicts with another class in this namespace, RunningStat.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
22
23
/**
24
 * Convenience class for working with XHProf profiling data
25
 * <https://github.com/phacility/xhprof>. XHProf can be installed as a PECL
26
 * package for use with PHP5 (Zend PHP) and is built-in to HHVM 3.3.0.
27
 *
28
 * @author Bryan Davis <[email protected]>
29
 * @copyright © 2014 Bryan Davis and Wikimedia Foundation.
30
 * @since 1.28
31
 */
32
class XhprofData {
33
34
	/**
35
	 * @var array $config
36
	 */
37
	protected $config;
38
39
	/**
40
	 * Hierarchical profiling data returned by xhprof.
41
	 * @var array $hieraData
42
	 */
43
	protected $hieraData;
44
45
	/**
46
	 * Per-function inclusive data.
47
	 * @var array $inclusive
48
	 */
49
	protected $inclusive;
50
51
	/**
52
	 * Per-function inclusive and exclusive data.
53
	 * @var array $complete
54
	 */
55
	protected $complete;
56
57
	/**
58
	 * Configuration data can contain:
59
	 * - include: Array of function names to include in profiling.
60
	 * - sort:    Key to sort per-function reports on.
61
	 *
62
	 * @param array $data Xhprof profiling data, as returned by xhprof_disable()
63
	 * @param array $config
64
	 */
65
	public function __construct( array $data, array $config = [] ) {
66
		$this->config = array_merge( [
67
			'include' => null,
68
			'sort' => 'wt',
69
		], $config );
70
71
		$this->hieraData = $this->pruneData( $data );
72
	}
73
74
	/**
75
	 * Get raw data collected by xhprof.
76
	 *
77
	 * Each key in the returned array is an edge label for the call graph in
78
	 * the form "caller==>callee". There is once special case edge labled
79
	 * simply "main()" which represents the global scope entry point of the
80
	 * application.
81
	 *
82
	 * XHProf will collect different data depending on the flags that are used:
83
	 * - ct:    Number of matching events seen.
84
	 * - wt:    Inclusive elapsed wall time for this event in microseconds.
85
	 * - cpu:   Inclusive elapsed cpu time for this event in microseconds.
86
	 *          (XHPROF_FLAGS_CPU)
87
	 * - mu:    Delta of memory usage from start to end of callee in bytes.
88
	 *          (XHPROF_FLAGS_MEMORY)
89
	 * - pmu:   Delta of peak memory usage from start to end of callee in
90
	 *          bytes. (XHPROF_FLAGS_MEMORY)
91
	 * - alloc: Delta of amount memory requested from malloc() by the callee,
92
	 *          in bytes. (XHPROF_FLAGS_MALLOC)
93
	 * - free:  Delta of amount of memory passed to free() by the callee, in
94
	 *          bytes. (XHPROF_FLAGS_MALLOC)
95
	 *
96
	 * @return array
97
	 * @see getInclusiveMetrics()
98
	 * @see getCompleteMetrics()
99
	 */
100
	public function getRawData() {
101
		return $this->hieraData;
102
	}
103
104
	/**
105
	 * Convert an xhprof data key into an array of ['parent', 'child']
106
	 * function names.
107
	 *
108
	 * The resulting array is left padded with nulls, so a key
109
	 * with no parent (eg 'main()') will return [null, 'function'].
110
	 *
111
	 * @return array
112
	 */
113
	public static function splitKey( $key ) {
114
		return array_pad( explode( '==>', $key, 2 ), -2, null );
115
	}
116
117
	/**
118
	 * Remove data for functions that are not included in the 'include'
119
	 * configuration array.
120
	 *
121
	 * @param array $data Raw xhprof data
122
	 * @return array
123
	 */
124
	protected function pruneData( $data ) {
125
		if ( !$this->config['include'] ) {
126
			return $data;
127
		}
128
129
		$want = array_fill_keys( $this->config['include'], true );
130
		$want['main()'] = true;
131
132
		$keep = [];
133
		foreach ( $data as $key => $stats ) {
134
			list( $parent, $child ) = self::splitKey( $key );
135
			if ( isset( $want[$parent] ) || isset( $want[$child] ) ) {
136
				$keep[$key] = $stats;
137
			}
138
		}
139
		return $keep;
140
	}
141
142
	/**
143
	 * Get the inclusive metrics for each function call. Inclusive metrics
144
	 * for given function include the metrics for all functions that were
145
	 * called from that function during the measurement period.
146
	 *
147
	 * See getRawData() for a description of the metric that are returned for
148
	 * each funcition call. The values for the wt, cpu, mu and pmu metrics are
149
	 * arrays with these values:
150
	 * - total: Cumulative value
151
	 * - min: Minimum value
152
	 * - mean: Mean (average) value
153
	 * - max: Maximum value
154
	 * - variance: Variance (spread) of the values
155
	 *
156
	 * @return array
157
	 * @see getRawData()
158
	 * @see getCompleteMetrics()
159
	 */
160
	public function getInclusiveMetrics() {
161
		if ( $this->inclusive === null ) {
162
			$main = $this->hieraData['main()'];
163
			$hasCpu = isset( $main['cpu'] );
164
			$hasMu = isset( $main['mu'] );
165
			$hasAlloc = isset( $main['alloc'] );
166
167
			$this->inclusive = [];
168
			foreach ( $this->hieraData as $key => $stats ) {
169
				list( $parent, $child ) = self::splitKey( $key );
0 ignored issues
show
The assignment to $parent is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
170
				if ( !isset( $this->inclusive[$child] ) ) {
171
					$this->inclusive[$child] = [
172
						'ct' => 0,
173
						'wt' => new RunningStat(),
174
					];
175
					if ( $hasCpu ) {
176
						$this->inclusive[$child]['cpu'] = new RunningStat();
177
					}
178
					if ( $hasMu ) {
179
						$this->inclusive[$child]['mu'] = new RunningStat();
180
						$this->inclusive[$child]['pmu'] = new RunningStat();
181
					}
182
					if ( $hasAlloc ) {
183
						$this->inclusive[$child]['alloc'] = new RunningStat();
184
						$this->inclusive[$child]['free'] = new RunningStat();
185
					}
186
				}
187
188
				$this->inclusive[$child]['ct'] += $stats['ct'];
189
				foreach ( $stats as $stat => $value ) {
190
					if ( $stat === 'ct' ) {
191
						continue;
192
					}
193
194
					if ( !isset( $this->inclusive[$child][$stat] ) ) {
195
						// Ignore unknown stats
196
						continue;
197
					}
198
199
					for ( $i = 0; $i < $stats['ct']; $i++ ) {
200
						$this->inclusive[$child][$stat]->addObservation(
201
							$value / $stats['ct']
202
						);
203
					}
204
				}
205
			}
206
207
			// Convert RunningStat instances to static arrays and add
208
			// percentage stats.
209
			foreach ( $this->inclusive as $func => $stats ) {
210
				foreach ( $stats as $name => $value ) {
211
					if ( $value instanceof RunningStat ) {
212
						$total = $value->m1 * $value->n;
213
						$percent = ( isset( $main[$name] ) && $main[$name] )
214
							? 100 * $total / $main[$name]
215
							: 0;
216
						$this->inclusive[$func][$name] = [
217
							'total' => $total,
218
							'min' => $value->min,
219
							'mean' => $value->m1,
220
							'max' => $value->max,
221
							'variance' => $value->m2,
222
							'percent' => $percent,
223
						];
224
					}
225
				}
226
			}
227
228
			uasort( $this->inclusive, self::makeSortFunction(
229
				$this->config['sort'], 'total'
230
			) );
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 sapce 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
				list( $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 View Code Duplication
	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 View Code Duplication
	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 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
				// Assumes all values are numeric.
374
				// Values for 'main()' will not have sub keys
375
				$valA = is_array( $a[$key] ) ? $a[$key][$sub] : $a[$key];
376
				$valB = is_array( $b[$key] ) ? $b[$key][$sub] : $b[$key];
377
				return $valB - $valA;
378
			} else {
379
				// Sort datum with the key before those without
380
				return isset( $a[$key] ) ? -1 : 1;
381
			}
382
		};
383
	}
384
}
385