The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package WxProf;
use strict; use warnings;
use lib '../lib';
use Wx;
use base qw(Wx::Frame Class::Accessor::Fast);
use List::Util qw(first);
use Wx qw(:textctrl :sizer :window :id);
use Wx qw(wxDefaultPosition wxDefaultSize wxVERTICAL wxNO_BORDER
          wxDEFAULT_FRAME_STYLE wxNO_FULL_REPAINT_ON_RESIZE wxCLIP_CHILDREN
          wxWHITE
          );
use Wx::Event qw(
    EVT_SIZE
    EVT_TREE_SEL_CHANGED
    EVT_MENU
    EVT_CLOSE
    EVT_GRID_SELECT_CELL
    EVT_BUTTON
    EVT_LEFT_DOWN
    EVT_MOUSE_EVENTS
    );

use Devel::WxProf::Data;
use Devel::WxProf::Grid;
use Devel::WxProf::Reader::WxProf;
use Devel::WxProf::Reader::DProf;
use Devel::WxProf::Preferences;
use Devel::WxProf::Treemap::Output::Imager;
use Devel::WxProf::Treemap::Squarified;

use UNIVERSAL::require;
use Module::Pluggable::Object;

use File::Path qw(rmtree);

__PACKAGE__->mk_accessors( qw(
    preferences
    filename
    pkg_grid
    sub_grid
    call_grid
    callee_tree
    callee_map
    callee_map_dc
    callee_map_data
    notebook) );

my @wx_defaults = (
        -1,
        wxDefaultPosition,
        wxDefaultSize,
        wxNO_FULL_REPAINT_ON_RESIZE|wxCLIP_CHILDREN
);

sub new {
    my( $class ) = @_;
    my $self = $class->SUPER::new(
        undef,
        -1,
        'wxprofile',
        wxDefaultPosition,
        [ 1024, 768 ],
        wxDEFAULT_FRAME_STYLE|wxNO_FULL_REPAINT_ON_RESIZE|wxCLIP_CHILDREN
    );

    $self->preferences( Devel::WxProf::Preferences->new() );

    Wx::InitAllImageHandlers();
    # create menu bar
    my $bar = Wx::MenuBar->new;
    my $file = Wx::Menu->new;
    $file->Append( wxID_OPEN, "Open" );
    $file->Append( wxID_EXIT, "E&xit" );

    my $help = Wx::Menu->new;
    $help->Append( wxID_ABOUT, "&About..." );

    $bar->Append( $file, "&File" );
    $bar->Append( $help, "&Help" );

    $self->SetMenuBar( $bar );

    my $main = Wx::ScrolledWindow->new($self, @wx_defaults);
    $main->SetScrollbars(20, 20, 55, 40);
    my $main_sizer = Wx::BoxSizer->new(wxVERTICAL);
    $main->SetSizer($main_sizer);

#    INFO: {
#        my $parent = Wx::Panel->new($main, @wx_defaults);
#        Wx::StaticText->new($parent, -1, 'Profile Data - table view');
#        $parent->Fit();
#        $main_sizer->AddSpacer(5);
#        $main_sizer->Add($parent);
#        $main_sizer->AddSpacer(10);
#    };


    CHILDREN: {
        my $parent = Wx::Panel->new($main, @wx_defaults);
        # $parent->SetScrollbars(20, 20, 55, 40);

        my $pkg_label = Wx::StaticText->new( $parent, -1, "Packages");
        my $pkg_grid = Devel::WxProf::Grid->new($parent, -1);

        my $sizer = Wx::FlexGridSizer->new(4,2,5,5);
        $parent->SetSizer($sizer);

        $self->setup_grid( $pkg_grid, 200, ('elapsed', 'calls', 'package') );
        $self->pkg_grid($pkg_grid);

        my $sub_label = Wx::StaticText->new( $parent, -1, "Subroutines",);
        my $sub_grid = Devel::WxProf::Grid->new($parent, -1);
        $self->setup_grid( $sub_grid, 400, ('elapsed', 'calls', 'sub') );
        $self->sub_grid($sub_grid);

        my $call_label = Wx::StaticText->new( $parent, -1, "Calls");
        my $call_grid = Devel::WxProf::Grid->new($parent, -1);
        $self->setup_grid( $call_grid, 200, ('elapsed', 'calls', 'sub') );
        $self->call_grid($call_grid);

        my $tree_label = Wx::StaticText->new( $parent, -1, "Callee Tree");

        my $callee_notebook = Wx::Notebook->new($parent, -1);
#
        my $callee_map;
        CALLEE_MAP: {
            #my $parent = $callee_notebook;
            my $parent = Wx::Panel->new($callee_notebook);
            # create a bitmap
            my $bmp = Wx::Bitmap->new(500, 400);
            # create a graphics device context
            my $temp_dc = Wx::MemoryDC->new();
            # select bitmap
            $temp_dc->SelectObject($bmp);
            # clear bitmap
            $temp_dc->Clear();

            # add (new cleared) bitmap to StaticBitmap display
            $callee_map = Wx::StaticBitmap->new($parent, -1, $bmp, wxDefaultPosition, wxDefaultSize, wxNO_BORDER);
            $callee_map->SetMinSize([500,400]);
            $callee_map->SetMaxSize([500,400]);
            $self->callee_map($callee_map);
            $callee_notebook->AddPage($parent, 'Map');
        }

        my $callee_tree;
        CALLEE__TREE: {
            my $parent = $callee_notebook;
            $callee_tree = Wx::TextCtrl->new($parent, -1, '', wxDefaultPosition, wxDefaultSize,
                wxTE_READONLY|wxTE_MULTILINE|wxNO_FULL_REPAINT_ON_RESIZE );
            $callee_tree->SetMinSize([500,400]);

            $self->callee_tree($callee_tree);
            $callee_notebook->AddPage($callee_tree, 'Text Tree');
        };

        foreach my $item (
                $pkg_label, $call_label,
                $pkg_grid, $call_grid)
        {
            $sizer->Add($item);
        }

        $sizer->AddSpacer(20);
        $sizer->AddSpacer(20);

        foreach my $item (
                $sub_label, $tree_label,
                $sub_grid, $callee_notebook,
                ) {
            $sizer->Add($item);
        }

        #die $sizer->GetRows();

        $parent->Fit();
        $parent->FitInside();

        # $navigator_notebook->AddPage($parent, 'List View');
        EVT_GRID_SELECT_CELL($pkg_grid, sub { $self->on_package_select(@_) } );
        EVT_GRID_SELECT_CELL($sub_grid, sub { $self->on_sub_select(@_) } );
        EVT_GRID_SELECT_CELL($call_grid, sub {
            $self->populate_callee_map(@_);
            $self->populate_callee_tree(@_);
        } );
        $main_sizer->Add($parent, 1, wxEXPAND);
    }
    EVT_CLOSE( $self, \&on_close );
    EVT_MENU( $self, wxID_ABOUT, \&on_about );
    EVT_MENU( $self, wxID_EXIT, sub { $self->Close } );
    EVT_MENU( $self, wxID_OPEN, \&on_open );

    $main->Fit();
    $main->FitInside();
    $self->SetIcon( Wx::GetWxPerlIcon() );
    $self->Show;

#    Wx::LogMessage( "Welcome to wxProfile!" );

    return $self;
}

sub setup_grid {
    my ($self,$grid, $height, @cols) = @_;

#   wxFont(int pointSize, wxFontFamily family, int style, wxFontWeight weight, const bool underline = false, const wxString& faceName = "", wxFontEncoding encoding = wxFONTENCODING_DEFAULT)
    my $font = Wx::Font->new(8);

    my $sizer = Wx::FlexGridSizer->new(0, scalar @cols, $#cols);

    $grid->SetSizer($sizer);
    $grid->CreateGrid(0, scalar @cols, -1);
    $grid->SetDefaultCellFont($font);
    $grid->SetLabelFont($font);
    $grid->SetRowLabelSize(32);
    $grid->EnableEditing(0);
    $grid->SetSelectionMode(1); # wxGridSelectRow

    for (0..$#cols) {
        $grid->SetColLabelValue($_, $cols[$_]);
    }

    $grid->SetMinSize([500,$height]);
    $grid->SetMaxSize([500,$height]);

    $grid->Fit();
    $grid->FitInside();
}

sub populate_pkg_grid {
    my ($self, $data) = @_;
    my $busy = Wx::BusyCursor->new();
    my $grid = $self->pkg_grid();
    my $data_from_ref = [ sort { $b->get_elapsed <=> $a->get_elapsed } values %{ $data } ];

    # die Data::Dumper::Dumper $data_from_ref;

    $grid->data($data_from_ref);
    $grid->AppendRows(scalar @{ $data_from_ref });

    for (my $i = 0; $i<scalar @{ $data_from_ref }; $i++ ) {
        $grid->SetCellValue($i, 0, $data_from_ref->[$i]->get_elapsed() );
        $grid->SetCellValue($i, 1, $data_from_ref->[$i]->get_calls() );
        $grid->SetCellValue($i, 2, $data_from_ref->[$i]->get_package() );
    }
    $grid->Fit();
    $grid->FitInside();

}

sub on_package_select {
    my ($self, $package_grid, $event) = @_;
    my $grid = $self->sub_grid();
    my $pkg = $package_grid->data()->[$event->GetRow()];
    $self->populate_sub_grid($pkg);
}

sub populate_sub_grid {
    my ($self, $pkg) = @_;
    if (not defined $pkg) {
        warn "no pkg - called from " , join " ", caller();
        return;
    }
    my $grid = $self->sub_grid();
    my @row_data = sort { $b->get_elapsed() <=> $a->get_elapsed() } values %{ $pkg->get_function };
    $grid->data( \@row_data );

    $grid->ClearGrid();

    my $rows = $grid->GetNumberRows();
    if ($rows > scalar @row_data) {
        $grid->DeleteRows(scalar @row_data, $rows - scalar @row_data);
    }
    elsif ($rows < scalar @row_data) {
        $grid->AppendRows(scalar @row_data - $rows);
    }

    for (my $i = 0; $i<scalar @row_data; $i++ ) {
        # use Data::Dumper; die Dumper $sub_from_ref->[$i]->get_calls;
        $grid->SetCellValue($i, 0, $row_data[$i]->get_elapsed() );
        $grid->SetCellValue($i, 1, $row_data[$i]->get_calls() );
        $grid->SetCellValue($i, 2, $row_data[$i]->get_function() );
    }
    $grid->Fit();
    $grid->FitInside();
    $grid->Layout();
    $grid->SelectRow(0);
}

sub select_package {
    my ($self, $package) = @_;
    my $grid = $self->pkg_grid();
    for my $row(0..$grid->GetNumberRows()) {
        if ($package eq $grid->GetCellValue($row,2)) {
            $grid->SelectRow($row);
            $grid->MakeCellVisible($row,0);
            return $grid->data()->[ $row ];
        }
    }
    return;
}

sub select_sub {
    my ($self, $package) = @_;
    my $grid = $self->sub_grid();
    for my $row(0..$grid->GetNumberRows()) {
        if ($package eq $grid->GetCellValue($row,2)) {
            $grid->SelectRow($row);
            $grid->MakeCellVisible($row,0);
            return $grid->data()->[ $row ];
        }
    }
    return;
}

sub on_sub_select {
    my ($self, $sub_grid, $event) = @_;
    my $pkg = $sub_grid->data()->[ $event->GetRow() ];
    $self->populate_call_grid($pkg);
}

sub populate_call_grid {
    my ($self, $pkg) = @_;
    my $busy = Wx::BusyCursor->new();

    my @row_data = @{ $pkg->get_child_nodes() };
    my $grid = $self->call_grid();
    $grid->data( \@row_data );

    $grid->ClearGrid();

    my $rows = $grid->GetNumberRows();
    if ($rows > scalar @row_data) {
        $grid->DeleteRows(scalar @row_data, $rows - scalar @row_data);
    }
    elsif ($rows < scalar @row_data) {
        $grid->AppendRows(scalar @row_data - $rows);
    }

    for (my $i = 0; $i<scalar @row_data; $i++ ) {
        # use Data::Dumper; die Dumper $sub_from_ref->[$i]->get_calls;
        $grid->SetCellValue($i, 0, $row_data[$i]->get_elapsed() );
        $grid->SetCellValue($i, 1, $row_data[$i]->get_calls() );
        $grid->SetCellValue($i, 2, $row_data[$i]->get_function() );
    }
    $grid->Fit();
    $grid->FitInside();
    $grid->Layout();
    $grid->SelectRow(0);
}

sub populate_callee_tree {
    my ($self, $call_grid, $event) = @_;
    my $busy = Wx::BusyCursor->new();
    my $data = $call_grid->data()->[ $event->GetRow() ];
    my $child_nodes = $data->get_child_nodes();
    my $callee_tree = $self->callee_tree();
    $callee_tree->Clear();
    $callee_tree->AppendText($data->get_function() . ": " . $data->get_elapsed() . "\n");
    $callee_tree->AppendText(join q{}, $self->generate_text_tree({
        data => $child_nodes,
        max_depth => 10,
    }));
}

sub populate_callee_map {
    my ($self, $call_grid, $event) = @_;
    my $busy = Wx::BusyCursor->new();
    my $data = $call_grid->data()->[ $event->GetRow() ];

    my $callee_map = $self->callee_map();

    my $file_id = ${ $data };

    my $dir = $self->preferences()->get_data_dir() . "/$$";
    mkdir $dir;
    my $filename = "$dir/$file_id.png";

    my $preferences = $self->preferences();

    if (! -r $filename) {
        my $imager = Devel::WxProf::Treemap::Output::Imager->new( WIDTH=>500, HEIGHT=>400,
            FONT_FILE => join ( '/', $preferences->get_font_dir(), $preferences->get_map_font_file() ),
            # '/usr/share/fonts/truetype/freefont/FreeSans.ttf',
            MIN_FONT_SIZE => $self->preferences()->get_map_font_size(),
            MAX_FONT_SIZE => $self->preferences()->get_map_font_size(),
        );

        my $map = Devel::WxProf::Treemap::Squarified->new(
            INPUT => $data,
            OUTPUT => $imager,
            SPACING => { left => 1, top => 1, right => 1, bottom => 1, min_width => 10, min_height => 10 },
            PADDING => { left => 1, top => 12, right => 1, bottom => 1 },
        );

        my @map_from = $map->map();
        $self->callee_map_data(\@map_from);

        $imager->save($filename);

    }

    my $file = IO::File->new( $filename, "r" );
    unless ($file) {
        print "Can't load $filename.";return undef
    };
    binmode $file;
    my $handler = Wx::PNGHandler->new();
    my $image = Wx::Image->new();
    my $bmp;    # used to hold the bitLabelmap.
    $handler->LoadFile( $image, $file );
    $bmp = Wx::Bitmap->new($image);

    if( $bmp->Ok() ) {
        #  create a static bitmap called ImageViewer that displays the
        #  selected image.
        $callee_map->SetBitmap( $bmp );
        # Wx::StaticBitmapWx::StaticBitmap->new($callee_map, -1, $bmp);
        my $dc = $self->callee_map_dc() || Wx::MemoryDC->new();
        $dc->SelectObject($bmp);
        $self->callee_map_dc($dc);
   }
}

sub generate_text_tree {
    my $self = shift;
    my $arg_ref = shift;
    my @result = @{ $arg_ref->{ data } };
    my $max_depth = $arg_ref->{ max_depth };
    my $indent = q{  };
    my $depth = 0;

    my @text = ();
    while (1) {
        my $node = shift @result;
        if (not defined $node) {
            $depth--;
            last if not @result;
            next;
        }
        push @text, $indent x $depth, $node->get_elapsed , q{ }, $node->get_package, q{ ::}, $node->get_function(), "\n";

        if ($depth < $max_depth) {
            my $children_from = $node->get_child_nodes;
            if (@{ $children_from }) {
                $depth++;
                @result = (@{ $children_from }, undef, @result);

            }
        }
        last if not @result;
    }
    return @text;
}

sub read_profile {
    my ($self, $filename) = @_;
    my $busy = Wx::BusyCursor->new();
    my $data = Devel::WxProf::Data->new({});

    my $reader = ($filename =~m{ tmon\.out$ }x)
        ? Devel::WxProf::Reader::DProf->new()
        : Devel::WxProf::Reader::WxProf->new();

    my @result = eval {
        $reader->read_file($filename)
    };
    if ($@) {
        Wx::LogMessage( "Error: $@");
        return;
    }
    $self->filename($filename);
    $self->_set_title($filename);
    $data->set_child_nodes(\@result);
    $self->populate_pkg_grid($reader->get_packages());
}

sub ask_for_filename {
    my ($self, $label) = shift;
    $label ||= 'Select file';
    my $default_dir = $self->preferences()->get_default_dir();
    my $dialog = Wx::FileDialog->new($self, $label, $default_dir);
    if ($dialog->ShowModal() == wxID_OK) {
        $self->preferences->set_default_dir($dialog->GetDirectory());
        return $dialog->GetPath()
    }
    return;
}

sub on_close {
    my( $self, $event ) = @_;

    Wx::Log::SetActiveTarget( $self->{old_log} );
    $event->Skip;
}

sub on_open {
    my( $self, $event ) = @_;
    my $filename = $self->ask_for_filename();
    if ($filename) {
        $self->read_profile($filename);
    }
}

sub on_about {
    my( $self ) = @_;
    use Wx qw(wxOK wxCENTRE wxVERSION_STRING);

    Wx::MessageBox( "wxprofile (c) 2008 Martin Kutter\n" .
                    "wxPerl $Wx::VERSION, " . wxVERSION_STRING,
                    "About wxprofile", wxOK|wxCENTRE, $self );
}

sub _set_title {
    my $self = shift;
    $self->SetTitle(shift);
}

sub _add_menus {
    my( $self, %menus ) = @_;

    while( my( $title, $menu ) = each %menus ) {
        $self->GetMenuBar->Insert( 1, $menu, $title );
    }
}

sub _remove_menus {
    my( $self ) = @_;

    while( $self->GetMenuBar->GetMenuCount > 2 ) {
        $self->GetMenuBar->Remove( 1 )->Destroy;
    }
}

sub DESTROY {
    rmtree $_[0]->preferences->get_data_dir() . "/$$";
}

my $app = Wx::SimpleApp->new;
my $locale = Wx::Locale->new( Wx::Locale::GetSystemLanguage );
my $profile = WxProf->new();
$profile->read_profile($ARGV[0]) if @ARGV;

# add mouse event handler
# must be added for $app - application filters events.
EVT_LEFT_DOWN($app, sub {
    my ($app, $event) = @_;

    # only handle clicks on our map special
    if ($event->GetEventObject() == $profile->callee_map) {
        my ($x,$y) = $event->GetPositionXY();
        my $found = first {
            $_->[2] > $x && $x > $_->[0]
            && $_->[3] > $y && $y > $_->[1]
        } @{ $profile->callee_map_data() };

        if ($found) {
            my ($package,$sub) = $found->[-1] =~m{^(.+)::([^:]+)$}x;
            # warn ($package, " ", $sub);
            # warn Data::Dumper::Dumper($profile->callee_map_data());
            my $pkg = $profile->select_package($package);
            if ($pkg) {
                $profile->populate_sub_grid($pkg);
                my $sub_data = $profile->select_sub($sub);
                $profile->populate_call_grid($sub_data)
                    if ($sub_data);
            }
        }
        else {
            warn "No sub found for coordinates x=$x, y=$y";
        }
    }
    $event->Skip();
});

$app->MainLoop;



exit 0;



1;

=pod

=head1 NAME

wxprofile - Graphical profile data analyzer

=head2 SYNOPSIS

 @> perl -d:WxProf myscript.pl
 @> wxprofile
 @> wxprofile PATH/TO/PROFILE/tmon.out

 # to use Devel::DProf for collecting data
 @> perl -d:DProf myscript.pl
 @> wxprofile tmon.out

 # or use Devel::Profiler for collecting data...
 @> perl "-MDevel::Profiler hz => 100000" myscript.pl
 @> wxprofile tmon.out

=head1 DESCRIPTION

For collecting profile data see L<Devel::WxProf>.

wxprofile is a graphical profile data analyzer for perl.

It supports the following profile formats:

=over

=item * WxProf

It's own (now deprecated) - Devel::WxProf outputs data compatible to
Devel::DProf

=item * Devel::DProf

Only Devel::DProf's new format is supported. This means that you cannot use
Devel::DProfLB - it uses Devel::DProf's old profile data format (as of 0.01).

You can also use Devel::Profiler for collecting profile data.

=back

=head2 Hints on interpreting profile data

wxprofile reports inclusive times. This does not mean, the sub with the
highest value is slowest - it probably just does most of the work.

wxprofile always displays wallclock ticks as profile data. The actual values
are dependent on the profile format and the resolution of your system's clock.

Wallclock profilers use the so-called stopwatch approach. Whether this is
useful or not is highly dependent on the application and the environment:
Stopwatch results are probably correct on a single user system with low
load, profiling a non-interactive application. For all other environments and
applications, stopwatch results have to be used with care.

=head2 Hints on collecting profile data

POSIX::clock() resolution - which is used by most profilers, like Devel::DProf
- provide 1/100s resolution on most systems. This is far from being useful for
profiling single runs. Devel::WxProf collects profile data with higher
resolution - at the cost of accuracy.

Devel::Profiler can be configured to use higher resolution as well, by
passing a hz value as import flag:

 perl "-MDevel::Profiler hz => 10000" myscript.pl

All perl profilers have their strong and weak points. Here's a quick
comparison:

=over

=item * Devel::DProf

 Method:                Debugger
 Times:                 system, user, wall
 Resolution:            system resolution, usually 1/100s
 profiles closures:     Yes
 profiles DESTROY:      Yes
 sub exit w/o return:   Yes
 outputs raw data:      Yes
 data format:           Devel::DProf (new)

=item * Devel::DProfLB

 Method:                Debugger
 Times:                 system, user, wall
 Resolution:            system resolution, usually 1/100s
 profiles closures:     Yes
 profiles DESTROY:      Yes
 sub exit w/o return:   ??
 outputs raw data:      Yes
 data format:           Devel::DProf (old)

=item * Devel::Profile

 Method:                ??
 Times:                 system, user, wall
 Resolution:            system resolution, usually 1/100s
 profiles closures:     Yes
 profiles DESTROY:      Yes
 sub exit w/o return:   ??
 outputs raw data:      No
 data format:           Devel::Profile

=item * Devel::Profiler

 Method:                Sub instrumentor
 Times:                 system, user, wall (only wall with higher resolution)
 Resolution:            configurable
 profiles closures:     No
 profiles DESTROY:      No
 sub exit w/o return:   ??
 outputs raw data:      Yes
 data format:           Devel::DProf (new)

=item * Devel::WxProf

 Method:                Debugger
 Times:                 wall
 Resolution:            1/10000s
 profiles closures:     Yes
 profiles DESTROY:      Yes
 sub exit w/o return:   No
 outputs raw data:      Yes
 data format:           Devel::DProf

=over

=head1 BUGS AND LIMITATIONS

Many.

See L<Devel::WxProf> for more information

=over

=item * Memory usage

wxprofile eats up around 20x more memory than your profile data.

You have been warned.

=item * Treemap proportions

The treemap proportions do not reflect times exactly. The treemap rectangles
include an additional top padding to keep the labels readable.

=item * Windows

Due to some incompatibilities in font handling, wxprofile is currently broken
on windows.

While it can be made to run with some minor changes, it looks a bit strange.
Don't know where THAT comes from.

Feel free to help ;-)

=back

=head1 LICENSE AND COPYRIGHT

Copyright 2008 Martin Kutter.

This program is free software. You may distribute/modify it under
the same terms as perl itself

=head1 AUTHOR

Martin Kutter E<lt>martin.kutter fen-net.deE<gt>

=head1 REPOSITORY INFORMATION

 $Rev: 583 $
 $LastChangedBy: kutterma $
 $Id: $
 $HeadURL: $

=cut