package DBI::ProfileDumper;
use strict;

=head1 NAME

DBI::ProfileDumper - profile DBI usage and output data to a file

=head1 SYNOPSIS

To profile an existing program using DBI::ProfileDumper, set the
DBI_PROFILE environment variable and run your program as usual.  For
example, using bash:

  DBI_PROFILE=DBI::ProfileDumper program.pl

Then analyze the generated file (F<dbi.prof>) with L<dbiprof|dbiprof>:

  dbiprof

You can also activate DBI::ProfileDumper from within your code:

  use DBI;

  # profile with default path (2) and output file (dbi.prof)
  $dbh->{Profile} = "DBI::ProfileDumper";

  # same thing, spelled out
  $dbh->{Profile} = "2/DBI::ProfileDumper/File/dbi.prof";

  # another way to say it
  use DBI::Profile qw(DBIprofile_Statement);
  $dbh->{Profile} = DBI::ProfileDumper->new(
                      { Path => [ DBIprofile_Statement ]
                        File => 'dbi.prof' });

  # using a custom path
  $dbh->{Profile} = DBI::ProfileDumper->new({ Path => [ "foo", "bar" ],
                                              File => 'dbi.prof' });


=head1 DESCRIPTION

DBI::ProfileDumper is a subclass of L<DBI::Profile|DBI::Profile> which
dumps profile data to disk instead of printing a summary to your
screen.  You can then use L<dbiprof|dbiprof> to analyze the data in
a number of interesting ways, or you can roll your own analysis using
L<DBI::ProfileData|DBI::ProfileData>.

B<NOTE:> For Apache/mod_perl applications, use
L<DBI::ProfileDumper::Apache|DBI::ProfileDumper::Apache>.

=head1 USAGE

One way to use this module is just to enable it in your C<$dbh>:

  $dbh->{Profile} = "DBI::ProfileDumper";

This will write out profile data by statement into a file called
F<dbi.prof>.  If you want to modify either of these properties, you
can construct the DBI::ProfileDumper object yourself:

  use DBI::Profile qw(DBIprofile_Statement);
  $dbh->{Profile} = DBI::ProfileDumper->new(
                      { Path => [ DBIprofile_Statement ]
                        File => 'dbi.prof' });

The C<Path> option takes the same values as in
L<DBI::Profile|DBI:Profile>.  The C<File> option gives the name of the
file where results will be collected.  If it already exists it will be
overwritten.

You can also activate this module by setting the DBI_PROFILE
environment variable:

  $ENV{DBI_PROFILE} = "DBI::ProfileDumper";

This will cause all DBI handles to share the same profiling object.

=head1 METHODS

The following methods are available to be called using the profile
object.  You can get access to the profile object from the Profile key
in any DBI handle:

  my $profile = $dbh->{Profile};

=over 4

=item $profile->flush_to_disk()

Flushes all collected profile data to disk and empties the Data hash.
This method may be called multiple times during a program run.

=item $profile->empty()

Clears the Data hash without writing to disk.

=back

=head1 DATA FORMAT

The data format written by DBI::ProfileDumper starts with a header
containing the version number of the module used to generate it.  Then
a block of variable declarations describes the profile.  After two
newlines, the profile data forms the body of the file.  For example:

  DBI::ProfileDumper 1.0
  Path = [ DBIprofile_Statement, DBIprofile_MethodName ]
  Program = t/42profile_data.t
  
  + 1 SELECT name FROM users WHERE id = ?
  + 2 prepare
  = 1 0.0312958955764771 0.000490069389343262 0.000176072120666504 0.00140702724456787 1023115819.83019 1023115819.86576
  + 2 execute
  1 0.0312958955764771 0.000490069389343262 0.000176072120666504 0.00140702724456787 1023115819.83019 1023115819.86576
  + 2 fetchrow_hashref
  = 1 0.0312958955764771 0.000490069389343262 0.000176072120666504 0.00140702724456787 1023115819.83019 1023115819.86576
  + 1 UPDATE users SET name = ? WHERE id = ?
  + 2 prepare
  = 1 0.0312958955764771 0.000490069389343262 0.000176072120666504 0.00140702724456787 1023115819.83019 1023115819.86576
  + 2 execute
  = 1 0.0312958955764771 0.000490069389343262 0.000176072120666504 0.00140702724456787 1023115819.83019 1023115819.86576

