MediaWiki  master
XhprofData.php
Go to the documentation of this file.
1 <?php
21 use Wikimedia\RunningStat;
22 
30 class XhprofData {
31 
35  protected $config;
36 
41  protected $hieraData;
42 
47  protected $inclusive;
48 
53  protected $complete;
54 
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 
98  public function getRawData() {
99  return $this->hieraData;
100  }
101 
112  public static function splitKey( $key ) {
113  return array_pad( explode( '==>', $key, 2 ), -2, null );
114  }
115 
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 
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 
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  [ $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 
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 
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 
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 
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 }
Convenience class for working with XHProf profiling data https://github.com/phacility/xhprof.
Definition: XhprofData.php:30
getCriticalPath( $metric='wt')
Find the critical path for the given metric.
Definition: XhprofData.php:334
array[] $complete
Per-function inclusive and exclusive data.
Definition: XhprofData.php:53
pruneData( $data)
Remove data for functions that are not included in the 'include' configuration array.
Definition: XhprofData.php:123
getInclusiveMetrics()
Get the inclusive metrics for each function call.
Definition: XhprofData.php:159
array[][] $inclusive
Per-function inclusive data.
Definition: XhprofData.php:47
static makeSortFunction( $key, $sub)
Make a closure to use as a sort function.
Definition: XhprofData.php:369
getCallees( $function)
Get a list of all callees from a given function.
Definition: XhprofData.php:319
getRawData()
Get raw data collected by xhprof.
Definition: XhprofData.php:98
__construct(array $data, array $config=[])
Configuration data can contain:
Definition: XhprofData.php:63
array $config
Definition: XhprofData.php:35
array[] $hieraData
Hierarchical profiling data returned by xhprof.
Definition: XhprofData.php:41
getCallers( $function)
Get a list of all callers of a given function.
Definition: XhprofData.php:303
static splitKey( $key)
Convert an xhprof data key into an array of ['parent', 'child'] function names.
Definition: XhprofData.php:112
getCompleteMetrics()
Get the inclusive and exclusive metrics for each function call.
Definition: XhprofData.php:246