File Coverage

blib/lib/Dancer/Plugin/NYTProf.pm
Criterion Covered Total %
statement 27 33 81.8
branch n/a
condition n/a
subroutine 9 10 90.0
pod n/a
total 36 43 83.7


line stmt bran cond sub pod time code
1             package Dancer::Plugin::NYTProf;
2              
3 2     2   22945 use strict;
  2         4  
  2         61  
4 2     2   928 use Capture::Tiny ':all';
  2         43976  
  2         247  
5 2     2   869 use Dancer::Plugin;
  2         85834  
  2         143  
6 2     2   13 use base 'Dancer::Plugin';
  2         2  
  2         117  
7 2     2   1213 use Dancer qw(:syntax);
  2         222570  
  2         9  
8 2     2   546 use Dancer::FileUtils;
  2         2  
  2         62  
9 2     2   8 use File::stat;
  2         2  
  2         10  
10 2     2   84 use File::Temp;
  2         2  
  2         124  
11 2     2   400 use File::Which;
  2         800  
  2         2214  
12              
13             our $VERSION = '0.40';
14              
15              
16             =head1 NAME
17              
18             Dancer::Plugin::NYTProf - easy Devel::NYTProf profiling for Dancer apps
19              
20             =head1 SYNOPSIS
21              
22             package MyApp;
23             use Dancer ':syntax';
24              
25             # enables profiling and "/nytprof"
26             use Dancer::Plugin::NYTProf;
27              
28             Or, if you want to enable it only under development environment (as you should!),
29             you can do something like:
30              
31             package MyApp;
32             use Dancer ':syntax';
33              
34             # enables profiling and "/nytprof"
35             if (setting('environment') eq 'development') {
36             eval 'use Dancer::Plugin::NYTProf';
37             }
38              
39             =head1 DESCRIPTION
40              
41             A plugin to provide easy profiling for Dancer applications, using the venerable
42             L.
43              
44             By simply loading this plugin, you'll have the detailed, helpful profiling
45             provided by Devel::NYTProf.
46              
47             Each individual request to your app is profiled. Going to the URL
48             C in your app will present a list of profiles; selecting one will
49             invoke C to generate the HTML reports (unless they already exist),
50             then serve them up.
51              
52             B This is an early version of this code which is still in development.
53             In general this isn't a plugin I'd advise to use in a production environment
54             anyway, but in particular, it uses C to execute C, and I
55             need to very carefully re-examine the code to make sure that user input cannot
56             be used to nefarious effect. You are recommended to only use this in your
57             development environment.
58              
59             =head1 CONFIGURATION
60              
61             The plugin will work by default without any configuration required - it will
62             default to writing profiling data into a dir named C within your Dancer
63             application's C, present profiling output at C (not yet
64             configurable), and profile all requests.
65              
66             Below is an example of the options you can configure:
67              
68             plugins:
69             NYTProf:
70             enabled: 1
71             profdir: '/tmp/profiledata'
72             nytprofhtml_path: '/usr/local/bin/nytprofhtml'
73             show_durations: 1
74              
75             =head2 profdir
76              
77             Where to store profiling data. Defaults to: C<$appdir/nytprof>
78              
79             =head2 nytprofhtml_path
80              
81             Path to the C script that comes with L. Defaults to
82             the first one we can find in your PATH environment. You should only need to
83             change this in very specific environments, where C can't be found by
84             this plugin.
85              
86             =head2 enabled
87              
88             Profiling comes with a penalty, and even in development environments you might
89             want to enable/disable it via configuration file. This lets you do so. You can
90             toggle this plugin by setting the C option to 0 or 1. It is, of course,
91             enabled by default.
92              
93             More configuration (such as the URL at which output is produced, and options to
94             control which requests get profiled) will be added in a future version. (If
95             there's something you'd like to see soon, do contact me and let me know - it'll
96             likely get done a lot quicker then!)
97              
98             =head2 show_durations
99              
100             When listing profile runs, show the duration of each run, extracted from the
101             profiling data. If you have a lot of profiled runs, this might get slow, so
102             this option is provided if you don't need the profile durations displayed when
103             listing profiles, preferring a faster list. Defaults to 1.
104              
105             =cut
106              
107              
108             my $setting = plugin_setting;
109              
110             # exit as quickly as possible if plugin is not enabled
111             return 1 if exists $setting->{enabled} && $setting->{enabled} != 1;
112              
113             # Work out where nytprof_html is, or die with a sensible error
114             my $nytprofhtml_path = $setting->{nytprofhtml_path}
115             || File::Which::which('nytprofhtml')
116             or die "Could not find nytprofhtml script. Ensure it's in your path, "
117             . "or set the nytprofhtml_path option in your config.";
118              
119              
120             # Need to load Devel::NYTProf at runtime after setting env var, as it will
121             # insist on creating an nytprof.out file immediately - even if we tell it not to
122             # start profiling.
123             # Dirty workaround: get a temp file, then let Devel::NYTProf use that, with
124             # addpid enabled so that it will append the PID too (so the filename won't
125             # exist), load Devel::NYTProf, then unlink the file.
126             # This is dirty, hacky shit that needs to die, but should make things work for
127             # now.
128             my $tempfh = File::Temp->new;
129             my $file = $tempfh->filename;
130             $tempfh = undef; # let the file get deleted
131             $ENV{NYTPROF} = "start=no:file=$file";
132             require Devel::NYTProf;
133             unlink $file;
134              
135             hook 'before' => sub {
136             my $path = request->path;
137              
138             # Make sure that the directories we need to put profiling data in exist,
139             # first:
140             $setting->{profdir} ||= Dancer::FileUtils::path(
141             setting('appdir'), 'nytprof'
142             );
143             if (! -d $setting->{profdir}) {
144             mkdir $setting->{profdir}
145             or die "$setting->{profdir} does not exist and cannot create - $!";
146             }
147             if (!-d Dancer::FileUtils::path($setting->{profdir}, 'html')) {
148             mkdir Dancer::FileUtils::path($setting->{profdir}, 'html')
149             or die "Could not create html dir.";
150             }
151              
152             # Go no further if this request was to view profiling output:
153             return if $path =~ m{^/nytprof};
154             return if $path =~ m{^/nytprof};
155              
156             # Now, fix up the path into something we can use for a filename:
157             $path =~ s{^/}{};
158             $path =~ s{/}{_s_}g;
159             $path =~ s{[^a-z0-9]}{_}gi;
160              
161             # Start profiling, and let the request continue
162             DB::enable_profile(
163             Dancer::FileUtils::path($setting->{profdir}, "nytprof.out.$path.$$")
164             );
165             };
166              
167             hook 'after' => sub {
168             DB::disable_profile();
169             DB::finish_profile();
170             };
171              
172             get '/nytprof' => sub {
173             require Devel::NYTProf::Data;
174             opendir my $dirh, $setting->{profdir}
175             or die "Unable to open profiles dir $setting->{profdir} - $!";
176             my @files = grep { /^nytprof\.out/ } readdir $dirh;
177             closedir $dirh;
178              
179             # HTML + CSS here is a bit ugly, but I want this to be usable as a
180             # single-file plugin that Just Works, without needing to copy over templates
181             # / CSS etc.
182             my $html = <
183             NYTProf profile run list
184            
187            
188            
189            

Profile run list

190            

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

191             produced by Devel::NYTProf.

192              
193            
194             LISTSTART
195              
196             for my $file (
197             sort {
198             (stat Dancer::FileUtils::path($setting->{profdir},$b))->ctime
199             <=>
200             (stat Dancer::FileUtils::path($setting->{profdir},$a))->ctime
201             } @files
202             ) {
203             my $fullfilepath = Dancer::FileUtils::path($setting->{profdir}, $file);
204             my $label = $file;
205             $label =~ s{nytprof\.out\.}{};
206             $label =~ s{_s_}{/}g;
207             $label =~ s{\.(\d+)$}{};
208             my $pid = $1; # refactor this crap
209             my $created = scalar localtime( (stat $fullfilepath)->ctime );
210              
211             # read the profile to find out the duration of the profiled request.
212             # Done in an eval to catch errors (e.g. if a profile run died mid-way,
213             # the data will be incomplete
214             my ($profile,$duration);
215              
216             if (!defined $setting->{show_durations} || $setting->{show_durations}) {
217             eval {
218             my ($stdout, $stderr, @result) = Capture::Tiny::capture {
219             $profile = Devel::NYTProf::Data->new(
220             { filename => $fullfilepath },
221             );
222             };
223             };
224             if ($profile) {
225             $duration = sprintf '%.4f secs',
226             $profile->attributes->{profiler_duration};
227             } else {
228             $duration = '??? seconds - corrupt profile data?';
229             }
230             }
231             $pid = "PID $pid";
232             my $url = request->uri_for("/nytprof/$file")->as_string;
233             $html .= qq{
  • $label (}
  • 234             . join(',', grep { defined $_ } ($pid, $created, $duration))
    235             . qq{)};
    236             }
    237              
    238             my $nytversion = $Devel::NYTProf::VERSION;
    239             $html .= <
    240            
    241              
    242            

    Generated by

    243             Dancer::Plugin::NYTProf v$VERSION
    244             (using
    245             Devel::NYTProf v$nytversion)

    246            
    247            
    248             LISTEND
    249              
    250             return $html;
    251             };
    252              
    253              
    254             # Serve up HTML reports
    255             get '/nytprof/html/**' => sub {
    256             my ($path) = splat;
    257             send_file Dancer::FileUtils::path(
    258             $setting->{profdir}, 'html', map { _safe_filename($_) } @$path
    259             ), system_path => 1;
    260             };
    261              
    262             get '/nytprof/:filename' => sub {
    263              
    264             my $profiledata = Dancer::FileUtils::path(
    265             $setting->{profdir}, _safe_filename(param('filename'))
    266             );
    267              
    268             if (!-f $profiledata) {
    269             send_error 'not_found';
    270             return "No such profile run found.";
    271             }
    272              
    273             # See if we already have the HTML for this run stored; if not, invoke
    274             # nytprofhtml to generate it
    275              
    276             # Right, do we already have generated HTML for this one? If so, use it
    277             my $htmldir = Dancer::FileUtils::path(
    278             $setting->{profdir}, 'html', _safe_filename(param('filename'))
    279             );
    280             if (! -f Dancer::FileUtils::path($htmldir, 'index.html')) {
    281             # TODO: scrutinise this very carefully to make sure it's not
    282             # exploitable
    283             system($nytprofhtml_path, "--file=$profiledata", "--out=$htmldir");
    284              
    285             if ($? == -1) {
    286             die "'$nytprofhtml_path' failed to execute: $!";
    287             } elsif ($? & 127) {
    288             die sprintf "'%s' died with signal %d, %s coredump",
    289             $nytprofhtml_path,,
    290             ($? & 127),
    291             ($? & 128) ? 'with' : 'without';
    292             } elsif ($? != 0) {
    293             die sprintf "'%s' exited with value %d",
    294             $nytprofhtml_path, $? >> 8;
    295             }
    296             }
    297              
    298             # Redirect off to view it:
    299             return redirect '/nytprof/html/'
    300             . param('filename') . '/index.html';
    301              
    302             };
    303              
    304              
    305             # Rudimentary security - remove any directory traversal or poison null
    306             # attempts. We're dealing with user input here, and if they're a sneaky
    307             # bastard, they could convince us to send a file we shouldn't, or have
    308             # nytprofhtml write its output to somewhere it shouldn't. We don't want that.
    309             sub _safe_filename {
    310 0     0     my $filename = shift;
    311 0           $filename =~ s/\\//g;
    312 0           $filename =~ s/\0//g;
    313 0           $filename =~ s/\.\.//g;
    314 0           $filename =~ s/[\/]//g;
    315 0           return $filename;
    316             }
    317              
    318             =head1 AUTHOR
    319              
    320             David Precious, C<< >>
    321              
    322              
    323             =head1 ACKNOWLEDGEMENTS
    324              
    325             Stefan Hornburg (racke)
    326              
    327             Neil Hooey (nhooey)
    328              
    329             J. Bobby Lopez (jbobbylopez)
    330              
    331             leejo
    332              
    333             Breno G. de Oliveira (garu)
    334              
    335              
    336             =head1 BUGS
    337              
    338             Please report any bugs or feature requests at
    339             L.
    340              
    341             =head1 CONTRIBUTING
    342              
    343             This module is developed on GitHub:
    344              
    345             L
    346              
    347             Bug reports, suggestions and pull requests all welcomed!
    348              
    349             =head1 SEE ALSO
    350              
    351             L
    352              
    353             L
    354              
    355             L
    356              
    357              
    358             =head1 LICENSE AND COPYRIGHT
    359              
    360             Copyright 2011-2014 David Precious.
    361              
    362             This program is free software; you can redistribute it and/or modify it
    363             under the terms of either: the GNU General Public License as published
    364             by the Free Software Foundation; or the Artistic License.
    365              
    366             See http://dev.perl.org/licenses/ for more information.
    367              
    368              
    369             =cut
    370              
    371             1; # Sam Kington didn't like that this said "End of Dancer::Plugin::NYTProf",
    372             # as it's fairly obvious. So, just for Sam's pleasure,
    373             # "It's the end of the world as we know it!" ... or something.