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