Back-compat for $wgProfiler set as a class. Fix for r89206
[lhc/web/wiklou.git] / includes / profiler / Profiler.php
1 <?php
2 /**
3 * @defgroup Profiler Profiler
4 *
5 * @file
6 * @ingroup Profiler
7 * This file is only included if profiling is enabled
8 */
9
10 /**
11 * Begin profiling of a function
12 * @param $functionname String: name of the function we will profile
13 */
14 function wfProfileIn( $functionname ) {
15 global $wgProfiler;
16 if ( $wgProfiler instanceof Profiler || isset( $wgProfiler['class'] ) ) {
17 Profiler::instance()->profileIn( $functionname );
18 }
19 }
20
21 /**
22 * Stop profiling of a function
23 * @param $functionname String: name of the function we have profiled
24 */
25 function wfProfileOut( $functionname = 'missing' ) {
26 global $wgProfiler;
27 if ( $wgProfiler instanceof Profiler || isset( $wgProfiler['class'] ) ) {
28 Profiler::instance()->profileOut( $functionname );
29 }
30 }
31
32 /**
33 * @ingroup Profiler
34 * @todo document
35 */
36 class Profiler {
37 protected $mStack = array(), $mWorkStack = array (), $mCollated = array (),
38 $mCalls = array (), $mTotals = array ();
39 protected $mTimeMetric = 'wall';
40 protected $mProfileID = false, $mCollateDone = false, $mTemplated = false;
41 private static $__instance = null;
42
43 function __construct( $params ) {
44 // Push an entry for the pre-profile setup time onto the stack
45 global $wgRequestTime;
46 if ( !empty( $wgRequestTime ) ) {
47 $this->mWorkStack[] = array( '-total', 0, $wgRequestTime, 0 );
48 $this->mStack[] = array( '-setup', 1, $wgRequestTime, 0, microtime(true), 0 );
49 } else {
50 $this->profileIn( '-total' );
51 }
52 if ( isset( $params['timeMetric'] ) ) {
53 $this->mTimeMetric = $params['timeMetric'];
54 }
55 if ( isset( $params['profileID'] ) ) {
56 $this->mProfileID = $params['profileID'];
57 }
58 }
59
60 /**
61 * Singleton
62 * @return Profiler
63 */
64 public static function instance() {
65 if( is_null( self::$__instance ) ) {
66 global $wgProfiler;
67 if( is_array( $wgProfiler ) ) {
68 if( !isset( $wgProfiler['class'] ) ) {
69 wfDebug( __METHOD__ . " called without \$wgProfiler['class']"
70 . " set, falling back to ProfilerStub for safety\n" );
71 $class = 'ProfilerStub';
72 } else {
73 $class = $wgProfiler['class'];
74 }
75 self::$__instance = new $class( $wgProfiler );
76 } elseif( $wgProfiler instanceof Profiler ) {
77 self::$__instance = $wgProfiler; // back-compat
78 } else {
79 wfDebug( __METHOD__ . ' called with bogus $wgProfiler setting,'
80 . " falling back to ProfilerStub for safety\n" );
81 self::$__instance = new ProfilerStub( $wgProfiler );
82 }
83 }
84 return self::$__instance;
85 }
86
87 /**
88 * Set the profiler to a specific profiler instance. Mostly for dumpHTML
89 * @param $p Profiler object
90 */
91 public static function setInstance( Profiler $p ) {
92 self::$__instance = $p;
93 }
94
95 /**
96 * Return whether this a stub profiler
97 *
98 * @return Boolean
99 */
100 public function isStub() {
101 return false;
102 }
103
104 public function setProfileID( $id ) {
105 $this->mProfileID = $id;
106 }
107
108 public function getProfileID() {
109 if ( $this->mProfileID === false ) {
110 return wfWikiID();
111 } else {
112 return $this->mProfileID;
113 }
114 }
115
116 /**
117 * Called by wfProfieIn()
118 *
119 * @param $functionname String
120 */
121 public function profileIn( $functionname ) {
122 global $wgDebugFunctionEntry;
123 if( $wgDebugFunctionEntry ){
124 $this->debug( str_repeat( ' ', count( $this->mWorkStack ) ) . 'Entering ' . $functionname . "\n" );
125 }
126
127 $this->mWorkStack[] = array( $functionname, count( $this->mWorkStack ), $this->getTime(), memory_get_usage() );
128 }
129
130 /**
131 * Called by wfProfieOut()
132 *
133 * @param $functionname String
134 */
135 public function profileOut( $functionname ) {
136 global $wgDebugFunctionEntry;
137 $memory = memory_get_usage();
138 $time = $this->getTime();
139
140 if( $wgDebugFunctionEntry ){
141 $this->debug( str_repeat( ' ', count( $this->mWorkStack ) - 1 ) . 'Exiting ' . $functionname . "\n" );
142 }
143
144 $bit = array_pop($this->mWorkStack);
145
146 if (!$bit) {
147 $this->debug("Profiling error, !\$bit: $functionname\n");
148 } else {
149 //if( $wgDebugProfiling ){
150 if( $functionname == 'close' ){
151 $message = "Profile section ended by close(): {$bit[0]}";
152 $this->debug( "$message\n" );
153 $this->mStack[] = array( $message, 0, '0 0', 0, '0 0', 0 );
154 }
155 elseif( $bit[0] != $functionname ){
156 $message = "Profiling error: in({$bit[0]}), out($functionname)";
157 $this->debug( "$message\n" );
158 $this->mStack[] = array( $message, 0, '0 0', 0, '0 0', 0 );
159 }
160 //}
161 $bit[] = $time;
162 $bit[] = $memory;
163 $this->mStack[] = $bit;
164 }
165 }
166
167 /**
168 * Close opened profiling sections
169 */
170 public function close() {
171 while( count( $this->mWorkStack ) ){
172 $this->profileOut( 'close' );
173 }
174 }
175
176 /**
177 * Mark this call as templated or not
178 *
179 * @param $t Boolean
180 */
181 function setTemplated( $t ) {
182 $this->mTemplated = $t;
183 }
184
185 /**
186 * Returns a profiling output to be stored in debug file
187 *
188 * @return String
189 */
190 public function getOutput() {
191 global $wgDebugFunctionEntry, $wgProfileCallTree;
192 $wgDebugFunctionEntry = false;
193
194 if( !count( $this->mStack ) && !count( $this->mCollated ) ){
195 return "No profiling output\n";
196 }
197
198 if( $wgProfileCallTree ) {
199 return $this->getCallTree();
200 } else {
201 return $this->getFunctionReport();
202 }
203 }
204
205 /**
206 * Returns a tree of function call instead of a list of functions
207 */
208 function getCallTree() {
209 return implode( '', array_map( array( &$this, 'getCallTreeLine' ), $this->remapCallTree( $this->mStack ) ) );
210 }
211
212 /**
213 * Recursive function the format the current profiling array into a tree
214 *
215 * @param $stack profiling array
216 */
217 function remapCallTree( $stack ) {
218 if( count( $stack ) < 2 ){
219 return $stack;
220 }
221 $outputs = array ();
222 for( $max = count( $stack ) - 1; $max > 0; ){
223 /* Find all items under this entry */
224 $level = $stack[$max][1];
225 $working = array ();
226 for( $i = $max -1; $i >= 0; $i-- ){
227 if( $stack[$i][1] > $level ){
228 $working[] = $stack[$i];
229 } else {
230 break;
231 }
232 }
233 $working = $this->remapCallTree( array_reverse( $working ) );
234 $output = array();
235 foreach( $working as $item ){
236 array_push( $output, $item );
237 }
238 array_unshift( $output, $stack[$max] );
239 $max = $i;
240
241 array_unshift( $outputs, $output );
242 }
243 $final = array();
244 foreach( $outputs as $output ){
245 foreach( $output as $item ){
246 $final[] = $item;
247 }
248 }
249 return $final;
250 }
251
252 /**
253 * Callback to get a formatted line for the call tree
254 */
255 function getCallTreeLine( $entry ) {
256 list( $fname, $level, $start, /* $x */, $end) = $entry;
257 $delta = $end - $start;
258 $space = str_repeat(' ', $level);
259 # The ugly double sprintf is to work around a PHP bug,
260 # which has been fixed in recent releases.
261 return sprintf( "%10s %s %s\n", trim( sprintf( "%7.3f", $delta * 1000.0 ) ), $space, $fname );
262 }
263
264 function getTime() {
265 if ( $this->mTimeMetric === 'user' ) {
266 return $this->getUserTime();
267 } else {
268 return microtime(true);
269 }
270 }
271
272 function getUserTime() {
273 $ru = getrusage();
274 return $ru['ru_utime.tv_sec'].' '.$ru['ru_utime.tv_usec'] / 1e6;
275 }
276
277 protected function collateData() {
278 if ( $this->mCollateDone ) {
279 return;
280 }
281 $this->mCollateDone = true;
282
283 $this->close();
284
285 $this->mCollated = array();
286 $this->mCalls = array();
287 $this->mMemory = array();
288
289 # Estimate profiling overhead
290 $profileCount = count($this->mStack);
291 self::calculateOverhead( $profileCount );
292
293 # First, subtract the overhead!
294 $overheadTotal = $overheadMemory = $overheadInternal = array();
295 foreach( $this->mStack as $entry ){
296 $fname = $entry[0];
297 $start = $entry[2];
298 $end = $entry[4];
299 $elapsed = $end - $start;
300 $memory = $entry[5] - $entry[3];
301
302 if( $fname == '-overhead-total' ){
303 $overheadTotal[] = $elapsed;
304 $overheadMemory[] = $memory;
305 }
306 elseif( $fname == '-overhead-internal' ){
307 $overheadInternal[] = $elapsed;
308 }
309 }
310 $overheadTotal = $overheadTotal ? array_sum( $overheadTotal ) / count( $overheadInternal ) : 0;
311 $overheadMemory = $overheadMemory ? array_sum( $overheadMemory ) / count( $overheadInternal ) : 0;
312 $overheadInternal = $overheadInternal ? array_sum( $overheadInternal ) / count( $overheadInternal ) : 0;
313
314 # Collate
315 foreach( $this->mStack as $index => $entry ){
316 $fname = $entry[0];
317 $start = $entry[2];
318 $end = $entry[4];
319 $elapsed = $end - $start;
320
321 $memory = $entry[5] - $entry[3];
322 $subcalls = $this->calltreeCount( $this->mStack, $index );
323
324 if( !preg_match( '/^-overhead/', $fname ) ){
325 # Adjust for profiling overhead (except special values with elapsed=0
326 if( $elapsed ) {
327 $elapsed -= $overheadInternal;
328 $elapsed -= ($subcalls * $overheadTotal);
329 $memory -= ($subcalls * $overheadMemory);
330 }
331 }
332
333 if( !array_key_exists( $fname, $this->mCollated ) ){
334 $this->mCollated[$fname] = 0;
335 $this->mCalls[$fname] = 0;
336 $this->mMemory[$fname] = 0;
337 $this->mMin[$fname] = 1 << 24;
338 $this->mMax[$fname] = 0;
339 $this->mOverhead[$fname] = 0;
340 }
341
342 $this->mCollated[$fname] += $elapsed;
343 $this->mCalls[$fname]++;
344 $this->mMemory[$fname] += $memory;
345 $this->mMin[$fname] = min($this->mMin[$fname], $elapsed);
346 $this->mMax[$fname] = max($this->mMax[$fname], $elapsed);
347 $this->mOverhead[$fname] += $subcalls;
348 }
349
350 $this->mCalls['-overhead-total'] = $profileCount;
351 arsort( $this->mCollated, SORT_NUMERIC );
352 }
353
354 /**
355 * Returns a list of profiled functions.
356 * Also log it into the database if $wgProfileToDatabase is set to true.
357 */
358 function getFunctionReport() {
359 $this->collateData();
360
361 $width = 140;
362 $nameWidth = $width - 65;
363 $format = "%-{$nameWidth}s %6d %13.3f %13.3f %13.3f%% %9d (%13.3f -%13.3f) [%d]\n";
364 $titleFormat = "%-{$nameWidth}s %6s %13s %13s %13s %9s\n";
365 $prof = "\nProfiling data\n";
366 $prof .= sprintf( $titleFormat, 'Name', 'Calls', 'Total', 'Each', '%', 'Mem' );
367
368 $total = isset( $this->mCollated['-total'] ) ? $this->mCollated['-total'] : 0;
369
370 foreach( $this->mCollated as $fname => $elapsed ){
371 $calls = $this->mCalls[$fname];
372 $percent = $total ? 100. * $elapsed / $total : 0;
373 $memory = $this->mMemory[$fname];
374 $prof .= sprintf($format, substr($fname, 0, $nameWidth), $calls, (float) ($elapsed * 1000), (float) ($elapsed * 1000) / $calls, $percent, $memory, ($this->mMin[$fname] * 1000.0), ($this->mMax[$fname] * 1000.0), $this->mOverhead[$fname]);
375 }
376 $prof .= "\nTotal: $total\n\n";
377
378 return $prof;
379 }
380
381 /**
382 * Dummy calls to wfProfileIn/wfProfileOut to calculate its overhead
383 */
384 protected static function calculateOverhead( $profileCount ) {
385 wfProfileIn( '-overhead-total' );
386 for( $i = 0; $i < $profileCount; $i++ ){
387 wfProfileIn( '-overhead-internal' );
388 wfProfileOut( '-overhead-internal' );
389 }
390 wfProfileOut( '-overhead-total' );
391 }
392
393 /**
394 * Counts the number of profiled function calls sitting under
395 * the given point in the call graph. Not the most efficient algo.
396 *
397 * @param $stack Array:
398 * @param $start Integer:
399 * @return Integer
400 * @private
401 */
402 function calltreeCount($stack, $start) {
403 $level = $stack[$start][1];
404 $count = 0;
405 for ($i = $start -1; $i >= 0 && $stack[$i][1] > $level; $i --) {
406 $count ++;
407 }
408 return $count;
409 }
410
411 /**
412 * Log the whole profiling data into the database.
413 */
414 public function logData(){
415 global $wgProfilePerHost, $wgProfileToDatabase;
416
417 # Do not log anything if database is readonly (bug 5375)
418 if( wfReadOnly() || !$wgProfileToDatabase ) {
419 return;
420 }
421
422 $dbw = wfGetDB( DB_MASTER );
423 if( !is_object( $dbw ) ) {
424 return;
425 }
426
427 $errorState = $dbw->ignoreErrors( true );
428
429 if( $wgProfilePerHost ){
430 $pfhost = wfHostname();
431 } else {
432 $pfhost = '';
433 }
434
435 $this->collateData();
436
437 foreach( $this->mCollated as $name => $elapsed ){
438 $eventCount = $this->mCalls[$name];
439 $timeSum = (float) ($elapsed * 1000);
440 $memorySum = (float)$this->mMemory[$name];
441 $name = substr($name, 0, 255);
442
443 // Kludge
444 $timeSum = ($timeSum >= 0) ? $timeSum : 0;
445 $memorySum = ($memorySum >= 0) ? $memorySum : 0;
446
447 $dbw->update( 'profiling',
448 array(
449 "pf_count=pf_count+{$eventCount}",
450 "pf_time=pf_time+{$timeSum}",
451 "pf_memory=pf_memory+{$memorySum}",
452 ),
453 array(
454 'pf_name' => $name,
455 'pf_server' => $pfhost,
456 ),
457 __METHOD__ );
458
459 $rc = $dbw->affectedRows();
460 if ( $rc == 0 ) {
461 $dbw->insert('profiling', array ('pf_name' => $name, 'pf_count' => $eventCount,
462 'pf_time' => $timeSum, 'pf_memory' => $memorySum, 'pf_server' => $pfhost ),
463 __METHOD__, array ('IGNORE'));
464 }
465 // When we upgrade to mysql 4.1, the insert+update
466 // can be merged into just a insert with this construct added:
467 // "ON DUPLICATE KEY UPDATE ".
468 // "pf_count=pf_count + VALUES(pf_count), ".
469 // "pf_time=pf_time + VALUES(pf_time)";
470 }
471
472 $dbw->ignoreErrors( $errorState );
473 }
474
475 /**
476 * Get the function name of the current profiling section
477 */
478 function getCurrentSection() {
479 $elt = end( $this->mWorkStack );
480 return $elt[0];
481 }
482
483 /**
484 * Add an entry in the debug log file
485 *
486 * @param $s String to output
487 */
488 function debug( $s ) {
489 if( defined( 'MW_COMPILED' ) || function_exists( 'wfDebug' ) ) {
490 wfDebug( $s );
491 }
492 }
493 }