callgraph_utils.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. <?php
  2. // Copyright (c) 2009 Facebook
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. //
  16. /*
  17. * This file contains callgraph image generation related XHProf utility
  18. * functions
  19. *
  20. */
  21. // Supported ouput format
  22. $xhprof_legal_image_types = array(
  23. "jpg" => 1,
  24. "gif" => 1,
  25. "png" => 1,
  26. "svg" => 1, // support scalable vector graphic
  27. "ps" => 1,
  28. );
  29. /**
  30. * Send an HTTP header with the response. You MUST use this function instead
  31. * of header() so that we can debug header issues because they're virtually
  32. * impossible to debug otherwise. If you try to commit header(), SVN will
  33. * reject your commit.
  34. *
  35. * @param string HTTP header name, like 'Location'
  36. * @param string HTTP header value, like 'http://www.example.com/'
  37. *
  38. */
  39. function xhprof_http_header($name, $value) {
  40. if (!$name) {
  41. xhprof_error('http_header usage');
  42. return null;
  43. }
  44. if (!is_string($value)) {
  45. xhprof_error('http_header value not a string');
  46. }
  47. header($name.': '.$value, true);
  48. }
  49. /**
  50. * Genearte and send MIME header for the output image to client browser.
  51. *
  52. * @author cjiang
  53. */
  54. function xhprof_generate_mime_header($type, $length) {
  55. switch ($type) {
  56. case 'jpg':
  57. $mime = 'image/jpeg';
  58. break;
  59. case 'gif':
  60. $mime = 'image/gif';
  61. break;
  62. case 'png':
  63. $mime = 'image/png';
  64. break;
  65. case 'svg':
  66. $mime = 'image/svg+xml'; // content type for scalable vector graphic
  67. break;
  68. case 'ps':
  69. $mime = 'application/postscript';
  70. default:
  71. $mime = false;
  72. }
  73. if ($mime) {
  74. xhprof_http_header('Content-type', $mime);
  75. xhprof_http_header('Content-length', (string)$length);
  76. }
  77. }
  78. /**
  79. * Generate image according to DOT script. This function will spawn a process
  80. * with "dot" command and pipe the "dot_script" to it and pipe out the
  81. * generated image content.
  82. *
  83. * @param dot_script, string, the script for DOT to generate the image.
  84. * @param type, one of the supported image types, see
  85. * $xhprof_legal_image_types.
  86. * @returns, binary content of the generated image on success. empty string on
  87. * failure.
  88. *
  89. * @author cjiang
  90. */
  91. function xhprof_generate_image_by_dot($dot_script, $type) {
  92. $descriptorspec = array(
  93. // stdin is a pipe that the child will read from
  94. 0 => array("pipe", "r"),
  95. // stdout is a pipe that the child will write to
  96. 1 => array("pipe", "w"),
  97. // stderr is a pipe that the child will write to
  98. 2 => array("pipe", "w")
  99. );
  100. $cmd = " dot -T".$type;
  101. $process = proc_open( $cmd, $descriptorspec, $pipes, sys_get_temp_dir(), array( 'PATH' => getenv( 'PATH' ) ) );
  102. if (is_resource($process)) {
  103. fwrite($pipes[0], $dot_script);
  104. fclose($pipes[0]);
  105. $output = stream_get_contents($pipes[1]);
  106. $err = stream_get_contents($pipes[2]);
  107. if (!empty($err)) {
  108. print "failed to execute cmd: \"$cmd\". stderr: `$err'\n";
  109. exit;
  110. }
  111. fclose($pipes[2]);
  112. fclose($pipes[1]);
  113. proc_close($process);
  114. return $output;
  115. }
  116. print "failed to execute cmd \"$cmd\"";
  117. exit();
  118. }
  119. /*
  120. * Get the children list of all nodes.
  121. */
  122. function xhprof_get_children_table($raw_data) {
  123. $children_table = array();
  124. foreach ($raw_data as $parent_child => $info) {
  125. list($parent, $child) = xhprof_parse_parent_child($parent_child);
  126. if (!isset($children_table[$parent])) {
  127. $children_table[$parent] = array($child);
  128. } else {
  129. $children_table[$parent][] = $child;
  130. }
  131. }
  132. return $children_table;
  133. }
  134. /**
  135. * Generate DOT script from the given raw phprof data.
  136. *
  137. * @param raw_data, phprof profile data.
  138. * @param threshold, float, the threshold value [0,1). The functions in the
  139. * raw_data whose exclusive wall times ratio are below the
  140. * threshold will be filtered out and won't apprear in the
  141. * generated image.
  142. * @param page, string(optional), the root node name. This can be used to
  143. * replace the 'main()' as the root node.
  144. * @param func, string, the focus function.
  145. * @param critical_path, bool, whether or not to display critical path with
  146. * bold lines.
  147. * @returns, string, the DOT script to generate image.
  148. *
  149. * @author cjiang
  150. */
  151. function xhprof_generate_dot_script($raw_data, $threshold, $source, $page,
  152. $func, $critical_path, $right=null,
  153. $left=null) {
  154. $max_width = 5;
  155. $max_height = 3.5;
  156. $max_fontsize = 35;
  157. $max_sizing_ratio = 20;
  158. $totals;
  159. if ($left === null) {
  160. // init_metrics($raw_data, null, null);
  161. }
  162. $sym_table = xhprof_compute_flat_info($raw_data, $totals);
  163. if ($critical_path) {
  164. $children_table = xhprof_get_children_table($raw_data);
  165. $node = "main()";
  166. $path = array();
  167. $path_edges = array();
  168. $visited = array();
  169. while ($node) {
  170. $visited[$node] = true;
  171. if (isset($children_table[$node])) {
  172. $max_child = null;
  173. foreach ($children_table[$node] as $child) {
  174. if (isset($visited[$child])) {
  175. continue;
  176. }
  177. if ($max_child === null ||
  178. abs($raw_data[xhprof_build_parent_child_key($node,
  179. $child)]["wt"]) >
  180. abs($raw_data[xhprof_build_parent_child_key($node,
  181. $max_child)]["wt"])) {
  182. $max_child = $child;
  183. }
  184. }
  185. if ($max_child !== null) {
  186. $path[$max_child] = true;
  187. $path_edges[xhprof_build_parent_child_key($node, $max_child)] = true;
  188. }
  189. $node = $max_child;
  190. } else {
  191. $node = null;
  192. }
  193. }
  194. }
  195. // if it is a benchmark callgraph, we make the benchmarked function the root.
  196. if ($source == "bm" && array_key_exists("main()", $sym_table)) {
  197. $total_times = $sym_table["main()"]["ct"];
  198. $remove_funcs = array("main()",
  199. "hotprofiler_disable",
  200. "call_user_func_array",
  201. "xhprof_disable");
  202. foreach ($remove_funcs as $cur_del_func) {
  203. if (array_key_exists($cur_del_func, $sym_table) &&
  204. $sym_table[$cur_del_func]["ct"] == $total_times) {
  205. unset($sym_table[$cur_del_func]);
  206. }
  207. }
  208. }
  209. // use the function to filter out irrelevant functions.
  210. if (!empty($func)) {
  211. $interested_funcs = array();
  212. foreach ($raw_data as $parent_child => $info) {
  213. list($parent, $child) = xhprof_parse_parent_child($parent_child);
  214. if ($parent == $func || $child == $func) {
  215. $interested_funcs[$parent] = 1;
  216. $interested_funcs[$child] = 1;
  217. }
  218. }
  219. foreach ($sym_table as $symbol => $info) {
  220. if (!array_key_exists($symbol, $interested_funcs)) {
  221. unset($sym_table[$symbol]);
  222. }
  223. }
  224. }
  225. $result = "digraph call_graph {\n";
  226. // Filter out functions whose exclusive time ratio is below threshold, and
  227. // also assign a unique integer id for each function to be generated. In the
  228. // meantime, find the function with the most exclusive time (potentially the
  229. // performance bottleneck).
  230. $cur_id = 0; $max_wt = 0;
  231. foreach ($sym_table as $symbol => $info) {
  232. if (empty($func) && abs($info["wt"] / $totals["wt"]) < $threshold) {
  233. unset($sym_table[$symbol]);
  234. continue;
  235. }
  236. if ($max_wt == 0 || $max_wt < abs($info["excl_wt"])) {
  237. $max_wt = abs($info["excl_wt"]);
  238. }
  239. $sym_table[$symbol]["id"] = $cur_id;
  240. $cur_id ++;
  241. }
  242. // Generate all nodes' information.
  243. foreach ($sym_table as $symbol => $info) {
  244. if ($info["excl_wt"] == 0) {
  245. $sizing_factor = $max_sizing_ratio;
  246. } else {
  247. $sizing_factor = $max_wt / abs($info["excl_wt"]) ;
  248. if ($sizing_factor > $max_sizing_ratio) {
  249. $sizing_factor = $max_sizing_ratio;
  250. }
  251. }
  252. $fillcolor = (($sizing_factor < 1.5) ?
  253. ", style=filled, fillcolor=red" : "");
  254. if ($critical_path) {
  255. // highlight nodes along critical path.
  256. if (!$fillcolor && array_key_exists($symbol, $path)) {
  257. $fillcolor = ", style=filled, fillcolor=yellow";
  258. }
  259. }
  260. $fontsize = ", fontsize="
  261. .(int)($max_fontsize / (($sizing_factor - 1) / 10 + 1));
  262. $width = ", width=".sprintf("%.1f", $max_width / $sizing_factor);
  263. $height = ", height=".sprintf("%.1f", $max_height / $sizing_factor);
  264. if ($symbol == "main()") {
  265. $shape = "octagon";
  266. $name = "Total: ".($totals["wt"] / 1000.0)." ms\\n";
  267. $name .= addslashes(isset($page) ? $page : $symbol);
  268. } else {
  269. $shape = "box";
  270. $name = addslashes($symbol)."\\nInc: ". sprintf("%.3f",$info["wt"] / 1000) .
  271. " ms (" . sprintf("%.1f%%", 100 * $info["wt"] / $totals["wt"]).")";
  272. }
  273. if ($left === null) {
  274. $label = ", label=\"".$name."\\nExcl: "
  275. .(sprintf("%.3f",$info["excl_wt"] / 1000.0))." ms ("
  276. .sprintf("%.1f%%", 100 * $info["excl_wt"] / $totals["wt"])
  277. . ")\\n".$info["ct"]." total calls\"";
  278. } else {
  279. if (isset($left[$symbol]) && isset($right[$symbol])) {
  280. $label = ", label=\"".addslashes($symbol).
  281. "\\nInc: ".(sprintf("%.3f",$left[$symbol]["wt"] / 1000.0))
  282. ." ms - "
  283. .(sprintf("%.3f",$right[$symbol]["wt"] / 1000.0))." ms = "
  284. .(sprintf("%.3f",$info["wt"] / 1000.0))." ms".
  285. "\\nExcl: "
  286. .(sprintf("%.3f",$left[$symbol]["excl_wt"] / 1000.0))
  287. ." ms - ".(sprintf("%.3f",$right[$symbol]["excl_wt"] / 1000.0))
  288. ." ms = ".(sprintf("%.3f",$info["excl_wt"] / 1000.0))." ms".
  289. "\\nCalls: ".(sprintf("%.3f",$left[$symbol]["ct"]))." - "
  290. .(sprintf("%.3f",$right[$symbol]["ct"]))." = "
  291. .(sprintf("%.3f",$info["ct"]))."\"";
  292. } else if (isset($left[$symbol])) {
  293. $label = ", label=\"".addslashes($symbol).
  294. "\\nInc: ".(sprintf("%.3f",$left[$symbol]["wt"] / 1000.0))
  295. ." ms - 0 ms = ".(sprintf("%.3f",$info["wt"] / 1000.0))
  296. ." ms"."\\nExcl: "
  297. .(sprintf("%.3f",$left[$symbol]["excl_wt"] / 1000.0))
  298. ." ms - 0 ms = "
  299. .(sprintf("%.3f",$info["excl_wt"] / 1000.0))." ms".
  300. "\\nCalls: ".(sprintf("%.3f",$left[$symbol]["ct"]))." - 0 = "
  301. .(sprintf("%.3f",$info["ct"]))."\"";
  302. } else {
  303. $label = ", label=\"".addslashes($symbol).
  304. "\\nInc: 0 ms - "
  305. .(sprintf("%.3f",$right[$symbol]["wt"] / 1000.0))
  306. ." ms = ".(sprintf("%.3f",$info["wt"] / 1000.0))." ms".
  307. "\\nExcl: 0 ms - "
  308. .(sprintf("%.3f",$right[$symbol]["excl_wt"] / 1000.0))
  309. ." ms = ".(sprintf("%.3f",$info["excl_wt"] / 1000.0))." ms".
  310. "\\nCalls: 0 - ".(sprintf("%.3f",$right[$symbol]["ct"]))
  311. ." = ".(sprintf("%.3f",$info["ct"]))."\"";
  312. }
  313. }
  314. $result .= "N" . $sym_table[$symbol]["id"];
  315. $result .= "[shape=$shape ".$label.$width
  316. .$height.$fontsize.$fillcolor."];\n";
  317. }
  318. // Generate all the edges' information.
  319. foreach ($raw_data as $parent_child => $info) {
  320. list($parent, $child) = xhprof_parse_parent_child($parent_child);
  321. if (isset($sym_table[$parent]) && isset($sym_table[$child]) &&
  322. (empty($func) ||
  323. (!empty($func) && ($parent == $func || $child == $func)))) {
  324. $label = $info["ct"] == 1 ? $info["ct"]." call" : $info["ct"]." calls";
  325. $headlabel = $sym_table[$child]["wt"] > 0 ?
  326. sprintf("%.1f%%", 100 * $info["wt"]
  327. / $sym_table[$child]["wt"])
  328. : "0.0%";
  329. $taillabel = ($sym_table[$parent]["wt"] > 0) ?
  330. sprintf("%.1f%%",
  331. 100 * $info["wt"] /
  332. ($sym_table[$parent]["wt"] - $sym_table["$parent"]["excl_wt"]))
  333. : "0.0%";
  334. $linewidth = 1;
  335. $arrow_size = 1;
  336. if ($critical_path &&
  337. isset($path_edges[xhprof_build_parent_child_key($parent, $child)])) {
  338. $linewidth = 10; $arrow_size = 2;
  339. }
  340. $result .= "N" . $sym_table[$parent]["id"] . " -> N"
  341. . $sym_table[$child]["id"];
  342. $result .= "[arrowsize=$arrow_size, color=grey, style=\"setlinewidth($linewidth)\","
  343. ." label=\""
  344. .$label."\", headlabel=\"".$headlabel
  345. ."\", taillabel=\"".$taillabel."\" ]";
  346. $result .= ";\n";
  347. }
  348. }
  349. $result = $result . "\n}";
  350. return $result;
  351. }
  352. function xhprof_render_diff_image($xhprof_runs_impl, $run1, $run2,
  353. $type, $threshold, $source) {
  354. $total1;
  355. $total2;
  356. $raw_data1 = $xhprof_runs_impl->get_run($run1, $source, $desc_unused);
  357. $raw_data2 = $xhprof_runs_impl->get_run($run2, $source, $desc_unused);
  358. // init_metrics($raw_data1, null, null);
  359. $children_table1 = xhprof_get_children_table($raw_data1);
  360. $children_table2 = xhprof_get_children_table($raw_data2);
  361. $symbol_tab1 = xhprof_compute_flat_info($raw_data1, $total1);
  362. $symbol_tab2 = xhprof_compute_flat_info($raw_data2, $total2);
  363. $run_delta = xhprof_compute_diff($raw_data1, $raw_data2);
  364. $script = xhprof_generate_dot_script($run_delta, $threshold, $source,
  365. null, null, true,
  366. $symbol_tab1, $symbol_tab2);
  367. $content = xhprof_generate_image_by_dot($script, $type);
  368. xhprof_generate_mime_header($type, strlen($content));
  369. echo $content;
  370. }
  371. /**
  372. * Generate image content from phprof run id.
  373. *
  374. * @param object $xhprof_runs_impl An object that implements
  375. * the iXHProfRuns interface
  376. * @param run_id, integer, the unique id for the phprof run, this is the
  377. * primary key for phprof database table.
  378. * @param type, string, one of the supported image types. See also
  379. * $xhprof_legal_image_types.
  380. * @param threshold, float, the threshold value [0,1). The functions in the
  381. * raw_data whose exclusive wall times ratio are below the
  382. * threshold will be filtered out and won't apprear in the
  383. * generated image.
  384. * @param func, string, the focus function.
  385. * @returns, string, the DOT script to generate image.
  386. *
  387. * @author cjiang
  388. */
  389. function xhprof_get_content_by_run($xhprof_runs_impl, $run_id, $type,
  390. $threshold, $func, $source,
  391. $critical_path) {
  392. if (!$run_id)
  393. return "";
  394. $raw_data = $xhprof_runs_impl->get_run($run_id, $source, $description);
  395. if (!$raw_data) {
  396. xhprof_error("Raw data is empty");
  397. return "";
  398. }
  399. $script = xhprof_generate_dot_script($raw_data, $threshold, $source,
  400. $description, $func, $critical_path);
  401. $content = xhprof_generate_image_by_dot($script, $type);
  402. return $content;
  403. }
  404. /**
  405. * Generate image from phprof run id and send it to client.
  406. *
  407. * @param object $xhprof_runs_impl An object that implements
  408. * the iXHProfRuns interface
  409. * @param run_id, integer, the unique id for the phprof run, this is the
  410. * primary key for phprof database table.
  411. * @param type, string, one of the supported image types. See also
  412. * $xhprof_legal_image_types.
  413. * @param threshold, float, the threshold value [0,1). The functions in the
  414. * raw_data whose exclusive wall times ratio are below the
  415. * threshold will be filtered out and won't apprear in the
  416. * generated image.
  417. * @param func, string, the focus function.
  418. * @param bool, does this run correspond to a PHProfLive run or a dev run?
  419. * @author cjiang
  420. */
  421. function xhprof_render_image($xhprof_runs_impl, $run_id, $type, $threshold,
  422. $func, $source, $critical_path) {
  423. $content = xhprof_get_content_by_run($xhprof_runs_impl, $run_id, $type,
  424. $threshold,
  425. $func, $source, $critical_path);
  426. if (!$content) {
  427. print "Error: either we can not find profile data for run_id ".$run_id
  428. ." or the threshold ".$threshold." is too small or you do not"
  429. ." have 'dot' image generation utility installed.";
  430. exit();
  431. }
  432. xhprof_generate_mime_header($type, strlen($content));
  433. echo $content;
  434. }