File Coverage

blib/lib/Mojolicious/Plugin/NYTProf.pm
Criterion Covered Total %
statement 132 141 93.6
branch 45 62 72.5
condition 17 25 68.0
subroutine 17 17 100.0
pod 1 1 100.0
total 212 246 86.1


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::NYTProf;
2              
3             =head1 NAME
4              
5             Mojolicious::Plugin::NYTProf - Auto handling of Devel::NYTProf in your Mojolicious app
6              
7             =for html
8             Build Status
9             Coverage Status
10              
11             =head1 VERSION
12              
13             0.21
14              
15             =head1 DESCRIPTION
16              
17             This plugin enables L to automatically generate Devel::NYTProf
18             profiles and routes for your app, it has been inspired by
19             L
20              
21             =head1 SYNOPSIS
22              
23             use Mojolicious::Lite;
24              
25             plugin NYTProf => {
26             nytprof => {
27             ... # see CONFIGURATION
28             },
29             };
30              
31             app->start;
32              
33             Or
34              
35             use Mojo::Base 'Mojolicious';
36              
37             ...
38              
39             sub startup {
40             my $self = shift;
41              
42             ...
43              
44             my $mojo_config = $self->plugin('Config');
45             $self->plugin(NYTProf => $mojo_config);
46             }
47              
48             Then run your app. Profiles generated can be seen by visting /nytprof and reports
49             will be generated on the fly when you click on a specific profile.
50              
51             =cut
52              
53 7     7   2942906 use strict;
  7         62  
  7         209  
54 7     7   41 use warnings;
  7         14  
  7         213  
55              
56 7     7   500 use Mojo::Base 'Mojolicious::Plugin';
  7         136625  
  7         44  
57 7     7   6540 use Time::HiRes 'gettimeofday';
  7         16  
  7         63  
58 7     7   1549 use File::Temp;
  7         11333  
  7         556  
59 7     7   3258 use File::Which;
  7         7420  
  7         388  
60 7     7   525 use File::Spec::Functions qw/catfile catdir/;
  7         825  
  7         17304  
61              
62             our $VERSION = '0.21';
63              
64             =head1 METHODS
65              
66             =head2 register
67              
68             Registers the plugin with your app - this will only do something if the nytprof
69             key exists in your config hash
70              
71             $self->register($app, \%config);
72              
73             =head1 HOOKS AND Devel::NYTProf
74              
75             The plugin adds hooks to control the level of profiling, Devel::NYTProf profiling
76             is started using a before_routes hook and the stopped with an around_dispatch hook.
77              
78             The consequence of this is that you should see profiling only for your routes and
79             rendering code and will not see most of the actual Mojolicious framework detail.
80              
81             You can override the hooks used to control when the profiling runs, see the
82             CONFIGURATION section below.
83              
84             =head1 CONFIGURATION
85              
86             Here's what you can control in myapp.conf:
87              
88             {
89             # Devel::NYTProf will only be loaded, and profiling enabled, if the nytprof
90             # key is present in your config file, so either remove it or comment it out
91             # to completely disable profiling.
92             nytprof => {
93              
94             # path to your nytprofhtml script (installed as part of Devel::NYTProf
95             # distribution). the plugin will do its best to try to find this so this
96             # is optional, just set if you have a none standard path
97             nytprofhtml_path => '/path/to/nytprofhtml',
98              
99             # path to store Devel::NYTProf output profiles and generated html pages.
100             # options, defaults to "/path/to/your/app/root/dir/nytprof"
101             profiles_dir => '/path/to/nytprof/profiles/'
102              
103             # set this to true to allow the plugin to run when in production mode
104             # the default value is 0 so you can deploy your app to prod without
105             # having to make any changes to config/plugin register
106             allow_production => 0,
107              
108             # Devel::NYTProf environment options, see the documentation at
109             # https://metacpan.org/pod/Devel::NYTProf#NYTPROF-ENVIRONMENT-VARIABLE
110             # for a complete list. N.B. you can't supply start or file as these
111             # are used internally in the plugin so will be ignored if passed
112             env => {
113             trace => 1,
114             log => "/path/to/foo/",
115             ....
116             },
117              
118             # when to enable Devel::NYTProf profiling - the pre_hook will run
119             # to enable_profile and the post_hook will run to disable_profile
120             # and finish_profile. the values show here are the defaults so you
121             # do not need to provide these options
122             #
123             # bear in mind the caveats in the Mojolicious docs regarding hooks
124             # and that they may not fire in the order you expect - this can
125             # affect the NYTProf output and cause some things not to appear
126             # (or appear in the wrong order). the defaults below should be
127             # sufficient for profiling your code, however you can change these
128             #
129             # N.B. there is nothing stopping you reversing the order of the
130             # hooks, which would cause the Mojolicious framework code to be
131             # profiled, or providing hooks that are the same or even invalid. these
132             # config options should probably be used with some care
133             pre_hook => 'before_routes',
134             post_hook => 'around_dispatch',
135             },
136             }
137              
138             =head1 nytprofhtml LOCATION
139              
140             The plugin does its best to find the path to your nytprofhtml executable, if
141             it cannot find it then it will die with an error. This also affects testing,
142             and any tests will be skipped if they cannot find nytprofhtml allowing you to
143             install the plugin - you will then need to make sure to set the path in your
144             config using nytprofhtml_path
145              
146             =cut
147              
148             sub register {
149 88     88 1 423684 my ($self, $app, $config) = @_;
150              
151 88 100       383 if (my $nytprof = $config->{nytprof}) {
152              
153 87 100 100     325 return if $app->mode eq 'production' and ! $nytprof->{allow_production};
154              
155 86         677 my $nytprofhtml_path;
156              
157 86 100       298 if ( $nytprofhtml_path = $nytprof->{nytprofhtml_path} ) {
158             # no sanity checking here, if a path is configured we use it
159             # and don't fall through to defaults
160             } else {
161 85         225 $nytprofhtml_path = _find_nytprofhtml();
162             }
163              
164 86 100 66     1346 $nytprofhtml_path && -e $nytprofhtml_path
165             or die "Could not find nytprofhtml script. Ensure it's in your path, "
166             . "or set the nytprofhtml_path option in your config.";
167              
168             # Devel::NYTProf will create an nytprof.out file immediately so
169             # we need to assign a tmp file and disable profiling from start
170 85   50     445 my $prof_dir = $nytprof->{profiles_dir} || 'nytprof';
171              
172 85         671 foreach my $dir ($prof_dir,catfile($prof_dir,'profiles')) {
173 170 50       2615 if (! -d $dir) {
174 0 0       0 mkdir $dir
175             or die "$dir does not exist and cannot create - $!";
176             }
177             }
178              
179             # disable config option is undocumented, it allows testing where we
180             # don't actually load or run Devel::NYTProf
181 85 100       488 if (!$nytprof->{disable}) {
182             # https://metacpan.org/pod/Devel::NYTProf#NYTPROF-ENVIRONMENT-VARIABLE
183             # options for Devel::NYTProf - any can be passed but will always set
184             # the start and file options here
185 4         16 $nytprof->{env}{start} = 'no';
186 4         10 s/([:=])/\\$1/g for grep{ defined() } values %{ $nytprof->{env} };
  6         31  
  4         19  
187              
188             $ENV{NYTPROF} = join( ':',
189 6         54 map { "$_=" . $nytprof->{env}{$_} }
190 4         11 keys %{ $nytprof->{env} }
  4         14  
191             );
192              
193 4         3401 require Devel::NYTProf;
194             }
195              
196 85         5726 $self->_add_hooks($app, $config, $nytprofhtml_path);
197             }
198             }
199              
200             sub _find_nytprofhtml {
201             # fall back, assume nytprofhtml_path in same dir as perl
202 92     92   865 my $nytprofhtml_path = $^X;
203 92         703 $nytprofhtml_path =~ s/w?perl[\d\.]*(?:\.exe)?$/nytprofhtml/;
204              
205 92 50       2370 if ( ! -e $nytprofhtml_path ) {
206             # last ditch attempt to find nytprofhtml, use File::Which
207             # (last ditch in that it may return a different nytprofhtml
208             # that is using a differently configured perl, e.g. system,
209             # this may die with incompat config errorrs but at least try)
210 0         0 $nytprofhtml_path = File::Which::which('nytprofhtml');
211             }
212              
213 92 50 33     1605 return $nytprofhtml_path && -e $nytprofhtml_path
214             ? $nytprofhtml_path : undef;
215             }
216              
217             sub _add_hooks {
218 85     85   278 my ($self, $app, $config, $nytprofhtml_path) = @_;
219              
220 85         200 my $nytprof = $config->{nytprof};
221 85   50     254 my $prof_dir = $nytprof->{profiles_dir} || 'nytprof';
222 85   100     245 my $pre_hook = $nytprof->{pre_hook} || 'before_routes';
223 85   100     284 my $post_hook = $nytprof->{post_hook} || 'around_dispatch';
224 85   100     233 my $disable = $nytprof->{disable} || 0;
225 85         328 my $log = $app->log;
226              
227             # add the nytprof/html directory to the static paths
228             # so we can serve these without having to add routes
229 85         1182 push @{$app->static->paths},catfile($prof_dir,'html');
  85         236  
230              
231             # put the actual profile files into a profiles sub directory
232             # to avoid confusion with the *dirs* in nytprof/html
233 85         1142 my $prof_sub_dir = catfile( $prof_dir,'profiles' );
234              
235             $app->hook($pre_hook => sub {
236              
237             # figure args based on what the hook is
238 2388     2388   670884 my ($tx, $app, $next, $c, $path);
239              
240 2388 100       6205 if ($pre_hook eq 'after_build_tx') {
    50          
241 693         1401 ($tx, $app) = @_[0,1];
242 693         1109 $path = $pre_hook; # TODO - need better identifier for this?
243             } elsif ($pre_hook =~ /around/) {
244 0         0 ($next, $c) = @_[0,1];
245             } else {
246 1695         2726 $c = $_[0];
247 1695         4166 $path = $c->req->url->to_string;
248 1695 100       221120 return if $c->stash->{'mojo.static'}; # static files
249             }
250              
251 2385 100       17683 return if $path =~ m{^/nytprof}; # viewing profiles
252 2381         6862 $path =~ s!^/!!g;
253 2381         4546 $path =~ s!/!-!g;
254 2381 50       6144 $path =~ s![:?]!-!g if $^O eq 'MSWin32';
255 2381         3814 $path =~ s!\?.*$!!g; # remove URL query params
256              
257 2381         6667 my ($sec, $usec) = gettimeofday;
258 2381         16269 my $profile = catfile($prof_sub_dir,"nytprof_out_${sec}_${usec}_${path}_$$");
259 2381 50 33     7484 if($^O eq 'MSWin32' && length($profile)>259){
260 0         0 my $overflow = length($profile) - 259;
261 0         0 $path = substr($path, 0,length($path) - $overflow -1);
262 0         0 $profile = catfile($prof_sub_dir,"nytprof_out_${sec}_${usec}_${path}_$$");
263             }
264 2381         7102 $log->debug( 'starting NYTProf' );
265             # note that we are passing a custom file to enable_profile, this results in
266             # a timing bug causing multiple calls to this plugin (in the order of 10^5)
267             # to gradually slow down. see GH #5
268 2381 100       57241 DB::enable_profile( $profile ) if ! $disable;
269 2381 50       8158 return $next->() if $pre_hook =~ /around/;
270 85         957 });
271              
272             $app->hook($post_hook => sub {
273             # first arg is $next if the hook matches around
274 2243 100   2243   412954 shift->() if $post_hook =~ /around/;
275 2243 100       80373 DB::finish_profile() if ! $disable;
276 2243         4589 $log->debug( 'finished NYTProf' );
277 85         1419 });
278              
279             $app->routes->get('/nytprof/profiles/:file'
280             => [file => qr/nytprof_out_\d+_\d+.*/]
281             => sub {
282 2     2   1367 $log->debug( "generating profile for $nytprofhtml_path" );
283 2         48 _generate_profile(@_,$prof_dir,$nytprofhtml_path)
284             }
285 85         927 );
286              
287 85     2   35110 $app->routes->get('/nytprof' => sub { _list_profiles(@_,$prof_sub_dir) });
  2         1533  
288             }
289              
290             sub _list_profiles {
291 2     2   6 my $self = shift;
292 2         5 my $prof_dir = shift;
293              
294 2         6 my @profiles = _profiles($prof_dir);
295 2         10 $self->app->log->debug( scalar( @profiles ) . ' profiles found' );
296              
297             # could use epl here, but users might be using a different Template engine
298 2 100       89 my $list = @profiles
299             ? '

Select a profile run output from the list to view the HTML reports as produced by Devel::NYTProf.

    '
300             : '

No profiles found

';
301              
302 2         7 foreach (@profiles) {
303 3         15 $list .= qq{
304            
  • 305             $_->{label}
    306             (PID $_->{pid}, $_->{created}, $_->{duration})
    307            
    308             };
    309             }
    310              
    311 2 100       10 $list .= '' if $list !~ /No profiles found/;
    312              
    313 2         10 my $html = <<"EndOfEp";
    314            
    315            
    316             NYTProf profile run list
    317            
    318            
    319            

    Profile run list

    320             $list
    321            
    322            
    323             EndOfEp
    324              
    325 2         11 $self->render(text => $html);
    326             }
    327              
    328             sub _profiles {
    329 7     7   39640 my $prof_dir = shift;
    330              
    331 7         2547 require Devel::NYTProf::Data;
    332 7 50       77575 opendir my $dirh, $prof_dir
    333             or die "Unable to open profiles dir $prof_dir - $!";
    334 7         200 my @files = grep { /^nytprof_out/ } readdir $dirh;
      29         111  
    335 7         90 closedir $dirh;
    336              
    337 7         23 my @profiles;
    338              
    339 7         48 for my $file ( sort {
    340 6         209 (stat catfile($prof_dir,$b))[10] <=> (stat catfile($prof_dir,$a))[10]
    341             } @files ) {
    342 8         18 my $profile;
    343 8         55 my $filepath = catfile($prof_dir,$file);
    344 8         21 my $label = $file;
    345 8         64 $label =~ s{nytprof_out_(\d+)_(\d+)_}{};
    346 8         39 my ($sec, $usec) = ($1,$2);
    347 8         20 $label =~ s{\.}{/}g;
    348 8         16 $label =~ s{/(\d+)$}{};
    349 8         18 my $pid = $1;
    350              
    351 8         15 my ($nytprof,$duration);
    352 8         15 eval { $nytprof = Devel::NYTProf::Data->new({filename => $filepath}); };
      8         60  
    353              
    354             $profile->{duration} = $nytprof && $nytprof->attributes->{profiler_duration}
    355             ? sprintf('%.4f secs', $nytprof->attributes->{profiler_duration})
    356 8 100 66     4517 : '??? seconds - corrupt profile data?';
    357              
    358 8         386 @{$profile}{qw/file url pid created label/}
      8         56  
    359             = ($file,"/nytprof/profiles/$file",$pid,scalar localtime($sec),$label);
    360 8         36 push(@profiles,$profile);
    361             }
    362              
    363 7         69 return @profiles;
    364             }
    365              
    366             sub _generate_profile {
    367 2     2   5 my $self = shift;
    368 2         6 my $htmldir = my $prof_dir = shift;
    369 2         4 my $nytprofhtml_path = shift;
    370              
    371 2         39 my $file = $self->stash('file');
    372 2         37 my $profile = catfile($prof_dir,'profiles',$file);
    373 2 100       76 return $self->reply->not_found if !-f $profile;
    374            
    375 1         13 foreach my $sub_dir (
    376             $htmldir,
    377             catfile($htmldir,'html'),
    378             catfile($htmldir,'html',$file),
    379             ) {
    380 3 100       54 if (! -d $sub_dir) {
    381 1 50       96 mkdir $sub_dir
    382             or die "$sub_dir does not exist and cannot create - $!";
    383             }
    384             }
    385              
    386 1         13 $htmldir = catfile($htmldir,'html',$file);
    387              
    388 1 50       25 if (! -f catfile($htmldir, 'index.html')) {
    389 1         260675 system($nytprofhtml_path, "--file=$profile", "--out=$htmldir");
    390              
    391 1 50       131 if ($? == -1) {
        50          
        50          
    392 0         0 die "'$nytprofhtml_path' failed to execute: $!";
    393             } elsif ($? & 127) {
    394 0 0       0 die sprintf "'%s' died with signal %d, %s coredump",
    395             $nytprofhtml_path,,($? & 127),($? & 128) ? 'with' : 'without';
    396             } elsif ($? != 0) {
    397 0         0 die sprintf "'%s' exited with value %d",
    398             $nytprofhtml_path, $? >> 8;
    399             }
    400             }
    401              
    402 1         83 $self->redirect_to("/${file}/index.html");
    403             }
    404              
    405             =head1 AUTHOR
    406              
    407             Lee Johnson - C
    408              
    409             =head1 LICENSE
    410              
    411             This library is free software; you can redistribute it and/or modify it under
    412             the same terms as Perl itself. If you would like to contribute documentation
    413             please raise an issue / pull request:
    414              
    415             https://github.com/Humanstate/mojolicious-plugin-nytprof
    416              
    417             =cut
    418              
    419             1;
    420              
    421             # vim: ts=2:sw=2:et