XhprofData.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  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. use Wikimedia\RunningStat;
  21. /**
  22. * Convenience class for working with XHProf profiling data
  23. * <https://github.com/phacility/xhprof>. XHProf can be installed as a PECL
  24. * package for use with PHP5 (Zend PHP) and is built-in to HHVM 3.3.0.
  25. *
  26. * @copyright © 2014 Wikimedia Foundation and contributors
  27. * @since 1.28
  28. */
  29. class XhprofData {
  30. /**
  31. * @var array $config
  32. */
  33. protected $config;
  34. /**
  35. * Hierarchical profiling data returned by xhprof.
  36. * @var array $hieraData
  37. */
  38. protected $hieraData;
  39. /**
  40. * Per-function inclusive data.
  41. * @var array[] $inclusive
  42. */
  43. protected $inclusive;
  44. /**
  45. * Per-function inclusive and exclusive data.
  46. * @var array[] $complete
  47. */
  48. protected $complete;
  49. /**
  50. * Configuration data can contain:
  51. * - include: Array of function names to include in profiling.
  52. * - sort: Key to sort per-function reports on.
  53. *
  54. * @param array $data Xhprof profiling data, as returned by xhprof_disable()
  55. * @param array $config
  56. */
  57. public function __construct( array $data, array $config = [] ) {
  58. $this->config = array_merge( [
  59. 'include' => null,
  60. 'sort' => 'wt',
  61. ], $config );
  62. $this->hieraData = $this->pruneData( $data );
  63. }
  64. /**
  65. * Get raw data collected by xhprof.
  66. *
  67. * Each key in the returned array is an edge label for the call graph in
  68. * the form "caller==>callee". There is once special case edge labled
  69. * simply "main()" which represents the global scope entry point of the
  70. * application.
  71. *
  72. * XHProf will collect different data depending on the flags that are used:
  73. * - ct: Number of matching events seen.
  74. * - wt: Inclusive elapsed wall time for this event in microseconds.
  75. * - cpu: Inclusive elapsed cpu time for this event in microseconds.
  76. * (XHPROF_FLAGS_CPU)
  77. * - mu: Delta of memory usage from start to end of callee in bytes.
  78. * (XHPROF_FLAGS_MEMORY)
  79. * - pmu: Delta of peak memory usage from start to end of callee in
  80. * bytes. (XHPROF_FLAGS_MEMORY)
  81. * - alloc: Delta of amount memory requested from malloc() by the callee,
  82. * in bytes. (XHPROF_FLAGS_MALLOC)
  83. * - free: Delta of amount of memory passed to free() by the callee, in
  84. * bytes. (XHPROF_FLAGS_MALLOC)
  85. *
  86. * @return array
  87. * @see getInclusiveMetrics()
  88. * @see getCompleteMetrics()
  89. */
  90. public function getRawData() {
  91. return $this->hieraData;
  92. }
  93. /**
  94. * Convert an xhprof data key into an array of ['parent', 'child']
  95. * function names.
  96. *
  97. * The resulting array is left padded with nulls, so a key
  98. * with no parent (eg 'main()') will return [null, 'function'].
  99. *
  100. * @param string $key
  101. * @return array
  102. */
  103. public static function splitKey( $key ) {
  104. return array_pad( explode( '==>', $key, 2 ), -2, null );
  105. }
  106. /**
  107. * Remove data for functions that are not included in the 'include'
  108. * configuration array.
  109. *
  110. * @param array $data Raw xhprof data
  111. * @return array
  112. */
  113. protected function pruneData( $data ) {
  114. if ( !$this->config['include'] ) {
  115. return $data;
  116. }
  117. $want = array_fill_keys( $this->config['include'], true );
  118. $want['main()'] = true;
  119. $keep = [];
  120. foreach ( $data as $key => $stats ) {
  121. list( $parent, $child ) = self::splitKey( $key );
  122. if ( isset( $want[$parent] ) || isset( $want[$child] ) ) {
  123. $keep[$key] = $stats;
  124. }
  125. }
  126. return $keep;
  127. }
  128. /**
  129. * Get the inclusive metrics for each function call. Inclusive metrics
  130. * for given function include the metrics for all functions that were
  131. * called from that function during the measurement period.
  132. *
  133. * See getRawData() for a description of the metric that are returned for
  134. * each funcition call. The values for the wt, cpu, mu and pmu metrics are
  135. * arrays with these values:
  136. * - total: Cumulative value
  137. * - min: Minimum value
  138. * - mean: Mean (average) value
  139. * - max: Maximum value
  140. * - variance: Variance (spread) of the values
  141. *
  142. * @return array[]
  143. * @see getRawData()
  144. * @see getCompleteMetrics()
  145. */
  146. public function getInclusiveMetrics() {
  147. if ( $this->inclusive === null ) {
  148. $main = $this->hieraData['main()'];
  149. $hasCpu = isset( $main['cpu'] );
  150. $hasMu = isset( $main['mu'] );
  151. $hasAlloc = isset( $main['alloc'] );
  152. $this->inclusive = [];
  153. foreach ( $this->hieraData as $key => $stats ) {
  154. list( $parent, $child ) = self::splitKey( $key );
  155. if ( !isset( $this->inclusive[$child] ) ) {
  156. $this->inclusive[$child] = [
  157. 'ct' => 0,
  158. 'wt' => new RunningStat(),
  159. ];
  160. if ( $hasCpu ) {
  161. $this->inclusive[$child]['cpu'] = new RunningStat();
  162. }
  163. if ( $hasMu ) {
  164. $this->inclusive[$child]['mu'] = new RunningStat();
  165. $this->inclusive[$child]['pmu'] = new RunningStat();
  166. }
  167. if ( $hasAlloc ) {
  168. $this->inclusive[$child]['alloc'] = new RunningStat();
  169. $this->inclusive[$child]['free'] = new RunningStat();
  170. }
  171. }
  172. $this->inclusive[$child]['ct'] += $stats['ct'];
  173. foreach ( $stats as $stat => $value ) {
  174. if ( $stat === 'ct' ) {
  175. continue;
  176. }
  177. if ( !isset( $this->inclusive[$child][$stat] ) ) {
  178. // Ignore unknown stats
  179. continue;
  180. }
  181. for ( $i = 0; $i < $stats['ct']; $i++ ) {
  182. $this->inclusive[$child][$stat]->addObservation(
  183. $value / $stats['ct']
  184. );
  185. }
  186. }
  187. }
  188. // Convert RunningStat instances to static arrays and add
  189. // percentage stats.
  190. foreach ( $this->inclusive as $func => $stats ) {
  191. foreach ( $stats as $name => $value ) {
  192. if ( $value instanceof RunningStat ) {
  193. $total = $value->getMean() * $value->getCount();
  194. $percent = ( isset( $main[$name] ) && $main[$name] )
  195. ? 100 * $total / $main[$name]
  196. : 0;
  197. $this->inclusive[$func][$name] = [
  198. 'total' => $total,
  199. 'min' => $value->min,
  200. 'mean' => $value->getMean(),
  201. 'max' => $value->max,
  202. 'variance' => $value->m2,
  203. 'percent' => $percent,
  204. ];
  205. }
  206. }
  207. }
  208. uasort( $this->inclusive, self::makeSortFunction(
  209. $this->config['sort'], 'total'
  210. ) );
  211. }
  212. return $this->inclusive;
  213. }
  214. /**
  215. * Get the inclusive and exclusive metrics for each function call.
  216. *
  217. * In addition to the normal data contained in the inclusive metrics, the
  218. * metrics have an additional 'exclusive' measurement which is the total
  219. * minus the totals of all child function calls.
  220. *
  221. * @return array[]
  222. * @see getRawData()
  223. * @see getInclusiveMetrics()
  224. */
  225. public function getCompleteMetrics() {
  226. if ( $this->complete === null ) {
  227. // Start with inclusive data
  228. $this->complete = $this->getInclusiveMetrics();
  229. foreach ( $this->complete as $func => $stats ) {
  230. foreach ( $stats as $stat => $value ) {
  231. if ( $stat === 'ct' ) {
  232. continue;
  233. }
  234. // Initialize exclusive data with inclusive totals
  235. $this->complete[$func][$stat]['exclusive'] = $value['total'];
  236. }
  237. // Add sapce for call tree information to be filled in later
  238. $this->complete[$func]['calls'] = [];
  239. $this->complete[$func]['subcalls'] = [];
  240. }
  241. foreach ( $this->hieraData as $key => $stats ) {
  242. list( $parent, $child ) = self::splitKey( $key );
  243. if ( $parent !== null ) {
  244. // Track call tree information
  245. $this->complete[$child]['calls'][$parent] = $stats;
  246. $this->complete[$parent]['subcalls'][$child] = $stats;
  247. }
  248. if ( isset( $this->complete[$parent] ) ) {
  249. // Deduct child inclusive data from exclusive data
  250. foreach ( $stats as $stat => $value ) {
  251. if ( $stat === 'ct' ) {
  252. continue;
  253. }
  254. if ( !isset( $this->complete[$parent][$stat] ) ) {
  255. // Ignore unknown stats
  256. continue;
  257. }
  258. $this->complete[$parent][$stat]['exclusive'] -= $value;
  259. }
  260. }
  261. }
  262. uasort( $this->complete, self::makeSortFunction(
  263. $this->config['sort'], 'exclusive'
  264. ) );
  265. }
  266. return $this->complete;
  267. }
  268. /**
  269. * Get a list of all callers of a given function.
  270. *
  271. * @param string $function Function name
  272. * @return array
  273. * @see getEdges()
  274. */
  275. public function getCallers( $function ) {
  276. $edges = $this->getCompleteMetrics();
  277. if ( isset( $edges[$function]['calls'] ) ) {
  278. return array_keys( $edges[$function]['calls'] );
  279. } else {
  280. return [];
  281. }
  282. }
  283. /**
  284. * Get a list of all callees from a given function.
  285. *
  286. * @param string $function Function name
  287. * @return array
  288. * @see getEdges()
  289. */
  290. public function getCallees( $function ) {
  291. $edges = $this->getCompleteMetrics();
  292. if ( isset( $edges[$function]['subcalls'] ) ) {
  293. return array_keys( $edges[$function]['subcalls'] );
  294. } else {
  295. return [];
  296. }
  297. }
  298. /**
  299. * Find the critical path for the given metric.
  300. *
  301. * @param string $metric Metric to find critical path for
  302. * @return array
  303. */
  304. public function getCriticalPath( $metric = 'wt' ) {
  305. $func = 'main()';
  306. $path = [
  307. $func => $this->hieraData[$func],
  308. ];
  309. while ( $func ) {
  310. $callees = $this->getCallees( $func );
  311. $maxCallee = null;
  312. $maxCall = null;
  313. foreach ( $callees as $callee ) {
  314. $call = "{$func}==>{$callee}";
  315. if ( $maxCall === null ||
  316. $this->hieraData[$call][$metric] >
  317. $this->hieraData[$maxCall][$metric]
  318. ) {
  319. $maxCallee = $callee;
  320. $maxCall = $call;
  321. }
  322. }
  323. if ( $maxCall !== null ) {
  324. $path[$maxCall] = $this->hieraData[$maxCall];
  325. }
  326. $func = $maxCallee;
  327. }
  328. return $path;
  329. }
  330. /**
  331. * Make a closure to use as a sort function. The resulting function will
  332. * sort by descending numeric values (largest value first).
  333. *
  334. * @param string $key Data key to sort on
  335. * @param string $sub Sub key to sort array values on
  336. * @return Closure
  337. */
  338. public static function makeSortFunction( $key, $sub ) {
  339. return function ( $a, $b ) use ( $key, $sub ) {
  340. if ( isset( $a[$key] ) && isset( $b[$key] ) ) {
  341. // Descending sort: larger values will be first in result.
  342. // Values for 'main()' will not have sub keys
  343. $valA = is_array( $a[$key] ) ? $a[$key][$sub] : $a[$key];
  344. $valB = is_array( $b[$key] ) ? $b[$key][$sub] : $b[$key];
  345. return $valB <=> $valA;
  346. } else {
  347. // Sort datum with the key before those without
  348. return isset( $a[$key] ) ? -1 : 1;
  349. }
  350. };
  351. }
  352. }