MediaWiki  REL1_23
Profiler.php
Go to the documentation of this file.
00001 <?php
00033 function wfProfileIn( $functionname ) {
00034     if ( Profiler::$__instance === null ) { // use this directly to reduce overhead
00035         Profiler::instance();
00036     }
00037     if ( !( Profiler::$__instance instanceof ProfilerStub ) ) {
00038         Profiler::$__instance->profileIn( $functionname );
00039     }
00040 }
00041 
00046 function wfProfileOut( $functionname = 'missing' ) {
00047     if ( Profiler::$__instance === null ) { // use this directly to reduce overhead
00048         Profiler::instance();
00049     }
00050     if ( !( Profiler::$__instance instanceof ProfilerStub ) ) {
00051         Profiler::$__instance->profileOut( $functionname );
00052     }
00053 }
00054 
00060 class ProfileSection {
00061     protected $name; // string; method name
00062     protected $enabled = false; // boolean; whether profiling is enabled
00063 
00075     public function __construct( $name ) {
00076         $this->name = $name;
00077         if ( Profiler::$__instance === null ) { // use this directly to reduce overhead
00078             Profiler::instance();
00079         }
00080         if ( !( Profiler::$__instance instanceof ProfilerStub ) ) {
00081             $this->enabled = true;
00082             Profiler::$__instance->profileIn( $this->name );
00083         }
00084     }
00085 
00086     function __destruct() {
00087         if ( $this->enabled ) {
00088             Profiler::$__instance->profileOut( $this->name );
00089         }
00090     }
00091 }
00092 
00097 class Profiler {
00098     protected $mStack = array(), $mWorkStack = array(), $mCollated = array(),
00099         $mCalls = array(), $mTotals = array(), $mPeriods = array();
00100     protected $mTimeMetric = 'wall';
00101     protected $mProfileID = false, $mCollateDone = false, $mTemplated = false;
00102 
00103     protected $mDBLockThreshold = 5.0; // float; seconds
00105     protected $mDBTrxHoldingLocks = array();
00107     protected $mDBTrxMethodTimes = array();
00108 
00110     public static $__instance = null; // do not call this outside Profiler and ProfileSection
00111 
00112     function __construct( $params ) {
00113         if ( isset( $params['timeMetric'] ) ) {
00114             $this->mTimeMetric = $params['timeMetric'];
00115         }
00116         if ( isset( $params['profileID'] ) ) {
00117             $this->mProfileID = $params['profileID'];
00118         }
00119 
00120         $this->addInitialStack();
00121     }
00122 
00127     public static function instance() {
00128         if ( self::$__instance === null ) {
00129             global $wgProfiler;
00130             if ( is_array( $wgProfiler ) ) {
00131                 if ( !isset( $wgProfiler['class'] ) ) {
00132                     $class = 'ProfilerStub';
00133                 } else {
00134                     $class = $wgProfiler['class'];
00135                 }
00136                 self::$__instance = new $class( $wgProfiler );
00137             } elseif ( $wgProfiler instanceof Profiler ) {
00138                 self::$__instance = $wgProfiler; // back-compat
00139             } else {
00140                 self::$__instance = new ProfilerStub( $wgProfiler );
00141             }
00142         }
00143         return self::$__instance;
00144     }
00145 
00150     public static function setInstance( Profiler $p ) {
00151         self::$__instance = $p;
00152     }
00153 
00159     public function isStub() {
00160         return false;
00161     }
00162 
00169     public function isPersistent() {
00170         return true;
00171     }
00172 
00173     public function setProfileID( $id ) {
00174         $this->mProfileID = $id;
00175     }
00176 
00177     public function getProfileID() {
00178         if ( $this->mProfileID === false ) {
00179             return wfWikiID();
00180         } else {
00181             return $this->mProfileID;
00182         }
00183     }
00184 
00188     protected function addInitialStack() {
00189         // Push an entry for the pre-profile setup time onto the stack
00190         $initial = $this->getInitialTime();
00191         if ( $initial !== null ) {
00192             $this->mWorkStack[] = array( '-total', 0, $initial, 0 );
00193             $this->mStack[] = array( '-setup', 1, $initial, 0, $this->getTime(), 0 );
00194         } else {
00195             $this->profileIn( '-total' );
00196         }
00197     }
00198 
00204     public function profileIn( $functionname ) {
00205         global $wgDebugFunctionEntry;
00206         if ( $wgDebugFunctionEntry ) {
00207             $this->debug( str_repeat( ' ', count( $this->mWorkStack ) ) . 'Entering ' . $functionname . "\n" );
00208         }
00209 
00210         $this->mWorkStack[] = array( $functionname, count( $this->mWorkStack ), $this->getTime(), memory_get_usage() );
00211     }
00212 
00218     public function profileOut( $functionname ) {
00219         global $wgDebugFunctionEntry;
00220         $memory = memory_get_usage();
00221         $time = $this->getTime();
00222 
00223         if ( $wgDebugFunctionEntry ) {
00224             $this->debug( str_repeat( ' ', count( $this->mWorkStack ) - 1 ) . 'Exiting ' . $functionname . "\n" );
00225         }
00226 
00227         $bit = array_pop( $this->mWorkStack );
00228 
00229         if ( !$bit ) {
00230             $this->debugGroup( 'profileerror', "Profiling error, !\$bit: $functionname" );
00231         } else {
00232             if ( $functionname == 'close' ) {
00233                 if ( $bit[0] != '-total' ) {
00234                     $message = "Profile section ended by close(): {$bit[0]}";
00235                     $this->debugGroup( 'profileerror', $message );
00236                     $this->mStack[] = array( $message, 0, 0.0, 0, 0.0, 0 );
00237                 }
00238             } elseif ( $bit[0] != $functionname ) {
00239                 $message = "Profiling error: in({$bit[0]}), out($functionname)";
00240                 $this->debugGroup( 'profileerror', $message );
00241                 $this->mStack[] = array( $message, 0, 0.0, 0, 0.0, 0 );
00242             }
00243             $bit[] = $time;
00244             $bit[] = $memory;
00245             $this->mStack[] = $bit;
00246             $this->updateTrxProfiling( $functionname, $time );
00247         }
00248     }
00249 
00253     public function close() {
00254         while ( count( $this->mWorkStack ) ) {
00255             $this->profileOut( 'close' );
00256         }
00257     }
00258 
00267     public function transactionWritingIn( $server, $db ) {
00268         $name = "{$server} ({$db})";
00269         if ( isset( $this->mDBTrxHoldingLocks[$name] ) ) {
00270             ++$this->mDBTrxHoldingLocks[$name]['refs'];
00271         } else {
00272             $this->mDBTrxHoldingLocks[$name] = array( 'refs' => 1, 'start' => microtime( true ) );
00273             $this->mDBTrxMethodTimes[$name] = array();
00274         }
00275     }
00276 
00283     protected function updateTrxProfiling( $method, $realtime ) {
00284         if ( !$this->mDBTrxHoldingLocks ) {
00285             return; // short-circuit
00286         // @TODO: hardcoded check is a tad janky (what about FOR UPDATE?)
00287         } elseif ( !preg_match( '/^query-m: (?!SELECT)/', $method )
00288             && $realtime < $this->mDBLockThreshold
00289         ) {
00290             return; // not a DB master query nor slow enough
00291         }
00292         $now = microtime( true );
00293         foreach ( $this->mDBTrxHoldingLocks as $name => $info ) {
00294             // Hacky check to exclude entries from before the first TRX write
00295             if ( ( $now - $realtime ) >= $info['start'] ) {
00296                 $this->mDBTrxMethodTimes[$name][] = array( $method, $realtime );
00297             }
00298         }
00299     }
00300 
00311     public function transactionWritingOut( $server, $db ) {
00312         $name = "{$server} ({$db})";
00313         if ( --$this->mDBTrxHoldingLocks[$name]['refs'] <= 0 ) {
00314             $slow = false;
00315             foreach ( $this->mDBTrxMethodTimes[$name] as $info ) {
00316                 list( $method, $realtime ) = $info;
00317                 if ( $realtime >= $this->mDBLockThreshold ) {
00318                     $slow = true;
00319                     break;
00320                 }
00321             }
00322             if ( $slow ) {
00323                 $dbs = implode( ', ', array_keys( $this->mDBTrxHoldingLocks ) );
00324                 $msg = "Sub-optimal transaction on DB(s) {$dbs}:\n";
00325                 foreach ( $this->mDBTrxMethodTimes[$name] as $i => $info ) {
00326                     list( $method, $realtime ) = $info;
00327                     $msg .= sprintf( "%d\t%.6f\t%s\n", $i, $realtime, $method );
00328                 }
00329                 $this->debugGroup( 'DBPerformance', $msg );
00330             }
00331             unset( $this->mDBTrxHoldingLocks[$name] );
00332             unset( $this->mDBTrxMethodTimes[$name] );
00333         }
00334     }
00335 
00341     function setTemplated( $t ) {
00342         $this->mTemplated = $t;
00343     }
00344 
00350     public function getOutput() {
00351         global $wgDebugFunctionEntry, $wgProfileCallTree;
00352         $wgDebugFunctionEntry = false;
00353 
00354         if ( !count( $this->mStack ) && !count( $this->mCollated ) ) {
00355             return "No profiling output\n";
00356         }
00357 
00358         if ( $wgProfileCallTree ) {
00359             return $this->getCallTree();
00360         } else {
00361             return $this->getFunctionReport();
00362         }
00363     }
00364 
00369     function getCallTree() {
00370         return implode( '', array_map( array( &$this, 'getCallTreeLine' ), $this->remapCallTree( $this->mStack ) ) );
00371     }
00372 
00379     function remapCallTree( $stack ) {
00380         if ( count( $stack ) < 2 ) {
00381             return $stack;
00382         }
00383         $outputs = array();
00384         for ( $max = count( $stack ) - 1; $max > 0; ) {
00385             /* Find all items under this entry */
00386             $level = $stack[$max][1];
00387             $working = array();
00388             for ( $i = $max -1; $i >= 0; $i-- ) {
00389                 if ( $stack[$i][1] > $level ) {
00390                     $working[] = $stack[$i];
00391                 } else {
00392                     break;
00393                 }
00394             }
00395             $working = $this->remapCallTree( array_reverse( $working ) );
00396             $output = array();
00397             foreach ( $working as $item ) {
00398                 array_push( $output, $item );
00399             }
00400             array_unshift( $output, $stack[$max] );
00401             $max = $i;
00402 
00403             array_unshift( $outputs, $output );
00404         }
00405         $final = array();
00406         foreach ( $outputs as $output ) {
00407             foreach ( $output as $item ) {
00408                 $final[] = $item;
00409             }
00410         }
00411         return $final;
00412     }
00413 
00418     function getCallTreeLine( $entry ) {
00419         list( $fname, $level, $start, /* $x */, $end ) = $entry;
00420         $delta = $end - $start;
00421         $space = str_repeat( ' ', $level );
00422         # The ugly double sprintf is to work around a PHP bug,
00423         # which has been fixed in recent releases.
00424         return sprintf( "%10s %s %s\n", trim( sprintf( "%7.3f", $delta * 1000.0 ) ), $space, $fname );
00425     }
00426 
00438     function getTime( $metric = false ) {
00439         if ( $metric === false ) {
00440             $metric = $this->mTimeMetric;
00441         }
00442 
00443         if ( $metric === 'cpu' || $this->mTimeMetric === 'user' ) {
00444             if ( !function_exists( 'getrusage' ) ) {
00445                 return 0;
00446             }
00447             $ru = getrusage();
00448             $time = $ru['ru_utime.tv_sec'] + $ru['ru_utime.tv_usec'] / 1e6;
00449             if ( $metric === 'cpu' ) {
00450                 # This is the time of system calls, added to the user time
00451                 # it gives the total CPU time
00452                 $time += $ru['ru_stime.tv_sec'] + $ru['ru_stime.tv_usec'] / 1e6;
00453             }
00454             return $time;
00455         } else {
00456             return microtime( true );
00457         }
00458     }
00459 
00471     protected function getInitialTime( $metric = false ) {
00472         global $wgRequestTime, $wgRUstart;
00473 
00474         if ( $metric === false ) {
00475             $metric = $this->mTimeMetric;
00476         }
00477 
00478         if ( $metric === 'cpu' || $this->mTimeMetric === 'user' ) {
00479             if ( !count( $wgRUstart ) ) {
00480                 return null;
00481             }
00482 
00483             $time = $wgRUstart['ru_utime.tv_sec'] + $wgRUstart['ru_utime.tv_usec'] / 1e6;
00484             if ( $metric === 'cpu' ) {
00485                 # This is the time of system calls, added to the user time
00486                 # it gives the total CPU time
00487                 $time += $wgRUstart['ru_stime.tv_sec'] + $wgRUstart['ru_stime.tv_usec'] / 1e6;
00488             }
00489             return $time;
00490         } else {
00491             if ( empty( $wgRequestTime ) ) {
00492                 return null;
00493             } else {
00494                 return $wgRequestTime;
00495             }
00496         }
00497     }
00498 
00499     protected function collateData() {
00500         if ( $this->mCollateDone ) {
00501             return;
00502         }
00503         $this->mCollateDone = true;
00504 
00505         $this->close();
00506 
00507         $this->mCollated = array();
00508         $this->mCalls = array();
00509         $this->mMemory = array();
00510 
00511         # Estimate profiling overhead
00512         $profileCount = count( $this->mStack );
00513         self::calculateOverhead( $profileCount );
00514 
00515         # First, subtract the overhead!
00516         $overheadTotal = $overheadMemory = $overheadInternal = array();
00517         foreach ( $this->mStack as $entry ) {
00518             $fname = $entry[0];
00519             $start = $entry[2];
00520             $end = $entry[4];
00521             $elapsed = $end - $start;
00522             $memory = $entry[5] - $entry[3];
00523 
00524             if ( $fname == '-overhead-total' ) {
00525                 $overheadTotal[] = $elapsed;
00526                 $overheadMemory[] = $memory;
00527             } elseif ( $fname == '-overhead-internal' ) {
00528                 $overheadInternal[] = $elapsed;
00529             }
00530         }
00531         $overheadTotal = $overheadTotal ? array_sum( $overheadTotal ) / count( $overheadInternal ) : 0;
00532         $overheadMemory = $overheadMemory ? array_sum( $overheadMemory ) / count( $overheadInternal ) : 0;
00533         $overheadInternal = $overheadInternal ? array_sum( $overheadInternal ) / count( $overheadInternal ) : 0;
00534 
00535         # Collate
00536         foreach ( $this->mStack as $index => $entry ) {
00537             $fname = $entry[0];
00538             $start = $entry[2];
00539             $end = $entry[4];
00540             $elapsed = $end - $start;
00541 
00542             $memory = $entry[5] - $entry[3];
00543             $subcalls = $this->calltreeCount( $this->mStack, $index );
00544 
00545             if ( !preg_match( '/^-overhead/', $fname ) ) {
00546                 # Adjust for profiling overhead (except special values with elapsed=0
00547                 if ( $elapsed ) {
00548                     $elapsed -= $overheadInternal;
00549                     $elapsed -= ( $subcalls * $overheadTotal );
00550                     $memory -= ( $subcalls * $overheadMemory );
00551                 }
00552             }
00553 
00554             if ( !array_key_exists( $fname, $this->mCollated ) ) {
00555                 $this->mCollated[$fname] = 0;
00556                 $this->mCalls[$fname] = 0;
00557                 $this->mMemory[$fname] = 0;
00558                 $this->mMin[$fname] = 1 << 24;
00559                 $this->mMax[$fname] = 0;
00560                 $this->mOverhead[$fname] = 0;
00561                 $this->mPeriods[$fname] = array();
00562             }
00563 
00564             $this->mCollated[$fname] += $elapsed;
00565             $this->mCalls[$fname]++;
00566             $this->mMemory[$fname] += $memory;
00567             $this->mMin[$fname] = min( $this->mMin[$fname], $elapsed );
00568             $this->mMax[$fname] = max( $this->mMax[$fname], $elapsed );
00569             $this->mOverhead[$fname] += $subcalls;
00570             $this->mPeriods[$fname][] = compact( 'start', 'end', 'memory', 'subcalls' );
00571         }
00572 
00573         $this->mCalls['-overhead-total'] = $profileCount;
00574         arsort( $this->mCollated, SORT_NUMERIC );
00575     }
00576 
00582     function getFunctionReport() {
00583         $this->collateData();
00584 
00585         $width = 140;
00586         $nameWidth = $width - 65;
00587         $format = "%-{$nameWidth}s %6d %13.3f %13.3f %13.3f%% %9d  (%13.3f -%13.3f) [%d]\n";
00588         $titleFormat = "%-{$nameWidth}s %6s %13s %13s %13s %9s\n";
00589         $prof = "\nProfiling data\n";
00590         $prof .= sprintf( $titleFormat, 'Name', 'Calls', 'Total', 'Each', '%', 'Mem' );
00591 
00592         $total = isset( $this->mCollated['-total'] ) ? $this->mCollated['-total'] : 0;
00593 
00594         foreach ( $this->mCollated as $fname => $elapsed ) {
00595             $calls = $this->mCalls[$fname];
00596             $percent = $total ? 100. * $elapsed / $total : 0;
00597             $memory = $this->mMemory[$fname];
00598             $prof .= sprintf( $format,
00599                 substr( $fname, 0, $nameWidth ),
00600                 $calls,
00601                 (float)( $elapsed * 1000 ),
00602                 (float)( $elapsed * 1000 ) / $calls,
00603                 $percent,
00604                 $memory,
00605                 ( $this->mMin[$fname] * 1000.0 ),
00606                 ( $this->mMax[$fname] * 1000.0 ),
00607                 $this->mOverhead[$fname]
00608             );
00609         }
00610         $prof .= "\nTotal: $total\n\n";
00611 
00612         return $prof;
00613     }
00614 
00618     public function getRawData() {
00619         $this->collateData();
00620 
00621         $profile = array();
00622         $total = isset( $this->mCollated['-total'] ) ? $this->mCollated['-total'] : 0;
00623         foreach ( $this->mCollated as $fname => $elapsed ) {
00624             $periods = array();
00625             foreach ( $this->mPeriods[$fname] as $period ) {
00626                 $period['start'] *= 1000;
00627                 $period['end'] *= 1000;
00628                 $periods[] = $period;
00629             }
00630             $profile[] = array(
00631                 'name' => $fname,
00632                 'calls' => $this->mCalls[$fname],
00633                 'elapsed' => $elapsed * 1000,
00634                 'percent' => $total ? 100. * $elapsed / $total : 0,
00635                 'memory' => $this->mMemory[$fname],
00636                 'min' => $this->mMin[$fname] * 1000,
00637                 'max' => $this->mMax[$fname] * 1000,
00638                 'overhead' => $this->mOverhead[$fname],
00639                 'periods' => $periods,
00640             );
00641         }
00642 
00643         return $profile;
00644     }
00645 
00649     protected static function calculateOverhead( $profileCount ) {
00650         wfProfileIn( '-overhead-total' );
00651         for ( $i = 0; $i < $profileCount; $i++ ) {
00652             wfProfileIn( '-overhead-internal' );
00653             wfProfileOut( '-overhead-internal' );
00654         }
00655         wfProfileOut( '-overhead-total' );
00656     }
00657 
00667     function calltreeCount( $stack, $start ) {
00668         $level = $stack[$start][1];
00669         $count = 0;
00670         for ( $i = $start -1; $i >= 0 && $stack[$i][1] > $level; $i-- ) {
00671             $count ++;
00672         }
00673         return $count;
00674     }
00675 
00679     public function logData() {
00680         global $wgProfilePerHost, $wgProfileToDatabase;
00681 
00682         # Do not log anything if database is readonly (bug 5375)
00683         if ( wfReadOnly() || !$wgProfileToDatabase ) {
00684             return;
00685         }
00686 
00687         $dbw = wfGetDB( DB_MASTER );
00688         if ( !is_object( $dbw ) ) {
00689             return;
00690         }
00691 
00692         if ( $wgProfilePerHost ) {
00693             $pfhost = wfHostname();
00694         } else {
00695             $pfhost = '';
00696         }
00697 
00698         try {
00699             $this->collateData();
00700 
00701             foreach ( $this->mCollated as $name => $elapsed ) {
00702                 $eventCount = $this->mCalls[$name];
00703                 $timeSum = (float)( $elapsed * 1000 );
00704                 $memorySum = (float)$this->mMemory[$name];
00705                 $name = substr( $name, 0, 255 );
00706 
00707                 // Kludge
00708                 $timeSum = $timeSum >= 0 ? $timeSum : 0;
00709                 $memorySum = $memorySum >= 0 ? $memorySum : 0;
00710 
00711                 $dbw->update( 'profiling',
00712                     array(
00713                         "pf_count=pf_count+{$eventCount}",
00714                         "pf_time=pf_time+{$timeSum}",
00715                         "pf_memory=pf_memory+{$memorySum}",
00716                     ),
00717                     array(
00718                         'pf_name' => $name,
00719                         'pf_server' => $pfhost,
00720                     ),
00721                     __METHOD__ );
00722 
00723                 $rc = $dbw->affectedRows();
00724                 if ( $rc == 0 ) {
00725                     $dbw->insert( 'profiling', array( 'pf_name' => $name, 'pf_count' => $eventCount,
00726                         'pf_time' => $timeSum, 'pf_memory' => $memorySum, 'pf_server' => $pfhost ),
00727                         __METHOD__, array( 'IGNORE' ) );
00728                 }
00729                 // When we upgrade to mysql 4.1, the insert+update
00730                 // can be merged into just a insert with this construct added:
00731                 //     "ON DUPLICATE KEY UPDATE ".
00732                 //     "pf_count=pf_count + VALUES(pf_count), ".
00733                 //     "pf_time=pf_time + VALUES(pf_time)";
00734             }
00735         } catch ( DBError $e ) {}
00736     }
00737 
00742     function getCurrentSection() {
00743         $elt = end( $this->mWorkStack );
00744         return $elt[0];
00745     }
00746 
00752     function debug( $s ) {
00753         if ( function_exists( 'wfDebug' ) ) {
00754             wfDebug( $s );
00755         }
00756     }
00757 
00764     function debugGroup( $group, $s ) {
00765         if ( function_exists( 'wfDebugLog' ) ) {
00766             wfDebugLog( $group, $s );
00767         }
00768     }
00769 
00775     protected function getContentType() {
00776         foreach ( headers_list() as $header ) {
00777             if ( preg_match( '#^content-type: (\w+/\w+);?#i', $header, $m ) ) {
00778                 return $m[1];
00779             }
00780         }
00781         return null;
00782     }
00783 }