]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/profiler/SectionProfiler.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / includes / profiler / SectionProfiler.php
1 <?php
2 /**
3  * Arbitrary section name based PHP profiling.
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  * http://www.gnu.org/copyleft/gpl.html
19  *
20  * @file
21  * @ingroup Profiler
22  */
23 use Wikimedia\ScopedCallback;
24
25 /**
26  * Custom PHP profiler for parser/DB type section names that xhprof/xdebug can't handle
27  *
28  * @since 1.25
29  */
30 class SectionProfiler {
31         /** @var array Map of (mem,real,cpu) */
32         protected $start;
33         /** @var array Map of (mem,real,cpu) */
34         protected $end;
35         /** @var array List of resolved profile calls with start/end data */
36         protected $stack = [];
37         /** @var array Queue of open profile calls with start data */
38         protected $workStack = [];
39
40         /** @var array Map of (function name => aggregate data array) */
41         protected $collated = [];
42         /** @var bool */
43         protected $collateDone = false;
44
45         /** @var bool Whether to collect the full stack trace or just aggregates */
46         protected $collateOnly = true;
47         /** @var array Cache of a standard broken collation entry */
48         protected $errorEntry;
49
50         /**
51          * @param array $params
52          */
53         public function __construct( array $params = [] ) {
54                 $this->errorEntry = $this->getErrorEntry();
55                 $this->collateOnly = empty( $params['trace'] );
56         }
57
58         /**
59          * @param string $section
60          * @return SectionProfileCallback
61          */
62         public function scopedProfileIn( $section ) {
63                 $this->profileInInternal( $section );
64
65                 return new SectionProfileCallback( $this, $section );
66         }
67
68         /**
69          * @param ScopedCallback &$section
70          */
71         public function scopedProfileOut( ScopedCallback &$section ) {
72                 $section = null;
73         }
74
75         /**
76          * Get the aggregated inclusive profiling data for each method
77          *
78          * The percent time for each time is based on the current "total" time
79          * used is based on all methods so far. This method can therefore be
80          * called several times in between several profiling calls without the
81          * delays in usage of the profiler skewing the results. A "-total" entry
82          * is always included in the results.
83          *
84          * @return array List of method entries arrays, each having:
85          *   - name    : method name
86          *   - calls   : the number of invoking calls
87          *   - real    : real time elapsed (ms)
88          *   - %real   : percent real time
89          *   - cpu     : real time elapsed (ms)
90          *   - %cpu    : percent real time
91          *   - memory  : memory used (bytes)
92          *   - %memory : percent memory used
93          *   - min_real : min real time in a call (ms)
94          *   - max_real : max real time in a call (ms)
95          */
96         public function getFunctionStats() {
97                 $this->collateData();
98
99                 $totalCpu = max( $this->end['cpu'] - $this->start['cpu'], 0 );
100                 $totalReal = max( $this->end['real'] - $this->start['real'], 0 );
101                 $totalMem = max( $this->end['memory'] - $this->start['memory'], 0 );
102
103                 $profile = [];
104                 foreach ( $this->collated as $fname => $data ) {
105                         $profile[] = [
106                                 'name' => $fname,
107                                 'calls' => $data['count'],
108                                 'real' => $data['real'] * 1000,
109                                 '%real' => $totalReal ? 100 * $data['real'] / $totalReal : 0,
110                                 'cpu' => $data['cpu'] * 1000,
111                                 '%cpu' => $totalCpu ? 100 * $data['cpu'] / $totalCpu : 0,
112                                 'memory' => $data['memory'],
113                                 '%memory' => $totalMem ? 100 * $data['memory'] / $totalMem : 0,
114                                 'min_real' => 1000 * $data['min_real'],
115                                 'max_real' => 1000 * $data['max_real']
116                         ];
117                 }
118
119                 $profile[] = [
120                         'name' => '-total',
121                         'calls' => 1,
122                         'real' => 1000 * $totalReal,
123                         '%real' => 100,
124                         'cpu' => 1000 * $totalCpu,
125                         '%cpu' => 100,
126                         'memory' => $totalMem,
127                         '%memory' => 100,
128                         'min_real' => 1000 * $totalReal,
129                         'max_real' => 1000 * $totalReal
130                 ];
131
132                 return $profile;
133         }
134
135         /**
136          * Clear all of the profiling data for another run
137          */
138         public function reset() {
139                 $this->start = null;
140                 $this->end = null;
141                 $this->stack = [];
142                 $this->workStack = [];
143                 $this->collated = [];
144                 $this->collateDone = false;
145         }
146
147         /**
148          * @return array Initial collation entry
149          */
150         protected function getZeroEntry() {
151                 return [
152                         'cpu'      => 0.0,
153                         'real'     => 0.0,
154                         'memory'   => 0,
155                         'count'    => 0,
156                         'min_real' => 0.0,
157                         'max_real' => 0.0
158                 ];
159         }
160
161         /**
162          * @return array Initial collation entry for errors
163          */
164         protected function getErrorEntry() {
165                 $entry = $this->getZeroEntry();
166                 $entry['count'] = 1;
167                 return $entry;
168         }
169
170         /**
171          * Update the collation entry for a given method name
172          *
173          * @param string $name
174          * @param float $elapsedCpu
175          * @param float $elapsedReal
176          * @param int $memChange
177          */
178         protected function updateEntry( $name, $elapsedCpu, $elapsedReal, $memChange ) {
179                 $entry =& $this->collated[$name];
180                 if ( !is_array( $entry ) ) {
181                         $entry = $this->getZeroEntry();
182                         $this->collated[$name] =& $entry;
183                 }
184                 $entry['cpu'] += $elapsedCpu;
185                 $entry['real'] += $elapsedReal;
186                 $entry['memory'] += $memChange > 0 ? $memChange : 0;
187                 $entry['count']++;
188                 $entry['min_real'] = min( $entry['min_real'], $elapsedReal );
189                 $entry['max_real'] = max( $entry['max_real'], $elapsedReal );
190         }
191
192         /**
193          * This method should not be called outside SectionProfiler
194          *
195          * @param string $functionname
196          */
197         public function profileInInternal( $functionname ) {
198                 // Once the data is collated for reports, any future calls
199                 // should clear the collation cache so the next report will
200                 // reflect them. This matters when trace mode is used.
201                 $this->collateDone = false;
202
203                 $cpu = $this->getTime( 'cpu' );
204                 $real = $this->getTime( 'wall' );
205                 $memory = memory_get_usage();
206
207                 if ( $this->start === null ) {
208                         $this->start = [ 'cpu' => $cpu, 'real' => $real, 'memory' => $memory ];
209                 }
210
211                 $this->workStack[] = [
212                         $functionname,
213                         count( $this->workStack ),
214                         $real,
215                         $cpu,
216                         $memory
217                 ];
218         }
219
220         /**
221          * This method should not be called outside SectionProfiler
222          *
223          * @param string $functionname
224          */
225         public function profileOutInternal( $functionname ) {
226                 $item = array_pop( $this->workStack );
227                 if ( $item === null ) {
228                         $this->debugGroup( 'profileerror', "Profiling error: $functionname" );
229                         return;
230                 }
231                 list( $ofname, /* $ocount */, $ortime, $octime, $omem ) = $item;
232
233                 if ( $functionname === 'close' ) {
234                         $message = "Profile section ended by close(): {$ofname}";
235                         $this->debugGroup( 'profileerror', $message );
236                         if ( $this->collateOnly ) {
237                                 $this->collated[$message] = $this->errorEntry;
238                         } else {
239                                 $this->stack[] = [ $message, 0, 0.0, 0.0, 0, 0.0, 0.0, 0 ];
240                         }
241                         $functionname = $ofname;
242                 } elseif ( $ofname !== $functionname ) {
243                         $message = "Profiling error: in({$ofname}), out($functionname)";
244                         $this->debugGroup( 'profileerror', $message );
245                         if ( $this->collateOnly ) {
246                                 $this->collated[$message] = $this->errorEntry;
247                         } else {
248                                 $this->stack[] = [ $message, 0, 0.0, 0.0, 0, 0.0, 0.0, 0 ];
249                         }
250                 }
251
252                 $realTime = $this->getTime( 'wall' );
253                 $cpuTime = $this->getTime( 'cpu' );
254                 $memUsage = memory_get_usage();
255
256                 if ( $this->collateOnly ) {
257                         $elapsedcpu = $cpuTime - $octime;
258                         $elapsedreal = $realTime - $ortime;
259                         $memchange = $memUsage - $omem;
260                         $this->updateEntry( $functionname, $elapsedcpu, $elapsedreal, $memchange );
261                 } else {
262                         $this->stack[] = array_merge( $item, [ $realTime, $cpuTime, $memUsage ] );
263                 }
264
265                 $this->end = [
266                         'cpu'      => $cpuTime,
267                         'real'     => $realTime,
268                         'memory'   => $memUsage
269                 ];
270         }
271
272         /**
273          * Returns a tree of function calls with their real times
274          * @return string
275          * @throws Exception
276          */
277         public function getCallTreeReport() {
278                 if ( $this->collateOnly ) {
279                         throw new Exception( "Tree is only available for trace profiling." );
280                 }
281                 return implode( '', array_map(
282                         [ $this, 'getCallTreeLine' ], $this->remapCallTree( $this->stack )
283                 ) );
284         }
285
286         /**
287          * Recursive function the format the current profiling array into a tree
288          *
289          * @param array $stack Profiling array
290          * @return array
291          */
292         protected function remapCallTree( array $stack ) {
293                 if ( count( $stack ) < 2 ) {
294                         return $stack;
295                 }
296                 $outputs = [];
297                 for ( $max = count( $stack ) - 1; $max > 0; ) {
298                         /* Find all items under this entry */
299                         $level = $stack[$max][1];
300                         $working = [];
301                         for ( $i = $max - 1; $i >= 0; $i-- ) {
302                                 if ( $stack[$i][1] > $level ) {
303                                         $working[] = $stack[$i];
304                                 } else {
305                                         break;
306                                 }
307                         }
308                         $working = $this->remapCallTree( array_reverse( $working ) );
309                         $output = [];
310                         foreach ( $working as $item ) {
311                                 array_push( $output, $item );
312                         }
313                         array_unshift( $output, $stack[$max] );
314                         $max = $i;
315
316                         array_unshift( $outputs, $output );
317                 }
318                 $final = [];
319                 foreach ( $outputs as $output ) {
320                         foreach ( $output as $item ) {
321                                 $final[] = $item;
322                         }
323                 }
324                 return $final;
325         }
326
327         /**
328          * Callback to get a formatted line for the call tree
329          * @param array $entry
330          * @return string
331          */
332         protected function getCallTreeLine( $entry ) {
333                 // $entry has (name, level, stime, scpu, smem, etime, ecpu, emem)
334                 list( $fname, $level, $startreal, , , $endreal ) = $entry;
335                 $delta = $endreal - $startreal;
336                 $space = str_repeat( ' ', $level );
337                 # The ugly double sprintf is to work around a PHP bug,
338                 # which has been fixed in recent releases.
339                 return sprintf( "%10s %s %s\n",
340                         trim( sprintf( "%7.3f", $delta * 1000.0 ) ), $space, $fname );
341         }
342
343         /**
344          * Populate collated data
345          */
346         protected function collateData() {
347                 if ( $this->collateDone ) {
348                         return;
349                 }
350                 $this->collateDone = true;
351                 // Close opened profiling sections
352                 while ( count( $this->workStack ) ) {
353                         $this->profileOutInternal( 'close' );
354                 }
355
356                 if ( $this->collateOnly ) {
357                         return; // already collated as methods exited
358                 }
359
360                 $this->collated = [];
361
362                 # Estimate profiling overhead
363                 $oldEnd = $this->end;
364                 $profileCount = count( $this->stack );
365                 $this->calculateOverhead( $profileCount );
366
367                 # First, subtract the overhead!
368                 $overheadTotal = $overheadMemory = $overheadInternal = [];
369                 foreach ( $this->stack as $entry ) {
370                         // $entry is (name,pos,rtime0,cputime0,mem0,rtime1,cputime1,mem1)
371                         $fname = $entry[0];
372                         $elapsed = $entry[5] - $entry[2];
373                         $memchange = $entry[7] - $entry[4];
374
375                         if ( $fname === '-overhead-total' ) {
376                                 $overheadTotal[] = $elapsed;
377                                 $overheadMemory[] = max( 0, $memchange );
378                         } elseif ( $fname === '-overhead-internal' ) {
379                                 $overheadInternal[] = $elapsed;
380                         }
381                 }
382                 $overheadTotal = $overheadTotal ?
383                         array_sum( $overheadTotal ) / count( $overheadInternal ) : 0;
384                 $overheadMemory = $overheadMemory ?
385                         array_sum( $overheadMemory ) / count( $overheadInternal ) : 0;
386                 $overheadInternal = $overheadInternal ?
387                         array_sum( $overheadInternal ) / count( $overheadInternal ) : 0;
388
389                 # Collate
390                 foreach ( $this->stack as $index => $entry ) {
391                         // $entry is (name,pos,rtime0,cputime0,mem0,rtime1,cputime1,mem1)
392                         $fname = $entry[0];
393                         $elapsedCpu = $entry[6] - $entry[3];
394                         $elapsedReal = $entry[5] - $entry[2];
395                         $memchange = $entry[7] - $entry[4];
396                         $subcalls = $this->calltreeCount( $this->stack, $index );
397
398                         if ( substr( $fname, 0, 9 ) !== '-overhead' ) {
399                                 # Adjust for profiling overhead (except special values with elapsed=0)
400                                 if ( $elapsed ) {
401                                         $elapsed -= $overheadInternal;
402                                         $elapsed -= ( $subcalls * $overheadTotal );
403                                         $memchange -= ( $subcalls * $overheadMemory );
404                                 }
405                         }
406
407                         $this->updateEntry( $fname, $elapsedCpu, $elapsedReal, $memchange );
408                 }
409
410                 $this->collated['-overhead-total']['count'] = $profileCount;
411                 arsort( $this->collated, SORT_NUMERIC );
412
413                 // Unclobber the end info map (the overhead checking alters it)
414                 $this->end = $oldEnd;
415         }
416
417         /**
418          * Dummy calls to calculate profiling overhead
419          *
420          * @param int $profileCount
421          */
422         protected function calculateOverhead( $profileCount ) {
423                 $this->profileInInternal( '-overhead-total' );
424                 for ( $i = 0; $i < $profileCount; $i++ ) {
425                         $this->profileInInternal( '-overhead-internal' );
426                         $this->profileOutInternal( '-overhead-internal' );
427                 }
428                 $this->profileOutInternal( '-overhead-total' );
429         }
430
431         /**
432          * Counts the number of profiled function calls sitting under
433          * the given point in the call graph. Not the most efficient algo.
434          *
435          * @param array $stack
436          * @param int $start
437          * @return int
438          */
439         protected function calltreeCount( $stack, $start ) {
440                 $level = $stack[$start][1];
441                 $count = 0;
442                 for ( $i = $start - 1; $i >= 0 && $stack[$i][1] > $level; $i-- ) {
443                         $count ++;
444                 }
445                 return $count;
446         }
447
448         /**
449          * Get the initial time of the request, based on getrusage()
450          *
451          * @param string|bool $metric Metric to use, with the following possibilities:
452          *   - user: User CPU time (without system calls)
453          *   - cpu: Total CPU time (user and system calls)
454          *   - wall (or any other string): elapsed time
455          *   - false (default): will fall back to default metric
456          * @return float
457          */
458         protected function getTime( $metric = 'wall' ) {
459                 if ( $metric === 'cpu' || $metric === 'user' ) {
460                         $ru = wfGetRusage();
461                         if ( !$ru ) {
462                                 return 0;
463                         }
464                         $time = $ru['ru_utime.tv_sec'] + $ru['ru_utime.tv_usec'] / 1e6;
465                         if ( $metric === 'cpu' ) {
466                                 # This is the time of system calls, added to the user time
467                                 # it gives the total CPU time
468                                 $time += $ru['ru_stime.tv_sec'] + $ru['ru_stime.tv_usec'] / 1e6;
469                         }
470                         return $time;
471                 } else {
472                         return microtime( true );
473                 }
474         }
475
476         /**
477          * Add an entry in the debug log file
478          *
479          * @param string $s String to output
480          */
481         protected function debug( $s ) {
482                 if ( function_exists( 'wfDebug' ) ) {
483                         wfDebug( $s );
484                 }
485         }
486
487         /**
488          * Add an entry in the debug log group
489          *
490          * @param string $group Group to send the message to
491          * @param string $s String to output
492          */
493         protected function debugGroup( $group, $s ) {
494                 if ( function_exists( 'wfDebugLog' ) ) {
495                         wfDebugLog( $group, $s );
496                 }
497         }
498 }
499
500 /**
501  * Subclass ScopedCallback to avoid call_user_func_array(), which is slow
502  *
503  * This class should not be used outside of SectionProfiler
504  */
505 class SectionProfileCallback extends ScopedCallback {
506         /** @var SectionProfiler */
507         protected $profiler;
508         /** @var string */
509         protected $section;
510
511         /**
512          * @param SectionProfiler $profiler
513          * @param string $section
514          */
515         public function __construct( SectionProfiler $profiler, $section ) {
516                 parent::__construct( null );
517                 $this->profiler = $profiler;
518                 $this->section = $section;
519         }
520
521         function __destruct() {
522                 $this->profiler->profileOutInternal( $this->section );
523         }
524 }