wiki.techinc.nl/includes/profiler/ProfilerXhprof.php
Chad Horohoe 4fa6c46c94 Remove per-template profiling
Creates absurd dependency on the profiler from the parser. This profiling
is also not compatible with function-level profiling that we're trying to
aim for with xhprof.

Change-Id: I3c780fcb06e3470a0cbf50311ba916a0b67ea49e
2014-11-12 18:18:30 -08:00

474 lines
13 KiB
PHP

<?php
/**
* @section LICENSE
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
/**
* Profiler wrapper for XHProf extension.
*
* Mimics the output of ProfilerSimpleText, ProfilerSimpleUDP or
* ProfilerSimpleTrace but using data collected via the XHProf PHP extension.
* This Profiler also produces data compatable with the debugging toolbar.
*
* To mimic ProfilerSimpleText results:
* @code
* $wgProfiler['class'] = 'ProfilerXhprof';
* $wgProfiler['flags'] = XHPROF_FLAGS_NO_BUILTINS;
* $wgProfiler['log'] = 'text';
* $wgProfiler['visible'] = true;
* @endcode
*
* To mimic ProfilerSimpleUDP results:
* @code
* $wgProfiler['class'] = 'ProfilerXhprof';
* $wgProfiler['flags'] = XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY | XHPROF_FLAGS_NO_BUILTINS;
* $wgProfiler['log'] = 'udpprofile';
* @endcode
*
* Similar to ProfilerSimpleTrace results, report the most expensive path
* through the application:
* @code
* $wgProfiler['class'] = 'ProfilerXhprof';
* $wgProfiler['flags'] = XHPROF_FLAGS_NO_BUILTINS;
* $wgProfiler['log'] = 'critpath';
* $wgProfiler['visible'] = true;
* @endcode
*
* Rather than obeying wfProfileIn() and wfProfileOut() calls placed in the
* application code, ProfilerXhprof profiles all functions using the XHProf
* PHP extenstion. For PHP5 users, this extension can be installed via PECL or
* your operating system's package manager. XHProf support is built into HHVM.
*
* To restrict the functions for which profiling data is collected, you can
* use either a whitelist ($wgProfiler['include']) or a blacklist
* ($wgProfiler['exclude']) containing an array of function names. The
* blacklist funcitonality is built into HHVM and will completely exclude the
* named functions from profiling collection. The whitelist is implemented by
* Xhprof class and will filter the data collected by XHProf before reporting.
* See documentation for the Xhprof class and the XHProf extension for
* additional information.
*
* Data reported to debug toolbar via getRawData() can be restricted by
* setting $wgProfiler['toolbarCutoff'] to a minumum cumulative wall clock
* percentage. Functions in the call graph which contribute less than this
* percentage to the total wall clock time measured will be excluded from the
* data sent to debug toolbar. The default cutoff is 0.1 (1/1000th of the
* total time measured).
*
* @author Bryan Davis <bd808@wikimedia.org>
* @copyright © 2014 Bryan Davis and Wikimedia Foundation.
* @ingroup Profiler
* @see Xhprof
* @see https://php.net/xhprof
* @see https://github.com/facebook/hhvm/blob/master/hphp/doc/profiling.md
*/
class ProfilerXhprof extends Profiler {
/**
* @var Xhprof $xhprof
*/
protected $xhprof;
/**
* Type of report to send when logData() is called.
* @var string $logType
*/
protected $logType;
/**
* Should profile report sent to in page content be visible?
* @var bool $visible
*/
protected $visible;
/**
* Minimum wall time precentage for a fucntion to be reported to the debug
* toolbar via getRawData().
* @var float $toolbarCutoff
*/
protected $toolbarCutoff;
/**
* @param array $params
* @see Xhprof::__construct()
*/
public function __construct( array $params = array() ) {
$params = array_merge(
array(
'log' => 'text',
'visible' => false,
'toolbarCutoff' => 0.1,
),
$params
);
parent::__construct( $params );
$this->logType = $params['log'];
$this->visible = $params['visible'];
$this->xhprof = new Xhprof( $params );
}
public function isStub() {
return false;
}
/**
* No-op for xhprof profiling.
*
* Use the 'include' configuration key instead if you need to constrain
* the functions that are profiled.
*
* @param string $functionname
*/
public function profileIn( $functionname ) {
global $wgDebugFunctionEntry;
if ( $wgDebugFunctionEntry ) {
$this->debug( "Entering {$functionname}" );
}
}
/**
* No-op for xhprof profiling.
*
* Use the 'include' configuration key instead if you need to constrain
* the functions that are profiled.
*
* @param string $functionname
*/
public function profileOut( $functionname ) {
global $wgDebugFunctionEntry;
if ( $wgDebugFunctionEntry ) {
$this->debug( "Exiting {$functionname}" );
}
}
/**
* No-op for xhprof profiling.
*/
public function close() {
}
/**
* Get data for the debugging toolbar.
*
* @return array
* @see https://www.mediawiki.org/wiki/Debugging_toolbar
*/
public function getRawData() {
$metrics = $this->xhprof->getCompleteMetrics();
$endEpoch = $this->getTime();
$profile = array();
foreach ( $metrics as $fname => $stats ) {
if ( $stats['wt']['percent'] < $this->toolbarCutoff ) {
// Ignore functions which are not significant contributors
// to the total elapsed time.
continue;
}
$record = array(
'name' => $fname,
'calls' => $stats['ct'],
'elapsed' => $stats['wt']['total'] / 1000,
'percent' => $stats['wt']['percent'],
'memory' => isset( $stats['mu'] ) ? $stats['mu']['total'] : 0,
'min' => $stats['wt']['min'] / 1000,
'max' => $stats['wt']['max'] / 1000,
'overhead' => array_reduce(
$stats['subcalls'],
function( $carry, $item ) {
return $carry + $item['ct'];
},
0
),
'periods' => array(),
);
// We are making up periods based on the call segments we have.
// What we don't have is the start time for each call (which
// actually may be a collection of multiple calls from the
// caller). We will pretend that all the calls happen sequentially
// and finish at the end of the end of the request.
foreach ( $stats['calls'] as $call ) {
$avgElapsed = $call['wt'] / 1000 / $call['ct'];
$avgMem = isset( $call['mu'] ) ? $call['mu'] / $call['ct'] : 0;
$start = $endEpoch - $avgElapsed;
for ( $i = 0; $i < $call['ct']; $i++ ) {
$callStart = $start + ( $avgElapsed * $i );
$record['periods'][] = array(
'start' => $callStart,
'end' => $callStart + $avgElapsed,
'memory' => $avgMem,
'subcalls' => 0,
);
}
}
$profile[] = $record;
}
return $profile;
}
/**
* Log the data to some store or even the page output.
*/
public function logData() {
if ( $this->logType === 'text' ) {
$this->logText();
} elseif ( $this->logType === 'udpprofile' ) {
$this->logToUdpprofile();
} elseif ( $this->logType === 'critpath' ){
$this->logCriticalPath();
} else {
wfLogWarning(
"Unknown ProfilerXhprof log type '{$this->logType}'"
);
}
}
/**
* Write a brief profile report to stdout.
*/
protected function logText() {
if ( $this->templated ) {
$ct = $this->getContentType();
if ( $ct === 'text/html' && $this->visible ) {
$prefix = '<pre>';
$suffix = '</pre>';
} elseif ( $ct === 'text/javascript' || $ct === 'text/css' ) {
$prefix = "\n/*";
$suffix = "*/\n";
} else {
$prefix = "<!--";
$suffix = "-->\n";
}
print $this->getSummaryReport( $prefix, $suffix );
}
}
/**
* Send collected information to a udpprofile daemon.
*
* @see http://git.wikimedia.org/tree/operations%2Fsoftware.git/master/udpprofile
* @see $wgUDPProfilerHost
* @see $wgUDPProfilerPort
* @see $wgUDPProfilerFormatString
*/
protected function logToUdpprofile() {
global $wgUDPProfilerHost, $wgUDPProfilerPort;
global $wgUDPProfilerFormatString;
if ( !function_exists( 'socket_create' ) ) {
return;
}
$metrics = $this->xhprof->getInclusiveMetrics();
$sock = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP );
$buffer = '';
$bufferSize = 0;
foreach ( $metrics as $func => $data ) {
$line = sprintf( $wgUDPProfilerFormatString,
$this->getProfileID(),
$data['ct'],
isset( $data['cpu'] ) ? $data['cpu']['total'] : 0,
isset( $data['cpu'] ) ? $data['cpu']['variance'] : 0,
$data['wt']['total'] / 1000,
$data['wt']['variance'],
$func,
isset( $data['mu'] ) ? $data['mu']['total'] : 0
);
$lineLength = strlen( $line );
if ( $lineLength + $bufferSize > 1400 ) {
// Line would exceed max packet size, send packet before
// appending to buffer.
socket_sendto( $sock, $buffer, $bufferSize, 0,
$wgUDPProfilerHost, $wgUDPProfilerPort
);
$buffer = '';
$bufferSize = 0;
}
$buffer .= $line;
$bufferSize += $lineLength;
}
// Send final buffer
socket_sendto( $sock, $buffer, $bufferSize, 0x100 /* EOF */,
$wgUDPProfilerHost, $wgUDPProfilerPort
);
}
/**
* Write a critical path report to stdout.
*/
protected function logCriticalPath() {
if ( $this->templated ) {
$ct = $this->getContentType();
if ( $ct === 'text/html' && $this->visible ) {
$prefix = '<pre>Critical path:';
$suffix = '</pre>';
} elseif ( $ct === 'text/javascript' || $ct === 'text/css' ) {
$prefix = "\n/* Critical path:";
$suffix = "*/\n";
} else {
$prefix = "<!-- Critical path:";
$suffix = "-->\n";
}
print $this->getCriticalPathReport( $prefix, $suffix );
}
}
/**
* Get the content type of the current request.
* @return string
*/
protected function getContentType() {
foreach ( headers_list() as $header ) {
if ( preg_match( '#^content-type: (\w+/\w+);?#i', $header, $m ) ) {
return $m[1];
}
}
return 'application/octet-stream';
}
/**
* Returns a profiling output to be stored in debug file
*
* @return string
*/
public function getOutput() {
return $this->getFunctionReport();
}
/**
* Get a report of profiled functions sorted by inclusive wall clock time
* in descending order.
*
* Each line of the report includes this data:
* - Function name
* - Number of times function was called
* - Total wall clock time spent in function in microseconds
* - Minimum wall clock time spent in function in microseconds
* - Average wall clock time spent in function in microseconds
* - Maximum wall clock time spent in function in microseconds
* - Percentage of total wall clock time spent in function
* - Total delta of memory usage from start to end of function in bytes
*
* @return string
*/
protected function getFunctionReport() {
$data = $this->xhprof->getInclusiveMetrics();
uasort( $data, Xhprof::makeSortFunction( 'wt', 'total' ) );
$width = 140;
$nameWidth = $width - 65;
$format = "%-{$nameWidth}s %6d %9d %9d %9d %9d %7.3f%% %9d";
$out = array();
$out[] = sprintf( "%-{$nameWidth}s %6s %9s %9s %9s %9s %7s %9s",
'Name', 'Calls', 'Total', 'Min', 'Each', 'Max', '%', 'Mem'
);
foreach ( $data as $func => $stats ) {
$out[] = sprintf( $format,
$func,
$stats['ct'],
$stats['wt']['total'],
$stats['wt']['min'],
$stats['wt']['mean'],
$stats['wt']['max'],
$stats['wt']['percent'],
isset( $stats['mu'] ) ? $stats['mu']['total'] : 0
);
}
return implode( "\n", $out );
}
/**
* Get a brief report of profiled functions sorted by inclusive wall clock
* time in descending order.
*
* Each line of the report includes this data:
* - Percentage of total wall clock time spent in function
* - Total wall clock time spent in function in seconds
* - Number of times function was called
* - Function name
*
* @param string $header Header text to prepend to report
* @param string $footer Footer text to append to report
* @return string
*/
protected function getSummaryReport( $header = '', $footer = '' ) {
$data = $this->xhprof->getInclusiveMetrics();
uasort( $data, Xhprof::makeSortFunction( 'wt', 'total' ) );
$format = '%6.2f%% %3.6f %6d - %s';
$out = array( $header );
foreach ( $data as $func => $stats ) {
$out[] = sprintf( $format,
$stats['wt']['percent'],
$stats['wt']['total'] / 1e6,
$stats['ct'],
$func
);
}
$out[] = $footer;
return implode( "\n", $out );
}
/**
* Get a brief report of the most costly code path by wall clock time.
*
* Each line of the report includes this data:
* - Total wall clock time spent in function in seconds
* - Function name
*
* @param string $header Header text to prepend to report
* @param string $footer Footer text to append to report
* @return string
*/
protected function getCriticalPathReport( $header = '', $footer = '' ) {
$data = $this->xhprof->getCriticalPath();
$out = array( $header );
$out[] = sprintf( "%7s %9s %9s %6s %4s",
'Time%', 'Time', 'Mem', 'Calls', 'Name'
);
$format = '%6.2f%% %9.6f %9d %6d %s%s';
$total = null;
$nest = 0;
foreach ( $data as $key => $stats ) {
list( $parent, $child ) = Xhprof::splitKey( $key );
if ( $total === null ) {
$total = $stats;
}
$out[] = sprintf( $format,
100 * $stats['wt'] / $total['wt'],
$stats['wt'] / 1e6,
isset( $stats['mu'] ) ? $stats['mu'] : 0,
$stats['ct'],
//$nest ? str_repeat( ' ', $nest - 1 ) . '┗ ' : '',
$nest ? str_repeat( ' ', $nest - 1 ) . '└─' : '',
$child
);
$nest++;
}
$out[] = $footer;
return implode( "\n", $out );
}
}