diff --git a/bin/pt-diskstats b/bin/pt-diskstats index 54d14d04..4741358e 100755 --- a/bin/pt-diskstats +++ b/bin/pt-diskstats @@ -1533,6 +1533,10 @@ use constant PTDEBUG => $ENV{PTDEBUG} || 0; use IO::Handle; use List::Util qw( max first ); +use ReadKeyMini qw( GetTerminalSize ); + +my (undef, $max_lines) = GetTerminalSize; + my $diskstat_colno_for; BEGIN { $diskstat_colno_for = { @@ -1576,9 +1580,11 @@ sub new { block_size => 512, show_inactive => $o->get('show-inactive'), sample_time => $o->get('sample-time') || 0, - columns_regex => qr/$columns/, - devices_regex => $devices ? qr/$devices/ : undef, + automatic_headers => $o->get('automatic-headers'), + columns_regex => qr/$columns/, + devices_regex => $devices ? qr/$devices/ : undef, interactive => 0, + force_header => 1, %args, @@ -1606,13 +1612,38 @@ sub new { _nochange_skips => [], _save_curr_as_prev => 1, - _print_header => 1, }; return bless $self, $class; } +sub active_device { + my ( $self, $dev ) = @_; + return $self->{_active_devices}->{$dev}; +} + +sub set_active_device { + my ($self, $dev, $val) = @_; + return $self->{_active_devices}->{$dev} = $val; +} + +sub clear_active_devices { + my ( $self ) = @_; + return $self->{_active_devices} = {}; +} + + +sub automatic_headers { + my ($self) = @_; + return $self->{automatic_headers}; +} + +sub set_automatic_headers { + my ($self, $new_val) = @_; + return $self->{automatic_headers} = $new_val; +} + sub curr_ts { my ($self) = @_; return $self->{_ts}->{curr} || 0; @@ -1732,9 +1763,19 @@ sub add_ordered_dev { } +sub force_header { + my ($self) = @_; + return $self->{force_header}; +} + +sub set_force_header { + my ($self, $new_val) = @_; + return $self->{force_header} = $new_val; +} + sub clear_state { my ($self) = @_; - $self->{_print_header} = 1; + $self->set_force_header(1); $self->clear_curr_stats(); $self->clear_prev_stats(); $self->clear_first_stats(); @@ -2159,25 +2200,42 @@ sub _print_device_if { my $dev_re = $self->devices_regex(); if ( $dev_re ) { + $self->_mark_if_active($dev); return $dev if $dev =~ $dev_re; } else { - if ( $self->show_inactive() || $self->active_device($dev) ) { + if ( $self->active_device($dev) ) { + return $dev; + } + elsif ( $self->show_inactive() ) { + $self->_mark_if_active($dev); return $dev; } else { - my $curr = $self->stats_for($dev); - my $first = $self->first_stats_for($dev); - if ( first { $curr->[$_] != $first->[$_] } READS..MS_WEIGHTED ) { - $self->set_active_device($dev, 1); - return $dev; - } + return $dev if $self->_mark_if_active($dev); } } push @{$self->{_nochange_skips}}, $dev; return; } +sub _mark_if_active { + my ($self, $dev) = @_; + + return $dev if $self->active_device($dev); + + my $curr = $self->stats_for($dev); + my $first = $self->first_stats_for($dev); + + return unless $curr && $first; + + if ( first { $curr->[$_] != $first->[$_] } READS..MS_WEIGHTED ) { + $self->set_active_device($dev, 1); + return $dev; + } + return; +} + sub _calc_stats_for_deltas { my ( $self, $elapsed ) = @_; my @end_stats; @@ -2240,32 +2298,30 @@ sub _calc_deltas { return $self->_calc_stats_for_deltas($elapsed); } +sub force_print_header { + my ($self, @args) = @_; + my $orig = $self->force_header(); + $self->force_header(1); + $self->print_header(@args); + $self->force_header($orig); + return; +} + sub print_header { my ($self, $header, @args) = @_; - if ( $self->{_print_header} ) { + if ( $self->force_header() ) { printf $header . "\n", @args; + $Diskstats::printed_lines--; + $Diskstats::printed_lines ||= $max_lines; } -} - -sub active_device { - my ( $self, $dev ) = @_; - return $self->{_active_devices}->{$dev}; -} - -sub set_active_device { - my ($self, $dev, $val) = @_; - return $self->{_active_devices}->{$dev} = $val; -} - -sub clear_active_devices { - my ( $self ) = @_; - return $self->{_active_devices} = {}; + return; } sub print_rows { my ($self, $format, $cols, $stat) = @_; printf $format . "\n", @{ $stat }{ qw( line_ts dev ), @$cols }; + $Diskstats::printed_lines--; } sub print_deltas { @@ -2282,11 +2338,28 @@ sub print_deltas { my $header_method = $args{header_callback} || "print_header"; my $rows_method = $args{rows_callback} || "print_rows"; + + $Diskstats::printed_lines ||= $max_lines; $self->$header_method( $header, "#ts", "device" ); - foreach my $stat ( $self->_calc_deltas() ) { - $self->$rows_method( $format, $cols, $stat ); + my @stats = $self->_calc_deltas(); + + while ( my @stats_chunk = splice @stats, 0, $Diskstats::printed_lines ) { + foreach my $stat ( @stats_chunk ) { + $self->$rows_method( $format, $cols, $stat ); + } + + if ( $Diskstats::printed_lines == 0 ) { + $Diskstats::printed_lines ||= $max_lines; + + if ( $self->automatic_headers() + && !$self->isa("DiskstatsGroupByAll") ) + { + local $self->{force_header} = 1; + $self->$header_method( $header, "#ts", "device" ); + } + } } } @@ -2473,7 +2546,6 @@ sub new { my ($class, %args) = @_; my $self = $class->SUPER::new(%args); $self->{_iterations} = 0; - $self->{_print_header} = 1; return $self; } @@ -2502,12 +2574,12 @@ sub group_by { header_callback => sub { my ($self, @args) = @_; - if ( $self->{_print_header} ) { + if ( $self->force_header() ) { my $method = $args{header_callback} || "print_header"; $self->$method(@args); } - $self->{_print_header} = undef; + $self->set_force_header(undef); }, rows_callback => $args{rows_callback}, ); @@ -2546,10 +2618,10 @@ sub group_by { sub clear_state { my ($self, @args) = @_; - my $orig_print_h = $self->{_print_header}; + my $orig_print_h = $self->{force_header}; $self->{_iterations} = 0; $self->SUPER::clear_state(@args); - $self->{_print_header} = $orig_print_h; + $self->{force_header} = $orig_print_h; } sub compute_line_ts { @@ -2618,7 +2690,6 @@ sub new { my $self = $class->SUPER::new(%args); $self->{_iterations} = 0; $self->{_save_curr_as_prev} = 0; - $self->{_print_header} = 1; return $self; } @@ -2658,10 +2729,10 @@ sub _sample_callback { header_callback => sub { my ( $self, $header, @args ) = @_; - if ( $self->{_print_header} ) { + if ( $self->force_header() ) { my $method = $args{header_callback} || "print_header"; $self->$method( $header, @args ); - $self->{_print_header} = undef; + $self->set_force_header(undef); } }, rows_callback => sub { @@ -2694,7 +2765,6 @@ sub clear_state { my ( $self, @args ) = @_; $self->{_iterations} = 0; $self->{_save_curr_as_prev} = 0; - $self->{_print_header} = 1; $self->SUPER::clear_state(@args); } @@ -2842,8 +2912,11 @@ my %actions = ( "Enter a disk/device pattern: " ), 'q' => sub { return 'last' }, 'p' => sub { + my (@args) = @_; print "Paused - press any key to continue\n"; - pause(@_); + pause(@args); + $Diskstats::printed_lines--; + print_header(@args) unless $Diskstats::printed_lines; return; }, ' ' => \&print_header, @@ -2957,8 +3030,8 @@ sub run_interactive { && !grep { $input eq $_ } qw( A S D ), ' ', "\n" ) { my $obj = $o->get("current_group_by_obj"); - local $obj->{_print_header} = 1; $obj->clear_state(); + local $obj->{force_header} = 1; group_by( select_obj => $sel, OptionParser => $o, @@ -3046,8 +3119,7 @@ sub print_header { my $obj = $o->get("current_group_by_obj"); my ($header) = $obj->design_print_formats(); - local $obj->{_print_header} = 1; - return $obj->print_header($header, "#ts", "device"); + return $obj->force_print_header($header, "#ts", "device"); } sub group_by { @@ -3089,7 +3161,7 @@ sub group_by { header_callback => $header_callback, ); $obj->set_interactive(1); - $obj->{_print_header} = 0; + $obj->set_force_header(0); } } @@ -3107,7 +3179,7 @@ sub help { $re ||= '(none)'; } - print <<"HELP"; + my $help = <<"HELP"; You can control this program by key presses: ------------------- Key ------------------- ---- Current Setting ---- A, D, S) Set the group-by mode $mode @@ -3120,7 +3192,17 @@ sub help { q) Quit the program ------------------- Press any key to continue ----------------------- HELP - pause(@_); + print $help; +=begin IGNORE + + my $lines = $help =~ tr/\n//; + + while ( $lines-- ) { + $Diskstats::printed_lines--; + print_header(%args) unless $Diskstats::printed_lines; + } +=cut + pause(%args); return; } @@ -3169,6 +3251,7 @@ sub get_blocking_input { ReadKeyMini::cbreak(); STDIN->blocking(0); + return $new_opt; } @@ -3553,6 +3636,12 @@ When in interactive mode, wait N seconds before printing to the screen. Show inactive devices. +=item --[no]automatic-headers + +default: yes + +Print the headers as often as needed to prevent it from scrolling out of view. + =item --help Show help and exit. diff --git a/lib/Diskstats.pm b/lib/Diskstats.pm index 3f075256..7d72677f 100644 --- a/lib/Diskstats.pm +++ b/lib/Diskstats.pm @@ -32,6 +32,10 @@ use constant PTDEBUG => $ENV{PTDEBUG} || 0; use IO::Handle; use List::Util qw( max first ); +use ReadKeyMini qw( GetTerminalSize ); + +my (undef, $max_lines) = GetTerminalSize; + my $diskstat_colno_for; BEGIN { $diskstat_colno_for = { @@ -79,9 +83,11 @@ sub new { block_size => 512, show_inactive => $o->get('show-inactive'), sample_time => $o->get('sample-time') || 0, - columns_regex => qr/$columns/, - devices_regex => $devices ? qr/$devices/ : undef, + automatic_headers => $o->get('automatic-headers'), + columns_regex => qr/$columns/, + devices_regex => $devices ? qr/$devices/ : undef, interactive => 0, + force_header => 1, %args, @@ -110,7 +116,6 @@ sub new { # Internal for now, but might need APIfying. _save_curr_as_prev => 1, - _print_header => 1, }; return bless $self, $class; @@ -118,6 +123,32 @@ sub new { # The next lot are accessors, plus some convenience functions. +sub active_device { + my ( $self, $dev ) = @_; + return $self->{_active_devices}->{$dev}; +} + +sub set_active_device { + my ($self, $dev, $val) = @_; + return $self->{_active_devices}->{$dev} = $val; +} + +sub clear_active_devices { + my ( $self ) = @_; + return $self->{_active_devices} = {}; +} + + +sub automatic_headers { + my ($self) = @_; + return $self->{automatic_headers}; +} + +sub set_automatic_headers { + my ($self, $new_val) = @_; + return $self->{automatic_headers} = $new_val; +} + sub curr_ts { my ($self) = @_; return $self->{_ts}->{curr} || 0; @@ -242,9 +273,19 @@ sub add_ordered_dev { # clear_stuff methods. Like the name says, they clear state stored inside # the object. +sub force_header { + my ($self) = @_; + return $self->{force_header}; +} + +sub set_force_header { + my ($self, $new_val) = @_; + return $self->{force_header} = $new_val; +} + sub clear_state { my ($self) = @_; - $self->{_print_header} = 1; + $self->set_force_header(1); $self->clear_curr_stats(); $self->clear_prev_stats(); $self->clear_first_stats(); @@ -740,22 +781,21 @@ sub _print_device_if { # device_regex was set explicitly, either through --devices-regex, # or by using the d option in interactive mode, and not leaving # it blank + $self->_mark_if_active($dev); return $dev if $dev =~ $dev_re; } else { - if ( $self->show_inactive() || $self->active_device($dev) ) { + if ( $self->active_device($dev) ) { # If --show-interactive is enabled, or we've seen # the device be active at least once. return $dev; } + elsif ( $self->show_inactive() ) { + $self->_mark_if_active($dev); + return $dev; + } else { - my $curr = $self->stats_for($dev); - my $first = $self->first_stats_for($dev); - if ( first { $curr->[$_] != $first->[$_] } READS..MS_WEIGHTED ) { - # It's different from the first one. Mark as active and return. - $self->set_active_device($dev, 1); - return $dev; - } + return $dev if $self->_mark_if_active($dev); } } # Not active, add it to the list of skips for debugging. @@ -763,6 +803,25 @@ sub _print_device_if { return; } +sub _mark_if_active { + my ($self, $dev) = @_; + + return $dev if $self->active_device($dev); + + my $curr = $self->stats_for($dev); + my $first = $self->first_stats_for($dev); + + return unless $curr && $first; + + # read 'any' instead of 'first' + if ( first { $curr->[$_] != $first->[$_] } READS..MS_WEIGHTED ) { + # It's different from the first one. Mark as active and return. + $self->set_active_device($dev, 1); + return $dev; + } + return; +} + sub _calc_stats_for_deltas { my ( $self, $elapsed ) = @_; my @end_stats; @@ -827,32 +886,31 @@ sub _calc_deltas { return $self->_calc_stats_for_deltas($elapsed); } +# Always print a header, disgreard the value of $self->force_header() +sub force_print_header { + my ($self, @args) = @_; + my $orig = $self->force_header(); + $self->force_header(1); + $self->print_header(@args); + $self->force_header($orig); + return; +} + sub print_header { my ($self, $header, @args) = @_; - if ( $self->{_print_header} ) { + if ( $self->force_header() ) { printf $header . "\n", @args; + $Diskstats::printed_lines--; + $Diskstats::printed_lines ||= $max_lines; } -} - -sub active_device { - my ( $self, $dev ) = @_; - return $self->{_active_devices}->{$dev}; -} - -sub set_active_device { - my ($self, $dev, $val) = @_; - return $self->{_active_devices}->{$dev} = $val; -} - -sub clear_active_devices { - my ( $self ) = @_; - return $self->{_active_devices} = {}; + return; } sub print_rows { my ($self, $format, $cols, $stat) = @_; printf $format . "\n", @{ $stat }{ qw( line_ts dev ), @$cols }; + $Diskstats::printed_lines--; } sub print_deltas { @@ -870,11 +928,34 @@ sub print_deltas { my $header_method = $args{header_callback} || "print_header"; my $rows_method = $args{rows_callback} || "print_rows"; + + $Diskstats::printed_lines ||= $max_lines; $self->$header_method( $header, "#ts", "device" ); - foreach my $stat ( $self->_calc_deltas() ) { - $self->$rows_method( $format, $cols, $stat ); + my @stats = $self->_calc_deltas(); + + # Split the stats in chunks no greater than how many lines + # we have left until printing the next header. + while ( my @stats_chunk = splice @stats, 0, $Diskstats::printed_lines ) { + # Print the stats + foreach my $stat ( @stats_chunk ) { + $self->$rows_method( $format, $cols, $stat ); + } + + if ( $Diskstats::printed_lines == 0 ) { + # If zero, reset the counter + $Diskstats::printed_lines ||= $max_lines; + + # If we are automagically printing headers and aren't in + # --group-by all, + if ( $self->automatic_headers() + && !$self->isa("DiskstatsGroupByAll") ) + { + local $self->{force_header} = 1; + $self->$header_method( $header, "#ts", "device" ); + } + } } } diff --git a/lib/DiskstatsGroupByDisk.pm b/lib/DiskstatsGroupByDisk.pm index f1a009b6..f825309c 100644 --- a/lib/DiskstatsGroupByDisk.pm +++ b/lib/DiskstatsGroupByDisk.pm @@ -34,7 +34,6 @@ sub new { my ($class, %args) = @_; my $self = $class->SUPER::new(%args); $self->{_iterations} = 0; - $self->{_print_header} = 1; return $self; } @@ -65,12 +64,12 @@ sub group_by { header_callback => sub { my ($self, @args) = @_; - if ( $self->{_print_header} ) { + if ( $self->force_header() ) { my $method = $args{header_callback} || "print_header"; $self->$method(@args); } - $self->{_print_header} = undef; + $self->set_force_header(undef); }, rows_callback => $args{rows_callback}, ); @@ -116,10 +115,10 @@ sub group_by { sub clear_state { my ($self, @args) = @_; - my $orig_print_h = $self->{_print_header}; + my $orig_print_h = $self->{force_header}; $self->{_iterations} = 0; $self->SUPER::clear_state(@args); - $self->{_print_header} = $orig_print_h; + $self->{force_header} = $orig_print_h; } sub compute_line_ts { diff --git a/lib/DiskstatsGroupBySample.pm b/lib/DiskstatsGroupBySample.pm index 99e49b80..2f594d74 100644 --- a/lib/DiskstatsGroupBySample.pm +++ b/lib/DiskstatsGroupBySample.pm @@ -35,7 +35,6 @@ sub new { my $self = $class->SUPER::new(%args); $self->{_iterations} = 0; $self->{_save_curr_as_prev} = 0; - $self->{_print_header} = 1; return $self; } @@ -86,10 +85,10 @@ sub _sample_callback { header_callback => sub { my ( $self, $header, @args ) = @_; - if ( $self->{_print_header} ) { + if ( $self->force_header() ) { my $method = $args{header_callback} || "print_header"; $self->$method( $header, @args ); - $self->{_print_header} = undef; + $self->set_force_header(undef); } }, rows_callback => sub { @@ -122,7 +121,6 @@ sub clear_state { my ( $self, @args ) = @_; $self->{_iterations} = 0; $self->{_save_curr_as_prev} = 0; - $self->{_print_header} = 1; $self->SUPER::clear_state(@args); } diff --git a/lib/DiskstatsMenu.pm b/lib/DiskstatsMenu.pm index e14466ba..ecfb9710 100644 --- a/lib/DiskstatsMenu.pm +++ b/lib/DiskstatsMenu.pm @@ -190,7 +190,7 @@ sub run_interactive { my $obj = $o->get("current_group_by_obj"); # Force it to print the header $obj->clear_state(); - local $obj->{_print_header} = 1; + local $obj->{force_header} = 1; group_by( select_obj => $sel, OptionParser => $o, @@ -291,8 +291,7 @@ sub print_header { my $obj = $o->get("current_group_by_obj"); my ($header) = $obj->design_print_formats(); - local $obj->{_print_header} = 1; - return $obj->print_header($header, "#ts", "device"); + return $obj->force_print_header($header, "#ts", "device"); } sub group_by { @@ -341,7 +340,7 @@ sub group_by { header_callback => $header_callback, ); $obj->set_interactive(1); - $obj->{_print_header} = 0; + $obj->set_force_header(0); } } @@ -372,7 +371,8 @@ sub help { q) Quit the program ------------------- Press any key to continue ----------------------- HELP - pause(@_); + + pause(%args); return; } @@ -421,6 +421,7 @@ sub get_blocking_input { ReadKeyMini::cbreak(); STDIN->blocking(0); + return $new_opt; } diff --git a/lib/ReadKeyMini.pm b/lib/ReadKeyMini.pm index d016585a..2ff8c154 100644 --- a/lib/ReadKeyMini.pm +++ b/lib/ReadKeyMini.pm @@ -135,11 +135,8 @@ sub readkey { # As per perlfaq8: -sub _GetTerminalSize { - if ( @_ ) { - die "My::Term::ReadKey doesn't implement GetTerminalSize with arguments"; - } - eval { require 'sys/ioctl.ph' }; +BEGIN { + eval { no warnings; local $^W; require 'sys/ioctl.ph' }; if ( !defined &TIOCGWINSZ ) { *TIOCGWINSZ = sub () { # Very few systems actually have ioctl.ph, thus it comes to this. @@ -150,13 +147,37 @@ sub _GetTerminalSize { : 0x40087468; }; } - open( TTY, "+<", "/dev/tty" ) or die "No tty: $OS_ERROR"; - my $winsize = ''; - unless ( ioctl( TTY, &TIOCGWINSZ, $winsize ) ) { - die sprintf "$0: ioctl TIOCGWINSZ (%08x: $OS_ERROR)\n", &TIOCGWINSZ; +} + +sub _GetTerminalSize { + if ( @_ ) { + die "My::Term::ReadKey doesn't implement GetTerminalSize with arguments"; } - my ( $row, $col, $xpixel, $ypixel ) = unpack( 'S4', $winsize ); - return ( $col, $row, $xpixel, $ypixel ); + + my ( $rows, $cols ); + + if ( open( TTY, "+<", "/dev/tty" ) ) { # Got a tty + my $winsize = ''; + if ( ioctl( TTY, &TIOCGWINSZ, $winsize ) ) { + ( $rows, $cols, my ( $xpixel, $ypixel ) ) = unpack( 'S4', $winsize ); + return ( $cols, $rows, $xpixel, $ypixel ); + } + } + + if ( $rows = `tput lines` ) { + chomp($rows); + chomp($cols = `tput cols`); + } + elsif ( my $stty = `stty -a` ) { + ($rows, $cols) = $stty =~ /([0-9]+) rows; ([0-9]+) columns;/; + } + else { + ($cols, $rows) = @ENV{qw( COLUMNS LINES )}; + $cols ||= 80; + $rows ||= 24; + } + + return ( $cols, $rows ); } } diff --git a/t/lib/Diskstats.t b/t/lib/Diskstats.t index 95e8ca16..7a9e19fe 100644 --- a/t/lib/Diskstats.t +++ b/t/lib/Diskstats.t @@ -322,12 +322,12 @@ is( "compute_line_ts has a sane default", ); -$obj->{_print_header} = 0; +$obj->set_force_header(0); is( - output( sub { $obj->print_header } ), + output( sub { $obj->print_header("asdasdas") } ), "", - "INTERNAL: _print_header works" + "force_header works" ); my $output = output( @@ -472,6 +472,7 @@ for my $test ( $obj->set_columns_regex(qr/ \A (?!.*io_s$|\s*[qs]time$) /x); $obj->set_show_inactive(1); + $obj->set_automatic_headers(0); for my $filename ( map "diskstats-00$_.txt", 1..5 ) { my $file = File::Spec->catfile( "t", "pt-diskstats", "samples", $filename ); diff --git a/t/pt-diskstats/pt-diskstats.t b/t/pt-diskstats/pt-diskstats.t index 1e10f20a..1a1d4d36 100644 --- a/t/pt-diskstats/pt-diskstats.t +++ b/t/pt-diskstats/pt-diskstats.t @@ -57,7 +57,9 @@ sub test_diskstats_file { sub { tie local *STDIN, TestInteractive => @commands; pt_diskstats::main( - qw(--show-inactive --group-by), $groupby, + '--show-inactive', + '--no-automatic-headers', + '--group-by', $groupby, '--columns-regex','cnc|rt|mb|busy|prg', $file); }, @@ -69,7 +71,6 @@ sub test_diskstats_file { } } - foreach my $file ( map "diskstats-00$_.txt", 1..5 ) { test_diskstats_file(file => $file); }