1package Devel::NYTProf::SubInfo;    # sub_subinfo
2
3use strict;
4use warnings;
5use Carp;
6
7use List::Util qw(min max);
8use Data::Dumper;
9
10use Devel::NYTProf::Util qw(
11    trace_level
12);
13use Devel::NYTProf::Constants qw(
14    NYTP_SIi_FID NYTP_SIi_FIRST_LINE NYTP_SIi_LAST_LINE
15    NYTP_SIi_CALL_COUNT NYTP_SIi_INCL_RTIME NYTP_SIi_EXCL_RTIME
16    NYTP_SIi_SUB_NAME NYTP_SIi_PROFILE
17    NYTP_SIi_REC_DEPTH NYTP_SIi_RECI_RTIME NYTP_SIi_CALLED_BY
18    NYTP_SIi_elements
19
20    NYTP_SCi_CALL_COUNT
21    NYTP_SCi_INCL_RTIME NYTP_SCi_EXCL_RTIME NYTP_SCi_RECI_RTIME
22    NYTP_SCi_REC_DEPTH NYTP_SCi_CALLING_SUB
23    NYTP_SCi_elements
24);
25
26# extra constants for private elements
27use constant {
28    NYTP_SIi_meta            => NYTP_SIi_elements + 1,
29    NYTP_SIi_cache           => NYTP_SIi_elements + 2,
30};
31
32
33sub fid        { shift->[NYTP_SIi_FID] || 0 }
34
35sub first_line { shift->[NYTP_SIi_FIRST_LINE] }
36
37sub last_line  { shift->[NYTP_SIi_LAST_LINE] }
38
39sub calls      { shift->[NYTP_SIi_CALL_COUNT] }
40
41sub incl_time  { shift->[NYTP_SIi_INCL_RTIME] }
42
43sub excl_time  { shift->[NYTP_SIi_EXCL_RTIME] }
44
45sub subname    { shift->[NYTP_SIi_SUB_NAME] }
46
47sub subname_without_package {
48    my $subname = shift->[NYTP_SIi_SUB_NAME];
49    $subname =~ s/.*:://;
50    return $subname;
51}
52
53sub profile    { shift->[NYTP_SIi_PROFILE] }
54
55sub package    { (my $pkg = shift->subname) =~ s/^(.*)::.*/$1/; return $pkg }
56
57sub recur_max_depth { shift->[NYTP_SIi_REC_DEPTH] }
58
59sub recur_incl_time { shift->[NYTP_SIi_RECI_RTIME] }
60
61
62# general purpose hash - mainly a hack to help kill off Reader.pm
63sub meta      { shift->[NYTP_SIi_meta()] ||= {} }
64# general purpose cache
65sub cache     { shift->[NYTP_SIi_cache()] ||= {} }
66
67
68# { fid => { line => [ count, incl_time ] } }
69sub caller_fid_line_places {
70    my ($self, $merge_evals) = @_;
71    carp "caller_fid_line_places doesn't merge evals yet" if $merge_evals;
72    # shallow clone to remove fid 0 is_sub hack
73    my %tmp = %{ $self->[NYTP_SIi_CALLED_BY] || {} };
74    delete $tmp{0};
75    return \%tmp;
76}
77
78sub called_by_subnames {
79    my ($self) = @_;
80    my $callers = $self->caller_fid_line_places || {};
81
82    my %subnames;
83    for my $sc (map { values %$_ } values %$callers) {
84        my $caller_subnames = $sc->[NYTP_SCi_CALLING_SUB];
85        @subnames{ keys %$caller_subnames } = (); # viv keys
86    }
87
88    return \%subnames;
89}
90
91sub is_xsub {
92    my $self = shift;
93
94    # XXX should test == 0 but some xsubs still have undef first_line etc
95    # XXX shouldn't include opcode
96    my $first = $self->first_line;
97    return undef if not defined $first;
98    return 1     if $first == 0 && $self->last_line == 0;
99    return 0;
100}
101
102sub is_opcode {
103    my $self = shift;
104    return 0 if $self->first_line or $self->last_line;
105    return 1 if $self->subname =~ m/(?:^CORE::|::CORE:)\w+$/;
106    return 0;
107}
108
109sub is_anon {
110    shift->subname =~ m/::__ANON__\b/;
111}
112
113sub kind {
114    my $self = shift;
115    return 'opcode' if $self->is_opcode;
116    return 'xsub'   if $self->is_xsub;
117    return 'perl';
118}
119
120sub fileinfo {
121    my $self = shift;
122    my $fid  = $self->fid;
123    if (!$fid) {
124        return undef;    # sub not have a known fid
125    }
126    $self->profile->fileinfo_of($fid);
127}
128
129sub clone {             # shallow
130    my $self = shift;
131    return bless [ @$self ] => ref $self;
132}
133
134sub _min {
135    my ($a, $b) = @_;
136    $a = $b if not defined $a;
137    $b = $a if not defined $b;
138    # either both are defined or both are undefined here
139    return undef unless defined $a;
140    return min($a, $b);
141}
142
143sub _max {
144    my ($a, $b) = @_;
145    $a = $b if not defined $a;
146    $b = $a if not defined $b;
147    # either both are defined or both are undefined here
148    return undef unless defined $a;
149    return max($a, $b);
150}
151
152
153sub _alter_fileinfo {
154    my ($self, $remove_fi, $new_fi) = @_;
155    my $remove_fid = ($remove_fi) ? $remove_fi->fid : 0;
156    my $new_fid    = (   $new_fi) ?    $new_fi->fid : 0;
157
158    if ($self->fid == $remove_fid) {
159        $self->[NYTP_SIi_FID] = $new_fid;
160
161        $remove_fi->_remove_sub_defined($self) if $remove_fi;
162        $new_fi->_add_new_sub_defined($self) if $new_fi;
163    }
164}
165
166
167sub _alter_called_by_fileinfo {
168    my ($self, $remove_fi, $new_fi) = @_;
169    my $remove_fid = ($remove_fi) ? $remove_fi->fid : 0;
170    my $new_fid    = (   $new_fi) ?    $new_fi->fid : 0;
171
172    # remove mentions of $remove_fid from called-by details
173    # { fid => { line => [ count, incl, excl, ... ] } }
174    if (my $called_by = $self->[NYTP_SIi_CALLED_BY]) {
175        my $cb = delete $called_by->{$remove_fid};
176
177        if ($cb && $new_fid) {
178            my $new_cb = $called_by->{$new_fid} ||= {};
179
180            warn sprintf "_alter_called_by_fileinfo: %s from fid %d to fid %d\n",
181                    $self->subname, $remove_fid, $new_fid
182                if trace_level() >= 4;
183
184            # merge $cb into $new_cb
185            while ( my ($line, $cb_li) = each %$cb ) {
186                my $dst_line_info = $new_cb->{$line} ||= [];
187                _merge_in_caller_info($dst_line_info, delete $cb->{$line},
188                    tag => "$line:".$self->subname,
189                );
190            }
191
192        }
193    }
194
195}
196
197
198
199
200# merge details of another sub into this one
201# there are very few cases where this is sane thing to do
202# it's meant for merging things like anon-subs in evals
203# e.g., "PPI::Node::__ANON__[(eval 286)[PPI/Node.pm:642]:4]"
204sub merge_in {
205    my ($self, $donor, %opts) = @_;
206    my $self_subname  = $self->subname;
207    my $donor_subname = $donor->subname;
208
209    warn sprintf "Merging sub %s into %s (%s)\n",
210            $donor_subname, $self_subname, join(" ", %opts)
211        if trace_level() >= 4;
212
213    # see also "case NYTP_TAG_SUB_CALLERS:" in load_profile_data_from_stream()
214    push @{ $self->meta->{merged_sub_names} }, $donor->subname;
215
216    $self->[NYTP_SIi_FIRST_LINE]  = _min($self->[NYTP_SIi_FIRST_LINE], $donor->[NYTP_SIi_FIRST_LINE]);
217    $self->[NYTP_SIi_LAST_LINE]   = _max($self->[NYTP_SIi_LAST_LINE],  $donor->[NYTP_SIi_LAST_LINE]);
218    $self->[NYTP_SIi_CALL_COUNT] += $donor->[NYTP_SIi_CALL_COUNT];
219    $self->[NYTP_SIi_INCL_RTIME] += $donor->[NYTP_SIi_INCL_RTIME];
220    $self->[NYTP_SIi_EXCL_RTIME] += $donor->[NYTP_SIi_EXCL_RTIME];
221    $self->[NYTP_SIi_REC_DEPTH]   = max($self->[NYTP_SIi_REC_DEPTH], $donor->[NYTP_SIi_REC_DEPTH]);
222    # adding reci_rtime is correct only if one sub doesn't call the other
223    $self->[NYTP_SIi_RECI_RTIME] += $donor->[NYTP_SIi_RECI_RTIME]; # XXX
224
225    # { fid => { line => [ count, incl_time, ... ] } }
226    my $dst_called_by = $self ->[NYTP_SIi_CALLED_BY] ||= {};
227    my $src_called_by = $donor->[NYTP_SIi_CALLED_BY] ||  {};
228
229    $opts{opts} ||= "merge in $donor_subname";
230
231    # iterate over src and merge into dst
232    while (my ($fid, $src_line_hash) = each %$src_called_by) {
233
234        my $dst_line_hash = $dst_called_by->{$fid};
235
236        # merge lines in %$src_line_hash into %$dst_line_hash
237        for my $line (keys %$src_line_hash) {
238            my $dst_line_info = $dst_line_hash->{$line} ||= [];
239            my $src_line_info = $src_line_hash->{$line};
240            delete $src_line_hash->{$line} unless $opts{src_keep};
241            _merge_in_caller_info($dst_line_info, $src_line_info, %opts);
242        }
243    }
244
245    return;
246}
247
248
249sub _merge_in_caller_info {
250    my ($dst_line_info, $src_line_info, %opts) = @_;
251    my $tag = ($opts{tag}) ? " $opts{tag}" : "";
252
253    if (!@$src_line_info) {
254        carp sprintf "_merge_in_caller_info%s skipped (empty donor)", $tag
255            if trace_level();
256        return;
257    }
258
259    if (trace_level() >= 5) {
260        carp sprintf "_merge_in_caller_info%s merging from $src_line_info -> $dst_line_info:", $tag;
261        warn sprintf " . %s\n", _fmt_sc($src_line_info);
262        warn sprintf " + %s\n", _fmt_sc($dst_line_info);
263    }
264    if (!@$dst_line_info) {
265        @$dst_line_info = (0) x NYTP_SCi_elements;
266        $dst_line_info->[NYTP_SCi_CALLING_SUB] = undef;
267    }
268
269    # merge @$src_line_info into @$dst_line_info
270    $dst_line_info->[$_] += $src_line_info->[$_] for (
271        NYTP_SCi_CALL_COUNT, NYTP_SCi_INCL_RTIME, NYTP_SCi_EXCL_RTIME,
272    );
273    $dst_line_info->[NYTP_SCi_REC_DEPTH] = max($dst_line_info->[NYTP_SCi_REC_DEPTH],
274                                                $src_line_info->[NYTP_SCi_REC_DEPTH]);
275    # ug, we can't really combine recursive incl_time, but this is better than undef
276    $dst_line_info->[NYTP_SCi_RECI_RTIME] = max($dst_line_info->[NYTP_SCi_RECI_RTIME],
277                                                $src_line_info->[NYTP_SCi_RECI_RTIME]);
278
279    my $src_cs = $src_line_info->[NYTP_SCi_CALLING_SUB]|| {};
280    my $dst_cs = $dst_line_info->[NYTP_SCi_CALLING_SUB]||={};
281    $dst_cs->{$_} = $src_cs->{$_} for keys %$src_cs;
282
283    warn sprintf " = %s\n", _fmt_sc($dst_line_info)
284        if trace_level() >= 5;
285
286    return;
287}
288
289sub _fmt_sc {
290    my ($sc) = @_;
291    return "(empty)" if !@$sc;
292    my $dst_cs = $sc->[NYTP_SCi_CALLING_SUB]||{};
293    my $by = join " & ", sort keys %$dst_cs;
294    sprintf "calls %d%s",
295        $sc->[NYTP_SCi_CALL_COUNT], ($by) ? ", by $by" : "";
296}
297
298
299sub caller_fids {
300    my ($self, $merge_evals) = @_;
301    my $callers = $self->caller_fid_line_places($merge_evals) || {};
302    my @fids = keys %$callers;
303    return @fids;    # count in scalar context
304}
305
306sub caller_count { return scalar shift->caller_places; } # XXX deprecate later
307
308# array of [ $fid, $line, $sub_call_info ], ...
309sub caller_places {
310    my ($self, $merge_evals) = @_;
311    my $callers = $self->caller_fid_line_places || {};
312
313    my @callers;
314    for my $fid (sort { $a <=> $b } keys %$callers) {
315        my $lines_hash = $callers->{$fid};
316        for my $line (sort { $a <=> $b } keys %$lines_hash) {
317            push @callers, [ $fid, $line, $lines_hash->{$line} ];
318        }
319    }
320
321    return @callers; # scalar: number of distinct calling locations
322}
323
324sub normalize_for_test {
325    my $self = shift;
326    my $profile = $self->profile;
327
328    # normalize eval sequence numbers in anon sub names to 0
329    $self->[NYTP_SIi_SUB_NAME] =~ s/ \( ((?:re_)?) eval \s \d+ \) /(${1}eval 0)/xg
330        if $self->[NYTP_SIi_SUB_NAME] =~ m/__ANON__/
331        && not $ENV{NYTPROF_TEST_SKIP_EVAL_NORM};
332
333    # zero subroutine inclusive time
334    $self->[NYTP_SIi_INCL_RTIME] = 0;
335    $self->[NYTP_SIi_EXCL_RTIME] = 0;
336    $self->[NYTP_SIi_RECI_RTIME] = 0;
337
338    # { fid => { line => [ count, incl, excl, ... ] } }
339    my $callers = $self->[NYTP_SIi_CALLED_BY] || {};
340
341    # calls from modules shipped with perl cause problems for tests
342    # because the line numbers vary between perl versions, so here we
343    # edit the line number of calls from these modules
344    for my $fid (keys %$callers) {
345        next if not $fid;
346        my $fileinfo = $profile->fileinfo_of($fid) or next;
347        next if $fileinfo->filename !~ /(AutoLoader|Exporter)\.pm$/;
348
349        # normalize the lines X,Y,Z to 1,2,3
350        my %lines = %{ delete $callers->{$fid} };
351        my @lines = @lines{sort { $a <=> $b } keys %lines};
352        $callers->{$fid} = { map { $_ => shift @lines } 1..@lines };
353    }
354
355    for my $sc (map { values %$_ } values %$callers) {
356        # zero per-call-location subroutine inclusive time
357        $sc->[NYTP_SCi_INCL_RTIME] =
358        $sc->[NYTP_SCi_EXCL_RTIME] =
359        $sc->[NYTP_SCi_RECI_RTIME] = 0;
360
361        if (not $ENV{NYTPROF_TEST_SKIP_EVAL_NORM}) {
362            # normalize eval sequence numbers in anon sub names to 0
363            my $names = $sc->[NYTP_SCi_CALLING_SUB]||{};
364            for my $subname (keys %$names) {
365                (my $newname = $subname) =~ s/ \( ((?:re_)?) eval \s \d+ \) /(${1}eval 0)/xg;
366                next if $newname eq $subname;
367                warn "Normalizing $subname to $newname overwrote other calling-sub data\n"
368                    if $names->{$newname};
369                $names->{$newname} = delete $names->{$subname};
370            }
371        }
372
373    }
374    return $self->[NYTP_SIi_SUB_NAME];
375}
376
377sub dump {
378    my ($self, $separator, $fh, $path, $prefix) = @_;
379
380    my ($fid, $l1, $l2, $calls) = @{$self}[
381        NYTP_SIi_FID, NYTP_SIi_FIRST_LINE, NYTP_SIi_LAST_LINE, NYTP_SIi_CALL_COUNT
382    ];
383    my @values = @{$self}[
384        NYTP_SIi_INCL_RTIME, NYTP_SIi_EXCL_RTIME,
385        NYTP_SIi_REC_DEPTH, NYTP_SIi_RECI_RTIME
386    ];
387    printf $fh "%s[ %s:%s-%s calls %s times %s ]\n",
388        $prefix,
389        map({ defined($_) ? $_ : 'undef' } $fid, $l1, $l2, $calls),
390        join(" ", map { defined($_) ? $_ : 'undef' } @values);
391
392    my @caller_places = $self->caller_places;
393    for my $cp (@caller_places) {
394        my ($fid, $line, $sc) = @$cp;
395        my @sc = @$sc;
396        $sc[NYTP_SCi_CALLING_SUB] = join "|", sort keys %{ $sc[NYTP_SCi_CALLING_SUB] };
397        printf $fh "%s%s%s%d:%d%s[ %s ]\n",
398            $prefix,
399            'called_by', $separator,
400            $fid, $line, $separator,
401            join(" ", map { defined($_) ? $_ : 'undef' } @sc);
402    }
403
404    # where a sub has had others merged into it, list them
405    my $merge_subs = $self->meta->{merged_sub_names} || [];
406    for my $ms (sort @$merge_subs) {
407        printf $fh "%s%s%s%s\n",
408            $prefix, 'merge_donor', $separator, $ms;
409    }
410}
411
412# vim:ts=8:sw=4:et
4131;
414