The lines beginning with C<+> signs signify keys.  The number after
the C<+> sign shows the nesting level of the key.  Lines beginning
with C<=> are the actual profile data, in the same order as
in DBI::Profile.

Note that the same path may be present multiple times in the data file
since C<format()> may be called more than once.  When read by
DBI::ProfileData the data points will be merged to produce a single
data set for each distinct path.

The key strings are transformed in three ways.  First, all backslashes
are doubled.  Then all newlines and carriage-returns are transformed
into C<\n> and C<\r> respectively.  Finally, any NULL bytes (C<\0>)
are entirely removed.  When DBI::ProfileData reads the file the first
two transformations will be reversed, but NULL bytes will not be
restored.

=head1 AUTHOR

Sam Tregar <sam@tregar.com>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2002 Sam Tregar

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

=cut

# inherit from DBI::Profile
use DBI::Profile;
use vars qw(@ISA $VERSION);
@ISA = ("DBI::Profile");
$VERSION = "1.0";

use Carp qw(croak);
use Symbol;

# validate params and setup default
sub new {
    my $pkg = shift;
    my $self = $pkg->SUPER::new(@_);

    # File defaults to dbi.prof
    $self->{File} = "dbi.prof" unless exists $self->{File};

    return $self;
}

# flush available data to disk
sub flush_to_disk {
    my $self = shift;
    my $data = $self->{Data};

    my $fh = gensym;
    if ($self->{_wrote_header}) {
        # append more data to the file
        open($fh, ">>$self->{File}") 
          or croak("Unable to open '$self->{File}' for profile output: $!");
    } else {
        # create new file and write the header
        open($fh, ">$self->{File}") 
          or croak("Unable to open '$self->{File}' for profile output: $!");
        $self->write_header($fh);
        $self->{_wrote_header} = 1;
    }

    $self->write_data($fh, $self->{Data}, 1);

    close($fh) or croak("Unable to close '$self->{File}': $!");

    $self->empty();
}

# empty out profile data
sub empty {
    shift->{Data} = {};
}

# write header to a filehandle
sub write_header {
    my ($self, $fh) = @_;

    # module name and version number
    print $fh ref($self), " ", $self->VERSION, "\n";

    # print out Path
    my @path_words;
    foreach (@{$self->{Path}}) {
        if ($_ eq DBI::Profile::DBIprofile_Statement) {
            push @path_words, "DBIprofile_Statement";
        } elsif ($_ eq DBI::Profile::DBIprofile_MethodName) {
            push @path_words, "DBIprofile_MethodName";
        } elsif ($_ eq DBI::Profile::DBIprofile_MethodClass) {
            push @path_words, "DBIprofile_MethodClass";
        } else {
            push @path_words, $_;
        }
    }
    print $fh "Path = [ ", join(', ', @path_words), " ]\n";

    # print out $0 and @ARGV
    print $fh "Program = $0";
    print $fh " ", join(", ", @ARGV) if @ARGV;
    print $fh "\n";

    # all done
    print $fh "\n";
}

# write data in the proscribed format
sub write_data {
    my ($self, $fh, $data, $level) = @_;

    # produce an empty profile for invalid $data
    return unless $data and UNIVERSAL::isa($data,'HASH');
    
    while (my ($key, $value) = each(%$data)) {
        # output a key
        print $fh "+ ", $level, " ", quote_key($key), "\n";
        if (UNIVERSAL::isa($value,'ARRAY')) {
            # output a data set for a leaf node
            printf $fh "= %4d %.6f %.6f %.6f %.6f %.6f %.6f\n", @$value;
        } else {
            # recurse through keys - this could be rewritten to use a
            # stack for some small performance gain
            $self->write_data($fh, $value, $level + 1);
        }
    }
}

# quote a key for output
sub quote_key {
    my $key = shift;
    $key =~ s!\\!\\\\!g;
    $key =~ s!\n!\\n!g;
    $key =~ s!\r!\\r!g;
    $key =~ s!\0!!g;
    return $key;
}

# flush data to disk when profile object goes out of scope
sub on_destroy {
    shift->flush_to_disk();
}

1;
