MediaWiki
REL1_23
|
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 }