convert ***sort builtins to use inout instead of references
[hiphop-php.git] / hphp / tools / perf-call-graph.php
blob0a8c75330952dc8b65773899939917e8f9152a0f
1 #!/usr/local/bin/php -j
2 <?hh
4 require(__DIR__.'/perf-lib.php');
6 # Returns true iff $sample contains a line containing $func.
7 function contains_frame(Vector $sample, string $func): bool {
8 foreach ($sample as $frame) {
9 if (strstr($frame, $func) !== false) return true;
11 return false;
14 # Node is used to construct to call graph. It contains an inclusive count of
15 # samples for itself and all of its children, along with a map from function
16 # names to children.
17 class Node {
18 private $kids = Map {};
20 public function __construct(private $name, private $count = 0) {}
22 # Add an edge from this function to $name, increasing the count of the child
23 # Node by one and returning it.
24 public function followEdge($name) {
25 if (!isset($this->kids[$name])) $this->kids[$name] = new Node($name);
27 $new_node = $this->kids[$name];
28 ++$new_node->count;
29 return $new_node;
32 # Print out the current node and all children.
33 public function show(
34 $total_count = null,
35 $indent = '',
36 ) {
37 if ($total_count === null) {
38 # We're the parent node and don't contain a useful name.
39 $total_count = $this->count;
40 } else {
41 # Prune any subtrees that are <0.5% of the total to keep the output
42 # readable.
43 if ($this->count / $total_count < 0.005) return;
44 printf("%s%.1f%% %s\n",
45 $indent,
46 $this->count / $total_count * 100,
47 $this->name);
48 $indent .= '- ';
51 $items = $this->kids->items()->toVector();
52 if ($items->count() == 0) return;
54 # If our only printable child is the body of this function, leave it out to
55 # keep the results clean.
56 if ($items[0][0] == '<body>' &&
57 ($items->count() == 1 || $items[1][1]->count / $total_count < 0.005)) {
58 return;
61 usort(inout $items, ($a, $b) ==> $b[1]->count - $a[1]->count);
62 foreach ($items as $pair) {
63 $pair[1]->show($total_count, $indent);
68 # Build and print a perf-annotated call graph of each stack trace in $samples.
69 # If $top is set, each trace will be truncated at the highest frame containing
70 # $top, if $root_last == false, or the lowest frame containing $top, if
71 # $root_last == true.
72 function treeify(
73 Vector $samples,
74 bool $reverse,
75 bool $root_last,
76 ?string $top
77 ) {
78 $root = new Node('', $samples->count());
79 $step = $reverse ? 1 : -1;
80 $search_step = $root_last ? -$step : $step;
82 foreach ($samples as $stack) {
83 $first = $reverse ? 0 : $stack->count() - 1;
84 $last = $reverse ? $stack->count() - 1 : 0;
85 $search_first = $root_last ? $last : $first;
86 $search_last = $root_last ? $first : $last;
88 if ($top !== null) {
89 for ($i = $search_first; $i !== $search_last + $search_step;
90 $i += $search_step) {
91 if (strpos($stack[$i], $top) !== false) break;
93 } else {
94 $i = $first;
97 $node = $root;
98 for (; $i !== $last + $step; $i += $step) {
99 $func = $stack[$i];
100 if ($func === 'HHVM::retInlHelper') continue;
101 $func = preg_replace('/^PHP::.+\.php::/', 'PHP::', $func);
103 $node = $node->followEdge($func);
105 if (!$reverse) {
106 # Add a final entry for exclusive time spent in the body of the bottom
107 # function.
108 $node->followEdge('<body>');
112 $root->show();
115 function usage($script_name) {
116 echo <<<EOT
117 Usage:
119 $script_name [--reverse] [--root-last] [--exe <name>] [symbol]...
121 This script expects the output of "perf script --fields comm,ip,sym" on stdin.
122 If no symbols are given, all samples will be combined into a single tree showing
123 the call graph, annotated with the inclusive cost of each node.
125 If one symbol is given, the call graph will instead be built from samples with
126 at least one frame containing the symbol. Samples will be truncated at the
127 highest frame containing the symbol (or the lowest, if --root-last is given).
128 Note that the symbol may appear anywhere in the function name, so using
129 'Foo::translate' will match both 'Foo::translate' and 'Foo::translateFrob'.
130 If you just want Foo::translate, use 'Foo::translate('.
132 Finally, if multiple symbols are given, a summary will be printed showing the
133 total number of samples, and how many samples contain each symbol.
135 If --reverse is given, the call graph will be inverted before tree formation,
136 showing callers of any given symbols, rather than callees.
138 By default, only samples from the 'hhvm' binary will be considered. Giving
139 --exe with an argument overrides this.
140 EOT;
143 function main($argv) {
144 ini_set('memory_limit', '64G');
145 if (posix_isatty(STDIN)) {
146 usage($argv[0]);
147 return 1;
149 array_shift(&$argv);
151 $reverse = false;
152 $root_last = false;
153 $functions = Set {};
154 $exe = 'hhvm';
155 for ($i = 0; $i < count($argv); ++$i) {
156 $arg = $argv[$i];
157 if ($arg=== '--reverse') {
158 $reverse = true;
159 } else if ($arg === '--root-last') {
160 $root_last = true;
161 } else if ($arg === '--exe') {
162 $exe = $argv[++$i];
163 } else {
164 $functions->add($arg);
168 $samples = read_perf_samples(STDIN, $exe);
169 $subsamples = Map {'all' => $samples};
170 foreach ($functions as $f) {
171 $subsamples[$f] = $samples->filter($s ==> contains_frame($s, $f));
174 if ($functions->isEmpty()) {
175 treeify($subsamples['all'], $reverse, $root_last, null);
176 } else if ($functions->count() === 1) {
177 $func = $functions->firstValue();
178 $sub = $subsamples[$func];
179 printf("Looking for pattern *%s*. %d of %d total samples (%.2f%%)\n\n",
180 $func, $sub->count(), $samples->count(),
181 $sub->count() / $samples->count() * 100);
182 treeify($sub, $reverse, $root_last, $func);
183 } else {
184 foreach ($subsamples as $k => $v) {
185 printf(
186 "%8d %5.1f %s\n", $v->count(), $v->count() / $samples->count() * 100, $k
191 return 0;
194 exit(main($argv));