File Coverage

blib/lib/Devel/NYTProf/Reader.pm
Criterion Covered Total %
statement 230 274 83.9
branch 77 134 57.4
condition 41 81 50.6
subroutine 25 25 100.0
pod 0 12 0.0
total 373 526 70.9


line stmt bran cond sub pod time code
1             # vim: ts=8 sw=4 expandtab:
2             ##########################################################
3             ## This script is part of the Devel::NYTProf distribution
4             ##
5             ## Copyright, contact and other information can be found
6             ## at the bottom of this file, or by going to:
7             ## http://metacpan.org/release/Devel-NYTProf/
8             ##
9             ###########################################################
10             package Devel::NYTProf::Reader;
11              
12             our $VERSION = '6.13';
13              
14 47     47   26473 use warnings;
  47         132  
  47         1564  
15 47     47   263 use strict;
  47         121  
  47         976  
16 47     47   240 use Carp;
  47         102  
  47         2629  
17 47     47   299 use Config;
  47         115  
  47         1844  
18              
19 47     47   272 use List::Util qw(sum max);
  47         120  
  47         3311  
20 47     47   2298 use Data::Dumper;
  47         22029  
  47         2448  
21              
22 47     47   1779 use Devel::NYTProf::Data;
  47         187  
  47         1948  
23 47         3505 use Devel::NYTProf::Util qw(
24             html_safe_filename
25             calculate_median_absolute_deviation
26             trace_level
27 47     47   289 );
  47         119  
28              
29             # These control the limits for what the script will consider ok to severe times
30             # specified in standard deviations from the mean time
31 47     47   332 use constant SEVERITY_SEVERE => 2.0; # above this deviation, a bottleneck
  47         127  
  47         3326  
32 47     47   310 use constant SEVERITY_BAD => 1.0;
  47         106  
  47         2527  
33 47     47   303 use constant SEVERITY_GOOD => 0.5; # within this deviation, okay
  47         112  
  47         177305  
34              
35              
36             # Static class variables
37             our $FLOAT_FORMAT = $Config{nvfformat};
38             $FLOAT_FORMAT =~ s/"//g;
39              
40             # Class methods
41             sub new {
42 3     3 0 871 my $class = shift;
43 3         9 my $file = shift;
44 3   50     16 my $opts = shift || {};
45              
46 3   50     89 my $self = {
47             file => $file || 'nytprof.out',
48             output_dir => '.',
49             suffix => '.csv',
50             header => "# Profile data generated by Devel::NYTProf::Reader\n"
51             . "# Version: v$Devel::NYTProf::Core::VERSION\n"
52             . "# More information at http://metacpan.org/release/Devel-NYTProf/\n"
53             . "# Format: time,calls,time/call,code\n",
54             datastart => '',
55             mk_report_source_line => undef,
56             mk_report_xsub_line => undef,
57             mk_report_separator_line => undef,
58             line => [
59             {},
60             {value => 'time', end => ',', default => '0'},
61             {value => 'calls', end => ',', default => '0'},
62             {value => 'time/call', end => ',', default => '0'},
63             {value => 'source', end => '', default => ''},
64             {end => "\n"}
65             ],
66             dataend => '',
67             footer => '',
68             merged_fids => '',
69             taintmsg => "# WARNING!\n"
70             . "# The source file used in generating this report has been modified\n"
71             . "# since generating the profiler database. It might be out of sync\n",
72             sawampersand => "# NOTE!\n"
73             . "# This file uses special regexp match variables that impact the performance\n"
74             . "# of all regular expression in the program!\n"
75             . "# See WARNING in http://perldoc.perl.org/perlre.html#Capture-buffers\n",
76             current_level => '',
77             };
78              
79 3         10 bless($self, $class);
80             $self->{profile} = Devel::NYTProf::Data->new({
81             %$opts,
82             filename => $self->{file},
83 3         55 });
84              
85 3         13 return $self;
86             }
87              
88              
89              
90             ##
91             sub set_param {
92 4     4 0 1374 my ($self, $param, $value) = @_;
93              
94 4 100       21 if (!exists $self->{$param}) {
95 1         217 confess "Attempt to set $param to $value failed: $param is not a valid " . "parameter\n";
96             }
97             else {
98 3 100       12 return $self->{$param} unless defined($value);
99 2         10 $self->{$param} = $value;
100             }
101 2         6 undef;
102             }
103              
104              
105             sub get_param {
106 15     15 0 548 my ($self, $param, $code_args) = @_;
107 15         28 my $value = $self->{$param};
108 15 100       34 if (ref $value eq 'CODE') {
109 1   50     7 $code_args ||= [];
110 1         3 $value = $value->(@$code_args);
111             }
112 15         37 return $value;
113             }
114              
115             ##
116             sub file_has_been_modified {
117 4     4 0 8 my $self = shift;
118 4         7 my $file = shift;
119 4 50       150 return undef unless -f $file;
120 0         0 my $mtime = (stat $file)[9];
121 0         0 return ($mtime > $self->{profile}{attribute}{basetime});
122             }
123              
124             ##
125             sub _output_additional {
126 1     1   1251 my ($self, $fname, $content) = @_;
127 1 50       77 open(OUT, '>', "$self->{output_dir}/$fname")
128             or confess "Unable to open $self->{output_dir}/$fname for writing; $!\n";
129 1         9 print OUT $content;
130 1         56 close OUT;
131             }
132              
133             ##
134             sub output_dir {
135 2     2 0 1603 my ($self, $dir) = @_;
136 2 100       14 return $self->{output_dir} unless defined($dir);
137 1 50       12 if (!mkdir $dir) {
138 1 50       19 confess "Unable to create directory $dir: $!\n" if !$! =~ /exists/;
139             }
140 1         10 $self->{output_dir} = $dir;
141             }
142              
143             ##
144             sub report {
145 1     1 0 3 my $self = shift;
146 1         4 my ($opts) = @_;
147              
148 1         2 my $level_additional_sub = $opts->{level_additional};
149 1         3 my $profile = $self->{profile};
150 1         4 my $modes = $profile->get_profile_levels;
151 1         3 my @levels = grep { {reverse %$modes}->{$_} } qw(sub block line);
  3         25  
152 1         3 for my $level (@levels) {
153             print "Writing $level reports to $self->{output_dir} directory\n"
154 3 50       11 unless $opts->{quiet};
155             $self->_generate_report($profile, $level,
156 3   33     15 show_progress => (not $opts->{quiet} and -t STDOUT)
157             );
158 3 50       13 $level_additional_sub->($profile, $level)
159             if $level_additional_sub;
160             }
161             }
162              
163             sub current_level {
164 12     12 0 26 my $self = shift;
165 12 100       73 $self->{current_level} = shift if @_;
166 12   100     61 return $self->{current_level} || 'line';
167             }
168              
169             sub fname_for_fileinfo {
170 10     10 0 1310 my ($self, $fi, $level) = @_;
171 10 100       232 confess "No fileinfo" unless $fi;
172 9   66     28 $level ||= $self->current_level;
173              
174 9         28 my $fname = $fi->filename_without_inc;
175              
176             # We want to have descriptive and unambiguous filename
177             # but we don't want to risk failure due to filenames being longer
178             # than MAXPATH (including the length of whatever dir we're writing
179             # the report files into). So we truncate to the last component if
180             # the filenames seems 'dangerously long'. XXX be smarter about this.
181             # This is safe from ambiguity because we add the fid to the filename below.
182 9   50     38 my $max_len = $ENV{NYTPROF_FNAME_TRIM} || 50;
183 9 50       69 $fname =~ s!/.*/!/.../! if length($fname) > $max_len; # remove dir path
184 9 50       25 $fname = "TOOLONG" if length($fname) > $max_len; # just in case
185              
186 9         25 $fname = html_safe_filename($fname);
187 9         25 $fname .= "-".$fi->fid; # to ensure uniqueness and for info
188 9 50       27 $fname .= "-$level" if $level;
189              
190 9         35 return $fname;
191             }
192              
193              
194             ##
195             sub _generate_report {
196 3     3   6 my $self = shift;
197 3         10 my ($profile, $LEVEL, %opts) = @_;
198              
199 3         8 $self->current_level($LEVEL);
200              
201 3 50       12 my @all_fileinfos = $profile->all_fileinfos
202             or carp "Profile report data contains no files";
203              
204 3         7 my @fis = @all_fileinfos;
205 3 100       8 if ($LEVEL ne 'line') {
206             # we only generate line-level reports for evals
207             # for efficiency and because some data model editing only
208             # is only implemented for line-level data
209 2         4 @fis = grep { not $_->is_eval } @fis;
  2         5  
210             }
211              
212 3         5 my $progress;
213 3         17 foreach my $fi (@fis) {
214              
215 3 50       8 if ($opts{show_progress}) {
216 0         0 local $| = 1;
217 0         0 ++$progress;
218 0         0 printf "\r %3d%% ... ", $progress/@fis*100;
219             }
220              
221 3         7 my $meta = $fi->meta;
222 3         9 my $filestr = $fi->filename;
223              
224             # { linenumber => { subname => [ count, time ] } }
225 3         5 my $subcalls_at_line = { %{ $fi->sub_call_lines } };
  3         8  
226 3   50     32 my $subcalls_max_line = max( keys %$subcalls_at_line ) || 0;
227              
228             # { linenumber => [ $subinfo, ... ] }
229 3         6 my $subdefs_at_line = { %{ $profile->subs_defined_in_file_by_line($filestr) } };
  3         29  
230 3   50     22 my $subdefs_max_line = max( keys %$subdefs_at_line ) || 0;
231 3         8 delete $subdefs_at_line->{0}; # xsubs handled separately
232              
233             # { linenumber => { fid => $fileinfo } }
234 3         5 my $evals_at_line = { %{ $fi->evals_by_line } };
  3         11  
235 3   50     15 my $evals_max_line = max( keys %$evals_at_line ) || 0;
236              
237             # note that a file may have no source lines executed, so no keys here
238             # (but is included because some xsubs in the package were executed)
239 3   50     11 my $lines_array = $fi->line_time_data([$LEVEL]) || [];
240 3         8 my $src_max_line = scalar @$lines_array;
241              
242 3         7 for ($src_max_line, $subcalls_max_line, $subdefs_max_line, $evals_max_line) {
243 12 50       26 next if $_ < 2**16;
244 0         0 warn "Ignoring indication that $filestr has $_ lines! (Possibly corrupt data)\n";
245 0         0 $_ = 0;
246             }
247              
248 3         7 my $max_linenum = max(
249             $src_max_line,
250             $subcalls_max_line,
251             $subdefs_max_line,
252             $evals_max_line,
253             );
254              
255 3 50 33     17 warn sprintf "%s max lines: %s (stmts %s, subcalls %s, subdefs %s, evals %s)\n",
256             $filestr, $max_linenum, scalar @$lines_array,
257             $subcalls_max_line, $subdefs_max_line, $evals_max_line
258             if trace_level() >= 4 or $max_linenum > 2**15;
259              
260 3         6 my %stats_accum; # holds all line times. used to find median
261             my %stats_by_line; # holds individual line stats
262 3         6 my $runningTotalTime = 0; # holds the running total
263             # (should equal sum of $stats_accum)
264 3         4 my $runningTotalCalls = 0; # holds the running total number of calls.
265              
266 3         10 for (my $linenum = 0; $linenum <= $max_linenum; ++$linenum) {
267              
268 60 100       113 if (my $subdefs = delete $subdefs_at_line->{$linenum}) {
269 9         29 $stats_by_line{$linenum}->{'subdef_info'} = $subdefs;
270             }
271              
272 60 100       143 if (my $subcalls = delete $subcalls_at_line->{$linenum}) {
273 27   50     86 my $line_stats = $stats_by_line{$linenum} ||= {};
274              
275 27         49 $line_stats->{'subcall_info'} = $subcalls;
276 27         52 $line_stats->{'subcall_count'} = sum(map { $_->[0] } values %$subcalls);
  27         81  
277 27         46 $line_stats->{'subcall_time'} = sum(map { $_->[1] } values %$subcalls);
  27         80  
278              
279 54         130 push @{$stats_accum{$_}}, $line_stats->{$_}
280 27         49 for (qw(subcall_count subcall_time));
281             }
282              
283 60 50       105 if (my $evalcalls = delete $evals_at_line->{$linenum}) {
284 0   0     0 my $line_stats = $stats_by_line{$linenum} ||= {};
285              
286             # %$evals => { fid => $fileinfo }
287 0         0 $line_stats->{'evalcall_info'} = $evalcalls;
288 0         0 $line_stats->{'evalcall_count'} = values %$evalcalls;
289              
290             # get list of evals, including nested evals
291 0         0 my @eval_fis = map { ($_, $_->has_evals(1)) } values %$evalcalls;
  0         0  
292 0         0 $line_stats->{'evalcall_count_nested'} = @eval_fis;
293             $line_stats->{'evalcall_stmts_time_nested'} = sum(
294 0         0 map { $_->sum_of_stmts_time } @eval_fis);
  0         0  
295             }
296              
297 60 100       113 if (my $stmts = $lines_array->[$linenum]) {
298 21 50       36 next if !@$stmts; # XXX happens for evals, investigate
299              
300 21         36 my ($stmt_time, $stmt_count) = @$stmts;
301 21   50     42 my $line_stats = $stats_by_line{$linenum} ||= {};
302              
303             # The debugger cannot stop on BEGIN{...} lines. A line in a begin
304             # may set a scalar reference to something that needs to be eval'd later.
305             # as a result, if the variable is expanded outside of the BEGIN, we'll
306             # see the original BEGIN line, but it won't have any calls or times
307             # associated. This will cause a divide by zero error.
308 21   50     38 $stmt_count ||= 1;
309              
310 21         31 $line_stats->{'time'} = $stmt_time;
311 21         42 $line_stats->{'calls'} = $stmt_count;
312 21         33 $line_stats->{'time/call'} = $stmt_time/$stmt_count;
313              
314 63         122 push @{$stats_accum{$_}}, $line_stats->{$_}
315 21         37 for (qw(time calls time/call));
316              
317 21         31 $runningTotalTime += $stmt_time;
318 21         30 $runningTotalCalls += $stmt_count;
319             }
320              
321 0         0 warn "$linenum: @{[ %{ $stats_by_line{$linenum} } ]}\n"
  0         0  
322 60 0 33     199 if trace_level() >= 3 && $stats_by_line{$linenum};
323             }
324              
325 3 50       7 warn "unprocessed keys in subdefs_at_line: @{[ keys %$subdefs_at_line ]}\n"
  0         0  
326             if %$subdefs_at_line;
327 3 50       6 warn "unprocessed keys in subcalls_at_line: @{[ keys %$subcalls_at_line ]}\n"
  0         0  
328             if %$subcalls_at_line;
329 3 50       16 warn "unprocessed keys in evals_at_line: @{[ keys %$evals_at_line ]}\n"
  0         0  
330             if %$evals_at_line;
331              
332 3         14 $meta->{'time'} = $runningTotalTime;
333 3         8 $meta->{'calls'} = $runningTotalCalls;
334 3 50       9 $meta->{'time/call'} =
335             ($runningTotalCalls) ? $runningTotalTime / $runningTotalCalls: 0;
336              
337             # Use Median Absolute Deviation Formula to get file deviations for each of
338             # calls, time and time/call values
339             my %stats_for_file = (
340             'calls' => calculate_median_absolute_deviation($stats_accum{'calls'}||[]),
341             'time' => calculate_median_absolute_deviation($stats_accum{'time'}||[]),
342             'time/call' => calculate_median_absolute_deviation($stats_accum{'time/call'}||[]),
343             subcall_count => calculate_median_absolute_deviation($stats_accum{subcall_count}||[]),
344 3   50     16 subcall_time => calculate_median_absolute_deviation($stats_accum{subcall_time}||[]),
      50        
      50        
      50        
      50        
345             );
346              
347             # the output file name that will be open later. Not including directory at this time.
348             # keep here so that the variable replacement subs can get at it.
349 3         21 my $fname = $self->fname_for_fileinfo($fi) . $self->{suffix};
350              
351             # localize header and footer for variable replacement
352 3         12 my $header = $self->get_param('header', [$profile, $fi, $fname, $LEVEL]);
353 3         11 my $datastart = $self->get_param('datastart', [$profile, $fi]);
354 3         8 my $dataend = $self->get_param('dataend', [$profile, $fi]);
355 3         5 my $FILE = $filestr;
356             #warn Dumper(\%stats_by_line);
357             # open output file
358             #warn "$self->{output_dir}/$fname";
359 3 50       292 open(OUT, ">", "$self->{output_dir}/$fname")
360             or confess "Unable to open $self->{output_dir}/$fname " . "for writing: $!\n";
361              
362             # begin output
363 3         20 print OUT $header;
364              
365             # If we don't have savesrc for the file then we'll be reading the current
366             # file contents which may have changed since the profile was run.
367             # In this case we need to warn the user as the report would be garbled.
368 3 50 33     14 print OUT $self->get_param('taintmsg', [$profile, $fi])
369             if !$fi->has_savesrc and $self->file_has_been_modified($filestr);
370              
371             print OUT $self->get_param('sawampersand', [$profile, $fi])
372             if $profile->{attribute}{sawampersand_fid}
373 3 50 33     14 && $fi->fid == $profile->{attribute}{sawampersand_fid};
374              
375             print OUT $self->get_param('merged_fids', [$profile, $fi])
376 3 50       9 if $fi->meta->{merged_fids};
377              
378 3         7 print OUT $datastart;
379              
380 3         5 my $LINE = 1; # line number in source code
381 3         8 my $src_lines = $fi->srclines_array;
382 3 50       9 if (!$src_lines) { # no savesrc, and no file available
383              
384 3         6 my $msg = '';
385 3 50       11 if ($fi->is_fake) {
    50          
    50          
    50          
386             # eg the "/unknown-eval-invoker"
387 0         0 $msg = "No source code available for synthetic (fake) file $filestr.",
388             }
389             elsif ($fi->is_eval) {
390 0         0 $msg = "No source code available for string eval $filestr.\nYou probably need to use a more recent version of perl. See savesrc option in documentation.",
391             }
392             elsif ($filestr =~ m{^/loader/0x[0-9a-zA-Z]+/}) {
393             # a synthetic file name that perl assigns when reading
394             # code returned by a CODE ref in @INC
395 0         0 $msg = "No source code available for 'file' loaded via CODE reference in \@INC.\nSee savesrc option in documentation.",
396             }
397             elsif (not $fi->is_file) {
398 0         0 $msg = "No source code available for non-file '$filestr'.\nYou probably need to use a more recent version of perl. See savesrc option in documentation.",
399             }
400             else {
401 3 50       16 $msg = "Unable to open '$filestr' for reading: $!"
402             unless $filestr =~ m{t/test01\.p$};
403              
404             # clarify some current Moose limitations XXX
405 3 50       12 if ($filestr =~ m!/(accessor .*) defined at /!) {
    50          
406 0         0 $msg = "Source for generated Moose $1 isn't available ($filestr: $!)";
407             }
408             elsif ($filestr =~ m!/(generated method \(unknown origin\))!) {
409 0         0 $msg = "Source for Moose $1 isn't available ($filestr: $!)";
410             }
411              
412             # the report will not be complete, but this doesn't need to be fatal
413 3         6 my $hint = '';
414 3 50 33     17 $hint .= " Try running $0 in the same directory as you ran Devel::NYTProf, "
      33        
415             . "or ensure \@INC is correct."
416             if $filestr ne '-e'
417             and $filestr !~ m:^/:
418             and not our $_generate_report_inc_hint++; # only once
419              
420             # If neither $msg nor $hint has been populated, no need to
421             # warn, thereby avoiding superfluous new line in test output
422 3 50 33     45 if ($msg or $hint) {
423             warn "$msg$hint\n"
424             # only once per filestr
425 0 0       0 unless our $_generate_report_filestr_warn->{$filestr}++;
426             }
427              
428             }
429              
430 3         8 $src_lines = [ $msg ];
431 3         8 $LINE = 0; # start numbering from 0 to flag fake contents
432             }
433              
434             # ensure we don't have any undef source lines
435             # (to avoid warnings from the code below)
436 3         6 my $src_undefs;
437 3   50     22 defined $_ or $_ = '' && ++$src_undefs for @$src_lines;
438             # XXX shouldn't be need but don't have a test case so grumble
439             # about it in the hope of getting a test case
440 3 50       6 warn sprintf "Saw %d missing (undef) lines in the %d lines of source code for %s\n",
441             $src_undefs, scalar @$src_lines, $filestr
442             if $src_undefs;
443              
444             # Since we use @$src_lines to drive the report generation, pad the array to
445             # ensure it has enough lines to include all the available profile info.
446             # Then the report is still useful even if we have no source code.
447 3         44 push @$src_lines, '' while @$src_lines < $max_linenum-1;
448              
449 3 50       10 if (my $z = $stats_by_line{0}) {
450             # typically indicates cases where we could do better
451 0 0       0 if (trace_level()) {
452 0         0 warn "$filestr has unexpected info for line 0: @{[ %$z ]}\n";
  0         0  
453             # sub defs: used to be xsubs but they're handled separately now
454             # so there are no known causes of this any more
455 0 0       0 if (my $i = $z->{subdef_info}) {
456 0         0 warn "0: @{[ map { $_->subname } @$i ]}\n"
  0         0  
  0         0  
457             }
458             # sub calls: they're typically END blocks that appear to be
459             # invoked from the main .pl script perl ran.
460             # Also some BEGINs and things like main::CORE:ftfile
461             # (see CPANDB's cpangraph script for some examples)
462 0 0       0 if (my $i = $z->{subcall_info}) {
463 0         0 warn sprintf "0: called %20s %s\n", $_, join " ", @{ $i->{$_} }
464 0         0 for sort keys %$i;
465             }
466             }
467              
468 0         0 $LINE = 0;
469 0         0 unshift @$src_lines, "Profile data that couldn't be associated with a specific line:";
470             }
471              
472             my $line_sub = $self->{mk_report_source_line}
473 3 50       8 or die "mk_report_source_line not set";
474              
475 3         6 my $prev_line = '-';
476 3         539 while ( @$src_lines ) {
477 45         63 my $line = shift @$src_lines;
478 45         62 chomp $line;
479              
480             # detect a series of blank lines, e.g. a chunk of pod savesrc didn't store
481             my $skip_blanks = (
482             $prev_line eq '' && $line eq '' && # blank behind and here
483             @$src_lines && $src_lines->[0] =~ /^\s*$/ && # blank ahead
484 45   100     309 !$stats_by_line{$LINE} # nothing to report
485             );
486              
487 45 50       82 if ($line =~ m/^\# \s* line \s+ (\d+) \b/x) {
488             # XXX we should be smarter about this - patches welcome!
489             # We should at least ignore the common AutoSplit case
490             # which we detect and workaround elsewhere.
491             warn "Ignoring '$line' directive at line $LINE - profile data for $filestr will be out of sync with source\n"
492 0 0       0 unless our $line_directive_warn->{$filestr}++; # once per file
493             }
494              
495             print OUT $line_sub->(
496             ($skip_blanks) ? "- -" : $LINE, $line,
497 45 100 100     154 $stats_by_line{$LINE} || {},
498             \%stats_for_file,
499             $profile,
500             $fi,
501             );
502              
503 45 100       751 if ($skip_blanks) {
504 9   33     58 while (
      66        
505             @$src_lines && $src_lines->[0] =~ /^\s*$/ &&
506             !$stats_by_line{$LINE+1}
507             ) {
508 9         13 shift @$src_lines;
509 9         50 $LINE++;
510             }
511             }
512 45         74 $prev_line = $line;
513             }
514             continue {
515 45         79 $LINE++;
516             }
517              
518 3         5 my $separator_sub = $self->{mk_report_separator_line};
519              
520             # iterate over xsubs
521             $line_sub = $self->{mk_report_xsub_line}
522 3 50       9 or die "mk_report_xsub_line not set";
523 3         10 my $subs_defined_in_file = $profile->subs_defined_in_file($filestr);
524 3         18 foreach my $subname (sort keys %$subs_defined_in_file) {
525 18         39 my $subinfo = $subs_defined_in_file->{$subname};
526 18         68 my $kind = $subinfo->kind;
527              
528 18 100       41 next if $kind eq 'perl';
529 6 100       15 next if $subinfo->calls == 0;
530              
531 3 50       7 if ($separator_sub) {
532 0         0 print OUT $separator_sub->($profile, $fi);
533 0         0 undef $separator_sub; # do mk_report_separator_line just once
534             }
535              
536 3         21 print OUT $line_sub->(
537             $subname,
538             "sub $subname; # $kind\n\t",
539             { subdef_info => [ $subinfo ], }, #stats_for_line
540             undef, # stats_for_file
541             $profile, $fi
542             );
543             }
544              
545 3         8 print OUT $dataend;
546 3         10 print OUT $self->get_param('footer', [$profile, $filestr]);
547 3         196 close OUT;
548             }
549 3 50       20 print "\n" if $opts{show_progress};
550             }
551              
552              
553             sub url_for_file {
554 11     11 0 25 my ($self, $file, $anchor, $level) = @_;
555 11 50       24 confess "No file specified" unless $file;
556 11   100     84 $level ||= '';
557              
558 11   66     53 my $url = $self->{_cache}{"url_for_file,$file,$level"} ||= do {
559 4         13 my $fi = $self->{profile}->fileinfo_of($file);
560 4 50       11 $level = 'line' if $fi->is_eval;
561 4         11 $self->fname_for_fileinfo($fi, $level) . ".html";
562             };
563              
564 11 100       36 $url .= "#$anchor" if defined $anchor;
565 11         44 return $url;
566             }
567              
568             sub href_for_file {
569 3     3 0 31 my $url = shift->url_for_file(@_);
570 3 50       24 return qq{href="$url"} if $url;
571 0         0 return $url;
572             }
573              
574              
575             sub url_for_sub {
576 8     8 0 33 my ($self, $sub, %opts) = @_;
577 8         15 my $profile = $self->{profile};
578              
579 8         21 my ($file, $fid, $first, $last, $fi) = $profile->file_line_range_of_sub($sub);
580 8 50       19 return "" unless $file;
581 8 100       16 if (!$first) {
582             # use sanitized subname as label for xsubs
583             # XXX must match what nytprofhtml does for xsubs
584 2         14 ($first = $sub) =~ s/\W/_/g;
585             }
586 8         23 return $self->url_for_file($fi, $first);
587             }
588              
589             sub href_for_sub {
590 4     4 0 21 my $url = shift->url_for_sub(@_);
591 4 50       24 return qq{href="$url"} if $url;
592 0           return $url;
593             }
594              
595              
596             1;