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; $iSetCellValue($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[$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[$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. 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 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 Emartin.kutter fen-net.deE =head1 REPOSITORY INFORMATION $Rev: 583 $ $LastChangedBy: kutterma $ $Id: $ $HeadURL: $ =cut