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.23
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 - you should start your app with the env variables:
49              
50             PERL5OPT='-d:NYTProf'
51             NYTPROF=start=no
52              
53             without this, things go a bit haywire (most obviously manifested as broken links
54             in the report) because otherwise any code compiled before the C call
55             cannot be covered, as described in the docs:
56             L
57              
58             Profiles generated can be seen by visting /nytprof and reports
59             will be generated on the fly when you click on a specific profile.
60              
61             =cut
62              
63 7     7   2790632 use strict;
  7         54  
  7         229  
64 7     7   34 use warnings;
  7         13  
  7         188  
65              
66 7     7   455 use Mojo::Base 'Mojolicious::Plugin';
  7         155289  
  7         40  
67 7     7   5690 use Time::HiRes 'gettimeofday';
  7         13  
  7         57  
68 7     7   1315 use File::Temp;
  7         8632  
  7         441  
69 7     7   2772 use File::Which;
  7         5984  
  7         331  
70 7     7   447 use File::Spec::Functions qw/catfile catdir/;
  7         718  
  7         15322  
71              
72             our $VERSION = '0.23';
73              
74             =head1 METHODS
75              
76             =head2 register
77              
78             Registers the plugin with your app - this will only do something if the nytprof
79             key exists in your config hash
80              
81             $self->register($app, \%config);
82              
83             =head1 HOOKS AND Devel::NYTProf
84              
85             The plugin adds hooks to control the level of profiling, Devel::NYTProf profiling
86             is started using a before_routes hook and the stopped with an around_dispatch hook.
87              
88             The consequence of this is that you should see profiling only for your routes and
89             rendering code and will not see most of the actual Mojolicious framework detail.
90              
91             The caveat with the use of hooks is that some hooks can fire out of order, and when
92             asynchronous code is used in your controllers you may see incomplete/odd profiling
93             behaviour - you can play around with the hook configuration to try to fix this.
94              
95             You can override the hooks used to control when the profiling runs, see the
96             CONFIGURATION section below.
97              
98             =head1 CONFIGURATION
99              
100             Here's what you can control in myapp.conf:
101              
102             {
103             # Devel::NYTProf will only be loaded, and profiling enabled, if the nytprof
104             # key is present in your config file, so either remove it or comment it out
105             # to completely disable profiling.
106             nytprof => {
107              
108             # path to your nytprofhtml script (installed as part of Devel::NYTProf
109             # distribution). the plugin will do its best to try to find this so this
110             # is optional, just set if you have a none standard path
111             nytprofhtml_path => '/path/to/nytprofhtml',
112              
113             # path to store Devel::NYTProf output profiles and generated html pages.
114             # options, defaults to "/path/to/your/app/root/dir/nytprof"
115             profiles_dir => '/path/to/nytprof/profiles/'
116              
117             # set this to true to allow the plugin to run when in production mode
118             # the default value is 0 so you can deploy your app to prod without
119             # having to make any changes to config/plugin register
120             allow_production => 0,
121              
122             # Devel::NYTProf environment options, see the documentation at
123             # https://metacpan.org/pod/Devel::NYTProf#NYTPROF-ENVIRONMENT-VARIABLE
124             # for a complete list. N.B. you can't supply start or file as these
125             # are used internally in the plugin so will be ignored if passed
126             env => {
127             trace => 1,
128             log => "/path/to/foo/",
129             ....
130             },
131              
132             # when to enable Devel::NYTProf profiling - the pre_hook will run
133             # to enable_profile and the post_hook will run to disable_profile
134             # and finish_profile. the values show here are the defaults so you
135             # do not need to provide these options
136             #
137             # bear in mind the caveats in the Mojolicious docs regarding hooks
138             # and that they may not fire in the order you expect - this can
139             # affect the NYTProf output and cause some things not to appear
140             # (or appear in the wrong order). the defaults below should be
141             # sufficient for profiling your code, however you can change these
142             #
143             # N.B. there is nothing stopping you reversing the order of the
144             # hooks, which would cause the Mojolicious framework code to be
145             # profiled, or providing hooks that are the same or even invalid. these
146             # config options should probably be used with some care
147             pre_hook => 'before_routes',
148             post_hook => 'around_dispatch',
149             },
150             }
151              
152             =head1 nytprofhtml LOCATION
153              
154             The plugin does its best to find the path to your nytprofhtml executable, if
155             it cannot find it then it will die with an error. This also affects testing,
156             and any tests will be skipped if they cannot find nytprofhtml allowing you to
157             install the plugin - you will then need to make sure to set the path in your
158             config using nytprofhtml_path
159              
160             =cut
161              
162             sub register {
163 88     88 1 445714 my ($self, $app, $config) = @_;
164              
165 88 100       438 if (my $nytprof = $config->{nytprof}) {
166              
167 87 100 100     364 return if $app->mode eq 'production' and ! $nytprof->{allow_production};
168              
169 86         739 my $nytprofhtml_path;
170              
171 86 100       351 if ( $nytprofhtml_path = $nytprof->{nytprofhtml_path} ) {
172             # no sanity checking here, if a path is configured we use it
173             # and don't fall through to defaults
174             } else {
175 85         386 $nytprofhtml_path = _find_nytprofhtml();
176             }
177              
178 86 100 66     1231 $nytprofhtml_path && -e $nytprofhtml_path
179             or die "Could not find nytprofhtml script. Ensure it's in your path, "
180             . "or set the nytprofhtml_path option in your config.";
181              
182             # Devel::NYTProf will create an nytprof.out file immediately so
183             # we need to assign a tmp file and disable profiling from start
184 85   50     410 my $prof_dir = $nytprof->{profiles_dir} || 'nytprof';
185              
186 85         694 foreach my $dir ($prof_dir,catfile($prof_dir,'profiles')) {
187 170 50       2505 if (! -d $dir) {
188 0 0       0 mkdir $dir
189             or die "$dir does not exist and cannot create - $!";
190             }
191             }
192              
193             # disable config option is undocumented, it allows testing where we
194             # don't actually load or run Devel::NYTProf
195 85 100       434 if (!$nytprof->{disable}) {
196             # https://metacpan.org/pod/Devel::NYTProf#NYTPROF-ENVIRONMENT-VARIABLE
197             # options for Devel::NYTProf - any can be passed but will always set
198             # the start and file options here
199 4         17 $nytprof->{env}{start} = 'no';
200 4         8 s/([:=])/\\$1/g for grep{ defined() } values %{ $nytprof->{env} };
  6         29  
  4         15  
201              
202             $ENV{NYTPROF} = join( ':',
203 6         46 map { "$_=" . $nytprof->{env}{$_} }
204 4         11 keys %{ $nytprof->{env} }
  4         11  
205             );
206              
207 4         3211 require Devel::NYTProf;
208             }
209              
210 85         4865 $self->_add_hooks($app, $config, $nytprofhtml_path);
211             }
212             }
213              
214             sub _find_nytprofhtml {
215             # fall back, assume nytprofhtml_path in same dir as perl
216 92     92   742 my $nytprofhtml_path = $^X;
217 92         651 $nytprofhtml_path =~ s/w?perl[\d\.]*(?:\.exe)?$/nytprofhtml/;
218              
219 92 50       2117 if ( ! -e $nytprofhtml_path ) {
220             # last ditch attempt to find nytprofhtml, use File::Which
221             # (last ditch in that it may return a different nytprofhtml
222             # that is using a differently configured perl, e.g. system,
223             # this may die with incompat config errorrs but at least try)
224 0         0 $nytprofhtml_path = File::Which::which('nytprofhtml');
225             }
226              
227 92 50 33     1506 return $nytprofhtml_path && -e $nytprofhtml_path
228             ? $nytprofhtml_path : undef;
229             }
230              
231             sub _add_hooks {
232 85     85   304 my ($self, $app, $config, $nytprofhtml_path) = @_;
233              
234 85         176 my $nytprof = $config->{nytprof};
235 85   50     354 my $prof_dir = $nytprof->{profiles_dir} || 'nytprof';
236 85   100     313 my $pre_hook = $nytprof->{pre_hook} || 'before_routes';
237 85   100     271 my $post_hook = $nytprof->{post_hook} || 'around_dispatch';
238 85   100     319 my $disable = $nytprof->{disable} || 0;
239 85         351 my $log = $app->log;
240              
241             # add the nytprof/html directory to the static paths
242             # so we can serve these without having to add routes
243 85         1056 push @{$app->static->paths},catfile($prof_dir,'html');
  85         352  
244              
245             # put the actual profile files into a profiles sub directory
246             # to avoid confusion with the *dirs* in nytprof/html
247 85         1275 my $prof_sub_dir = catfile( $prof_dir,'profiles' );
248              
249             $app->hook($pre_hook => sub {
250              
251             # figure args based on what the hook is
252 2388     2388   637264 my ($tx, $app, $next, $c, $path);
253              
254 2388 100       5722 if ($pre_hook eq 'after_build_tx') {
    50          
255 693         1272 ($tx, $app) = @_[0,1];
256 693         996 $path = $pre_hook; # TODO - need better identifier for this?
257             } elsif ($pre_hook =~ /around/) {
258 0         0 ($next, $c) = @_[0,1];
259             } else {
260 1695         2326 $c = $_[0];
261 1695         3784 $path = $c->req->url->to_string;
262 1695 100       190769 return if $c->stash->{'mojo.static'}; # static files
263             }
264              
265 2385 100       15458 return if $path =~ m{^/nytprof}; # viewing profiles
266 2381         5971 $path =~ s!^/!!g;
267 2381         3731 $path =~ s!/!-!g;
268 2381 50       5584 $path =~ s![:?]!-!g if $^O eq 'MSWin32';
269 2381         3217 $path =~ s!\?.*$!!g; # remove URL query params
270              
271 2381         5822 my ($sec, $usec) = gettimeofday;
272 2381         13716 my $profile = catfile($prof_sub_dir,"nytprof_out_${sec}_${usec}_${path}_$$");
273 2381 50 33     6197 if($^O eq 'MSWin32' && length($profile)>259){
274 0         0 my $overflow = length($profile) - 259;
275 0         0 $path = substr($path, 0,length($path) - $overflow -1);
276 0         0 $profile = catfile($prof_sub_dir,"nytprof_out_${sec}_${usec}_${path}_$$");
277             }
278 2381         6729 $log->debug( 'starting NYTProf' );
279             # note that we are passing a custom file to enable_profile, this results in
280             # a timing bug causing multiple calls to this plugin (in the order of 10^5)
281             # to gradually slow down. see GH #5
282 2381 100       17833 DB::enable_profile( $profile ) if ! $disable;
283 2381 50       6920 return $next->() if $pre_hook =~ /around/;
284 85         915 });
285              
286             $app->hook($post_hook => sub {
287             # first arg is $next if the hook matches around
288 2243 100   2243   370478 shift->() if $post_hook =~ /around/;
289 2243 100       63785 DB::finish_profile() if ! $disable;
290 2243         4011 $log->debug( 'finished NYTProf' );
291 85         1441 });
292              
293             $app->routes->get('/nytprof/profiles/:file'
294             => [file => qr/nytprof_out_\d+_\d+.*/]
295             => sub {
296 2     2   1192 $log->debug( "generating profile for $nytprofhtml_path" );
297 2         18 _generate_profile(@_,$prof_dir,$nytprofhtml_path)
298             }
299 85         832 );
300              
301 85     2   37060 $app->routes->get('/nytprof' => sub { _list_profiles(@_,$prof_sub_dir) });
  2         1256  
302             }
303              
304             sub _list_profiles {
305 2     2   5 my $self = shift;
306 2         5 my $prof_dir = shift;
307              
308 2         5 my @profiles = _profiles($prof_dir);
309 2         12 $self->app->log->debug( scalar( @profiles ) . ' profiles found' );
310              
311             # could use epl here, but users might be using a different Template engine
312 2 100       51 my $list = @profiles
313             ? '

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

    '
314             : '

No profiles found

';
315              
316 2         6 foreach (@profiles) {
317 3         13 $list .= qq{
318            
  • 319             $_->{label}
    320             (PID $_->{pid}, $_->{created}, $_->{duration})
    321            
    322             };
    323             }
    324              
    325 2 100       11 $list .= '' if $list !~ /No profiles found/;
    326              
    327 2         9 my $html = <<"EndOfEp";
    328            
    329            
    330             NYTProf profile run list
    331            
    332            
    333            

    Profile run list

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