File Coverage

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 1     1   199017 use strict;
  1         2  
  1         29  
4 1     1   566 use Capture::Tiny ':all';
  1         3095  
  1         120  
5 1     1   459 use Dancer::Plugin;
  1         1067  
  1         64  
6 1     1   5 use base 'Dancer::Plugin';
  1         1  
  1         81  
7 1     1   6 use Dancer qw(:syntax);
  1         4  
  1         4  
8 1     1   265 use Dancer::FileUtils;
  1         1  
  1         28  
9 1     1   3 use File::stat;
  1         1  
  1         5  
10 1     1   44 use File::Temp;
  1         1  
  1         59  
11 1     1   4 use File::Which;
  1         1  
  1         1225  
12              
13             our $VERSION = '0.50';
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             profiling_enabled: 1
72             profdir: '/tmp/profiledata'
73             nytprofhtml_path: '/usr/local/bin/nytprofhtml'
74             show_durations: 1
75              
76             =head2 profdir
77              
78             Where to store profiling data. Defaults to: C<$appdir/nytprof>
79              
80             =head2 nytprofhtml_path
81              
82             Path to the C script that comes with L. Defaults to
83             the first one we can find in your PATH environment. You should only need to
84             change this in very specific environments, where C can't be found by
85             this plugin.
86              
87             =head2 enabled
88              
89             Whether the plugin as a whole is enabled; disabling this setting will disable
90             profiling route executions, and also disable the route which serves up the
91             results at C. Enabled by default, so you only have to provide this
92             setting if you wish to set it to a false value.
93              
94             =head2 profiling_enabled
95              
96             Whether route executions are profiled or not; if this is set to a false value,
97             the before hook which would usually cause L to profile that
98             route execution will not do so. This allows you to disable profiling but still
99             be able to browse the results of existing profiled executions. Enabled by
100             default, so you only have to provide this setting if you wish to set it to a
101             false value.
102              
103             =head2 show_durations
104              
105             When listing profile runs, show the duration of each run, extracted from the
106             profiling data. If you have a lot of profiled runs, this might get slow, so
107             this option is provided if you don't need the profile durations displayed when
108             listing profiles, preferring a faster list. Defaults to 1.
109              
110             More configuration (such as the URL at which output is produced, and options to
111             control which requests get profiled) will be added in a future version. (If
112             there's something you'd like to see soon, do contact me and let me know - it'll
113             likely get done a lot quicker then!)
114              
115             =cut
116              
117              
118             my $setting = plugin_setting;
119              
120             if (!exists $setting->{enabled} || $setting->{enabled}) {
121              
122             # Work out where nytprof_html is, or die with a sensible error
123             my $nytprofhtml_path = $setting->{nytprofhtml_path}
124             || File::Which::which('nytprofhtml')
125             or die "Could not find nytprofhtml script. Ensure it's in your path, "
126             . "or set the nytprofhtml_path option in your config.";
127              
128              
129             # Make sure that the directories we need to put profiling data in exist
130             # first:
131             $setting->{profdir} ||= Dancer::FileUtils::path(
132             setting('appdir'), 'nytprof'
133             );
134             if (! -d $setting->{profdir}) {
135             mkdir $setting->{profdir}
136             or die "$setting->{profdir} does not exist and cannot create"
137             . " - $!";
138             }
139             if (!-d Dancer::FileUtils::path($setting->{profdir}, 'html')) {
140             mkdir Dancer::FileUtils::path($setting->{profdir}, 'html')
141             or die "Could not create html dir.";
142             }
143              
144             # Need to load Devel::NYTProf at runtime after setting env var, as it will
145             # insist on creating an nytprof.out file immediately - even if we tell it
146             # not to start profiling. Dirty workaround: get a temp file, then let
147             # Devel::NYTProf use that, with addpid enabled so that it will append the
148             # PID too (so the filename won't exist), load Devel::NYTProf, then unlink
149             # the file. This is dirty, hacky shit that needs to die, but should make
150             # things work for now.
151             my $tempfh = File::Temp->new;
152             my $file = $tempfh->filename;
153             $tempfh = undef; # let the file get deleted
154             $ENV{NYTPROF} = "start=no:file=$file";
155             require Devel::NYTProf;
156             unlink $file;
157              
158             # Set up the hook that will start profiling each route execution.
159             hook 'before' => sub {
160             my $path = request->path;
161              
162             # Do nothing if profiling is disabled, or if we're disabled globally.
163             # (Note: if we were disabled globally by the config file's enabled
164             # setting, then this hook won't have even been installed - but check
165             # anyway in order to do the expected thing if someone has modified the
166             # config at runtime)
167             return if ((exists $setting->{enabled} && !$setting->{enabled})
168             || (exists $setting->{profiling_enabled} &&
169             !$setting->{profiling_enabled})
170             );
171              
172             # Go no further if this request was to view profiling output:
173             return if $path =~ m{^/nytprof};
174              
175             # Now, fix up the path into something we can use for a filename:
176             $path =~ s{^/}{};
177             $path =~ s{/}{_s_}g;
178             $path =~ s{[^a-z0-9]}{_}gi;
179              
180             # Start profiling, and let the request continue
181             if (
182             !exists $setting->{profiling_enabled}
183             || $setting->{profiling_enabled}
184             ) {
185             DB::enable_profile(
186             Dancer::FileUtils::path(
187             $setting->{profdir}, "nytprof.out.$path.$$"
188             )
189             );
190             }
191             };
192              
193             hook 'after' => sub {
194             if (!exists $setting->{profiling_enabled}
195             || $setting->{profiling_enabled})
196             {
197             DB::disable_profile();
198             DB::finish_profile();
199             }
200             };
201              
202             get '/nytprof' => sub {
203             # First of all, if we were enabled initially, so the route got
204             # installed, but later enabled was set to a false value at runtime,
205             # refuse to serve:
206             if (exists $setting->{enabled} && !$setting->{enabled}) {
207             return "Disabled via 'enabled' setting";
208             }
209              
210             require Devel::NYTProf::Data;
211             opendir my $dirh, $setting->{profdir}
212             or die "Unable to open profiles dir $setting->{profdir} - $!";
213             my @files = grep { /^nytprof\.out/ } readdir $dirh;
214             closedir $dirh;
215              
216             # HTML + CSS here is a bit ugly, but I want this to be usable as a
217             # single-file plugin that Just Works, without needing to copy over templates
218             # / CSS etc.
219             my $html = <
220             NYTProf profile run list
221            
224            
225            
226            

Profile run list

227            

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

228             produced by Devel::NYTProf.

229              
230            
231             LISTSTART
232              
233             for my $file (
234             sort {
235             (stat Dancer::FileUtils::path($setting->{profdir},$b))->ctime
236             <=>
237             (stat Dancer::FileUtils::path($setting->{profdir},$a))->ctime
238             } @files
239             ) {
240             my $fullfilepath = Dancer::FileUtils::path(
241             $setting->{profdir}, $file,
242             );
243             my $label = $file;
244             $label =~ s{nytprof\.out\.}{};
245             $label =~ s{_s_}{/}g;
246             $label =~ s{\.(\d+)$}{};
247             my $pid = $1; # refactor this crap
248             my $created = scalar localtime( (stat $fullfilepath)->ctime );
249              
250             # read the profile to find out the duration of the profiled request.
251             # Done in an eval to catch errors (e.g. if a profile run died,
252             # the data will be incomplete)
253             my ($profile,$duration);
254              
255             if (!defined $setting->{show_durations}
256             || $setting->{show_durations})
257             {
258             eval {
259             my ($stdout, $stderr, @result) = Capture::Tiny::capture {
260             $profile = Devel::NYTProf::Data->new(
261             { filename => $fullfilepath },
262             );
263             };
264             };
265             if ($profile) {
266             $duration = sprintf '%.4f secs',
267             $profile->attributes->{profiler_duration};
268             } else {
269             $duration = '??? seconds - corrupt profile data?';
270             }
271             }
272             $pid = "PID $pid";
273             my $url = request->uri_for("/nytprof/$file")->as_string;
274             $html .= qq{
  • $label (}
  • 275             . join(',', grep { defined $_ } ($pid, $created, $duration))
    276             . qq{)};
    277             }
    278              
    279             my $nytversion = $Devel::NYTProf::VERSION;
    280             $html .= <
    281            
    282              
    283            

    Generated by

    284             Dancer::Plugin::NYTProf v$VERSION
    285             (using
    286             Devel::NYTProf v$nytversion)

    287            
    288            
    289             LISTEND
    290              
    291             return $html;
    292             };
    293              
    294              
    295             # Serve up HTML reports
    296             get '/nytprof/html/**' => sub {
    297             # First of all, if we were enabled initially, so the route got
    298             # installed, but later enabled was set to a false value at runtime,
    299             # refuse to serve:
    300             if (exists $setting->{enabled} && !$setting->{enabled}) {
    301             return "Disabled via 'enabled' setting";
    302             }
    303              
    304             my ($path) = splat;
    305             send_file Dancer::FileUtils::path(
    306             $setting->{profdir}, 'html', map { _safe_filename($_) } @$path
    307             ), system_path => 1;
    308             };
    309              
    310             get '/nytprof/:filename' => sub {
    311             # First of all, if we were enabled initially, so the route got
    312             # installed, but later enabled was set to a false value at runtime,
    313             # refuse to serve:
    314             if (exists $setting->{enabled} && !$setting->{enabled}) {
    315             return "Disabled via 'enabled' setting";
    316             }
    317              
    318             my $profiledata = Dancer::FileUtils::path(
    319             $setting->{profdir}, _safe_filename(param('filename'))
    320             );
    321              
    322             if (!-f $profiledata) {
    323             send_error 'not_found';
    324             return "No such profile run found.";
    325             }
    326              
    327             # See if we already have the HTML for this run stored; if not, invoke
    328             # nytprofhtml to generate it
    329              
    330             # Right, do we already have generated HTML for this one? If so, use it
    331             my $htmldir = Dancer::FileUtils::path(
    332             $setting->{profdir}, 'html', _safe_filename(param('filename'))
    333             );
    334             if (! -f Dancer::FileUtils::path($htmldir, 'index.html')) {
    335             # TODO: scrutinise this very carefully to make sure it's not
    336             # exploitable
    337             system($nytprofhtml_path, "--file=$profiledata", "--out=$htmldir");
    338              
    339             if ($? == -1) {
    340             die "'$nytprofhtml_path' failed to execute: $!";
    341             } elsif ($? & 127) {
    342             die sprintf "'%s' died with signal %d, %s coredump",
    343             $nytprofhtml_path,,
    344             ($? & 127),
    345             ($? & 128) ? 'with' : 'without';
    346             } elsif ($? != 0) {
    347             die sprintf "'%s' exited with value %d",
    348             $nytprofhtml_path, $? >> 8;
    349             }
    350             }
    351              
    352             # Redirect off to view it:
    353             return redirect '/nytprof/html/'
    354             . param('filename') . '/index.html';
    355              
    356             };
    357              
    358             }
    359              
    360             # Rudimentary security - remove any directory traversal or poison null
    361             # attempts. We're dealing with user input here, and if they're a sneaky
    362             # bastard, they could convince us to send a file we shouldn't, or have
    363             # nytprofhtml write its output to somewhere it shouldn't. We don't want that.
    364             sub _safe_filename {
    365 0     0     my $filename = shift;
    366 0           $filename =~ s/\\//g;
    367 0           $filename =~ s/\0//g;
    368 0           $filename =~ s/\.\.//g;
    369 0           $filename =~ s/[\/]//g;
    370 0           return $filename;
    371             }
    372              
    373             =head1 AUTHOR
    374              
    375             David Precious, C<< >>
    376              
    377              
    378             =head1 ACKNOWLEDGEMENTS
    379              
    380             Stefan Hornburg (racke)
    381              
    382             Neil Hooey (nhooey)
    383              
    384             J. Bobby Lopez (jbobbylopez)
    385              
    386             leejo
    387              
    388             Breno G. de Oliveira (garu)
    389              
    390              
    391             =head1 BUGS
    392              
    393             Please report any bugs or feature requests at
    394             L.
    395              
    396             =head1 CONTRIBUTING
    397              
    398             This module is developed on GitHub:
    399              
    400             L
    401              
    402             Bug reports, suggestions and pull requests all welcomed!
    403              
    404             =head1 SEE ALSO
    405              
    406             L
    407              
    408             L
    409              
    410             L
    411              
    412              
    413             =head1 LICENSE AND COPYRIGHT
    414              
    415             Copyright 2011-2014 David Precious.
    416              
    417             This program is free software; you can redistribute it and/or modify it
    418             under the terms of either: the GNU General Public License as published
    419             by the Free Software Foundation; or the Artistic License.
    420              
    421             See http://dev.perl.org/licenses/ for more information.
    422              
    423              
    424             =cut
    425              
    426             1; # Sam Kington didn't like that this said "End of Dancer::Plugin::NYTProf",
    427             # as it's fairly obvious. So, just for Sam's pleasure,
    428             # "It's the end of the world as we know it!" ... or something.