File Coverage

blib/lib/Catalyst/Controller/LeakTracker.pm
Criterion Covered Total %
statement 3 3 100.0
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 4 4 100.0


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3             package Catalyst::Controller::LeakTracker;
4 1     1   37734 use parent qw(Catalyst::Controller);
  1         376  
  1         6  
5              
6             use Moose;
7              
8             our $VERSION = "0.08";
9              
10             use Data::Dumper ();
11             use Devel::Cycle ();
12             use Devel::Size ();
13             use Tie::RefHash::Weak ();
14             use YAML::XS ();
15             use Scalar::Util qw(weaken);
16              
17             use namespace::clean -except => "meta";
18              
19             {
20             package Catalyst::Controller::LeakTracker::Template;
21              
22             use Template::Declare::Tags 'HTML'; # conflicts with Moose
23             }
24              
25             my $size_of_empty_array = Devel::Size::total_size([]);
26              
27             sub end : Private { } # don't get Root's one
28              
29             sub order_by {
30             my ( $self, $objects, $field, $mode ) = @_;
31             return () unless @$objects;
32              
33             my $order_by_meth = {
34             num => sub {
35             sort { $a->{$field} <=> $b->{$field} } @{$_[0]}
36             },
37             num_desc => sub {
38             sort { $b->{$field} <=> $a->{$field} } @{$_[0]}
39             },
40             lex => sub {
41             sort { $a->{$field} cmp $b->{$field} } @{$_[0]}
42             },
43             lex_desc => sub {
44             sort { $b->{$field} cmp $a->{$field} } @{$_[0]}
45             },
46             };
47             my $order_by_map;
48             if ( $mode && $mode eq 'desc' ) {
49             $order_by_map = {
50             ( map {
51             $_ => $order_by_meth->{'num_desc'}
52             } qw/id time leaks size/ ),
53             ( map {
54             $_ => $order_by_meth->{'lex_desc'}
55             } qw/action uri class/ ),
56             };
57             }
58             else {
59             $order_by_map = {
60             ( map {
61             $_ => $order_by_meth->{'num'}
62             } qw/id time leaks size/ ),
63             ( map {
64             $_ => $order_by_meth->{'lex'}
65             } qw/action uri class/ ),
66             };
67             }
68             if ( my $meth = $order_by_map->{$field} ) {
69             return $meth->($objects);
70             }
71             else {
72             return @$objects;
73             }
74             }
75              
76             sub list_requests : Chained {
77             my ( $self, $c ) = @_;
78             my $params = $c->req->params;
79              
80             my $only_leaking = !$params->{'all'};
81              
82             my $log = $c->devel_events_log; # FIXME used for repping, switch to exported when that api is available.
83              
84             my @request_ids = $c->get_all_request_ids;
85              
86             pop @request_ids; # current request
87              
88             my @requests;
89              
90             foreach my $request_id ( @request_ids ) {
91             my $tracker = $c->get_object_tracker_by_id($request_id) || next;
92             my $leaked = $tracker->live_objects;
93              
94             my $n_leaks = scalar( keys %$leaked );
95              
96             next if $only_leaking and $n_leaks == 0;
97              
98             my @events = $c->get_request_events($request_id);
99              
100             my ( undef, %req ) = @{ $events[0] };
101              
102             my (undef, %dispatch) = $log->matcher->first( match => "dispatch", events => \@events );
103             scalar keys %dispatch or next;
104              
105             my $size = ( Devel::Size::total_size([ keys %$leaked ]) - $size_of_empty_array );
106              
107             push @requests, {
108             id => $request_id,
109             time => $req{time},
110             uri => $dispatch{uri},
111             action => $dispatch{action_name},
112             leaks => $n_leaks,
113             size => $size,
114             }
115             }
116             my ( $order_by, $order_by_desc )
117             = map { $params->{$_} } qw/order_by order_by_desc/;
118             @requests = $self->order_by(
119             \@requests,
120             $order_by || 'id',
121             $order_by_desc ? 'desc' : 'asc',
122             ) if @requests;
123              
124             my @fields = qw(id time action leaks size uri);
125              
126             my %fmt = map { $_ => sub { $_[0] } } @fields;
127              
128             $fmt{id} = sub {
129             package Catalyst::Controller::LeakTracker::Template;
130             my $id = shift;
131             return a { attr { href => $c->uri_for( $self->action_for("request"), $id ) } $id };
132             };
133              
134             $fmt{time} = sub {
135             scalar localtime(int(shift));
136             };
137              
138             $fmt{size} = sub {
139             use Number::Bytes::Human;
140             my $h = Number::Bytes::Human->new;
141             $h->set_options(zero => '-');
142             $h->format(shift);
143             };
144              
145             $c->response->body( "" . do { package Catalyst::Controller::LeakTracker::Template;
146             html {
147             head { }
148             body {
149             table {
150             attr { border => 1, style => "border: 1px solid black; padding: 0.3em" };
151             row {
152             map {
153             my $desc = ( $order_by_desc || ( $order_by || '') ne $_) ? 0 : 1;
154             th {
155             a {
156             attr {
157             href => $c->req->uri_with({
158             order_by => $_,
159             order_by_desc => $desc,
160             })
161             } $_
162             }
163             }
164             } @fields
165             };
166              
167             foreach my $req ( @requests ) {
168             row {
169             foreach my $field (@fields) {
170             my $formatter = $fmt{$field};
171              
172             cell {
173             attr { style => "padding: 0.2em" }
174             $formatter->( $req->{$field} );
175             }
176             }
177             }
178             }
179             }
180             }
181             }
182             });
183              
184             $c->res->content_type("text/html");
185             }
186              
187             sub leak : Chained {
188             my ( $self, $c, $request_id, $id ) = @_;
189              
190             my $obj_entry = $c->get_object_entry_by_id($request_id, $id) || die "No such object: $id";
191              
192             my $obj = $obj_entry->{object};
193              
194             my @stack = $c->generate_stack_for_event( $request_id, $id );
195              
196             @stack = reverse @stack[2..$#stack]; # skip _DISPATCH and _ACTION
197              
198             my $stack_dump = "$obj_entry->{file} line $obj_entry->{line} (package $obj_entry->{package})\n"
199             . join("\n", map {" in action $_->{action_name} $obj_entry->{file} line $obj_entry->{line} (controller $_->{class})" } @stack);
200              
201             local $Data::Dumper::Maxdepth = $c->request->param("maxdepth") || 0;
202             my $obj_dump = Data::Dumper::Dumper($obj);
203              
204             my $cycles = $self->_cycle_report($obj);
205              
206             $c->response->content_type("text/html");
207             $c->response->body( "" . do { package Catalyst::Controller::LeakTracker::Template;
208             html {
209             head { }
210             body {
211             h1 { "Stack" }
212             pre { $stack_dump }
213             h1 { "Cycles" }
214             pre { $cycles }
215             h1 { "Object" }
216             pre { $obj_dump }
217             }
218             }
219             });
220             }
221              
222             # stolen from Test::Memory::Cycle
223              
224             my %shortnames;
225             my $new_shortname = "A";
226              
227             sub _ref_shortname {
228             my $ref = shift;
229             my $refstr = "$ref";
230             my $refdisp = $shortnames{ $refstr };
231             if ( !$refdisp ) {
232             my $sigil = ref($ref) . " ";
233             $sigil = '%' if $sigil eq "HASH ";
234             $sigil = '@' if $sigil eq "ARRAY ";
235             $sigil = '$' if $sigil eq "REF ";
236             $sigil = '&' if $sigil eq "CODE ";
237             $refdisp = $shortnames{ $refstr } = $sigil . $new_shortname++;
238             }
239              
240             return $refdisp;
241             }
242              
243             sub _cycle_report {
244             my ( $self, $obj ) = @_;
245              
246             my @diags;
247             my $cycle_no;
248              
249             # Callback function that is called once for each memory cycle found.
250             my $callback = sub {
251             my $path = shift;
252             $cycle_no++;
253             push( @diags, "Cycle #$cycle_no" );
254             foreach (@$path) {
255             my ($type,$index,$ref,$value) = @$_;
256              
257             my $str = 'Unknown! This should never happen!';
258             my $refdisp = _ref_shortname( $ref );
259             my $valuedisp = _ref_shortname( $value );
260              
261             $str = sprintf( ' %s => %s', $refdisp, $valuedisp ) if $type eq 'SCALAR';
262             $str = sprintf( ' %s => %s', "${refdisp}->[$index]", $valuedisp ) if $type eq 'ARRAY';
263             $str = sprintf( ' %s => %s', "${refdisp}->{$index}", $valuedisp ) if $type eq 'HASH';
264             $str = sprintf( ' closure %s => %s', "${refdisp}, $index", $valuedisp ) if $type eq 'CODE';
265              
266             push( @diags, $str );
267             }
268             };
269              
270             Devel::Cycle::find_cycle( $obj, $callback );
271              
272             return join("\n", @diags);
273             }
274              
275              
276              
277             sub request : Chained {
278             my ( $self, $c, $request_id ) = @_;
279             my $params = $c->req->params;
280              
281             my $log = $params->{'event_log'};
282              
283             my $log_output = $log && YAML::XS::Dump($c->get_request_events($request_id));
284              
285             my $tracker = $c->get_object_tracker_by_id($request_id);
286             my $live_objects = $tracker->live_objects;
287              
288             my @leaks = map {
289             my $object = $_->{object};
290              
291             +{
292             %$_,
293             size => Devel::Size::total_size($object),
294             class => ref $object,
295             }
296             } values %$live_objects;
297             my ( $order_by, $order_by_desc )
298             = map { $params->{$_} } qw/order_by order_by_desc/;
299             @leaks = $self->order_by(
300             \@leaks,
301             $order_by || 'id',
302             $order_by_desc ? 'desc' : 'asc',
303             ) if @leaks;
304              
305              
306             my @fields = qw/id size class/;
307              
308             my %fmt = map { $_ => sub { $_[0] } } @fields;
309              
310             $fmt{id} = sub {
311             package Catalyst::Controller::LeakTracker::Template;
312             my $id = shift;
313             return a { attr { href => $c->uri_for( $self->action_for("leak"), $request_id, $id ) } $id };
314             };
315              
316             $fmt{size} = sub {
317             use Number::Bytes::Human;
318             my $h = Number::Bytes::Human->new;
319             $h->set_options(zero => '-');
320             $h->format(shift);
321             };
322              
323             my $leaks = sub {
324             package Catalyst::Controller::LeakTracker::Template;
325             table {
326             attr { border => "1", style => "border: 1px solid black; padding: 0.3em" }
327             row {
328             map {
329             my $desc = ( $order_by_desc || ( $order_by || '') ne $_) ? 0 : 1;
330             th {
331             attr {
332             style => "padding: 0.2em",
333             };
334             a {
335             attr {
336             href => $c->req->uri_with({
337             order_by => $_,
338             order_by_desc => $desc,
339             })
340             } $_
341             };
342             }
343             } @fields
344             };
345              
346             foreach my $leak ( @leaks ) {
347             row {
348             foreach my $field ( @fields ) {
349             my $formatter = $fmt{$field};
350              
351             cell {
352             attr { style => "padding: 0.2em" }
353             $formatter->($leak->{$field});
354             }
355             }
356             }
357             }
358             }
359             };
360              
361             $c->res->content_type("text/html");
362              
363             $c->res->body( "" . do { package Catalyst::Controller::LeakTracker::Template;
364             html {
365             head { }
366             body {
367             h1 { "Leaks" }
368             pre { $leaks->() }
369              
370             $log ? (
371             h1 { "Events" }
372             pre { $log_output }
373             ) : ()
374             }
375             }
376             });
377             }
378              
379             sub make_leak : Chained {
380             my ( $self, $c, $n ) = @_;
381              
382             $n ||= 1;
383              
384             $n = 300 if $n > 300;
385              
386             for ( 1 .. $n ) {
387             my $object = bless {}, "class::a";
388             $object->{foo}{self} = $object;
389             }
390              
391             my $object2 = bless {}, "class::b";
392             $object2->{foo}{self} = $object2;
393             weaken($object2->{foo}{self});
394              
395             my $object3 = bless [], "class::c";
396             push @$object3, $object3, map { [ 1 .. $n ] } 1 .. $n;
397              
398             $c->res->body("it leaks " . ( $n + 1 ) . " objects");
399             }
400              
401             __PACKAGE__;
402              
403             __END__
404              
405             =pod
406              
407             =head1 NAME
408              
409             Catalyst::Controller::LeakTracker - Inspect leaks found by L<Catalyst::Plugin::LeakTracker>
410              
411             =head1 SYNOPSIS
412              
413             # in MyApp.pm
414              
415             package MyApp;
416              
417             use Catalyst qw(
418             LeakTracker
419             );
420              
421             #### in SomeController.pm
422              
423             package MyApp::Controller::Leaks;
424             use Moose;
425              
426             use parent qw(Catalyst::Controller::LeakTracker);
427              
428             sub index :Path :Args(0) {
429             my ( $self, $c ) = @_;
430             $c->forward("list_requests"); # redirect to request listing view
431             }
432              
433             =head1 DESCRIPTION
434              
435             This controller uses L<Catalyst::Controller::LeakTracker> to display leak info
436             on a per request basis.
437              
438             =head1 ACTIONS
439              
440             =over 4
441              
442             =item list_requests
443              
444             List the leaking requests this process has handled so far.
445              
446             If the C<all> parameter is set to a true value, then all requests (even non
447             leaking ones) are listed.
448              
449             =item request $request_id
450              
451             Detail the leaks for a given request, and also dump the event log for that request.
452              
453             =item object $request_id $event_id
454              
455             Detail the object created in $event_id.
456              
457             Displays a stack dump, a L<Devel::Cycle> report, and a L<Data::Dumper> output.
458              
459             If the C<maxdepth> param is set, C<$Data::Dumper::Maxdepth> is set to that value.
460              
461             =item make_leak [ $how_many ]
462              
463             Artificially leak some objects, to make sure everything is working properly
464              
465             =back
466              
467             =head1 CAVEATS
468              
469             In forking environments each child will have its own leak tracking. To avoid
470             confusion run your apps under the development server or temporarily configure
471             fastcgi or whatever to only use one child process.
472              
473             =head1 TODO
474              
475             This is yucky example code. But it's useful. Patches welcome.
476              
477             =over 4
478              
479             =item L<Template::Declare>
480              
481             Instead of yucky HTML strings
482              
483             =item CSS
484              
485             I can't do that well, I didn't bother trying
486              
487             =item Nicer displays
488              
489             <pre> ... </pre>
490              
491             Only goes so far...
492              
493             The event log is in most dire need for this.
494              
495             =item Filtering, etc
496              
497             Of objects, requests, etc. Javascript or serverside, it doesn't matter.
498              
499             =item JSON/YAML/XML feeds
500              
501             Maybe it's useful for someone.
502              
503             =back
504              
505             =head1 MINI-TUTORIAL
506              
507             =head2 Why use LeakTracker?
508              
509             You have a Catalyst application that is consuming more and more
510             memory over time. You would like to find out what classes are
511             involved and where you may have cyclic references.
512              
513             =head2 How to use LeakTracker?
514              
515             Once you've plugged LeakTracker into your Catalyst application
516             (see L</SYNOPSIS>), then you can easily get statistics via
517             Catalyst::Controller::LeakTracker. Just create a new controller exclusively
518             for reporting on the objects that are not being garbage collected.
519             Here is how:
520              
521             package MyAss::Controller::Leaks;
522            
523             sub BEGIN {
524             use Moose;
525             extends 'Catalyst::Controller::LeakTracker';
526             }
527            
528             # redirect leaks/ to the report about memory consumed by each request
529             sub index : Path : Args(0) {
530             my ( $self, $c ) = @_;
531             $c->forward("list_requests");
532             }
533            
534             1
535            
536             In effect, the controller above turns the URI C<$c.request.base/leaks>
537             into a report on the objects that still have references to them, and
538             thus consuming memory.
539              
540             =head2 How to Interpret the Results?
541              
542             The results found at B<leaks/> are I<per request>. The results include
543             the Catalyst actions requested and how much memory each consumed. One can
544             "drill-down" on the request ID and get a report of all objects that the request
545             has left lingering about. It's tits, try it out for yourself.
546              
547             =head2 When to Not Use LeakTracker?
548              
549             In Production, because it adds a significant amount of overhead
550             to your application.
551              
552             =head1 SEE ALSO
553              
554             L<Devel::Events>, L<Catalyst::Plugin::LeakTracker>,
555             L<http://blog.jrock.us/articles/Plugging%20a%20leaky%20whale.pod>,
556             L<Devel::Size>, L<Devel::Cycle>
557              
558             =head1 AUTHOR
559              
560             Yuval Kogman <nothingmuch@woobling.org>
561              
562             =head1 CONTRIBUTORS
563              
564             Mateu X. Hunter <hunter@missoula.org>
565              
566             Wallace Reis <wreis@cpan.org>
567              
568             =head1 COPYRIGHT & LICENSE
569              
570             Copyright (c) Yuval Kogman. All rights reserved
571             This program is free software; you can redistribute it and/or modify it
572             under the terms of the MIT license or the same terms as Perl itself.
573              
574             =cut