MediaWiki master
XhprofData.php
Go to the documentation of this file.
1<?php
7namespace Wikimedia;
8
9use Closure;
10
19
23 protected $config;
24
29 protected $hieraData;
30
35 protected $inclusive;
36
41 protected $complete;
42
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
86 public function getRawData() {
87 return $this->hieraData;
88 }
89
100 public static function splitKey( $key ) {
101 return array_pad( explode( '==>', $key, 2 ), -2, null );
102 }
103
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
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
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
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
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
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
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
374class_alias( XhprofData::class, 'XhprofData' );
Convenience class for working with XHProf profiling data https://github.com/phacility/xhprof.
static makeSortFunction( $key, $sub)
Make a closure to use as a sort function.
getCriticalPath( $metric='wt')
Find the critical path for the given metric.
__construct(array $data, array $config=[])
Configuration data can contain:
getInclusiveMetrics()
Get the inclusive metrics for each function call.
getCallers( $function)
Get a list of all callers of a given function.
pruneData( $data)
Remove data for functions that are not included in the 'include' configuration array.
array[][] $inclusive
Per-function inclusive data.
array[] $hieraData
Hierarchical profiling data returned by xhprof.
getRawData()
Get raw data collected by xhprof.
getCompleteMetrics()
Get the inclusive and exclusive metrics for each function call.
array[] $complete
Per-function inclusive and exclusive data.
static splitKey( $key)
Convert an xhprof data key into an array of ['parent', 'child'] function names.
getCallees( $function)
Get a list of all callees from a given function.