[docs][TT# 1667] Get rid of wrong documentation
[parrot.git] / tools / dev / pprof2cg.pl
blobafe3b7043216163b7247f8590faf8ec377e7a4a1
1 #! perl
3 # Copyright (C) 2009, Parrot Foundation.
4 # $Id$
6 use strict;
7 use warnings;
9 =head1 NAME
11 tools/dev/pprof2cg.pl
13 =head1 DESCRIPTION
15 Convert the output of Parrot's profiling runcore to a Callgrind-compatible
16 format.
18 =head1 SYNOPSIS
20 perl tools/dev/pprof2cg.pl parrot.pprof.1234
22 =head1 USAGE
24 Generate a profile by passing C<-Rprofiling> to parrot, for example C<./parrot
25 -Rprofiling perl6.pbc hello.p6>. Once execution completes, C<parrot> will
26 print a message specifying the location of the parrot profile (pprof). The
27 profile will be named parrot.pprof.XXXX, where XXXX is the PID of the parrot
28 process unless another name is specified by the B<PARROT_PROFILING_OUTPUT>
29 environment variable.
31 To generate a Callgrind-compatible profile, run this script with the pprof
32 filename as the first argument. The output file usable by kcachegrind will be
33 in parrot.out.XXXX, where XXXX again is the PID of the original parrot process.
35 =head1 ENVIRONMENT VARIABLES
37 =head2 PARROT_PROFILING_OUTPUT
39 If the environment variable PARROT_PROFILING_OUTPUT is set, the profiling
40 runcore will attempt to use its value as the profile filename. Note that it
41 does not check whether the file already exists and will happily overwrite
42 existing files.
44 =cut
46 main(@ARGV);
48 =head1 INTERNAL DATA STRUCTURES
50 =over 4
52 =item notes
54 Parrot's execution model is built on continuation-passing style and does not
55 precisely fit the straightforward function-based format that
56 Callgrind-compatible tools expect. For this reason, the profiling runcore
57 captures information about context switches (CS lines in the pprof file) and
58 pprof2cg.pl maintains a context stack that functions similarly to a typical
59 call stack. pprof2cg.pl then maps these context switches as if they were
60 function calls and returns. See C<$call_stack> for more information.
62 =item C<$call_stack>
64 Variables which are named C<$call_stack> hold a reference to an array of hashes
65 which contain information about the currently active contexts. When collecting
66 timing information about an op, it is necessary to add that information to all
67 function calls on the stack because Callgrind-compatible tools expect the cost
68 of a function call to include the cost of all calls made by that function, etc.
70 When a context switch is detected, C<process_line> looks at the context stack
71 to determine if the context switch looks like a function call (if the context
72 hasn't been seen before) or a return (if the context is somewhere on the
73 stack). There are some other cases that the code handles, but these can be
74 ignored for now in the interest of simplicity. If the context has been seen,
75 C<process_line> shifts contexts off the stack until it finds the context that
76 has been switched to. When C<process_line> detects a new context, it adds a
77 fake op representing a function call to C<$stats> and unshifts a new context
78 onto the stack.
80 Each element of C<@$call_stack> contains the information needed to uniquely
81 identify the site of the original context switch.
83 =item C<$stats>
85 Variables which are named C<$stats> contain a reference to a deeply nested
86 HoHoH.. which contains all information gathered about a profiled PIR program.
87 The nested hashes and arrays refer to the file, namespace, line of source code
88 and op number, respectively. The op number is used to allow multiple
89 instructions per line because PIR instructions often represent multiple
90 low-level instructions. This also makes it easy to inject pseudo-ops to
91 represent function calls.
93 Each op always has a time value representing the total amount of time spent in
94 that op. Ops may also have an op_name value that gives the name of the op.
95 When control flow similar to a function call is detected, a pseudo-op
96 representing a function call is injected. These pseudo-ops have zero cost when
97 initialized and are used to determine the total time spent between when the
98 context becomes active and when control flow returns to or past the context.
99 Although they're not exactly like functions calls, they're close enough that it
100 may help to think of them as such.
102 Uncomment the print_stats line in main to see a representation of the data
103 contained in C<$stats>.
105 =back
107 =head1 FUNCTIONS
109 =over 4
111 =item C<main>
113 This function is minimal driver for the other functions in this file, taking
114 the name of a Parrot profile and writing a Callgrind-compatible profile to a
115 similarly-named file.
117 =cut
119 sub main {
120 my $filename = shift;
121 my $stats = {};
123 die "Usage: $0 filename\n"
124 unless defined $filename;
126 $stats->{global_stats}{total_time} = 0;
128 open(my $in_fh, '<', $filename) or die "couldn't open $filename for reading: $!";
130 process_input($in_fh, $stats);
132 close($in_fh) or die "couldn't close $filename: $!";
134 unless ($filename =~ s/pprof/out/) {
135 $filename = "$filename.out";
138 open(my $out_fh, '>', $filename) or die "couldn't open $filename for writing: $!";
139 my $cg_profile = get_cg_profile($stats);
140 print $out_fh $cg_profile;
141 close($out_fh) or die "couldn't close $filename: $!";
142 print "$filename can now be used with kcachegrind or other callgrind-compatible tools.\n";
145 =item C<process_input>
147 This function takes a file handle open to a Parrot profile and a reference
148 to a hash of fine-grained statistics about the current PIR program. It
149 modifies the statistics according to the information from the Parrot profile.
151 =cut
153 sub process_input {
154 my ($input, $stats) = @_;
155 my $call_stack = [];
157 while(my $line = <$input>) {
158 if ($line =~ /^OP:(.*)$/) {
159 # Decode string in the format C<{x{key1:value1}x}{x{key2:value2}x}>
160 my %op_hash = $1 =~ /{x{([^:]+):(.*?)}x}/g
161 or die "invalidly formed line '$line'";
163 my $cur_ctx = $call_stack->[0]
164 or die "input file did not specify an initial context";
166 # If we've already seen this line, bump the op number. Otherwise reset it.
167 if (exists $cur_ctx->{line} && $op_hash{line} == $cur_ctx->{line}) {
168 $cur_ctx->{op_num}++;
170 else {
171 $cur_ctx->{op_num} = 0;
174 $cur_ctx->{line} = $op_hash{line};
175 my $extra = { op_name => $op_hash{op} };
176 my $time = $op_hash{time};
178 $stats->{global_stats}{total_time} += $time;
179 store_stats($stats, $cur_ctx, $time, $extra);
181 # Add the time spent by this op to each op on the call "stack".
182 $stats->{ $_->{file} }{ $_->{ns} }{ $_->{line} }[ $_->{op_num} ]{time} += $time
183 for @$call_stack[1 .. $#$call_stack];
185 #context switch
186 elsif ($line =~ /^CS:(.*)$/) {
188 # Decode string in the format C<{x{key1:value1}x}{x{key2:value2}x}>
189 my %cs_hash = $1 =~ /{x{([^:]+):(.*?)}x}/g
190 or die "invalidly formed line '$line'";
192 if (!@$call_stack) {
193 $call_stack->[0] = \%cs_hash;
195 else {
196 my $cur_ctx = $call_stack->[0];
197 my $hash_ctx = $cs_hash{ctx};
198 my $is_redundant = $cur_ctx->{ctx} eq $hash_ctx;
199 my $reused_ctx = $is_redundant && $cur_ctx->{sub} ne $cs_hash{sub};
201 # If we're calling a new sub with the same context, modify the
202 # current context to have the name and address of the new sub.
203 if ($reused_ctx) {
204 $cur_ctx->{ns} = $cs_hash{ns};
205 $cur_ctx->{sub} = $cs_hash{sub};
208 # The new context is the same as the old one, so don't modify the call stack.
209 elsif ($is_redundant) {
210 # This space intentionally left blank.
213 # If the new context isn't in the current call stack, unshift
214 # it onto the start of the stack.
215 elsif (!grep {$_->{ctx} eq $hash_ctx} @$call_stack) {
216 $cur_ctx->{op_num}++;
217 my $extra = {
218 op_name => "CALL",
219 target => $cs_hash{ns}
221 store_stats($stats, $call_stack->[0], 0, $extra);
222 unshift @$call_stack, \%cs_hash;
224 else {
225 #shift contexts off the stack until one matches the current ctx
226 shift @$call_stack while $call_stack->[0]{ctx} ne $hash_ctx;
229 #print Dumper($call_stack);
231 elsif ($line =~ /^VERSION:(\d+)$/) {
232 my $version = $1;
233 if ($version != 2) {
234 die "profile was generated by an incompatible version of the profiling runcore.";
237 elsif ($line =~ /^CLI:(.*)$/) {
238 $stats->{'global_stats'}{'cli'} = $1;
240 elsif ($line =~ /^END_OF_RUNLOOP:(.*)$/) {
241 # This is the end of an outermost runloop. Several of these can
242 # occur during the execution of a script, e.g. for :init subs.
243 @$call_stack = ();
245 elsif ($line =~ /^AN:/) {
246 #ignore annotations for now
248 elsif ($line =~ /^#/) {
249 #comments are always ignored
251 else {
252 die "Unrecognized line format: '$line'";
257 =item C<print_stats>
259 This function prints a complete, human-readable representation of the
260 statistical data that have been collected into the C<$stats> argument to
261 stdout. It is primarily intended to ease debugging and is not necessary to
262 create a Callgrind-compatible profile.
264 =cut
266 sub print_stats {
267 my $stats = shift;
269 for my $file (grep {$_ ne 'global_stats'} sort keys %$stats) {
270 for my $ns (sort keys %{ $stats->{$file} }) {
271 for my $line_num (sort {$a<=>$b} keys %{ $stats->{$file}{$ns} }) {
272 for my $op_num (0 .. $#{$stats->{$file}{$ns}{$line_num}}) {
274 print "$file $ns line/op:$line_num/$op_num ";
276 for my $attr (sort keys %{ $stats->{$file}{$ns}{$line_num}[$op_num] }) {
277 print "{ $attr => $stats->{$file}{$ns}{$line_num}[$op_num]{$attr} } ";
279 print "\n";
282 print "\n";
287 =item C<store_stats>
289 This function adds statistical data to the C<$stats> hash reference. The
290 C<$loc> argument specifies information such as the namespace, file, line
291 and op number where the data should go. C<$time> is an integer representing
292 the amount of time spent at the specified location. C<$extra> contains any
293 ancillary data that should be stored in the hash. This includes data on
294 (faked) subroutine calls and op names.
296 =cut
298 sub store_stats {
299 my ($stats, $loc, $time, $extra) = @_;
301 my $by_op = ( $stats->{ $loc->{file} }{ $loc->{ns} }{ $loc->{line} }[ $loc->{op_num} ] ||= {} );
303 if ($by_op->{hits}) {
304 $by_op->{hits} ++;
305 $by_op->{time} += $time;
307 else {
308 $by_op->{hits} = 1;
309 $by_op->{time} = $time;
311 $by_op->{$_} = $extra->{$_} for keys %$extra;
315 =item C<get_cg_profile>
317 This function takes a reference to a hash of statistical information about a
318 PIR program and returns a string containing a Callgrind-compatible profile.
319 Although some information in the profile may not be accurate (namely PID and
320 creator), tools such as kcachegrind are able to consume files generated by this
321 function.
323 =cut
325 sub get_cg_profile {
327 my $stats = shift;
328 my @output = ();
330 push @output, <<"HEADER";
331 version: 1
332 creator: 3.4.1-Debian
333 pid: 5751
334 cmd: $stats->{'global_stats'}{'cli'}
336 part: 1
337 desc: I1 cache:
338 desc: D1 cache:
339 desc: L2 cache:
340 desc: Timerange: Basic block 0 - $stats->{'global_stats'}{'total_time'}
341 desc: Trigger: Program termination
342 positions: line
343 events: Ir
344 summary: $stats->{'global_stats'}{'total_time'}
346 HEADER
348 for my $file (grep {$_ ne 'global_stats'} keys %$stats) {
350 push @output, "fl=$file";
352 for my $ns (keys %{ $stats->{$file} }) {
353 push @output, "\nfn=$ns";
355 for my $line (sort keys %{ $stats->{$file}{$ns} }) {
357 my $curr_op = 0;
358 my $line_stats = $stats->{$file}{$ns}{$line};
359 my $op_count = scalar(@$line_stats);
360 my $op_time = 0;
362 while ($curr_op < $op_count && $line_stats->[$curr_op]{'op_name'} ne 'CALL') {
363 $op_time += $line_stats->[$curr_op]{'time'};
364 $curr_op++;
366 push @output, "$line $op_time";
368 if ($curr_op < $op_count && $line_stats->[$curr_op]{'op_name'} eq 'CALL') {
369 my $call_target = $line_stats->[$curr_op]{'target'};
370 my $call_count = $line_stats->[$curr_op]{'hits'};
371 my $call_cost = $line_stats->[$curr_op]{'time'};
373 push @output, "cfn=$call_target";
374 push @output, "calls=$call_count $call_cost";
377 if ($curr_op < $op_count) {
378 $op_time = 0;
379 while ($curr_op < $op_count) {
380 $op_time += $line_stats->[$curr_op]{'time'};
381 $curr_op++;
383 push @output, "$line $op_time";
389 push @output, "totals: $stats->{'global_stats'}{'total_time'}";
390 return join("\n", @output);
393 =back
395 =cut
397 # Local Variables:
398 # mode: cperl
399 # cperl-indent-level: 4
400 # fill-column: 100
401 # End:
402 # vim: expandtab shiftwidth=4: