1<?php 2/** 3 * This program is free software; you can redistribute it and/or modify 4 * it under the terms of the GNU General Public License as published by 5 * the Free Software Foundation; either version 2 of the License, or 6 * (at your option) any later version. 7 * 8 * This program is distributed in the hope that it will be useful, 9 * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 * GNU General Public License for more details. 12 * 13 * You should have received a copy of the GNU General Public License along 14 * with this program; if not, write to the Free Software Foundation, Inc., 15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 * http://www.gnu.org/copyleft/gpl.html 17 * 18 * @file 19 */ 20 21use MediaWiki\Logger\LoggerFactory; 22use Psr\Log\LoggerInterface; 23use Wikimedia\Rdbms\TransactionProfiler; 24use Wikimedia\ScopedCallback; 25 26/** 27 * @defgroup Profiler Profiler 28 */ 29 30/** 31 * Profiler base class that defines the interface and some shared 32 * functionality. 33 * 34 * @ingroup Profiler 35 */ 36abstract class Profiler { 37 /** @var string|bool Profiler ID for bucketing data */ 38 protected $profileID = false; 39 /** @var array All of the params passed from $wgProfiler */ 40 protected $params = []; 41 /** @var IContextSource Current request context */ 42 protected $context = null; 43 /** @var TransactionProfiler */ 44 protected $trxProfiler; 45 /** @var LoggerInterface */ 46 protected $logger; 47 /** @var bool */ 48 private $allowOutput = false; 49 50 /** @var Profiler */ 51 private static $instance = null; 52 53 /** 54 * @param array $params See $wgProfiler. 55 */ 56 public function __construct( array $params ) { 57 if ( isset( $params['profileID'] ) ) { 58 $this->profileID = $params['profileID']; 59 } 60 $this->params = $params; 61 $this->trxProfiler = new TransactionProfiler(); 62 $this->logger = LoggerFactory::getInstance( 'profiler' ); 63 } 64 65 /** 66 * Singleton 67 * @return Profiler 68 */ 69 final public static function instance() { 70 if ( self::$instance === null ) { 71 global $wgProfiler; 72 73 $params = $wgProfiler + [ 74 'class' => ProfilerStub::class, 75 'sampling' => 1, 76 'threshold' => 0.0, 77 'output' => [], 78 ]; 79 80 $inSample = mt_rand( 0, $params['sampling'] - 1 ) === 0; 81 // wfIsCLI() is not available yet 82 if ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' || !$inSample ) { 83 $params['class'] = ProfilerStub::class; 84 } 85 86 if ( !is_array( $params['output'] ) ) { 87 $params['output'] = [ $params['output'] ]; 88 } 89 90 self::$instance = new $params['class']( $params ); 91 } 92 return self::$instance; 93 } 94 95 /** 96 * Replace the current profiler with $profiler if no non-stub profiler is set 97 * 98 * @param Profiler $profiler 99 * @throws MWException 100 * @since 1.25 101 */ 102 final public static function replaceStubInstance( Profiler $profiler ) { 103 if ( self::$instance && !( self::$instance instanceof ProfilerStub ) ) { 104 throw new MWException( 'Could not replace non-stub profiler instance.' ); 105 } else { 106 self::$instance = $profiler; 107 } 108 } 109 110 /** 111 * @param string $id 112 */ 113 public function setProfileID( $id ) { 114 $this->profileID = $id; 115 } 116 117 /** 118 * @return string 119 */ 120 public function getProfileID() { 121 if ( $this->profileID === false ) { 122 return WikiMap::getCurrentWikiDbDomain()->getId(); 123 } else { 124 return $this->profileID; 125 } 126 } 127 128 /** 129 * Sets the context for this Profiler 130 * 131 * @param IContextSource $context 132 * @since 1.25 133 */ 134 public function setContext( $context ) { 135 $this->context = $context; 136 } 137 138 /** 139 * Gets the context for this Profiler 140 * 141 * @return IContextSource 142 * @since 1.25 143 */ 144 public function getContext() { 145 if ( $this->context ) { 146 return $this->context; 147 } else { 148 $this->logger->warning( __METHOD__ . " called before setContext, " . 149 "fallback to RequestContext::getMain()." ); 150 return RequestContext::getMain(); 151 } 152 } 153 154 public function profileIn( $functionname ) { 155 wfDeprecated( __METHOD__, '1.33' ); 156 } 157 158 public function profileOut( $functionname ) { 159 wfDeprecated( __METHOD__, '1.33' ); 160 } 161 162 /** 163 * Mark the start of a custom profiling frame (e.g. DB queries). 164 * The frame ends when the result of this method falls out of scope. 165 * 166 * @param string $section 167 * @return ScopedCallback|null 168 * @since 1.25 169 */ 170 abstract public function scopedProfileIn( $section ); 171 172 /** 173 * @param SectionProfileCallback|null &$section 174 */ 175 public function scopedProfileOut( SectionProfileCallback &$section = null ) { 176 $section = null; 177 } 178 179 /** 180 * @return TransactionProfiler 181 * @since 1.25 182 */ 183 public function getTransactionProfiler() { 184 return $this->trxProfiler; 185 } 186 187 /** 188 * Close opened profiling sections 189 */ 190 abstract public function close(); 191 192 /** 193 * Get all usable outputs. 194 * 195 * @throws MWException 196 * @return ProfilerOutput[] 197 * @since 1.25 198 */ 199 private function getOutputs() { 200 $outputs = []; 201 foreach ( $this->params['output'] as $outputType ) { 202 // The class may be specified as either the full class name (for 203 // example, 'ProfilerOutputStats') or (for backward compatibility) 204 // the trailing portion of the class name (for example, 'stats'). 205 $outputClass = strpos( $outputType, 'ProfilerOutput' ) === false 206 ? 'ProfilerOutput' . ucfirst( $outputType ) 207 : $outputType; 208 if ( !class_exists( $outputClass ) ) { 209 throw new MWException( "'$outputType' is an invalid output type" ); 210 } 211 $outputInstance = new $outputClass( $this, $this->params ); 212 if ( $outputInstance->canUse() ) { 213 $outputs[] = $outputInstance; 214 } 215 } 216 return $outputs; 217 } 218 219 /** 220 * Log the data to the backing store for all ProfilerOutput instances that have one 221 * 222 * @since 1.25 223 */ 224 public function logData() { 225 $request = $this->getContext()->getRequest(); 226 227 $timeElapsed = $request->getElapsedTime(); 228 $timeElapsedThreshold = $this->params['threshold']; 229 if ( $timeElapsed <= $timeElapsedThreshold ) { 230 return; 231 } 232 233 $outputs = []; 234 foreach ( $this->getOutputs() as $output ) { 235 if ( !$output->logsToOutput() ) { 236 $outputs[] = $output; 237 } 238 } 239 240 if ( $outputs ) { 241 $stats = $this->getFunctionStats(); 242 foreach ( $outputs as $output ) { 243 $output->log( $stats ); 244 } 245 } 246 } 247 248 /** 249 * Log the data to the script/request output for all ProfilerOutput instances that do so 250 * 251 * @throws MWException 252 * @since 1.26 253 */ 254 public function logDataPageOutputOnly() { 255 if ( !$this->allowOutput ) { 256 return; 257 } 258 259 $outputs = []; 260 foreach ( $this->getOutputs() as $output ) { 261 if ( $output->logsToOutput() ) { 262 $outputs[] = $output; 263 } 264 } 265 266 if ( $outputs ) { 267 $stats = $this->getFunctionStats(); 268 foreach ( $outputs as $output ) { 269 $output->log( $stats ); 270 } 271 } 272 } 273 274 /** 275 * Get the Content-Type for deciding how to format appended profile output. 276 * 277 * Disabled by default. Enable via setAllowOutput(). 278 * 279 * @see ProfilerOutputText 280 * @since 1.25 281 * @return string|null Returns null if disabled or no Content-Type found. 282 */ 283 public function getContentType() { 284 if ( $this->allowOutput ) { 285 foreach ( headers_list() as $header ) { 286 if ( preg_match( '#^content-type: (\w+/\w+);?#i', $header, $m ) ) { 287 return $m[1]; 288 } 289 } 290 } 291 return null; 292 } 293 294 /** 295 * Enable appending profiles to standard output. 296 * 297 * @since 1.34 298 */ 299 public function setAllowOutput() { 300 $this->allowOutput = true; 301 } 302 303 /** 304 * Whether appending profiles is allowed. 305 * 306 * @since 1.34 307 * @return bool 308 */ 309 public function getAllowOutput() { 310 return $this->allowOutput; 311 } 312 313 /** 314 * Get the aggregated inclusive profiling data for each method 315 * 316 * The percent time for each time is based on the current "total" time 317 * used is based on all methods so far. This method can therefore be 318 * called several times in between several profiling calls without the 319 * delays in usage of the profiler skewing the results. A "-total" entry 320 * is always included in the results. 321 * 322 * When a call chain involves a method invoked within itself, any 323 * entries for the cyclic invocation should be demarked with "@". 324 * This makes filtering them out easier and follows the xhprof style. 325 * 326 * @return array[] List of method entries arrays, each having: 327 * - name : method name 328 * - calls : the number of invoking calls 329 * - real : real time elapsed (ms) 330 * - %real : percent real time 331 * - cpu : CPU time elapsed (ms) 332 * - %cpu : percent CPU time 333 * - memory : memory used (bytes) 334 * - %memory : percent memory used 335 * - min_real : min real time in a call (ms) 336 * - max_real : max real time in a call (ms) 337 * @since 1.25 338 */ 339 abstract public function getFunctionStats(); 340 341 /** 342 * Returns a profiling output to be stored in debug file 343 * 344 * @return string 345 */ 346 abstract public function getOutput(); 347} 348