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 | |||||||
9 | |||||||
10 | |||||||
11 | =head1 VERSION | ||||||
12 | |||||||
13 | 0.22 | ||||||
14 | |||||||
15 | =head1 DESCRIPTION | ||||||
16 | |||||||
17 | This plugin enables L |
||||||
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 | |
||||||
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 |