File Coverage

blib/lib/Dancer/Plugin/NYTProf.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package Dancer::Plugin::NYTProf;
2              
3 1     1   35208 use strict;
  1         2  
  1         34  
4 1     1   429 use Dancer::Plugin;
  0            
  0            
5             use base 'Dancer::Plugin';
6             use Dancer qw(:syntax);
7             use Dancer::FileUtils;
8             use File::stat;
9             use File::Temp;
10             use File::Which;
11              
12             our $VERSION = '0.31';
13              
14              
15             =head1 NAME
16              
17             Dancer::Plugin::NYTProf - easy Devel::NYTProf profiling for Dancer apps
18              
19             =head1 DESCRIPTION
20              
21             A plugin to provide easy profiling for Dancer applications, using the venerable
22             L.
23              
24             By simply loading this plugin, you'll have the detailed, helpful profiling
25             provided by Devel::NYTProf.
26              
27             Each individual request to your app is profiled. Going to the URL
28             C in your app will present a list of profiles; selecting one will
29             invoke C to generate the HTML reports (unless they already exist),
30             then serve them up.
31              
32             B This is an early version of this code which is still in development.
33             In general this isn't a plugin I'd advise to use in a production environment
34             anyway, but in particular, it uses C to execute C, and I
35             need to very carefully re-examine the code to make sure that user input cannot
36             be used to nefarious effect. You are recommended to only use this in your
37             development environment.
38              
39             =head1 CONFIGURATION
40              
41             The plugin will work by default without any configuration required - it will
42             default to writing profiling data into a dir named C within your Dancer
43             application's C, present profiling output at C (not yet
44             configurable), and profile all requests.
45              
46             Below is an example of the options you can configure:
47              
48             plugins:
49             NYTProf:
50             profdir: '/tmp/profiledata'
51             nytprofhtmlpath: '/usr/local/bin/nytprofhtml'
52              
53             More configuration (such as the URL at which output is produced, and options to
54             control which requests get profiled) will be added in a future version. (If
55             there's something you'd like to see soon, do contact me and let me know - it'll
56             likely get done a lot quicker then!)
57              
58             =cut
59              
60              
61             my $setting = plugin_setting;
62              
63             # Work out where nytprof_html is, or die with a sensible error
64             my $nytprofhtml_path = $setting->{nytprofhtml_path}
65             || File::Which::which('nytprofhtml')
66             or die "Could not find nytprofhtml script. Ensure it's in your path, "
67             . "or set the nytprofhtml_path option in your config.";
68              
69              
70             # Need to load Devel::NYTProf at runtime after setting env var, as it will
71             # insist on creating an nytprof.out file immediately - even if we tell it not to
72             # start profiling.
73             # Dirty workaround: get a temp file, then let Devel::NYTProf use that, with
74             # addpid enabled so that it will append the PID too (so the filename won't
75             # exist), load Devel::NYTProf, then unlink the file.
76             # This is dirty, hacky shit that needs to die, but should make things work for
77             # now.
78             my $tempfh = File::Temp->new;
79             my $file = $tempfh->filename;
80             $tempfh = undef; # let the file get deleted
81             $ENV{NYTPROF} = "start=no:file=$file";
82             require Devel::NYTProf;
83             unlink $file;
84              
85             hook 'before' => sub {
86             my $path = request->path;
87              
88             # Make sure that the directories we need to put profiling data in exist,
89             # first:
90             $setting->{profdir} ||= Dancer::FileUtils::path(
91             setting('appdir'), 'nytprof'
92             );
93             if (! -d $setting->{profdir}) {
94             mkdir $setting->{profdir}
95             or die "$setting->{profdir} does not exist and cannot create - $!";
96             }
97             if (!-d Dancer::FileUtils::path($setting->{profdir}, 'html')) {
98             mkdir Dancer::FileUtils::path($setting->{profdir}, 'html')
99             or die "Could not create html dir.";
100             }
101              
102             # Go no further if this request was to view profiling output:
103             return if $path =~ m{^/nytprof};
104             return if $path =~ m{^/nytprof};
105              
106             # Now, fix up the path into something we can use for a filename:
107             $path =~ s{^/}{};
108             $path =~ s{/}{_s_}g;
109             $path =~ s{[^a-z0-9]}{_}gi;
110              
111             # Start profiling, and let the request continue
112             DB::enable_profile(
113             Dancer::FileUtils::path($setting->{profdir}, "nytprof.out.$path.$$")
114             );
115             };
116              
117             hook 'after' => sub {
118             DB::disable_profile();
119             DB::finish_profile();
120             };
121              
122             get '/nytprof' => sub {
123             require Devel::NYTProf::Data;
124             opendir my $dirh, $setting->{profdir}
125             or die "Unable to open profiles dir $setting->{profdir} - $!";
126             my @files = grep { /^nytprof\.out/ } readdir $dirh;
127             closedir $dirh;
128              
129             # HTML + CSS here is a bit ugly, but I want this to be usable as a
130             # single-file plugin that Just Works, without needing to copy over templates
131             # / CSS etc.
132             my $html = <
133             NYTProf profile run list
134            
137            
138            
139            

Profile run list

140            

Select a profile run output from the list to view the HTML reports as

141             produced by Devel::NYTProf.

142              
143            
144             LISTSTART
145              
146             for my $file (
147             sort {
148             (stat Dancer::FileUtils::path($setting->{profdir},$b))->ctime
149             <=>
150             (stat Dancer::FileUtils::path($setting->{profdir},$a))->ctime
151             } @files
152             ) {
153             my $fullfilepath = Dancer::FileUtils::path($setting->{profdir}, $file);
154             my $label = $file;
155             $label =~ s{nytprof\.out\.}{};
156             $label =~ s{_s_}{/}g;
157             $label =~ s{\.(\d+)$}{};
158             my $pid = $1; # refactor this crap
159             my $created = scalar localtime( (stat $fullfilepath)->ctime );
160              
161             # read the profile to find out the duration of the profiled request.
162             # Done in an eval to catch errors (e.g. if a profile run died mid-way,
163             # the data will be incomplete
164             my ($profile,$duration);
165             eval {
166             $profile = Devel::NYTProf::Data->new({ filename => $fullfilepath});
167             };
168             if ($profile) {
169             $duration = sprintf '%.4f secs',
170             $profile->attributes->{profiler_duration};
171             } else {
172             $duration = '??? seconds - corrupt profile data?';
173             }
174             my $url = request->uri_for("/nytprof/$file")->as_string;
175             $html .= qq{
  • $label}
  • 176             . qq{ (PID $pid, $created, $duration)};
    177             }
    178              
    179             $html .= <
    180            
    181              
    182            

    Generated by

    183             Dancer::Plugin::NYTProf v$VERSION

    184            
    185            
    186             LISTEND
    187              
    188             return $html;
    189             };
    190              
    191              
    192             # Serve up HTML reports
    193             get '/nytprof/html/**' => sub {
    194             my ($path) = splat;
    195             send_file Dancer::FileUtils::path(
    196             $setting->{profdir}, 'html', map { _safe_filename($_) } @$path
    197             ), system_path => 1;
    198             };
    199              
    200             get '/nytprof/:filename' => sub {
    201              
    202             my $profiledata = Dancer::FileUtils::path(
    203             $setting->{profdir}, _safe_filename(param('filename'))
    204             );
    205              
    206             if (!-f $profiledata) {
    207             send_error 'not_found';
    208             return "No such profile run found.";
    209             }
    210            
    211             # See if we already have the HTML for this run stored; if not, invoke
    212             # nytprofhtml to generate it
    213              
    214             # Right, do we already have generated HTML for this one? If so, use it
    215             my $htmldir = Dancer::FileUtils::path(
    216             $setting->{profdir}, 'html', _safe_filename(param('filename'))
    217             );
    218             if (! -f Dancer::FileUtils::path($htmldir, 'index.html')) {
    219             # TODO: scrutinise this very carefully to make sure it's not
    220             # exploitable
    221             system($nytprofhtml_path, "--file=$profiledata", "--out=$htmldir");
    222              
    223             if ($? == -1) {
    224             die "'$nytprofhtml_path' failed to execute: $!";
    225             } elsif ($? & 127) {
    226             die sprintf "'%s' died with signal %d, %s coredump",
    227             $nytprofhtml_path,,
    228             ($? & 127),
    229             ($? & 128) ? 'with' : 'without';
    230             } elsif ($? != 0) {
    231             die sprintf "'%s' exited with value %d",
    232             $nytprofhtml_path, $? >> 8;
    233             }
    234             }
    235              
    236             # Redirect off to view it:
    237             return redirect '/nytprof/html/'
    238             . param('filename') . '/index.html';
    239              
    240             };
    241              
    242              
    243             # Rudimentary security - remove any directory traversal or poison null
    244             # attempts. We're dealing with user input here, and if they're a sneaky
    245             # bastard, they could convince us to send a file we shouldn't, or have
    246             # nytprofhtml write its output to somewhere it shouldn't. We don't want that.
    247             sub _safe_filename {
    248             my $filename = shift;
    249             $filename =~ s/\\//g;
    250             $filename =~ s/\0//g;
    251             $filename =~ s/\.\.//g;
    252             $filename =~ s/[\/]//g;
    253             return $filename;
    254             }
    255              
    256             =head1 AUTHOR
    257              
    258             David Precious, C<< >>
    259              
    260              
    261             =head1 ACKNOWLEDGEMENTS
    262              
    263             Stefan Hornburg (racke)
    264              
    265             Neil Hooey (nhooey)
    266              
    267             J. Bobby Lopez (jbobbylopez)
    268              
    269             leejo
    270              
    271              
    272             =head1 BUGS
    273              
    274             Please report any bugs or feature requests at
    275             L.
    276              
    277             =head1 CONTRIBUTING
    278              
    279             This module is developed on GitHub:
    280              
    281             L
    282              
    283             Bug reports, suggestions and pull requests all welcomed!
    284              
    285             =head1 SEE ALSO
    286              
    287             L
    288              
    289             L
    290              
    291             L
    292              
    293              
    294             =head1 LICENSE AND COPYRIGHT
    295              
    296             Copyright 2011-12 David Precious.
    297              
    298             This program is free software; you can redistribute it and/or modify it
    299             under the terms of either: the GNU General Public License as published
    300             by the Free Software Foundation; or the Artistic License.
    301              
    302             See http://dev.perl.org/licenses/ for more information.
    303              
    304              
    305             =cut
    306              
    307             1; # Sam Kington didn't like that this said "End of Dancer::Plugin::NYTProf",
    308             # as it's fairly obvious. So, just for Sam's pleasure,
    309             # "It's the end of the world as we know it!" ... or something.