File Coverage

blib/lib/Devel/MojoProf.pm
Criterion Covered Total %
statement 85 89 95.5
branch 32 46 69.5
condition 7 13 53.8
subroutine 24 27 88.8
pod 4 4 100.0
total 152 179 84.9


line stmt bran cond sub pod time code
1             package Devel::MojoProf;
2 6     6   1180539 use Mojo::Base -base;
  6         64  
  6         37  
3              
4 6     6   3858 use Class::Method::Modifiers 'install_modifier';
  6         8973  
  6         496  
5 6     6   2581 use Devel::MojoProf::Reporter;
  6         23  
  6         54  
6 6     6   3065 use Mojo::Loader 'load_class';
  6         25999  
  6         370  
7 6     6   43 use Scalar::Util 'blessed';
  6         13  
  6         259  
8 6     6   35 use Time::HiRes qw(gettimeofday tv_interval);
  6         13  
  6         56  
9              
10 6   50 6   1068 use constant CALLER => $ENV{DEVEL_MOJOPROF_CALLER} // 1;
  6         14  
  6         10718  
11              
12             our $VERSION = '0.04';
13              
14             # Required for "perl -d:MojoProf ..."
15       0     DB->can('DB') or *DB::DB = sub { };
16              
17             sub add_profiling_for {
18 8 50   8 1 1003 my $params = ref $_[-1] eq 'HASH' ? pop : {};
19 8         26 my $self = _instance(shift);
20 8 100       82 return $self->tap($self->can("_add_profiling_for_$_[0]")) if @_ == 1;
21              
22 2 50       7 return unless my $target = $self->_ensure_loaded(shift);
23 2         231841 while (my $method = shift) {
24 2 50       12 next if $self->{installed}{$target}{$method}++;
25 2 50       13 $self->_add_profiling_for_method($target, $method, ref $_[0] ? shift : undef, $params);
26             }
27              
28 2         803 return $self;
29             }
30              
31             sub import {
32 5     5   48 my $class = shift;
33 5 50       26 my @flags = @_ ? @_ : qw(-mysql -pg -redis -sqlite -ua);
34              
35 5         12 $class->add_profiling_for($_) for map { s!^-!!; $_ } @flags;
  5         28  
  5         28  
36             }
37              
38             sub new {
39 6     6 1 50 my $self = shift->SUPER::new(@_);
40 6   33     119 $self->reporter($self->{reporter} || Devel::MojoProf::Reporter->new);
41 6         20 $self;
42             }
43              
44             sub reporter {
45 11     11 1 4503 my $self = shift;
46 11 100       68 return $self->{reporter} unless @_;
47 7 100       53 $self->{reporter} = blessed $_[0] ? $_[0] : Devel::MojoProf::Reporter->new->handler($_[0]);
48 7         44 return $self;
49             }
50              
51 7     7 1 1382 sub singleton { state $self = __PACKAGE__->new }
52              
53             sub _add_profiling_for_method {
54 2     2   8 my ($self, $target, $method, $make_message, $params) = @_;
55              
56 2         43 my %params = (ignore_caller => qr{^$target}, %$params);
57 2   50 0   11 $make_message ||= sub { shift; join ' ', @_ };
  0         0  
  0         0  
58              
59             install_modifier $target => around => $method => sub {
60 13     13   10585 my ($orig, @args) = @_;
61 13         32 my $wantarray = wantarray;
62 13         66 my %report = (class => $target, method => $method);
63              
64 13         59 _add_caller_to_report(\%params, \%report) if CALLER;
65              
66 13 100       64 my $cb = ref $args[-1] eq 'CODE' ? pop @args : undef;
67 2         132210 push @args, sub { $self->_report_for(\%report, $make_message->(@args)); $cb->(@_) }
  2         22  
68 13 100       54 if $cb;
69              
70 13         65 $report{t0} = [gettimeofday];
71 13 50       67 my @res = $wantarray ? $orig->(@args) : (scalar $orig->(@args));
72              
73 13 100 33     545558 if ($cb) {
    50          
74 2         7 1; # do nothing
75             }
76             elsif (blessed $res[0] and $res[0]->isa('Mojo::Promise')) {
77 0         0 $res[0]->finally($self->_report_for(\%report, $make_message->(@args)));
78             }
79             else {
80 11         58 $self->_report_for(\%report, $make_message->(@args));
81             }
82              
83 13 50       203 return $wantarray ? @res : $res[0];
84 2         22 };
85             }
86              
87             sub _add_caller_to_report {
88 13     13   31 my ($params, $report) = @_;
89              
90 13         27 my $i = 0;
91 13         218 while (my @caller = caller($i++)) {
92 66 100 100     863 next if $caller[0] eq 'Devel::MojoProf' or $caller[0] =~ $params->{ignore_caller};
93 13         85 @$report{qw(file line)} = @caller[1, 2];
94 13         40 last;
95             }
96             }
97              
98             sub _add_profiling_for_pg {
99 1     1   8 my $self = shift;
100 1 50       3 $self->add_profiling_for('Mojo::Pg::Database', query => \&_make_desc_for_db, query_p => \&_make_desc_for_db)
101             if $self->_ensure_loaded('Mojo::Pg', 1);
102             }
103              
104             sub _add_profiling_for_mysql {
105 1     1   12 my $self = shift;
106 1 50       3 $self->add_profiling_for('Mojo::mysql::Database', query => \&_make_desc_for_db, query_p => \&_make_desc_for_db)
107             if $self->_ensure_loaded('Mojo::mysql', 1);
108             }
109              
110             sub _add_profiling_for_redis {
111 1     1   7 my $self = shift;
112 1 50       3 $self->add_profiling_for('Mojo::Redis::Connection', 'write_p', {ignore_caller => qr{^Mojo::Redis}})
113             if $self->_ensure_loaded('Mojo::Redis', 1);
114             }
115              
116             sub _add_profiling_for_sqlite {
117 1     1   9 my $self = shift;
118 1 50       4 $self->add_profiling_for('Mojo::SQLite::Database', query => \&_make_desc_for_db)
119             if $self->_ensure_loaded('Mojo::SQLite', 1);
120             }
121              
122             sub _add_profiling_for_ua {
123 2     2   23 shift->add_profiling_for('Mojo::UserAgent', start => \&_make_desc_for_ua);
124             }
125              
126             sub _ensure_loaded {
127 7     7   108 my ($self, $target, $no_warn) = @_;
128 7 100       32 return $target unless my $e = load_class $target;
129 5 50       1721 die "[Devel::MojoProf] Could not load $target: $e" if ref $e;
130 5 50       15 warn "[Devel::MojoProf] Could not find module $target\n" unless $no_warn;
131 5         22 return;
132             }
133              
134 8 100   8   39 sub _instance { ref $_[0] ? $_[0] : shift->singleton }
135              
136 0     0   0 sub _make_desc_for_db { $_[1] }
137 13     13   55 sub _make_desc_for_ua { sprintf '%s %s', $_[1]->req->method, $_[1]->req->url->to_abs }
138              
139             sub _report_for {
140 13     13   6142 my ($self, $report, $message) = @_;
141 13         73 @$report{qw(elapsed message)} = (tv_interval($report->{t0}), $message);
142 13         385 $self->{reporter}->report($report, $self);
143             }
144              
145             1;
146              
147             =encoding utf8
148              
149             =head1 NAME
150              
151             Devel::MojoProf - Profile blocking, non-blocking a promise based Mojolicious APIs
152              
153             =head1 SYNOPSIS
154              
155             $ perl -d:MojoProf myapp.pl
156             $ perl -d:MojoProf -e'Mojo::UserAgent->new->get("https://mojolicious.org")'
157             $ DEVEL_MOJOPROF_OUT_CSV=1 perl -d:MojoProf myapp.pl
158              
159             See L for how C works.
160              
161             =head1 DESCRIPTION
162              
163             L can add profiling output for blocking, non-blocking and
164             promise based methods. It can be customized to log however you want, but the
165             default is to print a line like the one below to STDERR:
166              
167             0.00038ms [Mojo::Pg::Database::query_p] SELECT 1 as whatever at path/to/app.pl line 23
168              
169             =head1 ATTRIBUTES
170              
171             =head2 reporter
172              
173             my $obj = $prof->reporter;
174             my $prof = $prof->reporter($reporter_class->new);
175              
176             Holds a reporter object that is capable of creating reports by the measurements
177             done by C<$prof>. Holds by default an instance of L.
178              
179             =head1 METHODS
180              
181             =head2 add_profiling_for
182              
183             my $prof = $prof->add_profiling_for($moniker);
184             my $prof = $prof->add_profiling_for($class => $method1, $method2, ...);
185             my $prof = $prof->add_profiling_for($class => $method1 => $make_message, ...);
186             my $prof = $prof->add_profiling_for($class => $method1 => $make_message, ..., \%params);
187             my $prof = $prof->add_profiling_for($class => $method1 => $make_message, ..., \%params);
188             my $prof = Devel::MojoProf->add_profiling_for(...);
189              
190             Used to add profiling for either a C<$moniker> (short module identifier) or a
191             class and method. This method can also be called as a class method.
192              
193             The supported C<$moniker> are for now "mysql", "pg", "redis", "sqlite" and
194             "ua". See L for more details.
195              
196             It is also possible to manually add support for other custom modules. Here is
197             an example:
198              
199             $prof->add_profiling_for("My::Cool::Class", "get_stuff" => sub {
200             my ($my_cool_obj, @args) = @_;
201             return "This will be the 'message' part in the report";
202             });
203              
204             The CODE ref passed in will get all the arguments that the C method
205             gets, and the return value should be a string that becomes the C part
206             in the C<$report> hash-ref passed to the L.
207              
208             C<%params> is optional and can have the following keys:
209              
210             =over 2
211              
212             =item * ignore_caller
213              
214             Defaults to a regex holding the C<$class>, but can set to any class that you
215             want to skip to generate the C key for the L method.
216              
217             =back
218              
219             =head2 import
220              
221             use Devel::MojoProf (); # disable auto-detect
222             use Devel::MojoProf; # All of the modules from the list below
223             use Devel::MojoProf -mysql;
224             use Devel::MojoProf -pg;
225             use Devel::MojoProf -redis;
226             use Devel::MojoProf -sqlite;
227             use Devel::MojoProf -ua;
228             use Devel::MojoProf -pg, -redis, -ua; # Load multiple
229              
230             Used to automatically L know modules. Currently supported
231             modules are L, L, L, L and
232             L.
233              
234             Please submit a PR or create an issue if you think more modules should be
235             supported at L.
236              
237             =head2 singleton
238              
239             my $prof = Devel::MojoProf->singleton;
240              
241             Used to retrive the singleton object that is used by L when
242             called as a class method.
243              
244             =head1 AUTHOR
245              
246             Jan Henning Thorsen
247              
248             =head1 COPYRIGHT AND LICENSE
249              
250             Copyright (C) 2018, Jan Henning Thorsen.
251              
252             This program is free software, you can redistribute it and/or modify it
253             under the terms of the Artistic License version 2.0.
254              
255             =head1 SEE ALSO
256              
257             This module is inspired by L.
258              
259             =cut