File Coverage

blib/lib/App/Chart/Gtk2/Diagnostics.pm
Criterion Covered Total %
statement 15 17 88.2
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 21 23 91.3


line stmt bran cond sub pod time code
1             # Copyright 2007, 2008, 2009, 2010, 2011, 2016 Kevin Ryde
2              
3             # This file is part of Chart.
4             #
5             # Chart is free software; you can redistribute it and/or modify it under the
6             # terms of the GNU General Public License as published by the Free Software
7             # Foundation; either version 3, or (at your option) any later version.
8             #
9             # Chart is distributed in the hope that it will be useful, but WITHOUT ANY
10             # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11             # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12             # details.
13             #
14             # You should have received a copy of the GNU General Public License along
15             # with Chart. If not, see <http://www.gnu.org/licenses/>.
16              
17              
18             package App::Chart::Gtk2::Diagnostics;
19 1     1   447 use 5.010;
  1         3  
20 1     1   5 use strict;
  1         2  
  1         17  
21 1     1   5 use warnings;
  1         2  
  1         86  
22 1     1   9 use List::Util qw(min max);
  1         3  
  1         68  
23 1     1   5 use Scalar::Util;
  1         2  
  1         28  
24 1     1   144 use Gtk2;
  0            
  0            
25             use Locale::TextDomain ('App-Chart');
26              
27             use Gtk2::Ex::Units;
28             use App::Chart;
29              
30             # uncomment this to run the ### lines
31             #use Smart::Comments;
32              
33             use Glib::Object::Subclass 'Gtk2::Dialog';
34              
35             sub popup {
36             my ($class, $parent) = @_;
37             require App::Chart::Gtk2::Ex::ToplevelBits;
38             my $self = App::Chart::Gtk2::Ex::ToplevelBits::popup ($class,
39             screen => $parent);
40             $self->refresh;
41             return $self;
42             }
43              
44             Gtk2::Rc->parse_string (<<'HERE');
45             style "Chart_fixed_width_font" {
46             font_name = "Courier 12"
47             }
48             widget_class "App__Chart__Gtk2__Diagnostics.*.GtkTextView" style:gtk "Chart_fixed_width_font"
49             HERE
50              
51             use constant RESPONSE_REFRESH => 0;
52              
53             sub INIT_INSTANCE {
54             my ($self) = @_;
55             my $vbox = $self->vbox;
56              
57             $self->set_title (__('Chart: Diagnostics'));
58             $self->add_buttons ('gtk-close' => 'close',
59             'gtk-refresh' => RESPONSE_REFRESH);
60             $self->signal_connect (response => \&_do_response);
61              
62             my $scrolled = Gtk2::ScrolledWindow->new;
63             $scrolled->set_policy ('never', 'automatic');
64             $vbox->pack_start ($scrolled, 1,1,0);
65              
66             my $textbuf = $self->{'textbuf'} = Gtk2::TextBuffer->new;
67             $textbuf->set_text ('');
68              
69             my $textview = $self->{'textview'}
70             = Gtk2::TextView->new_with_buffer ($textbuf);
71             $textview->set (wrap_mode => 'char',
72             editable => 0);
73             $scrolled->add ($textview);
74              
75             $vbox->show_all;
76              
77             # with a sensible rows and columns size for the TextView
78             Gtk2::Ex::Units::set_default_size_with_subsizes
79             ($self,
80             [$textview, '60 ems', -1],
81             [$scrolled, -1, '40 lines']);
82              
83             # limit to 80% screen height
84             my ($width, $height) = $self->get_default_size;
85             $height = min ($height, 0.8 * $self->get_screen->get_height);
86             $self->set_default_size ($width, $height);
87             }
88              
89             sub _do_response {
90             my ($self, $response) = @_;
91              
92             if ($response eq RESPONSE_REFRESH) {
93             $self->refresh;
94              
95             } elsif ($response eq 'close') {
96             # close signal as per a keyboard Esc close; it defaults to raising
97             # 'delete-event', which in turn defaults to a destroy
98             $self->signal_emit ('close');
99             }
100             }
101              
102             sub refresh {
103             my ($self) = @_;
104             ### refresh: "$self"
105             my $textview = $self->{'textview'};
106              
107             # can be a bit slow counting the database the first time, so show busy
108             require Gtk2::Ex::WidgetCursor;
109             Gtk2::Ex::WidgetCursor->busy;
110              
111             require Gtk2::Ex::TextBufferBits;
112             Gtk2::Ex::TextBufferBits::replace_lines
113             ($textview->get_buffer, $self->str());
114             }
115              
116             sub str {
117             my ($class_or_self) = @_;
118             my $self = ref $class_or_self ? $class_or_self : undef;
119              
120             # mallinfo and mstats before loading other stuff, mallinfo first since
121             # mstats is quite likely not available, and mallinfo first then avoids
122             # counting Devel::Peek
123             my $mallinfo;
124             if (eval { require Devel::Mallinfo; }) {
125             $mallinfo = Devel::Mallinfo::mallinfo();
126             }
127              
128             # mstats_fillhash() croaks if no perl malloc in the running perl
129             my %mstats;
130             require Devel::Peek;
131             ## no critic (RequireCheckingReturnValueOfEval)
132             eval { Devel::Peek::mstats_fillhash(\%mstats) };
133             ## use critic
134              
135             my $str = '';
136              
137             if (App::Chart::DBI->can('has_instance') # if loaded
138             && App::Chart::DBI->has_instance) { # and DBI connected
139             my $dbh = App::Chart::DBI->instance;
140              
141             require DBI::Const::GetInfoType;
142             $str .= "Database: "
143             . $dbh->get_info($DBI::Const::GetInfoType::GetInfoType{'SQL_DBMS_NAME'})
144             . " "
145             . $dbh->get_info($DBI::Const::GetInfoType::GetInfoType{'SQL_DBMS_VER'})
146             . "\n";
147             {
148             # as per App::Chart::DBI code
149             my ($dbversion) = $dbh->selectrow_array
150             ("SELECT value FROM extra WHERE key='database-schema-version'");
151             $str .= " schema version: @{[$dbversion//'undef']}\n";
152             }
153             {
154             my ($count) = $dbh->selectrow_array('SELECT COUNT(*) FROM info');
155             $str .= " symbols: $count\n";
156             my ($daily) = $dbh->selectrow_array('SELECT COUNT(*) FROM daily');
157             $str .= sprintf (" daily records: %d (%d per symbol)\n",
158             $daily, $daily / $count);
159             }
160             {
161             my ($count) = $dbh->selectrow_array('SELECT COUNT(*) FROM latest');
162             $str .= " latest records: $count\n";
163             }
164             {
165             my ($count) = $dbh->selectrow_array('SELECT COUNT(*) FROM intraday_image');
166             $str .= " intraday images: $count\n";
167             }
168             } else {
169             $str .= "Database not connected yet\n";
170             }
171              
172             if (App::Chart::DBI->can('database_filename')) { # when loaded
173             require File::Basename;
174             require File::stat;
175             foreach my $filename (App::Chart::DBI::database_filename(),
176             App::Chart::DBI::notes_filename()) {
177             my $st = File::stat::stat ($filename);
178             my $size = $st->blocks * 512;
179             $str .= sprintf (" %.1f Mb%s in %s\n",
180             $size/1e6,
181             $st->size > $size ? ' (sparse)' : '',
182             Glib::filename_display_name($filename));
183             }
184             }
185              
186             $str .= "\n";
187              
188             {
189             my $count = (App::Chart::Series::Database->can('new')
190             ? keys %App::Chart::Series::Database::cache
191             : 'not loaded yet');
192             $str .= "Cached series: $count\n";
193             }
194             {
195             my $count;
196             if (! App::Chart::Latest->can('get')) {
197             $count = 'not loaded yet';
198             } elsif (my $t = tied %App::Chart::Latest::get_cache) {
199             $count = scalar(keys %App::Chart::Latest::get_cache)
200             . " of " . $t->{'max_count'};
201             } else {
202             $count = 'uninitialized';
203             }
204             $str .= "Cached latest LRU: $count\n";
205             }
206             $str .= "\n";
207              
208             # if BSD::Resource available, only selected info bits
209             if (eval { require BSD::Resource; }) {
210             my ($usertime, $systemtime,
211             $maxrss, $ixrss, $idrss, $isrss, $minflt, $majflt, $nswap,
212             $inblock, $oublock, $msgsnd, $msgrcv,
213             $nsignals, $nvcsw, $nivcsw)
214             = BSD::Resource::getrusage ();
215             $str .= "getrusage (BSD::Resource)\n";
216             $str .= " user time: $usertime (seconds)\n";
217             $str .= " system time: $systemtime (seconds)\n";
218             # linux kernel 2.6.22 doesn't give memory info
219             if ($maxrss) { $str .= " max resident: $maxrss\n"; }
220             if ($ixrss) { $str .= " shared mem: $ixrss\n"; }
221             if ($idrss) { $str .= " unshared mem: $idrss\n"; }
222             if ($isrss) { $str .= " unshared stack: $isrss\n"; }
223             # linux kernel 2.4 didn't count context switches
224             if ($nvcsw) { $str .= " voluntary yields: $nvcsw\n"; }
225             if ($nivcsw) { $str .= " involuntary yields: $nivcsw\n"; }
226             }
227             $str .= "\n";
228              
229             if ($mallinfo) {
230             $str .= "mallinfo (Devel::Mallinfo)\n" . hash_format ($mallinfo);
231             } else {
232             $str .= "(Devel::Mallinfo not available.)\n";
233             }
234             $str .= "\n";
235              
236             if (%mstats) {
237             $str .= "mstat (Devel::Peek)\n" . hash_format (\%mstats);
238             } else {
239             $str .= "(Devel::Peek -- no mstat() in this perl)\n";
240             }
241              
242             if (eval { require Devel::Arena; }) {
243             $str .= "\n";
244             my $stats = Devel::Arena::sv_stats();
245             my $magic = $stats->{'magic'};
246             $stats->{'magic'} # mung to reduce verbosity
247             = scalar(keys %$magic) . ' total '
248             . List::Util::sum (map {$magic->{$_}->{'total'}} keys %$magic);
249             $str .= "SV stats (Devel::Arena)\n" . hash_format ($stats);
250              
251             my $shared = Devel::Arena::shared_string_table_effectiveness();
252             $str .= "Shared string effectiveness:\n" . hash_format ($shared);
253             } else {
254             $str .= "(Devel::Arena -- module not available)\n";
255             }
256              
257             if (eval { require Devel::SawAmpersand; 1 }) {
258             $str .= 'PL_sawampersand is '
259             . (Devel::SawAmpersand::sawampersand()
260             ? "true, which is bad!"
261             : "false, good")
262             . " (Devel::SawAmpersand)\n";
263             } else {
264             $str .= "(Devel::SawAmpersand -- module not available.)\n";
265             }
266             $str .= "\n";
267              
268             $str .= "Modules loaded: " . (scalar keys %INC) . "\n";
269             {
270             $str .= "Module versions:\n";
271             my @modulenames = ('Gtk2',
272             'Glib',
273             'DBI',
274             'DBD::SQLite',
275             'LWP',
276             'Devel::Arena',
277             # 'Devel::Mallinfo',
278             'Devel::Peek',
279             'Devel::StackTrace',
280             'Gtk2::Ex::Datasheet::DBI',
281             # 'Gtk2::Ex::NoShrink',
282             'Gtk2::Ex::TickerView',
283             'HTML::TableExtract',
284             'Number::Format',
285             'Set::IntSpan::Fast',
286             ['Compress::Raw::Zlib', 'ZLIB_VERSION'],
287             ['Finance::TA', 'TA_GetVersionString'],
288             # no apparent version number in geniustrader
289             );
290             my $width = max (map {length} @modulenames);
291             $str .= sprintf (" %-*s%s\n", $width+2, 'Perl', $^V);
292             foreach my $modulename (@modulenames) {
293             my $funcname;
294             if (ref($modulename)) {
295             ($modulename,$funcname) = @$modulename;
296             }
297             my $version = $modulename->VERSION;
298             if (defined $version && defined $funcname) {
299             my $func = $modulename->can($funcname);
300             $version .= "\n" . ($func
301             ? " and $funcname " . $func->()
302             : " (no $funcname)");
303             }
304             if (defined $version) {
305             $str .= sprintf (" %-*s%s\n", $width+2, $modulename, $version);
306             } else {
307             $version = '(not loaded)';
308             }
309             }
310             }
311             # Full report is a bit too big:
312             # if (eval { require Module::Versions::Report; }) {
313             # $str .= Module::Versions::Report::report()
314             # . "\n";
315             # }
316              
317             $str .= "\n";
318             $str .= objects_report();
319             {
320             $str .= "GdkColorAlloc cells: ";
321             if (! App::Chart::Gtk2::Ex::GdkColorAlloc->can('new')) {
322             $str .= "not loaded\n";
323             } else {
324             my $obj_count = scalar keys %App::Chart::Gtk2::Ex::GdkColorAlloc::color_to_colormap;
325             $str .= "$obj_count\n";
326             # on $pix_count pixels\n";
327             # my %pixels;
328             # $pixels{map {$_->pixel} values %App::Chart::Gtk2::Ex::GdkColorAlloc::color_to_colormap}
329             # = 1;
330             # my $pix_count = scalar keys %pixels;
331             # $str .= "$obj_count on $pix_count pixels\n";
332             }
333             }
334              
335             if ($self) {
336             $str .= "\n";
337             $str .= $self->Xresource_report;
338             }
339              
340             return $str;
341             }
342              
343             sub objects_report {
344             if (! eval { require Devel::FindBlessedRefs; 1 }) {
345             return "(Devel::FindBlessedRefs not available)\n";
346             }
347             my $str = "Glib/Gtk objects (Devel::FindBlessedRefs)\n";
348             my %seen = ('Glib::Object' => {},
349             'Glib::Boxed' => {});
350             Devel::FindBlessedRefs::find_refs_by_coderef
351             (sub {
352             my ($obj) = @_;
353             my $class = Scalar::Util::blessed($obj) || return;
354             ($obj->isa('Glib::Object') || $obj->isa('Glib::Boxed')) or return;
355             my $addr = Scalar::Util::refaddr ($obj);
356             $seen{$class}->{$addr} = 1;
357             });
358             my @classes = sort keys %seen;
359             my $traverse;
360             $traverse = sub {
361             my ($depth, $class_list) = @_;
362             my @toplevels = grep {is_toplevel_class ($_,$class_list)} @$class_list;
363             foreach my $class (@toplevels) {
364             my $count = scalar keys %{$seen{$class}};
365             $str .= sprintf "%*s%s %d\n", 2*$depth, '', $class, $count;
366             my @subclasses = grep {$_ ne $class && $_->isa($class)} @$class_list;
367             $traverse->($depth+1, \@subclasses);
368             }
369             };
370             $traverse->(1, \@classes);
371             return $str;
372             }
373              
374             sub Xresource_report {
375             my ($self) = @_;
376              
377             my $window = $self->window
378             || return "(X-Resource -- no window realized, no server connection)\n";
379             $window->can('XID')
380             || return "(X-Resource -- not running on X11)\n";
381             my $xid = $window->XID;
382             eval { require X11::Protocol; 1 }
383             || return "(X-Resource -- X11::Protocol module not available)\n";
384              
385             my $display = $window->get_display;
386             my $display_name = $display->get_name;
387             my $X = eval { X11::Protocol->new ($display_name) }
388             || return "(X-Resource -- cannot connect to \"$display_name\": $@)\n";
389             my $ret;
390             if (! eval {
391             if (! $X->init_extension ('X-Resource')) {
392             $ret = "(X-Resource -- server doesn't have this extension\n";
393             } else {
394             $ret = "X-Resource server resources (X11::Protocol)\n";
395             if (my @res = $X->XResourceQueryClientResources ($xid)) {
396             my $count_width = 0;
397             for (my $i = 1; $i <= $#res; $i++) {
398             $count_width = max($count_width, length($res[$i]));
399             }
400             while (@res) {
401             my $type_atom = shift @res;
402             my $count = shift @res;
403             $ret .= sprintf (" %*d %s\n",
404             $count_width,$count, $X->atom_name($type_atom));
405             }
406             } else {
407             $ret = " no resources in use\n";
408             }
409             }
410             1;
411             }) {
412             (my $err = $@) =~ s/^/ /mg;
413             $ret .= $err;
414             }
415             return $ret;
416             }
417              
418             #------------------------------------------------------------------------------
419             # generic helpers
420              
421             # return true if $class is not a subclass of anything in $class_list (an
422             # arrayref)
423             sub is_toplevel_class {
424             my ($class, $class_list) = @_;
425             return ! List::Util::first {$class ne $_ && $class->isa($_)} @$class_list;
426             }
427              
428             # return a string of the contents of a hash (passed as a hashref)
429             sub hash_format {
430             my ($h) = @_;
431             my $nf = App::Chart::number_formatter();
432              
433             require Scalar::Util;
434             my %mung;
435             foreach my $key (keys %$h) {
436             my $value = $h->{$key};
437             if (Scalar::Util::looks_like_number ($value)) {
438             $mung{$key} = $nf->format_number ($value);
439             } elsif (ref ($_) && ref($_) eq 'HASH') {
440             $mung{$key} = "subhash, " . scalar(keys %{$_}) . " keys";
441             } else {
442             $mung{$key} = $value;
443             }
444             }
445              
446             my $field_width = max (map {length} keys %mung);
447             my $value_width = max (map {length} values %mung);
448              
449             return join ('', map { sprintf (" %-*s %*s\n",
450             $field_width, $_,
451             $value_width, $mung{$_})
452             } sort keys %mung);
453             }
454              
455             1;
456             __END__
457              
458             =head1 NAME
459              
460             App::Chart::Gtk2::Diagnostics -- diagnostics dialog module
461              
462             =head1 SYNOPSIS
463              
464             use App::Chart::Gtk2::Diagnostics;
465             App::Chart::Gtk2::Diagnostics->popup();
466              
467             =head1 WIDGET HIERARCHY
468              
469             C<App::Chart::Gtk2::Diagnostics> is a subclass of C<Gtk2::Dialog>.
470              
471             Gtk2::Widget
472             Gtk2::Container
473             Gtk2::Bin
474             Gtk2::Window
475             Gtk2::Dialog
476             App::Chart::Gtk2::Diagnostics
477              
478             =head1 DESCRIPTION
479              
480             A C<App::Chart::Gtk2::Diagnostics> dialog shows various bits of diagnostic
481             information like memory use, database size, etc.
482              
483             =head1 FUNCTIONS
484              
485             =over 4
486              
487             =item C<< App::Chart::Gtk2::Diagnostics->popup() >>
488              
489             Present a C<Diagnostics> dialog to the user. C<popup()> creates and then
490             re-uses a single dialog, re-presenting it (C<< $widget->present() >>) and
491             refreshing its contents each time. A single diagnostics dialog like this
492             will be enough for most uses.
493              
494             =item C<< $dialog = App::Chart::Gtk2::Diagnostics->new() >>
495              
496             Create and return a new Diagnostics dialog widget. Initially it's empty and
497             C<refresh()> must be called to put some diagnostic information in it.
498              
499             =item C<< $diagnostics->refresh() >>
500              
501             Refresh the information displayed in C<$diagnostics>. The "Refresh" button
502             in the dialog calls this.
503              
504             =item C<< $str = App::Chart::Gtk2::Diagnostics->str() >>
505              
506             Return the diagnostics in string form, as would be shown in a dialog. This
507             just makes a string, no dialog is opened, created or updated.
508              
509             =back
510              
511             =head1 SEE ALSO
512              
513             L<App::Chart>, L<Gtk2::Dialog>
514              
515             =head1 HOME PAGE
516              
517             L<http://user42.tuxfamily.org/chart/index.html>
518              
519             =head1 LICENCE
520              
521             Copyright 2007, 2008, 2009, 2010, 2011, 2016 Kevin Ryde
522              
523             Chart is free software; you can redistribute it and/or modify it under the
524             terms of the GNU General Public License as published by the Free Software
525             Foundation; either version 3, or (at your option) any later version.
526              
527             Chart is distributed in the hope that it will be useful, but WITHOUT ANY
528             WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
529             FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
530             details.
531              
532             You should have received a copy of the GNU General Public License along with
533             Chart; see the file F<COPYING>. Failing that, see
534             L<http://www.gnu.org/licenses/>.
535              
536             =cut
537              
538             # Local variables:
539             # compile-command: "perl -MApp::Chart::Gtk2::Diagnostics -e 'print App::Chart::Gtk2::Diagnostics->str'"
540             # End: