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.22
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   2978418 use strict;
  7         57  
  7         193  
54 7     7   36 use warnings;
  7         13  
  7         202  
55              
56 7     7   419 use Mojo::Base 'Mojolicious::Plugin';
  7         153168  
  7         49  
57 7     7   5747 use Time::HiRes 'gettimeofday';
  7         22  
  7         64  
58 7     7   1378 use File::Temp;
  7         8460  
  7         497  
59 7     7   2769 use File::Which;
  7         6338  
  7         341  
60 7     7   380 use File::Spec::Functions qw/catfile catdir/;
  7         682  
  7         14584  
61              
62             our $VERSION = '0.22';
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             The caveat with the use of hooks is that some hooks can fire out of order, and when
82             asynchronous code is used in your controllers you may see incomplete/odd profiling
83             behaviour - you can play around with the hook configuration to try to fix this.
84              
85             You can override the hooks used to control when the profiling runs, see the
86             CONFIGURATION section below.
87              
88             =head1 CONFIGURATION
89              
90             Here's what you can control in myapp.conf:
91              
92             {
93             # Devel::NYTProf will only be loaded, and profiling enabled, if the nytprof
94             # key is present in your config file, so either remove it or comment it out
95             # to completely disable profiling.
96             nytprof => {
97              
98             # path to your nytprofhtml script (installed as part of Devel::NYTProf
99             # distribution). the plugin will do its best to try to find this so this
100             # is optional, just set if you have a none standard path
101             nytprofhtml_path => '/path/to/nytprofhtml',
102              
103             # path to store Devel::NYTProf output profiles and generated html pages.
104             # options, defaults to "/path/to/your/app/root/dir/nytprof"
105             profiles_dir => '/path/to/nytprof/profiles/'
106              
107             # set this to true to allow the plugin to run when in production mode
108             # the default value is 0 so you can deploy your app to prod without
109             # having to make any changes to config/plugin register
110             allow_production => 0,
111              
112             # Devel::NYTProf environment options, see the documentation at
113             # https://metacpan.org/pod/Devel::NYTProf#NYTPROF-ENVIRONMENT-VARIABLE
114             # for a complete list. N.B. you can't supply start or file as these
115             # are used internally in the plugin so will be ignored if passed
116             env => {
117             trace => 1,
118             log => "/path/to/foo/",
119             ....
120             },
121              
122             # when to enable Devel::NYTProf profiling - the pre_hook will run
123             # to enable_profile and the post_hook will run to disable_profile
124             # and finish_profile. the values show here are the defaults so you
125             # do not need to provide these options
126             #
127             # bear in mind the caveats in the Mojolicious docs regarding hooks
128             # and that they may not fire in the order you expect - this can
129             # affect the NYTProf output and cause some things not to appear
130             # (or appear in the wrong order). the defaults below should be
131             # sufficient for profiling your code, however you can change these
132             #
133             # N.B. there is nothing stopping you reversing the order of the
134             # hooks, which would cause the Mojolicious framework code to be
135             # profiled, or providing hooks that are the same or even invalid. these
136             # config options should probably be used with some care
137             pre_hook => 'before_routes',
138             post_hook => 'around_dispatch',
139             },
140             }
141              
142             =head1 nytprofhtml LOCATION
143              
144             The plugin does its best to find the path to your nytprofhtml executable, if
145             it cannot find it then it will die with an error. This also affects testing,
146             and any tests will be skipped if they cannot find nytprofhtml allowing you to
147             install the plugin - you will then need to make sure to set the path in your
148             config using nytprofhtml_path
149              
150             =cut
151              
152             sub register {
153 88     88 1 505516 my ($self, $app, $config) = @_;
154              
155 88 100       544 if (my $nytprof = $config->{nytprof}) {
156              
157 87 100 100     468 return if $app->mode eq 'production' and ! $nytprof->{allow_production};
158              
159 86         820 my $nytprofhtml_path;
160              
161 86 100       353 if ( $nytprofhtml_path = $nytprof->{nytprofhtml_path} ) {
162             # no sanity checking here, if a path is configured we use it
163             # and don't fall through to defaults
164             } else {
165 85         297 $nytprofhtml_path = _find_nytprofhtml();
166             }
167              
168 86 100 66     1371 $nytprofhtml_path && -e $nytprofhtml_path
169             or die "Could not find nytprofhtml script. Ensure it's in your path, "
170             . "or set the nytprofhtml_path option in your config.";
171              
172             # Devel::NYTProf will create an nytprof.out file immediately so
173             # we need to assign a tmp file and disable profiling from start
174 85   50     582 my $prof_dir = $nytprof->{profiles_dir} || 'nytprof';
175              
176 85         882 foreach my $dir ($prof_dir,catfile($prof_dir,'profiles')) {
177 170 50       2926 if (! -d $dir) {
178 0 0       0 mkdir $dir
179             or die "$dir does not exist and cannot create - $!";
180             }
181             }
182              
183             # disable config option is undocumented, it allows testing where we
184             # don't actually load or run Devel::NYTProf
185 85 100       498 if (!$nytprof->{disable}) {
186             # https://metacpan.org/pod/Devel::NYTProf#NYTPROF-ENVIRONMENT-VARIABLE
187             # options for Devel::NYTProf - any can be passed but will always set
188             # the start and file options here
189 4         15 $nytprof->{env}{start} = 'no';
190 4         9 s/([:=])/\\$1/g for grep{ defined() } values %{ $nytprof->{env} };
  6         30  
  4         16  
191              
192             $ENV{NYTPROF} = join( ':',
193 6         50 map { "$_=" . $nytprof->{env}{$_} }
194 4         9 keys %{ $nytprof->{env} }
  4         13  
195             );
196              
197 4         3013 require Devel::NYTProf;
198             }
199              
200 85         5438 $self->_add_hooks($app, $config, $nytprofhtml_path);
201             }
202             }
203              
204             sub _find_nytprofhtml {
205             # fall back, assume nytprofhtml_path in same dir as perl
206 92     92   792 my $nytprofhtml_path = $^X;
207 92         802 $nytprofhtml_path =~ s/w?perl[\d\.]*(?:\.exe)?$/nytprofhtml/;
208              
209 92 50       2964 if ( ! -e $nytprofhtml_path ) {
210             # last ditch attempt to find nytprofhtml, use File::Which
211             # (last ditch in that it may return a different nytprofhtml
212             # that is using a differently configured perl, e.g. system,
213             # this may die with incompat config errorrs but at least try)
214 0         0 $nytprofhtml_path = File::Which::which('nytprofhtml');
215             }
216              
217 92 50 33     1692 return $nytprofhtml_path && -e $nytprofhtml_path
218             ? $nytprofhtml_path : undef;
219             }
220              
221             sub _add_hooks {
222 85     85   362 my ($self, $app, $config, $nytprofhtml_path) = @_;
223              
224 85         199 my $nytprof = $config->{nytprof};
225 85   50     316 my $prof_dir = $nytprof->{profiles_dir} || 'nytprof';
226 85   100     345 my $pre_hook = $nytprof->{pre_hook} || 'before_routes';
227 85   100     283 my $post_hook = $nytprof->{post_hook} || 'around_dispatch';
228 85   100     305 my $disable = $nytprof->{disable} || 0;
229 85         441 my $log = $app->log;
230              
231             # add the nytprof/html directory to the static paths
232             # so we can serve these without having to add routes
233 85         1242 push @{$app->static->paths},catfile($prof_dir,'html');
  85         300  
234              
235             # put the actual profile files into a profiles sub directory
236             # to avoid confusion with the *dirs* in nytprof/html
237 85         1320 my $prof_sub_dir = catfile( $prof_dir,'profiles' );
238              
239             $app->hook($pre_hook => sub {
240              
241             # figure args based on what the hook is
242 2388     2388   720473 my ($tx, $app, $next, $c, $path);
243              
244 2388 100       6516 if ($pre_hook eq 'after_build_tx') {
    50          
245 693         1539 ($tx, $app) = @_[0,1];
246 693         1141 $path = $pre_hook; # TODO - need better identifier for this?
247             } elsif ($pre_hook =~ /around/) {
248 0         0 ($next, $c) = @_[0,1];
249             } else {
250 1695         2626 $c = $_[0];
251 1695         4763 $path = $c->req->url->to_string;
252 1695 100       223696 return if $c->stash->{'mojo.static'}; # static files
253             }
254              
255 2385 100       17938 return if $path =~ m{^/nytprof}; # viewing profiles
256 2381         6950 $path =~ s!^/!!g;
257 2381         4377 $path =~ s!/!-!g;
258 2381 50       6077 $path =~ s![:?]!-!g if $^O eq 'MSWin32';
259 2381         4068 $path =~ s!\?.*$!!g; # remove URL query params
260              
261 2381         7327 my ($sec, $usec) = gettimeofday;
262 2381         16962 my $profile = catfile($prof_sub_dir,"nytprof_out_${sec}_${usec}_${path}_$$");
263 2381 50 33     7431 if($^O eq 'MSWin32' && length($profile)>259){
264 0         0 my $overflow = length($profile) - 259;
265 0         0 $path = substr($path, 0,length($path) - $overflow -1);
266 0         0 $profile = catfile($prof_sub_dir,"nytprof_out_${sec}_${usec}_${path}_$$");
267             }
268 2381         8007 $log->debug( 'starting NYTProf' );
269             # note that we are passing a custom file to enable_profile, this results in
270             # a timing bug causing multiple calls to this plugin (in the order of 10^5)
271             # to gradually slow down. see GH #5
272 2381 100       20916 DB::enable_profile( $profile ) if ! $disable;
273 2381 50       8440 return $next->() if $pre_hook =~ /around/;
274 85         1084 });
275              
276             $app->hook($post_hook => sub {
277             # first arg is $next if the hook matches around
278 2243 100   2243   393513 shift->() if $post_hook =~ /around/;
279 2243 100       65846 DB::finish_profile() if ! $disable;
280 2243         4697 $log->debug( 'finished NYTProf' );
281 85         1713 });
282              
283             $app->routes->get('/nytprof/profiles/:file'
284             => [file => qr/nytprof_out_\d+_\d+.*/]
285             => sub {
286 2     2   1118 $log->debug( "generating profile for $nytprofhtml_path" );
287 2         16 _generate_profile(@_,$prof_dir,$nytprofhtml_path)
288             }
289 85         968 );
290              
291 85     2   41470 $app->routes->get('/nytprof' => sub { _list_profiles(@_,$prof_sub_dir) });
  2         1238  
292             }
293              
294             sub _list_profiles {
295 2     2   5 my $self = shift;
296 2         5 my $prof_dir = shift;
297              
298 2         7 my @profiles = _profiles($prof_dir);
299 2         15 $self->app->log->debug( scalar( @profiles ) . ' profiles found' );
300              
301             # could use epl here, but users might be using a different Template engine
302 2 100       56 my $list = @profiles
303             ? '

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

    '
304             : '

No profiles found

';
305              
306 2         6 foreach (@profiles) {
307 3         13 $list .= qq{
308            
  • 309             $_->{label}
    310             (PID $_->{pid}, $_->{created}, $_->{duration})
    311            
    312             };
    313             }
    314              
    315 2 100       14 $list .= '' if $list !~ /No profiles found/;
    316              
    317 2         8 my $html = <<"EndOfEp";
    318            
    319            
    320             NYTProf profile run list
    321            
    322            
    323            

    Profile run list

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