#!/usr/bin/perl
##########################################################
## This script is part of the Devel::NYTProf distribution
##
## Copyright, contact and other information can be found
## at the bottom of this file, or by going to:
## http://search.cpan.org/~akaplan/Devel-NYTProf
##
##########################################################
# $Id: nytprofhtml 774 2009-06-18 20:44:25Z tim.bunce $
###########################################################
use warnings;
use strict;
use Carp;
use Getopt::Long;
use List::Util qw(sum max);
use File::Copy;
use Devel::NYTProf::Reader;
use Devel::NYTProf::Core;
use Devel::NYTProf::Util qw(
fmt_float fmt_time fmt_incl_excl_time
calculate_median_absolute_deviation
get_abs_paths_alternation_regex
html_safe_filename
);
our $VERSION = '2.10';
if ($VERSION != $Devel::NYTProf::Core::VERSION) {
die "$0 version '$VERSION' doesn't match version '$Devel::NYTProf::Core::VERSION' of $INC{'Devel/NYTProf/Core.pm'}\n";
}
# These control the limits for what the script will consider ok to severe times
# specified in standard deviations from the mean time
use constant SEVERITY_SEVERE => 2.0; # above this deviation, a bottleneck
use constant SEVERITY_BAD => 1.0;
use constant SEVERITY_GOOD => 0.5; # within this deviation, okay
use constant NUMERIC_PRECISION => 5;
my @on_ready_js;
my %opt = (
file => 'nytprof.out',
out => 'nytprof',
);
GetOptions(\%opt, qw/file|f=s delete|d out|o=s lib|l=s help|h open/)
or do {
usage();
exit 1;
};
if (defined($opt{help})) {
usage();
exit;
}
# handle file selection option
if (!-r $opt{file}) {
die "$0: Unable to access $opt{file}\n";
}
# handle handle output location
if (!-e $opt{out}) {
# will be created
}
elsif (!-d $opt{out}) {
die "$0: Specified output directory `$opt{out}' is a file. whoops!\n";
}
elsif (!-w $opt{out}) {
die "$0: Unable to write to output directory `$opt{out}'\n";
}
# handle deleting old db's
if (defined($opt{'delete'})) {
_delete();
}
# handle custom lib path
if (defined($opt{lib})) {
if (-d $opt{lib}) {
unshift(@INC, $opt{lib});
}
else {
die "$0: Specified lib directory `$opt{lib}' does not exist.\n";
}
}
print "Generating report...\n";
my $reporter = new Devel::NYTProf::Reader($opt{file});
# place to store this crap
$reporter->output_dir($opt{out});
# set formatting for html
$reporter->set_param(
'header',
sub {
my ($profile, $filestr, $output_filestr, $level) = @_;
my $profile_level_buttons =
get_level_buttons($profile->get_profile_levels, $output_filestr, $level);
my $subhead = qq{ $profile_level_buttons
For ${ \($profile->{attribute}{application}) }
};
get_html_header("NYTProf Profile: !~FILENAME~!")
. get_page_header(
profile => $profile,
title => "Performance Profile",
subtitle => $subhead,
mode => qq/-$level/
)
. qq{
| File |
!~FILENAME~! |
| Statements Executed |
!~TOTAL_CALLS~! |
| Total Time |
!~TOTAL_TIME~! seconds |
};
}
);
$reporter->set_param(
'taintmsg',
qq{
WARNING!
\n
The source file used to generate this report was modified
after the profiler database was generated. The database might be out of sync, you should regenerate it. This page might not make any sense!
\n}
);
sub calc_mad_from_objects {
my ($ary, $meth, $ignore_zeros) = @_;
return calculate_median_absolute_deviation([map { scalar $_->$meth } @$ary], $ignore_zeros,);
}
sub calc_mad_from_hashes {
my ($ary, $meth, $ignore_zeros) = @_;
return calculate_median_absolute_deviation([map { scalar $_->{$meth} } @$ary], $ignore_zeros,
);
}
sub subroutine_table {
my ($profile, $filestr, $max_subs, $sortby) = @_;
$sortby ||= 'excl_time';
my $subs_in_file = ($filestr)
? $profile->subs_defined_in_file($filestr, 0)
: $profile->subname_subinfo_map;
return "" unless $subs_in_file && %$subs_in_file;
my $inc_path_regex = get_abs_paths_alternation_regex([$profile->inc], qr/^|\[/);
# XXX slow - use Schwartzian transform or via XS or Sort::Key
my @subs =
sort { $b->$sortby <=> $a->$sortby or $a->subname cmp $b->subname }
values %$subs_in_file;
# in the overall summary, don't show subs that were never called
@subs = grep { $_->calls > 0 } @subs if !$filestr;
my $dev_incl_time = calc_mad_from_objects(\@subs, 'incl_time', 1);
my $dev_excl_time = calc_mad_from_objects(\@subs, 'excl_time', 1);
my $dev_calls = calc_mad_from_objects(\@subs, 'calls', 1);
my $dev_call_count = calc_mad_from_objects(\@subs, 'caller_count', 1);
my $dev_call_fids = calc_mad_from_objects(\@subs, 'caller_fids', 1);
my @subs_to_show = ($max_subs) ? splice @subs, 0, $max_subs : @subs;
my $qualifier = (@subs > @subs_to_show) ? "Top $max_subs " : "";
my $max_pkg_name_len = max(map { length($_->package) } @subs_to_show);
my $sub_links;
my $sortby_desc = ($sortby eq 'excl_time') ? "exclusive time" : "inclusive time";
$sub_links .= qq{
${qualifier}Subroutines — ordered by $sortby_desc
| Calls |
P |
F |
Exclusive Time |
Inclusive Time |
Subroutine |
};
# XXX may not be appropriate if profiling wasn't continuous
my $profiler_duration = $profile->{attribute}{profiler_duration};
my @rows;
$sub_links .= "\n";
for my $sub (@subs_to_show) {
$sub_links .= "";
$sub_links .= determine_severity($sub->calls || 0, $dev_calls);
$sub_links .= determine_severity($sub->caller_count || 0, $dev_call_count);
$sub_links .= determine_severity($sub->caller_fids || 0, $dev_call_fids);
$sub_links .= determine_severity($sub->excl_time || 0, $dev_excl_time, 1,
sprintf("%.1f%%", $sub->excl_time/$profiler_duration*100)
);
$sub_links .= determine_severity($sub->incl_time || 0, $dev_incl_time, 1,
sprintf("%.1f%%", $sub->incl_time/$profiler_duration*100)
);
my @hints;
push @hints, 'xsub' if $sub->is_xsub;
# package and subname
my $subname = $sub->subname;
if (ref $subname) { # subs have been merged
push @hints, sprintf "merge of %d subs", scalar @$subname;
$subname = $subname->[0];
}
my ($pkg, $subr) = ($subname =~ /^(.*::)(.*?)$/) ? ($1, $2) : ('', $subname);
# remove OWN filename from eg __ANON__[(eval 3)[/long/path/name.pm:99]:53]
# becomes __ANON__[(eval 3)[:99]:53]
# XXX doesn't work right if $filestr isn't full filename
$subr =~ s/\Q$filestr\E:(\d+)/:$1/g;
# remove @INC prefix from other paths
$subr =~ s/$inc_path_regex//; # for __ANON__[/very/long/path...]
$sub_links .= qq{| };
# hidden span is for tablesorter to sort on
$sub_links .= sprintf(qq{%s::%s}, $pkg, $subr);
my $href = $reporter->href_for_sub($subname);
$sub_links .= sprintf qq{%*s%s%s | },
$max_pkg_name_len+2, $pkg, $href, $subr,
(@hints) ? "(".join(", ",@hints).")" : "";
$sub_links .= "
\n";
}
$sub_links .= q{
};
# make table sortable if it contains all the subs
push @on_ready_js, q{
$("#subs_table").tablesorter({
headers: {
3: { sorter: 'fmt_time' },
4: { sorter: 'fmt_time' }
}
});
} if @subs_to_show == @subs;
return $sub_links;
}
# http://www.jquery.info/The-TreeMap-plugin
#
sub package_tables {
my ($profile) = @_;
my $pkg_html = "";
# XXX may not be appropriate if profiling wasn't continuous
my $profiler_duration = $profile->{attribute}{profiler_duration};
# [
# undef, # depth 0
# { # depth 1
# "main::" => [ [ subinfo1, subinfo2 ] ], # 2 subs in 1 pkg
# "Foo::" => [ [ subinfo3 ], [ subinfo4 ] ] # 2 subs in 2 pkg
# }
# { # depth 2
# "Foo::Bar::" => [ [ subinfo3 ] ] # 1 sub in 1 pkg
# "Foo::Baz::" => [ [ subinfo4 ] ] # 1 sub in 1 pkg
# }
# ]
my $pkg_depth = $profile->packages_at_depth_subinfo({
include_unused_subs => 0,
rollup_packages => 1,
merge_subinfos => 1,
});
# default:
# { pkgname => [ subinfo1, subinfo2, ... ], ... }
# merged:
# { pkgname => [ single_merged_subinfo ], ... }
my $package_subinfo_map = $profile->package_subinfo_map(1);
# generate a separate table for each depth
for my $depth (0..@$pkg_depth-1) {
my $pkgs_subinfos = { %{ $pkg_depth->[$depth] || {} } };
next if not %$pkgs_subinfos;
# add info for raw (un-rolledup) packages from lower depths
for my $d (0..$depth-1) {
my $p = $pkg_depth->[$d] or next;
for my $higher_pkg (keys %$p) {
my $higher_pkg_info = $package_subinfo_map->{$higher_pkg}
or next;
$pkgs_subinfos->{$higher_pkg} = $higher_pkg_info;
}
}
my %pkg_summary;
while ( my ($pkg_name, $subinfos) = each %$pkgs_subinfos) {
my $pi = $pkg_summary{$pkg_name} ||= { pkg_name => $pkg_name };
# merge all sub infos into one pseudo-sub for package
my $sub;
for my $si (@$subinfos) {
++$pi->{num_packages};
my $n = $si->subname;
($sub) ? $sub->merge_in($si) : ($sub = $si->clone);
}
$pi->{merged_sub} = $sub;
$pi->{excl_time} = $sub->excl_time;
}
my $dev_excl_time = calc_mad_from_hashes([values %pkg_summary], 'excl_time', 1);
my $table_id = "pkg_table_$depth";
$pkg_html .= qq{
Packages - subroutine times rolled up to level $depth package name
Exclusive Time |
Package Name Prefix |
};
$pkg_html .= "\n";
for my $pi (sort { $b->{excl_time} <=> $a->{excl_time} } values %pkg_summary) {
$pkg_html .= "";
$pkg_html .= determine_severity($pi->{excl_time} || 0, $dev_excl_time, 1,
sprintf("%.1f%%", $pi->{excl_time}/$profiler_duration*100)
);
$pkg_html .= qq{| };
my $name = $pi->{pkg_name};
$name .= " (includes $pi->{num_packages} packages)" if $pi->{num_packages} > 1;
$pkg_html .= _escape_html($name);
$pkg_html .= qq{ | };
$pkg_html .= "
\n";
}
$pkg_html .= q{
};
push @on_ready_js, qq{
\$("#$table_id").tablesorter({
headers: {
0: { sorter: 'fmt_time' }
}
});
};
# no point in generating deeper levels if there isn't any more detail
# (e.g. A::B contains no subs just a single package A::B::C)
last if not grep { $_->{num_packages} > 1 } values %pkg_summary;
}
return $pkg_html;
}
$reporter->set_param(
'datastart',
sub {
my ($profile, $filestr) = @_;
my $sub_links = subroutine_table($profile, $filestr, undef, undef);
return qq{$sub_links
| Line | Stmts. | Exclusive Time | Avg. |
\n
};
}
);
$reporter->set_param( footer => sub {
my ($profile, $filestr) = @_;
my $footer = get_footer($profile);
return "
$footer