File Coverage

blib/lib/Mojolicious/Plugin/LeakTracker.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1 1     1   789 use strict;
  1         1  
  1         35  
2 1     1   4 use warnings;
  1         2  
  1         56  
3             package Mojolicious::Plugin::LeakTracker;
4             {
5             $Mojolicious::Plugin::LeakTracker::VERSION = '1.00';
6             }
7             # ABSTRACT: Helps you track down memory leaks in your code
8 1     1   703 use Mojo::Base 'Mojolicious::Plugin';
  1         9668  
  1         6  
9              
10 1     1   1370 use Devel::Events::Filter::Stamp;
  0            
  0            
11             use Devel::Events::Filter::RemoveFields;
12             use Devel::Events::Filter::Stringify;
13              
14             use Devel::Events::Handler::Log::Memory;
15             use Devel::Events::Handler::Multiplex;
16             use Devel::Events::Handler::ObjectTracker;
17             use Devel::Events::Generator::Objects;
18              
19             use Devel::Cycle ();
20             use Devel::Size ();
21             use Data::Dumper ();
22              
23             sub register {
24             my $self = shift;
25             my $app = shift;
26             my $_args = shift || {};
27             my %args = ( loglevel => undef, ignore_mode => 0, %{$_args} );
28              
29             # if app is in production mode and ignore_mode is not 1, bail out
30             if($app->mode eq 'production' && $args{'ignore_mode'} < 1) {
31             $app->log->info('Not enabling LeakTracker plugin, you are in production mode!');
32             return 0;
33             }
34              
35             my $loglevel = $args{'loglevel'} || ($app->mode eq 'production') ? 'info' : 'debug'; # yes, bad
36             $app->attr(lt_loglevel => sub { $loglevel });
37              
38             $app->attr($_ => undef) for(qw/devel_events_log devel_events_multiplexer devel_events_filters devel_events_generator/);
39              
40             $app->helper(lt_log => sub {
41             my $self = shift;
42             my $l = $app->lt_loglevel;
43              
44             $app->log->$l(sprintf('[LeakTracker] [%d]: %s', $$, join(' ', @_)));
45             });
46              
47             $app->helper(create_devel_events_object_tracker => sub {
48             return Devel::Events::Handler::ObjectTracker->new();
49             });
50              
51             my $log = $self->create_devel_events_log;
52             my $filtered_log = $self->create_devel_events_log_filter($log);
53             my $multiplexer = $self->create_devel_events_multiplexer;
54             my $filters = $self->create_devel_events_filter_chain($multiplexer);
55             my $generator = $self->create_devel_events_objects_event_generator($filters);
56              
57             $app->devel_events_log($log);
58             $app->devel_events_multiplexer($multiplexer);
59             $app->devel_events_filters($filters);
60             $app->devel_events_generator($generator);
61              
62             $app->hook(after_build_tx => sub {
63             my ($tx, $app) = (@_);
64              
65             $tx->on(request => sub {
66             my $tx = shift;
67              
68             my $tracker = $app->create_devel_events_object_tracker;
69             $tx->{lt_tracker} = $tracker;
70             $app->devel_events_multiplexer->add_handler($tracker);
71              
72             my $generator = $app->devel_events_generator;
73             $tx->{lt_generator} = $generator;
74             $generator->enable;
75             });
76              
77             $tx->on(finish => sub {
78             my $tx = shift;
79              
80             my $generator = $tx->{lt_generator};
81             $generator->disable;
82              
83             my $tracker = $tx->{lt_tracker};
84             if(my $n_leaked = scalar(keys(%{$tracker->live_objects}))) {
85             $self->dump_leak_info($app => $tx->{lt_tracker});
86             }
87             });
88             });
89             }
90              
91             sub dump_leak_info {
92             my $self = shift;
93             my $app = shift;
94             my $tracker = shift;
95              
96             my $live_objects = $tracker->live_objects;
97             my @leaks = map {
98             my $object = $_->{object};
99              
100             +{
101             %$_,
102             size => Devel::Size::total_size($object),
103             class => ref $object,
104             }
105             } values %$live_objects;
106              
107             $app->lt_log('Request finished with ', scalar(keys(%$live_objects)), ' live objects');
108              
109             foreach my $leak (@leaks) {
110             $self->dump_single_leak($app => $leak);
111             }
112             }
113              
114             sub dump_single_leak {
115             my $self = shift;
116             my $app = shift;
117             my $leak = shift;
118              
119             my $obj = $leak->{object};
120             my $cycles = $self->_cycle_report($obj);
121              
122             $app->lt_log(sprintf("class: %s\n\tsize: %d\n\tfile: %s\n\tline: %d\n\tpackage: %s\n\tCycle report:\n\t\t%s", $leak->{class}, $leak->{size}, $leak->{file}, $leak->{line}, $leak->{package}, $cycles));
123             }
124              
125             my %shortnames;
126             my $new_shortname = "A";
127              
128             sub _ref_shortname {
129             my $ref = shift;
130             my $refstr = "$ref";
131             my $refdisp = $shortnames{ $refstr };
132             if ( !$refdisp ) {
133             my $sigil = ref($ref) . " ";
134             $sigil = '%' if $sigil eq "HASH ";
135             $sigil = '@' if $sigil eq "ARRAY ";
136             $sigil = '$' if $sigil eq "REF ";
137             $sigil = '&' if $sigil eq "CODE ";
138             $refdisp = $shortnames{ $refstr } = $sigil . $new_shortname++;
139             }
140              
141             return $refdisp;
142             }
143              
144             sub _cycle_report {
145             my ( $self, $obj ) = @_;
146              
147             my @diags;
148             my $cycle_no;
149              
150             # Callback function that is called once for each memory cycle found.
151             my $callback = sub {
152             my $path = shift;
153             $cycle_no++;
154             push( @diags, "Cycle #$cycle_no" );
155             foreach (@$path) {
156             my ($type,$index,$ref,$value) = @$_;
157              
158             my $str = 'Unknown! This should never happen!';
159             my $refdisp = _ref_shortname( $ref );
160             my $valuedisp = _ref_shortname( $value );
161              
162             $str = sprintf( ' %s => %s', $refdisp, $valuedisp ) if $type eq 'SCALAR';
163             $str = sprintf( ' %s => %s', "${refdisp}->[$index]", $valuedisp ) if $type eq 'ARRAY';
164             $str = sprintf( ' %s => %s', "${refdisp}->{$index}", $valuedisp ) if $type eq 'HASH';
165             $str = sprintf( ' closure %s => %s', "${refdisp}, $index", $valuedisp ) if $type eq 'CODE';
166              
167             push( @diags, $str );
168             }
169             };
170              
171             Devel::Cycle::find_cycle( $obj, $callback );
172              
173             return (wantarray) ? @diags : join("\n", @diags);
174             }
175              
176             ###
177             ## utility stuff below
178             #
179              
180             sub create_devel_events_log {
181             return Devel::Events::Handler::Log::Memory->new();
182             }
183              
184             sub create_devel_events_log_filter {
185             my $self = shift;
186             my $log = shift;
187              
188             return Devel::Events::Filter::Stringify->new(handler => $log);
189             }
190              
191             sub create_devel_events_multiplexer {
192             return Devel::Events::Handler::Multiplex->new();
193             }
194              
195             sub create_devel_events_objects_event_generator {
196             my $self = shift;
197             my $filters = shift;
198              
199             return Devel::Events::Generator::Objects->new(handler => $filters);
200             }
201              
202             sub create_devel_events_filter_chain {
203             my $self = shift;
204             my $multiplexer = shift;
205              
206             return Devel::Events::Filter::Stamp->new(
207             handler => Devel::Events::Filter::RemoveFields->new(
208             fields => [qw/generator/],
209             handler => $multiplexer,
210             )
211             );
212             }
213              
214              
215              
216              
217             1;
218              
219              
220             =pod
221              
222             =head1 NAME
223              
224             Mojolicious::Plugin::LeakTracker - Helps you track down memory leaks in your code
225              
226             =head1 VERSION
227              
228             version 1.00
229              
230             =head1 SYNOPSIS
231              
232             $app->plugin('leak_tracker', \%options);
233              
234             =head1 NAME
235              
236             Mojolicious::Plugin::LeakTracker - Helps you track down memory leaks and circular references in your Mojolicious app
237              
238             =head1 PLUGIN OPTIONS
239              
240             =head2 ignore_mode
241              
242             When this is set to a true value, modes are ignored. By default the plugin will not install it's hooks or set up the tracking environment if you are in production mode. Setting ignore_mode to a true value will make the plugin run in production mode regardless.
243              
244             =head2 loglevel
245              
246             Can be set to any valid log method name applicable to $app->log (e.g. debug to log using $app->log->debug). By default set to 'debug' for development mode, and 'info' for production mode. Here as a way to override the default behaviour.
247              
248             =head1 INTERPRETING THE RESULTS
249              
250             At the beginning of each transaction (the C<after_build_tx> hook), an event handler is attached to the C<request> and C<finish> events that the transaction emits. Tracking of leaks is done between these two stages.
251              
252             If a transaction finishes, and there are still live objects present, this is reported in the app log. Each live object's class, package, file, and size are dumped, as well as a cycle report; the cycle report lists circular references.
253              
254             Note that this plugin is not a magic CSI bullet that will point you straight to the source of a leak, but it is a way to get a better idea of where to look and what may potentially be causing them.
255              
256             =head1 KNOWN ISSUES
257              
258             =over 4
259              
260             =item * This plugin was smacked together in a hurry, and has a lot of dead/loose/useless code floating around in it.
261              
262             =item * Cyclic references may be falsely reported for modules that implement their own cyclic-reference-busting logic for when they are destroyed; also things like caching, and lazy-loaded objects may cause a false report.
263              
264             =back
265              
266             =head1 AUTHOR
267              
268             Ben van Staveren C<<madcat@cpan.org>>
269              
270             =head1 BUG REPORTING/CONTRIBUTING
271              
272             Please report any bugs or feature requests through the web interface at L<https://github.com/benvanstaveren/Mojolicious-Plugin-LeakTracker/issues>.
273             You can fork my Git repository at L<https://github.com/benvanstaveren/Mojolicious-Plugin-LeakTracker/> if you want to make changes or supply me with patches.
274              
275             =head1 ACKNOWLEDGMENTS
276              
277             Based in part on L<Catalyst::Plugin::LeakTracker>, with some additional beating to make it fit Mojolicious' request handling.
278              
279             =head1 AUTHOR
280              
281             Ben van Staveren <madcat@cpan.org>
282              
283             =head1 COPYRIGHT AND LICENSE
284              
285             This software is copyright (c) 2013 by Ben van Staveren.
286              
287             This is free software; you can redistribute it and/or modify it under
288             the same terms as the Perl 5 programming language system itself.
289              
290             =cut
291              
292              
293             __END__