The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Catalyst::Plugin::MemoryUsage;
BEGIN {
  $Catalyst::Plugin::MemoryUsage::AUTHORITY = 'cpan:YANICK';
}
{
  $Catalyst::Plugin::MemoryUsage::VERSION = '0.4.0';
}
#ABSTRACT: Profile memory usage of requests

use strict;
use warnings;

use namespace::autoclean;
use Moose::Role;
use MRO::Compat;

use Memory::Usage;

use Devel::CheckOS;
use Text::SimpleTable;
use Number::Bytes::Human qw/ format_bytes /;
use List::Util qw/ max /;

our @SUPPORTED_OSES = qw/ Linux NetBSD /;

our $os_not_supported = Devel::CheckOS::os_isnt( @SUPPORTED_OSES );

if ( $os_not_supported ) {
    warn "OS not supported by Catalyst::Plugin::MemoryUsage\n",
         "\tStats will not be collected\n";
}


has memory_usage => (
    is => 'rw',
    default => sub { Memory::Usage->new },
);

our $_memory_usage_report;
our $_memory_usage_record_actions;


after setup_finalize => sub {
    my $c = shift;

    my %config = %{ $c->config->{'Plugin::MemoryUsage'} || {} };

    $_memory_usage_report = 
        exists $config{report} ? $config{report} : $c->debug;

    $_memory_usage_record_actions = 
        exists $config{action_milestones} 
            ? $config{action_milestones} : $c->debug;
};




sub reset_memory_usage {
    my $self = shift;

    $self->memory_usage( Memory::Usage->new );
}

sub memory_usage_report {
    my $self = shift;

    my $title_width = max 10,
        map { length $_->[1] } @{ $self->memory_usage->state };

    my $table = Text::SimpleTable->new( 
        [$title_width, ''],
        [4, 'vsz'],
        [4, 'delta'],
        [4, 'rss'],
        [4, 'delta'],
        [4, 'shared'],
        [4, 'delta'],
        [4, 'code'],
        [4, 'delta'],
        [4, 'data'],
        [4, 'delta'],
    );

    my @previous;

    for my $s ( @{ $self->memory_usage->state } ) {
        my ( undef, $msg, @sizes ) = @$s;

        my @data = map { $_ ? format_bytes( 1024 * $_) : '' } map { 
            ( $sizes[$_], @previous ? $sizes[$_] - $previous[$_]  : 0 )
        } 0..4;
        @previous = @sizes;

        $table->row( $msg, @data );
    }

    return $table->draw;
}

unless ( $os_not_supported ) {

after execute => sub {
    return unless $_memory_usage_record_actions;

    my $c = shift;
    $c->memory_usage->record( "after " . join " : ", @_ );
};

around prepare => sub {
    my $orig = shift;
    my $self = shift;

    my $c = $self->$orig(@_);

    $c->memory_usage->record('preparing for the request') 
        if $_memory_usage_record_actions;

    return $c;
};

after finalize => sub {
    return unless $_memory_usage_report;

    my $c = shift;
    $c->log->debug(
        sprintf(qq{[%s] memory usage of request "%s" from "%s"\n},
            [split m{::}, __PACKAGE__]->[-1],
            $c->req->uri,
            $c->req->address,
        ),
        $c->memory_usage_report
    );
};

}

1;

__END__

=pod

=head1 NAME

Catalyst::Plugin::MemoryUsage - Profile memory usage of requests

=head1 VERSION

version 0.4.0

=head1 SYNOPSIS

In YourApp.pm:

    package YourApp;

    use Catalyst qw/
        MemoryUsage
    /;

In a Controller class:

    sub foo :Path( '/foo' ) {
         # ...
         
         something_big_and_scary();
         
         $c->memory_usage->record( 'finished running iffy code' );
         
         # ...
    }

In yourapp.conf:

    <Plugin::MemoryUsage>
        report            1
        action_milestones 1
    </Plugin::MemoryUsage>

=head1 DESCRIPTION

C<Catalyst::Plugin::MemoryUsage> adds a memory usage profile to your debugging
log, which looks like this:   

 [debug] [MemoryUsage] memory usage of request "http://localhost/index" from "127.0.0.1"
 .--------------------------------------------------+------+------+------+------+------+------+------+------+------+------.
 |                                                  | vsz  | del- | rss  | del- | sha- | del- | code | del- | data | del- |
 |                                                  |      | ta   |      | ta   | red  | ta   |      | ta   |      | ta   |
 +--------------------------------------------------+------+------+------+------+------+------+------+------+------+------+
 | preparing for the request                        | 28M  |      | 22M  |      | 2.2M |      | 1.1M |      | 20M  |      |
 | after TestApp::Controller::Root : root/_BEGIN    | 28M  |      | 22M  |      | 2.2M |      | 1.1M |      | 20M  |      |
 | after TestApp::Controller::Root : root/_AUTO     | 28M  |      | 22M  |      | 2.2M |      | 1.1M |      | 20M  |      |
 | in the middle of index                           | 28M  |      | 22M  |      | 2.2M |      | 1.1M |      | 20M  |      |
 | after TestApp::Controller::Root : root/index     | 28M  |      | 22M  |      | 2.2M |      | 1.1M |      | 20M  |      |
 | after TestApp::Controller::Root : root/_ACTION   | 28M  |      | 22M  |      | 2.2M |      | 1.1M |      | 20M  |      |
 | after TestApp::Controller::Root : root/_END      | 28M  |      | 22M  |      | 2.2M |      | 1.1M |      | 20M  |      |
 | after TestApp::Controller::Root : root/_DISPATCH | 28M  |      | 22M  |      | 2.2M |      | 1.1M |      | 20M  |      |
 '--------------------------------------------------+------+------+------+------+------+------+------+------+------+------'  

=head1 CONFIGURATION

=head2 report

If true, the memory usage is reported automatically (at debug level)
at the end of the request.  

Defaults to true if we are in debugging mode,
false otherwise.

=head2 action_milestones

If true, automatically adds milestones for each action, as seen in the
DESCRIPTION.  

Defaults to true if we are in debugging mode,
false otherwise.

=head1 METHODS

=head2 C<memory_usage()>

Returns the L<Memory::Usage> object available to the context.

To record more measure points for the memory profiling, use the C<record()>
method of that object:

    sub foo :Path {
        my ( $self, $c) = @_;

        ...

        big_stuff();

        $c->memory_usage->record( "done with big_stuff()" );

        ...
    }

=head2 C<reset_memory_usage()>

Discards the current C<Memory::Usage> object, along with its recorded data,
and replaces it by a shiny new one.

=head1 BUGS AND LIMITATIONS

C<Memory::Usage>, which is the module C<Catalyst::Plugin::MemoryUsage> relies
on to get its statistics, only work for Linux-based platforms. Consequently,
for the time being C<Catalyst::Plugin::MemoryUsage> only work on Linux and
NetBSD. This being said, patches are most welcome. :-)

=head1 SEE ALSO

L<Memory::Usage>

=head1 AUTHOR

Yanick Champoux <yanick@babyl.dyndns.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2010 by Yanick Champoux.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut