PageRenderTime 42ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 0ms

/includes/profiler/SectionProfiler.php

https://gitlab.com/link233/bootmw
PHP | 529 lines | 306 code | 48 blank | 175 comment | 33 complexity | 1508b6c7dbb5bc023222a49dd11d00a5 MD5 | raw file
  1. <?php
  2. /**
  3. * Arbitrary section name based PHP profiling.
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. * @ingroup Profiler
  22. * @author Aaron Schulz
  23. */
  24. /**
  25. * Custom PHP profiler for parser/DB type section names that xhprof/xdebug can't handle
  26. *
  27. * @since 1.25
  28. */
  29. class SectionProfiler {
  30. /** @var array Map of (mem,real,cpu) */
  31. protected $start;
  32. /** @var array Map of (mem,real,cpu) */
  33. protected $end;
  34. /** @var array List of resolved profile calls with start/end data */
  35. protected $stack = [];
  36. /** @var array Queue of open profile calls with start data */
  37. protected $workStack = [];
  38. /** @var array Map of (function name => aggregate data array) */
  39. protected $collated = [];
  40. /** @var bool */
  41. protected $collateDone = false;
  42. /** @var bool Whether to collect the full stack trace or just aggregates */
  43. protected $collateOnly = true;
  44. /** @var array Cache of a standard broken collation entry */
  45. protected $errorEntry;
  46. /** @var callable Cache of a profile out callback */
  47. protected $profileOutCallback;
  48. /**
  49. * @param array $params
  50. */
  51. public function __construct( array $params = [] ) {
  52. $this->errorEntry = $this->getErrorEntry();
  53. $this->collateOnly = empty( $params['trace'] );
  54. $this->profileOutCallback = function ( $profiler, $section ) {
  55. $profiler->profileOutInternal( $section );
  56. };
  57. }
  58. /**
  59. * @param string $section
  60. * @return ScopedCallback
  61. */
  62. public function scopedProfileIn( $section ) {
  63. $this->profileInInternal( $section );
  64. return new SectionProfileCallback( $this, $section );
  65. }
  66. /**
  67. * @param ScopedCallback $section
  68. */
  69. public function scopedProfileOut( ScopedCallback &$section ) {
  70. $section = null;
  71. }
  72. /**
  73. * Get the aggregated inclusive profiling data for each method
  74. *
  75. * The percent time for each time is based on the current "total" time
  76. * used is based on all methods so far. This method can therefore be
  77. * called several times in between several profiling calls without the
  78. * delays in usage of the profiler skewing the results. A "-total" entry
  79. * is always included in the results.
  80. *
  81. * @return array List of method entries arrays, each having:
  82. * - name : method name
  83. * - calls : the number of invoking calls
  84. * - real : real time elapsed (ms)
  85. * - %real : percent real time
  86. * - cpu : real time elapsed (ms)
  87. * - %cpu : percent real time
  88. * - memory : memory used (bytes)
  89. * - %memory : percent memory used
  90. * - min_real : min real time in a call (ms)
  91. * - max_real : max real time in a call (ms)
  92. */
  93. public function getFunctionStats() {
  94. $this->collateData();
  95. $totalCpu = max( $this->end['cpu'] - $this->start['cpu'], 0 );
  96. $totalReal = max( $this->end['real'] - $this->start['real'], 0 );
  97. $totalMem = max( $this->end['memory'] - $this->start['memory'], 0 );
  98. $profile = [];
  99. foreach ( $this->collated as $fname => $data ) {
  100. $profile[] = [
  101. 'name' => $fname,
  102. 'calls' => $data['count'],
  103. 'real' => $data['real'] * 1000,
  104. '%real' => $totalReal ? 100 * $data['real'] / $totalReal : 0,
  105. 'cpu' => $data['cpu'] * 1000,
  106. '%cpu' => $totalCpu ? 100 * $data['cpu'] / $totalCpu : 0,
  107. 'memory' => $data['memory'],
  108. '%memory' => $totalMem ? 100 * $data['memory'] / $totalMem : 0,
  109. 'min_real' => 1000 * $data['min_real'],
  110. 'max_real' => 1000 * $data['max_real']
  111. ];
  112. }
  113. $profile[] = [
  114. 'name' => '-total',
  115. 'calls' => 1,
  116. 'real' => 1000 * $totalReal,
  117. '%real' => 100,
  118. 'cpu' => 1000 * $totalCpu,
  119. '%cpu' => 100,
  120. 'memory' => $totalMem,
  121. '%memory' => 100,
  122. 'min_real' => 1000 * $totalReal,
  123. 'max_real' => 1000 * $totalReal
  124. ];
  125. return $profile;
  126. }
  127. /**
  128. * Clear all of the profiling data for another run
  129. */
  130. public function reset() {
  131. $this->start = null;
  132. $this->end = null;
  133. $this->stack = [];
  134. $this->workStack = [];
  135. $this->collated = [];
  136. $this->collateDone = false;
  137. }
  138. /**
  139. * @return array Initial collation entry
  140. */
  141. protected function getZeroEntry() {
  142. return [
  143. 'cpu' => 0.0,
  144. 'real' => 0.0,
  145. 'memory' => 0,
  146. 'count' => 0,
  147. 'min_real' => 0.0,
  148. 'max_real' => 0.0
  149. ];
  150. }
  151. /**
  152. * @return array Initial collation entry for errors
  153. */
  154. protected function getErrorEntry() {
  155. $entry = $this->getZeroEntry();
  156. $entry['count'] = 1;
  157. return $entry;
  158. }
  159. /**
  160. * Update the collation entry for a given method name
  161. *
  162. * @param string $name
  163. * @param float $elapsedCpu
  164. * @param float $elapsedReal
  165. * @param int $memChange
  166. */
  167. protected function updateEntry( $name, $elapsedCpu, $elapsedReal, $memChange ) {
  168. $entry =& $this->collated[$name];
  169. if ( !is_array( $entry ) ) {
  170. $entry = $this->getZeroEntry();
  171. $this->collated[$name] =& $entry;
  172. }
  173. $entry['cpu'] += $elapsedCpu;
  174. $entry['real'] += $elapsedReal;
  175. $entry['memory'] += $memChange > 0 ? $memChange : 0;
  176. $entry['count']++;
  177. $entry['min_real'] = min( $entry['min_real'], $elapsedReal );
  178. $entry['max_real'] = max( $entry['max_real'], $elapsedReal );
  179. }
  180. /**
  181. * This method should not be called outside SectionProfiler
  182. *
  183. * @param string $functionname
  184. */
  185. public function profileInInternal( $functionname ) {
  186. // Once the data is collated for reports, any future calls
  187. // should clear the collation cache so the next report will
  188. // reflect them. This matters when trace mode is used.
  189. $this->collateDone = false;
  190. $cpu = $this->getTime( 'cpu' );
  191. $real = $this->getTime( 'wall' );
  192. $memory = memory_get_usage();
  193. if ( $this->start === null ) {
  194. $this->start = [ 'cpu' => $cpu, 'real' => $real, 'memory' => $memory ];
  195. }
  196. $this->workStack[] = [
  197. $functionname,
  198. count( $this->workStack ),
  199. $real,
  200. $cpu,
  201. $memory
  202. ];
  203. }
  204. /**
  205. * This method should not be called outside SectionProfiler
  206. *
  207. * @param string $functionname
  208. */
  209. public function profileOutInternal( $functionname ) {
  210. $item = array_pop( $this->workStack );
  211. if ( $item === null ) {
  212. $this->debugGroup( 'profileerror', "Profiling error: $functionname" );
  213. return;
  214. }
  215. list( $ofname, /* $ocount */, $ortime, $octime, $omem ) = $item;
  216. if ( $functionname === 'close' ) {
  217. $message = "Profile section ended by close(): {$ofname}";
  218. $this->debugGroup( 'profileerror', $message );
  219. if ( $this->collateOnly ) {
  220. $this->collated[$message] = $this->errorEntry;
  221. } else {
  222. $this->stack[] = [ $message, 0, 0.0, 0.0, 0, 0.0, 0.0, 0 ];
  223. }
  224. $functionname = $ofname;
  225. } elseif ( $ofname !== $functionname ) {
  226. $message = "Profiling error: in({$ofname}), out($functionname)";
  227. $this->debugGroup( 'profileerror', $message );
  228. if ( $this->collateOnly ) {
  229. $this->collated[$message] = $this->errorEntry;
  230. } else {
  231. $this->stack[] = [ $message, 0, 0.0, 0.0, 0, 0.0, 0.0, 0 ];
  232. }
  233. }
  234. $realTime = $this->getTime( 'wall' );
  235. $cpuTime = $this->getTime( 'cpu' );
  236. $memUsage = memory_get_usage();
  237. if ( $this->collateOnly ) {
  238. $elapsedcpu = $cpuTime - $octime;
  239. $elapsedreal = $realTime - $ortime;
  240. $memchange = $memUsage - $omem;
  241. $this->updateEntry( $functionname, $elapsedcpu, $elapsedreal, $memchange );
  242. } else {
  243. $this->stack[] = array_merge( $item, [ $realTime, $cpuTime, $memUsage ] );
  244. }
  245. $this->end = [
  246. 'cpu' => $cpuTime,
  247. 'real' => $realTime,
  248. 'memory' => $memUsage
  249. ];
  250. }
  251. /**
  252. * Returns a tree of function calls with their real times
  253. * @return string
  254. * @throws Exception
  255. */
  256. public function getCallTreeReport() {
  257. if ( $this->collateOnly ) {
  258. throw new Exception( "Tree is only available for trace profiling." );
  259. }
  260. return implode( '', array_map(
  261. [ $this, 'getCallTreeLine' ], $this->remapCallTree( $this->stack )
  262. ) );
  263. }
  264. /**
  265. * Recursive function the format the current profiling array into a tree
  266. *
  267. * @param array $stack Profiling array
  268. * @return array
  269. */
  270. protected function remapCallTree( array $stack ) {
  271. if ( count( $stack ) < 2 ) {
  272. return $stack;
  273. }
  274. $outputs = [];
  275. for ( $max = count( $stack ) - 1; $max > 0; ) {
  276. /* Find all items under this entry */
  277. $level = $stack[$max][1];
  278. $working = [];
  279. for ( $i = $max -1; $i >= 0; $i-- ) {
  280. if ( $stack[$i][1] > $level ) {
  281. $working[] = $stack[$i];
  282. } else {
  283. break;
  284. }
  285. }
  286. $working = $this->remapCallTree( array_reverse( $working ) );
  287. $output = [];
  288. foreach ( $working as $item ) {
  289. array_push( $output, $item );
  290. }
  291. array_unshift( $output, $stack[$max] );
  292. $max = $i;
  293. array_unshift( $outputs, $output );
  294. }
  295. $final = [];
  296. foreach ( $outputs as $output ) {
  297. foreach ( $output as $item ) {
  298. $final[] = $item;
  299. }
  300. }
  301. return $final;
  302. }
  303. /**
  304. * Callback to get a formatted line for the call tree
  305. * @param array $entry
  306. * @return string
  307. */
  308. protected function getCallTreeLine( $entry ) {
  309. // $entry has (name, level, stime, scpu, smem, etime, ecpu, emem)
  310. list( $fname, $level, $startreal, , , $endreal ) = $entry;
  311. $delta = $endreal - $startreal;
  312. $space = str_repeat( ' ', $level );
  313. # The ugly double sprintf is to work around a PHP bug,
  314. # which has been fixed in recent releases.
  315. return sprintf( "%10s %s %s\n",
  316. trim( sprintf( "%7.3f", $delta * 1000.0 ) ), $space, $fname );
  317. }
  318. /**
  319. * Populate collated data
  320. */
  321. protected function collateData() {
  322. if ( $this->collateDone ) {
  323. return;
  324. }
  325. $this->collateDone = true;
  326. // Close opened profiling sections
  327. while ( count( $this->workStack ) ) {
  328. $this->profileOutInternal( 'close' );
  329. }
  330. if ( $this->collateOnly ) {
  331. return; // already collated as methods exited
  332. }
  333. $this->collated = [];
  334. # Estimate profiling overhead
  335. $oldEnd = $this->end;
  336. $profileCount = count( $this->stack );
  337. $this->calculateOverhead( $profileCount );
  338. # First, subtract the overhead!
  339. $overheadTotal = $overheadMemory = $overheadInternal = [];
  340. foreach ( $this->stack as $entry ) {
  341. // $entry is (name,pos,rtime0,cputime0,mem0,rtime1,cputime1,mem1)
  342. $fname = $entry[0];
  343. $elapsed = $entry[5] - $entry[2];
  344. $memchange = $entry[7] - $entry[4];
  345. if ( $fname === '-overhead-total' ) {
  346. $overheadTotal[] = $elapsed;
  347. $overheadMemory[] = max( 0, $memchange );
  348. } elseif ( $fname === '-overhead-internal' ) {
  349. $overheadInternal[] = $elapsed;
  350. }
  351. }
  352. $overheadTotal = $overheadTotal ?
  353. array_sum( $overheadTotal ) / count( $overheadInternal ) : 0;
  354. $overheadMemory = $overheadMemory ?
  355. array_sum( $overheadMemory ) / count( $overheadInternal ) : 0;
  356. $overheadInternal = $overheadInternal ?
  357. array_sum( $overheadInternal ) / count( $overheadInternal ) : 0;
  358. # Collate
  359. foreach ( $this->stack as $index => $entry ) {
  360. // $entry is (name,pos,rtime0,cputime0,mem0,rtime1,cputime1,mem1)
  361. $fname = $entry[0];
  362. $elapsedCpu = $entry[6] - $entry[3];
  363. $elapsedReal = $entry[5] - $entry[2];
  364. $memchange = $entry[7] - $entry[4];
  365. $subcalls = $this->calltreeCount( $this->stack, $index );
  366. if ( substr( $fname, 0, 9 ) !== '-overhead' ) {
  367. # Adjust for profiling overhead (except special values with elapsed=0)
  368. if ( $elapsed ) {
  369. $elapsed -= $overheadInternal;
  370. $elapsed -= ( $subcalls * $overheadTotal );
  371. $memchange -= ( $subcalls * $overheadMemory );
  372. }
  373. }
  374. $this->updateEntry( $fname, $elapsedCpu, $elapsedReal, $memchange );
  375. }
  376. $this->collated['-overhead-total']['count'] = $profileCount;
  377. arsort( $this->collated, SORT_NUMERIC );
  378. // Unclobber the end info map (the overhead checking alters it)
  379. $this->end = $oldEnd;
  380. }
  381. /**
  382. * Dummy calls to calculate profiling overhead
  383. *
  384. * @param int $profileCount
  385. */
  386. protected function calculateOverhead( $profileCount ) {
  387. $this->profileInInternal( '-overhead-total' );
  388. for ( $i = 0; $i < $profileCount; $i++ ) {
  389. $this->profileInInternal( '-overhead-internal' );
  390. $this->profileOutInternal( '-overhead-internal' );
  391. }
  392. $this->profileOutInternal( '-overhead-total' );
  393. }
  394. /**
  395. * Counts the number of profiled function calls sitting under
  396. * the given point in the call graph. Not the most efficient algo.
  397. *
  398. * @param array $stack
  399. * @param int $start
  400. * @return int
  401. */
  402. protected 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. * Get the initial time of the request, based on getrusage()
  412. *
  413. * @param string|bool $metric Metric to use, with the following possibilities:
  414. * - user: User CPU time (without system calls)
  415. * - cpu: Total CPU time (user and system calls)
  416. * - wall (or any other string): elapsed time
  417. * - false (default): will fall back to default metric
  418. * @return float
  419. */
  420. protected function getTime( $metric = 'wall' ) {
  421. if ( $metric === 'cpu' || $metric === 'user' ) {
  422. $ru = wfGetRusage();
  423. if ( !$ru ) {
  424. return 0;
  425. }
  426. $time = $ru['ru_utime.tv_sec'] + $ru['ru_utime.tv_usec'] / 1e6;
  427. if ( $metric === 'cpu' ) {
  428. # This is the time of system calls, added to the user time
  429. # it gives the total CPU time
  430. $time += $ru['ru_stime.tv_sec'] + $ru['ru_stime.tv_usec'] / 1e6;
  431. }
  432. return $time;
  433. } else {
  434. return microtime( true );
  435. }
  436. }
  437. /**
  438. * Add an entry in the debug log file
  439. *
  440. * @param string $s String to output
  441. */
  442. protected function debug( $s ) {
  443. if ( function_exists( 'wfDebug' ) ) {
  444. wfDebug( $s );
  445. }
  446. }
  447. /**
  448. * Add an entry in the debug log group
  449. *
  450. * @param string $group Group to send the message to
  451. * @param string $s String to output
  452. */
  453. protected function debugGroup( $group, $s ) {
  454. if ( function_exists( 'wfDebugLog' ) ) {
  455. wfDebugLog( $group, $s );
  456. }
  457. }
  458. }
  459. /**
  460. * Subclass ScopedCallback to avoid call_user_func_array(), which is slow
  461. *
  462. * This class should not be used outside of SectionProfiler
  463. */
  464. class SectionProfileCallback extends ScopedCallback {
  465. /** @var SectionProfiler */
  466. protected $profiler;
  467. /** @var string */
  468. protected $section;
  469. /**
  470. * @param SectionProfiler $profiler
  471. * @param string $section
  472. */
  473. public function __construct( SectionProfiler $profiler, $section ) {
  474. parent::__construct( null );
  475. $this->profiler = $profiler;
  476. $this->section = $section;
  477. }
  478. function __destruct() {
  479. $this->profiler->profileOutInternal( $this->section );
  480. }
  481. }