MediaWiki master
XhprofData.php
Go to the documentation of this file.
1<?php
7use Wikimedia\RunningStat;
8
17
21 protected $config;
22
27 protected $hieraData;
28
33 protected $inclusive;
34
39 protected $complete;
40
49 public function __construct( array $data, array $config = [] ) {
50 $this->config = $config + [
51 'include' => null,
52 'sort' => 'wt',
53 ];
54
55 $this->hieraData = $this->pruneData( $data );
56 }
57
84 public function getRawData() {
85 return $this->hieraData;
86 }
87
98 public static function splitKey( $key ) {
99 return array_pad( explode( '==>', $key, 2 ), -2, null );
100 }
101
109 protected function pruneData( $data ) {
110 if ( !$this->config['include'] ) {
111 return $data;
112 }
113
114 $want = array_fill_keys( $this->config['include'], true );
115 $want['main()'] = true;
116
117 $keep = [];
118 foreach ( $data as $key => $stats ) {
119 [ $parent, $child ] = self::splitKey( $key );
120 if ( ( $parent !== null && isset( $want[$parent] ) ) || isset( $want[$child] ) ) {
121 $keep[$key] = $stats;
122 }
123 }
124 return $keep;
125 }
126
145 public function getInclusiveMetrics() {
146 if ( $this->inclusive === null ) {
147 $main = $this->hieraData['main()'];
148 $hasCpu = isset( $main['cpu'] );
149 $hasMu = isset( $main['mu'] );
150 $hasAlloc = isset( $main['alloc'] );
151
152 $inclusive = [];
153 foreach ( $this->hieraData as $key => $stats ) {
154 [ , $child ] = self::splitKey( $key );
155 if ( !isset( $inclusive[$child] ) ) {
156 $inclusive[$child] = [
157 'ct' => 0,
158 'wt' => new RunningStat(),
159 ];
160 if ( $hasCpu ) {
161 $inclusive[$child]['cpu'] = new RunningStat();
162 }
163 if ( $hasMu ) {
164 $inclusive[$child]['mu'] = new RunningStat();
165 $inclusive[$child]['pmu'] = new RunningStat();
166 }
167 if ( $hasAlloc ) {
168 $inclusive[$child]['alloc'] = new RunningStat();
169 $inclusive[$child]['free'] = new RunningStat();
170 }
171 }
172
173 $inclusive[$child]['ct'] += $stats['ct'];
174 foreach ( $stats as $stat => $value ) {
175 if ( $stat === 'ct' ) {
176 continue;
177 }
178
179 if ( !isset( $inclusive[$child][$stat] ) ) {
180 // Ignore unknown stats
181 continue;
182 }
183
184 for ( $i = 0; $i < $stats['ct']; $i++ ) {
185 $inclusive[$child][$stat]->addObservation(
186 $value / $stats['ct']
187 );
188 }
189 }
190 }
191
192 // Convert RunningStat instances to static arrays and add
193 // percentage stats.
194 foreach ( $inclusive as $func => $stats ) {
195 foreach ( $stats as $name => $value ) {
196 if ( $value instanceof RunningStat ) {
197 $total = $value->getMean() * $value->getCount();
198 $percent = ( isset( $main[$name] ) && $main[$name] )
199 ? 100 * $total / $main[$name]
200 : 0;
201 $inclusive[$func][$name] = [
202 'total' => $total,
203 'min' => $value->min,
204 'mean' => $value->getMean(),
205 'max' => $value->max,
206 'variance' => $value->m2,
207 'percent' => $percent,
208 ];
209 }
210 }
211 }
212
213 uasort( $inclusive, self::makeSortFunction(
214 $this->config['sort'], 'total'
215 ) );
216 $this->inclusive = $inclusive;
217 }
218 return $this->inclusive;
219 }
220
232 public function getCompleteMetrics() {
233 if ( $this->complete === null ) {
234 // Start with inclusive data
235 $this->complete = $this->getInclusiveMetrics();
236
237 foreach ( $this->complete as $func => $stats ) {
238 foreach ( $stats as $stat => $value ) {
239 if ( $stat === 'ct' ) {
240 continue;
241 }
242 // Initialize exclusive data with inclusive totals
243 $this->complete[$func][$stat]['exclusive'] = $value['total'];
244 }
245 // Add space for call tree information to be filled in later
246 $this->complete[$func]['calls'] = [];
247 $this->complete[$func]['subcalls'] = [];
248 }
249
250 foreach ( $this->hieraData as $key => $stats ) {
251 [ $parent, $child ] = self::splitKey( $key );
252 if ( $parent !== null ) {
253 // Track call tree information
254 $this->complete[$child]['calls'][$parent] = $stats;
255 $this->complete[$parent]['subcalls'][$child] = $stats;
256 }
257
258 if ( $parent !== null && isset( $this->complete[$parent] ) ) {
259 // Deduct child inclusive data from exclusive data
260 foreach ( $stats as $stat => $value ) {
261 if ( $stat === 'ct' ) {
262 continue;
263 }
264
265 if ( !isset( $this->complete[$parent][$stat] ) ) {
266 // Ignore unknown stats
267 continue;
268 }
269
270 $this->complete[$parent][$stat]['exclusive'] -= $value;
271 }
272 }
273 }
274
275 uasort( $this->complete, self::makeSortFunction(
276 $this->config['sort'], 'exclusive'
277 ) );
278 }
279 return $this->complete;
280 }
281
289 public function getCallers( $function ) {
290 $edges = $this->getCompleteMetrics();
291 if ( isset( $edges[$function]['calls'] ) ) {
292 return array_keys( $edges[$function]['calls'] );
293 } else {
294 return [];
295 }
296 }
297
305 public function getCallees( $function ) {
306 $edges = $this->getCompleteMetrics();
307 if ( isset( $edges[$function]['subcalls'] ) ) {
308 return array_keys( $edges[$function]['subcalls'] );
309 } else {
310 return [];
311 }
312 }
313
320 public function getCriticalPath( $metric = 'wt' ) {
321 $func = 'main()';
322 $path = [
323 $func => $this->hieraData[$func],
324 ];
325 while ( $func ) {
326 $callees = $this->getCallees( $func );
327 $maxCallee = null;
328 $maxCall = null;
329 foreach ( $callees as $callee ) {
330 $call = "{$func}==>{$callee}";
331 if ( $maxCall === null ||
332 $this->hieraData[$call][$metric] >
333 $this->hieraData[$maxCall][$metric]
334 ) {
335 $maxCallee = $callee;
336 $maxCall = $call;
337 }
338 }
339 if ( $maxCall !== null ) {
340 $path[$maxCall] = $this->hieraData[$maxCall];
341 }
342 $func = $maxCallee;
343 }
344 return $path;
345 }
346
355 public static function makeSortFunction( $key, $sub ) {
356 return static function ( $a, $b ) use ( $key, $sub ) {
357 if ( isset( $a[$key] ) && isset( $b[$key] ) ) {
358 // Descending sort: larger values will be first in result.
359 // Values for 'main()' will not have sub keys
360 $valA = is_array( $a[$key] ) ? $a[$key][$sub] : $a[$key];
361 $valB = is_array( $b[$key] ) ? $b[$key][$sub] : $b[$key];
362 return $valB <=> $valA;
363 } else {
364 // Sort datum with the key before those without
365 return isset( $a[$key] ) ? -1 : 1;
366 }
367 };
368 }
369}
Convenience class for working with XHProf profiling data https://github.com/phacility/xhprof.
getCriticalPath( $metric='wt')
Find the critical path for the given metric.
array[] $complete
Per-function inclusive and exclusive data.
pruneData( $data)
Remove data for functions that are not included in the 'include' configuration array.
getInclusiveMetrics()
Get the inclusive metrics for each function call.
array[][] $inclusive
Per-function inclusive data.
static makeSortFunction( $key, $sub)
Make a closure to use as a sort function.
getCallees( $function)
Get a list of all callees from a given function.
getRawData()
Get raw data collected by xhprof.
__construct(array $data, array $config=[])
Configuration data can contain:
array $config
array[] $hieraData
Hierarchical profiling data returned by xhprof.
getCallers( $function)
Get a list of all callers of a given function.
static splitKey( $key)
Convert an xhprof data key into an array of ['parent', 'child'] function names.
getCompleteMetrics()
Get the inclusive and exclusive metrics for each function call.