1*ea0fec79Sdormando#!/usr/bin/perl 2*ea0fec79Sdormando# dormando's awesome memcached top utility! 3*ea0fec79Sdormando# 4*ea0fec79Sdormando# Copyright 2009 Dormando ([email protected]). All rights reserved. 5*ea0fec79Sdormando# 6*ea0fec79Sdormando# Use and distribution licensed under the BSD license. See 7*ea0fec79Sdormando# the COPYING file for full text. 8*ea0fec79Sdormando 9*ea0fec79Sdormandouse strict; 10*ea0fec79Sdormandouse warnings FATAL => 'all'; 11*ea0fec79Sdormando 12*ea0fec79Sdormandouse AnyEvent; 13*ea0fec79Sdormandouse AnyEvent::Socket; 14*ea0fec79Sdormandouse AnyEvent::Handle; 15*ea0fec79Sdormandouse Getopt::Long; 16*ea0fec79Sdormandouse YAML qw/Dump Load LoadFile/; 17*ea0fec79Sdormandouse Term::ReadKey qw/ReadMode ReadKey GetTerminalSize/; 18*ea0fec79Sdormando 19*ea0fec79Sdormandoour $VERSION = '0.1'; 20*ea0fec79Sdormando 21*ea0fec79Sdormandomy $CLEAR = `clear`; 22*ea0fec79Sdormandomy @TERM_SIZE = (); 23*ea0fec79Sdormando$|++; 24*ea0fec79Sdormando 25*ea0fec79Sdormandomy %opts = (); 26*ea0fec79SdormandoGetOptions(\%opts, 'help|h', 'config=s'); 27*ea0fec79Sdormando 28*ea0fec79Sdormandoif ($opts{help}) { 29*ea0fec79Sdormando show_help(); exit; 30*ea0fec79Sdormando} 31*ea0fec79Sdormando 32*ea0fec79Sdormando$SIG{INT} = sub { 33*ea0fec79Sdormando ReadMode('normal'); 34*ea0fec79Sdormando print "\n"; 35*ea0fec79Sdormando exit; 36*ea0fec79Sdormando}; 37*ea0fec79Sdormando 38*ea0fec79Sdormando# TODO: make this load from central location, and merge in homedir changes. 39*ea0fec79Sdormando# then merge Getopt::Long stuff on top of that 40*ea0fec79Sdormando# TODO: Set a bunch of defaults and merge in. 41*ea0fec79Sdormandomy $CONF = load_config(); 42*ea0fec79Sdormandomy %CONS = (); 43*ea0fec79Sdormandomy $LAST_RUN = time; # time after the last loop cycle. 44*ea0fec79Sdormandomy $TIME_SINCE_LAST_RUN = time; # time since last loop cycle. 45*ea0fec79Sdormandomy $loop_timer; 46*ea0fec79Sdormandomy $main_cond; 47*ea0fec79Sdormandomy $prev_stats_results; 48*ea0fec79Sdormando 49*ea0fec79Sdormandomy %display_modes = ( 50*ea0fec79Sdormando 't' => \&display_top_mode, 51*ea0fec79Sdormando '?' => \&display_help_mode, 52*ea0fec79Sdormando 'h' => \&display_help_mode, 53*ea0fec79Sdormando); 54*ea0fec79Sdormando 55*ea0fec79Sdormandomy %column_compute = ( 56*ea0fec79Sdormando 'hostname' => { stats => [], code => \&compute_hostname}, 57*ea0fec79Sdormando 'hit_rate' => { stats => ['get_hits', 'get_misses'], 58*ea0fec79Sdormando code => \&compute_hit_rate }, 59*ea0fec79Sdormando 'fill_rate' => { stats => ['bytes', 'limit_maxbytes'], 60*ea0fec79Sdormando code => \&compute_fill_rate }, 61*ea0fec79Sdormando); 62*ea0fec79Sdormando 63*ea0fec79Sdormandomy %column_format = ( 64*ea0fec79Sdormando 'hit_rate' => \&format_percent, 65*ea0fec79Sdormando 'fill_rate' => \&format_percent, 66*ea0fec79Sdormando); 67*ea0fec79Sdormando 68*ea0fec79Sdormando# This can collapse into %column_compute 69*ea0fec79Sdormandomy %column_format_totals = ( 70*ea0fec79Sdormando 'hit_rate' => 0, 71*ea0fec79Sdormando 'fill_rate' => 0, 72*ea0fec79Sdormando); 73*ea0fec79Sdormando 74*ea0fec79SdormandoReadMode('cbreak'); 75*ea0fec79Sdormandomy $LAST_KEY = ''; 76*ea0fec79Sdormandomy $read_keys = AnyEvent->io ( 77*ea0fec79Sdormando fh => \*STDIN, poll => 'r', 78*ea0fec79Sdormando cb => sub { 79*ea0fec79Sdormando $LAST_KEY = ReadKey(-1); 80*ea0fec79Sdormando # If there is a running timer, cancel it. 81*ea0fec79Sdormando # Don't want to interrupt a main loop run. 82*ea0fec79Sdormando # fire_main_loop()'s iteration will pick up the keypress. 83*ea0fec79Sdormando if ($loop_timer) { 84*ea0fec79Sdormando $loop_timer = undef; 85*ea0fec79Sdormando $main_cond->send; 86*ea0fec79Sdormando } 87*ea0fec79Sdormando } 88*ea0fec79Sdormando); 89*ea0fec79Sdormando 90*ea0fec79Sdormando# start main loop 91*ea0fec79Sdormandofire_main_loop(); 92*ea0fec79Sdormando 93*ea0fec79Sdormando### AnyEvent related code. 94*ea0fec79Sdormando 95*ea0fec79Sdormandosub fire_main_loop { 96*ea0fec79Sdormando for (;;) { 97*ea0fec79Sdormando $loop_timer = undef; 98*ea0fec79Sdormando $main_cond = AnyEvent->condvar; 99*ea0fec79Sdormando my $time_taken = main_loop(); 100*ea0fec79Sdormando my $delay = $CONF->{delay} - $time_taken; 101*ea0fec79Sdormando $delay = 0 if $delay < 0; 102*ea0fec79Sdormando $loop_timer = AnyEvent->timer( 103*ea0fec79Sdormando after => $delay, 104*ea0fec79Sdormando cb => $main_cond, 105*ea0fec79Sdormando ); 106*ea0fec79Sdormando $main_cond->recv; 107*ea0fec79Sdormando } 108*ea0fec79Sdormando} 109*ea0fec79Sdormando 110*ea0fec79Sdormandosub main_loop { 111*ea0fec79Sdormando my $start = AnyEvent->now; # use ->time to find the end. 112*ea0fec79Sdormando maintain_connections(); 113*ea0fec79Sdormando 114*ea0fec79Sdormando my $cv = AnyEvent->condvar; 115*ea0fec79Sdormando 116*ea0fec79Sdormando # FIXME: Need to dump early if there're no connected conns 117*ea0fec79Sdormando # FIXME: Make this only fetch stats from cons we care to visualize? 118*ea0fec79Sdormando # maybe keep everything anyway to maintain averages? 119*ea0fec79Sdormando my %stats_results = (); 120*ea0fec79Sdormando while (my ($hostname, $con) = each %CONS) { 121*ea0fec79Sdormando $cv->begin; 122*ea0fec79Sdormando call_stats($con, ['', 'items', 'slabs'], sub { 123*ea0fec79Sdormando $stats_results{$hostname} = shift; 124*ea0fec79Sdormando $cv->end; 125*ea0fec79Sdormando }); 126*ea0fec79Sdormando } 127*ea0fec79Sdormando $cv->recv; 128*ea0fec79Sdormando 129*ea0fec79Sdormando # Short circuit since we don't have anything to compare to. 130*ea0fec79Sdormando unless ($prev_stats_results) { 131*ea0fec79Sdormando $prev_stats_results = \%stats_results; 132*ea0fec79Sdormando return $CONF->{delay}; 133*ea0fec79Sdormando } 134*ea0fec79Sdormando 135*ea0fec79Sdormando # Semi-exact global time diff for stats that want to average 136*ea0fec79Sdormando # themselves per-second. 137*ea0fec79Sdormando my $this_run = AnyEvent->time; 138*ea0fec79Sdormando $TIME_SINCE_LAST_RUN = $this_run - $LAST_RUN; 139*ea0fec79Sdormando $LAST_RUN = $this_run; 140*ea0fec79Sdormando 141*ea0fec79Sdormando # Done all our fetches. Drive the display. 142*ea0fec79Sdormando display_run($prev_stats_results, \%stats_results); 143*ea0fec79Sdormando $prev_stats_results = \%stats_results; 144*ea0fec79Sdormando 145*ea0fec79Sdormando my $end = AnyEvent->time; 146*ea0fec79Sdormando my $diff = $LAST_RUN - $start; 147*ea0fec79Sdormando print "loop took: $diff"; 148*ea0fec79Sdormando return $diff; 149*ea0fec79Sdormando} 150*ea0fec79Sdormando 151*ea0fec79Sdormandosub maintain_connections { 152*ea0fec79Sdormando my $cv = AnyEvent->condvar; 153*ea0fec79Sdormando 154*ea0fec79Sdormando $cv->begin (sub { shift->send }); 155*ea0fec79Sdormando for my $host (@{$CONF->{servers}}) { 156*ea0fec79Sdormando next if $CONS{$host}; 157*ea0fec79Sdormando $cv->begin; 158*ea0fec79Sdormando $CONS{$host} = connect_memcached($host, sub { 159*ea0fec79Sdormando if ($_[0] eq 'err') { 160*ea0fec79Sdormando print "Failed connecting to $host: ", $_[1], "\n"; 161*ea0fec79Sdormando delete $CONS{$host}; 162*ea0fec79Sdormando } 163*ea0fec79Sdormando $cv->end; 164*ea0fec79Sdormando }); 165*ea0fec79Sdormando } 166*ea0fec79Sdormando $cv->end; 167*ea0fec79Sdormando 168*ea0fec79Sdormando $cv->recv; 169*ea0fec79Sdormando} 170*ea0fec79Sdormando 171*ea0fec79Sdormandosub connect_memcached { 172*ea0fec79Sdormando my ($fullhost, $cb) = @_; 173*ea0fec79Sdormando my ($host, $port) = split /:/, $fullhost; 174*ea0fec79Sdormando 175*ea0fec79Sdormando my $con; $con = AnyEvent::Handle->new ( 176*ea0fec79Sdormando connect => [$host => $port], 177*ea0fec79Sdormando on_connect => sub { 178*ea0fec79Sdormando $cb->('con'); 179*ea0fec79Sdormando }, 180*ea0fec79Sdormando on_connect_error => sub { 181*ea0fec79Sdormando $cb->('err', $!); 182*ea0fec79Sdormando $con->destroy; 183*ea0fec79Sdormando }, 184*ea0fec79Sdormando on_eof => sub { 185*ea0fec79Sdormando $cb->('err', $!); 186*ea0fec79Sdormando $con->destroy; 187*ea0fec79Sdormando }, 188*ea0fec79Sdormando ); 189*ea0fec79Sdormando return $con; 190*ea0fec79Sdormando} 191*ea0fec79Sdormando 192*ea0fec79Sdormando# Function's getting a little weird since I started optimizing it. 193*ea0fec79Sdormando# As of my first set of production tests, this routine is where we spend 194*ea0fec79Sdormando# almost all of our processing time. 195*ea0fec79Sdormandosub call_stats { 196*ea0fec79Sdormando my ($con, $cmds, $cb) = @_; 197*ea0fec79Sdormando 198*ea0fec79Sdormando my $stats = {}; 199*ea0fec79Sdormando my $num_types = @$cmds; 200*ea0fec79Sdormando 201*ea0fec79Sdormando my $reader; $reader = sub { 202*ea0fec79Sdormando my ($con, $results) = @_; 203*ea0fec79Sdormando { 204*ea0fec79Sdormando my %temp = (); 205*ea0fec79Sdormando for my $line (split(/\n/, $results)) { 206*ea0fec79Sdormando my ($k, $v) = (split(/\s+/, $line))[1,2]; 207*ea0fec79Sdormando $temp{$k} = $v; 208*ea0fec79Sdormando } 209*ea0fec79Sdormando $stats->{$cmds->[0]} = \%temp; 210*ea0fec79Sdormando } 211*ea0fec79Sdormando shift @$cmds; 212*ea0fec79Sdormando unless (@$cmds) { 213*ea0fec79Sdormando # Out of commands to process, return goodies. 214*ea0fec79Sdormando $cb->($stats); 215*ea0fec79Sdormando return; 216*ea0fec79Sdormando } 217*ea0fec79Sdormando }; 218*ea0fec79Sdormando 219*ea0fec79Sdormando for my $cmd (@$cmds) { 220*ea0fec79Sdormando $con->push_write('stats ' . $cmd . "\n"); 221*ea0fec79Sdormando $stats->{$cmd} = {}; 222*ea0fec79Sdormando $con->push_read(line => "END\r\n", $reader); 223*ea0fec79Sdormando } 224*ea0fec79Sdormando} 225*ea0fec79Sdormando 226*ea0fec79Sdormando### Compute routines 227*ea0fec79Sdormando 228*ea0fec79Sdormandosub compute_hostname { 229*ea0fec79Sdormando return $_[0]; 230*ea0fec79Sdormando} 231*ea0fec79Sdormando 232*ea0fec79Sdormandosub compute_hit_rate { 233*ea0fec79Sdormando my $s = $_[1]; 234*ea0fec79Sdormando my $total = $s->{get_hits} + $s->{get_misses}; 235*ea0fec79Sdormando return 'NA' unless $total; 236*ea0fec79Sdormando return $s->{get_hits} / $total; 237*ea0fec79Sdormando} 238*ea0fec79Sdormando 239*ea0fec79Sdormandosub compute_fill_rate { 240*ea0fec79Sdormando my $s = $_[1]; 241*ea0fec79Sdormando return $s->{bytes} / $s->{limit_maxbytes}; 242*ea0fec79Sdormando} 243*ea0fec79Sdormando 244*ea0fec79Sdormandosub format_column { 245*ea0fec79Sdormando my ($col, $val) = @_; 246*ea0fec79Sdormando my $res; 247*ea0fec79Sdormando $col =~ s/^all_//; 248*ea0fec79Sdormando if ($column_format{$col}) { 249*ea0fec79Sdormando if (ref($column_format{$col}) eq 'CODE') { 250*ea0fec79Sdormando return $column_format{$col}->($val); 251*ea0fec79Sdormando } else { 252*ea0fec79Sdormando return $val .= $column_format{$col}; 253*ea0fec79Sdormando } 254*ea0fec79Sdormando } else { 255*ea0fec79Sdormando return format_commas($val); 256*ea0fec79Sdormando } 257*ea0fec79Sdormando} 258*ea0fec79Sdormando 259*ea0fec79Sdormandosub column_can_total { 260*ea0fec79Sdormando my $col = shift; 261*ea0fec79Sdormando $col =~ s/^all_//; 262*ea0fec79Sdormando return 1 unless exists $column_format_totals{$col}; 263*ea0fec79Sdormando return $column_format_totals{$col}; 264*ea0fec79Sdormando} 265*ea0fec79Sdormando 266*ea0fec79Sdormando### Display routines 267*ea0fec79Sdormando 268*ea0fec79Sdormando# If there isn't a specific column type computer, see if we just want to 269*ea0fec79Sdormando# look at the specific stat and return it. 270*ea0fec79Sdormando# If column is a generic type and of 'all_cmd_get' format, return the more 271*ea0fec79Sdormando# complete stat instead of the diffed stat. 272*ea0fec79Sdormandosub compute_column { 273*ea0fec79Sdormando my ($col, $host, $prev_stats, $curr_stats) = @_; 274*ea0fec79Sdormando my $diff_stats = 1; 275*ea0fec79Sdormando $diff_stats = 0 if ($col =~ s/^all_//); 276*ea0fec79Sdormando 277*ea0fec79Sdormando # Really should decide on whether or not to flatten the hash :/ 278*ea0fec79Sdormando my $find_stat = sub { 279*ea0fec79Sdormando for my $type (keys %{$_[0]}) { 280*ea0fec79Sdormando return $_[0]->{$type}->{$_[1]} if exists $_[0]->{$type}->{$_[1]}; 281*ea0fec79Sdormando } 282*ea0fec79Sdormando }; 283*ea0fec79Sdormando 284*ea0fec79Sdormando my $diff_stat = sub { 285*ea0fec79Sdormando my $stat = shift; 286*ea0fec79Sdormando return 'NA' unless defined $find_stat->($curr_stats, $stat); 287*ea0fec79Sdormando if ($diff_stats) { 288*ea0fec79Sdormando my $diff = eval { 289*ea0fec79Sdormando return ($find_stat->($curr_stats, $stat) 290*ea0fec79Sdormando - $find_stat->($prev_stats, $stat)) 291*ea0fec79Sdormando / $TIME_SINCE_LAST_RUN; 292*ea0fec79Sdormando }; 293*ea0fec79Sdormando return 'NA' if ($@); 294*ea0fec79Sdormando return $diff; 295*ea0fec79Sdormando } else { 296*ea0fec79Sdormando return $find_stat->($curr_stats, $stat); 297*ea0fec79Sdormando } 298*ea0fec79Sdormando }; 299*ea0fec79Sdormando 300*ea0fec79Sdormando if (my $comp = $column_compute{$col}) { 301*ea0fec79Sdormando my %s = (); 302*ea0fec79Sdormando for my $stat (@{$comp->{stats}}) { 303*ea0fec79Sdormando $s{$stat} = $diff_stat->($stat); 304*ea0fec79Sdormando } 305*ea0fec79Sdormando return $comp->{code}->($host, \%s); 306*ea0fec79Sdormando } else { 307*ea0fec79Sdormando return $diff_stat->($col); 308*ea0fec79Sdormando } 309*ea0fec79Sdormando return 'NA'; 310*ea0fec79Sdormando} 311*ea0fec79Sdormando 312*ea0fec79Sdormando# We have a bunch of stats from a bunch of connections. 313*ea0fec79Sdormando# At this point we run a particular display mode, capture the lines, then 314*ea0fec79Sdormando# truncate and display them. 315*ea0fec79Sdormandosub display_run { 316*ea0fec79Sdormando my $prev_stats = shift; 317*ea0fec79Sdormando my $curr_stats = shift; 318*ea0fec79Sdormando @TERM_SIZE = GetTerminalSize; 319*ea0fec79Sdormando die "cannot detect terminal size" unless $TERM_SIZE[0] && $TERM_SIZE[1]; 320*ea0fec79Sdormando 321*ea0fec79Sdormando if ($LAST_KEY eq 'q') { 322*ea0fec79Sdormando print "\n"; 323*ea0fec79Sdormando ReadMode('normal'); exit; 324*ea0fec79Sdormando } 325*ea0fec79Sdormando 326*ea0fec79Sdormando if ($LAST_KEY ne $CONF->{mode} && exists $display_modes{$LAST_KEY}) { 327*ea0fec79Sdormando $CONF->{prev_mode} = $CONF->{mode}; 328*ea0fec79Sdormando $CONF->{mode} = $LAST_KEY; 329*ea0fec79Sdormando } elsif ($CONF->{mode} eq 'h' || $CONF->{mode} eq '?') { 330*ea0fec79Sdormando # Bust out of help mode on any key. 331*ea0fec79Sdormando $CONF->{mode} = $CONF->{prev_mode}; 332*ea0fec79Sdormando } 333*ea0fec79Sdormando my $lines = $display_modes{$CONF->{mode}}->($prev_stats, $curr_stats); 334*ea0fec79Sdormando display_lines($lines) if $lines; 335*ea0fec79Sdormando} 336*ea0fec79Sdormando 337*ea0fec79Sdormando# Default "top" mode. 338*ea0fec79Sdormando# create a set of computed columns as requested by the config. 339*ea0fec79Sdormando# this has gotten a little out of hand... needs more cleanup/abstraction. 340*ea0fec79Sdormandosub display_top_mode { 341*ea0fec79Sdormando my $prev_stats = shift; 342*ea0fec79Sdormando my $curr_stats = shift; 343*ea0fec79Sdormando 344*ea0fec79Sdormando my @columns = @{$CONF->{top_mode}->{columns}}; 345*ea0fec79Sdormando my @rows = (); 346*ea0fec79Sdormando my @tot_row = (); 347*ea0fec79Sdormando 348*ea0fec79Sdormando # Round one. 349*ea0fec79Sdormando for my $host (sort keys %{$curr_stats}) { 350*ea0fec79Sdormando my @row = (); 351*ea0fec79Sdormando for my $colnum (0 .. @columns-1) { 352*ea0fec79Sdormando my $col = $columns[$colnum]; 353*ea0fec79Sdormando my $res = compute_column($col, $host, $prev_stats->{$host}, 354*ea0fec79Sdormando $curr_stats->{$host}); 355*ea0fec79Sdormando $tot_row[$colnum] += $res if is_numeric($res); 356*ea0fec79Sdormando push @row, $res; 357*ea0fec79Sdormando } 358*ea0fec79Sdormando push(@rows, \@row); 359*ea0fec79Sdormando } 360*ea0fec79Sdormando 361*ea0fec79Sdormando # Sort rows by sort column (ascending or descending) 362*ea0fec79Sdormando if (my $sort = $CONF->{top_mode}->{sort_column}) { 363*ea0fec79Sdormando my $order = $CONF->{top_mode}->{sort_order} || 'asc'; 364*ea0fec79Sdormando my $colnum = 0; 365*ea0fec79Sdormando for (0 .. @columns-1) { $colnum = $_ if $columns[$_] eq $sort; } 366*ea0fec79Sdormando my @newrows; 367*ea0fec79Sdormando if ($order eq 'asc') { 368*ea0fec79Sdormando if (is_numeric($rows[0]->[$colnum])) { 369*ea0fec79Sdormando @newrows = sort { $a->[$colnum] <=> $b->[$colnum] } @rows; 370*ea0fec79Sdormando } else { 371*ea0fec79Sdormando @newrows = sort { $a->[$colnum] cmp $b->[$colnum] } @rows; 372*ea0fec79Sdormando } 373*ea0fec79Sdormando } else { 374*ea0fec79Sdormando if (is_numeric($rows[0]->[$colnum])) { 375*ea0fec79Sdormando @newrows = sort { $b->[$colnum] <=> $a->[$colnum] } @rows; 376*ea0fec79Sdormando } else { 377*ea0fec79Sdormando @newrows = sort { $b->[$colnum] cmp $a->[$colnum] } @rows; 378*ea0fec79Sdormando } 379*ea0fec79Sdormando } 380*ea0fec79Sdormando @rows = @newrows; 381*ea0fec79Sdormando } 382*ea0fec79Sdormando 383*ea0fec79Sdormando # Format each column after the sort... 384*ea0fec79Sdormando { 385*ea0fec79Sdormando my @newrows = (); 386*ea0fec79Sdormando for my $row (@rows) { 387*ea0fec79Sdormando my @newrow = (); 388*ea0fec79Sdormando for my $colnum (0 .. @columns-1) { 389*ea0fec79Sdormando push @newrow, is_numeric($row->[$colnum]) ? 390*ea0fec79Sdormando format_column($columns[$colnum], $row->[$colnum]) : 391*ea0fec79Sdormando $row->[$colnum]; 392*ea0fec79Sdormando } 393*ea0fec79Sdormando push @newrows, \@newrow; 394*ea0fec79Sdormando } 395*ea0fec79Sdormando @rows = @newrows; 396*ea0fec79Sdormando } 397*ea0fec79Sdormando 398*ea0fec79Sdormando # Create average and total rows. 399*ea0fec79Sdormando my @avg_row = (); 400*ea0fec79Sdormando for my $col (0 .. @columns-1) { 401*ea0fec79Sdormando if (is_numeric($tot_row[$col])) { 402*ea0fec79Sdormando my $countable_rows = 0; 403*ea0fec79Sdormando for my $row (@rows) { 404*ea0fec79Sdormando next unless $row->[$col]; 405*ea0fec79Sdormando $countable_rows++ unless $row->[$col] eq 'NA'; 406*ea0fec79Sdormando } 407*ea0fec79Sdormando $countable_rows = 1 unless $countable_rows; 408*ea0fec79Sdormando push @avg_row, format_column($columns[$col], 409*ea0fec79Sdormando sprintf('%.2f', $tot_row[$col] / $countable_rows)); 410*ea0fec79Sdormando } else { 411*ea0fec79Sdormando push @avg_row, 'NA'; 412*ea0fec79Sdormando } 413*ea0fec79Sdormando $tot_row[$col] = 'NA' unless defined $tot_row[$col]; 414*ea0fec79Sdormando $tot_row[$col] = 'NA' unless (column_can_total($columns[$col])); 415*ea0fec79Sdormando $tot_row[$col] = format_column($columns[$col], $tot_row[$col]) 416*ea0fec79Sdormando unless $tot_row[$col] eq 'NA'; 417*ea0fec79Sdormando } 418*ea0fec79Sdormando unshift @rows, \@avg_row; 419*ea0fec79Sdormando unshift @rows, ['AVERAGE:']; 420*ea0fec79Sdormando unshift @rows, \@tot_row; 421*ea0fec79Sdormando unshift @rows, ['TOTAL:']; 422*ea0fec79Sdormando 423*ea0fec79Sdormando # Round two. Pass @rows into a function which returns an array with the 424*ea0fec79Sdormando # desired format spacing for each column. 425*ea0fec79Sdormando unshift @rows, \@columns; 426*ea0fec79Sdormando my $spacing = find_optimal_spacing(\@rows); 427*ea0fec79Sdormando 428*ea0fec79Sdormando my @display_lines = (); 429*ea0fec79Sdormando for my $row (@rows) { 430*ea0fec79Sdormando my $line = ''; 431*ea0fec79Sdormando for my $col (0 .. @$row-1) { 432*ea0fec79Sdormando my $space = $spacing->[$col]; 433*ea0fec79Sdormando $line .= sprintf("%-${space}s ", $row->[$col]); 434*ea0fec79Sdormando } 435*ea0fec79Sdormando push @display_lines, $line; 436*ea0fec79Sdormando } 437*ea0fec79Sdormando 438*ea0fec79Sdormando return \@display_lines; 439*ea0fec79Sdormando} 440*ea0fec79Sdormando 441*ea0fec79Sdormandosub display_help_mode { 442*ea0fec79Sdormando my $help = <<"ENDHELP"; 443*ea0fec79Sdormando 444*ea0fec79Sdormandodormando's awesome memcached top utility version v$VERSION 445*ea0fec79Sdormando 446*ea0fec79SdormandoThis early version requires you to edit the ~/.damemtop/damemtop.yaml 447*ea0fec79Sdormando(or /etc/damemtop.yaml) file in order to change options. 448*ea0fec79SdormandoSee --help for more info. 449*ea0fec79Sdormando 450*ea0fec79SdormandoHit any key to exit help. 451*ea0fec79SdormandoENDHELP 452*ea0fec79Sdormando my @lines = split /\n/, $help; 453*ea0fec79Sdormando display_lines(\@lines); 454*ea0fec79Sdormando $LAST_KEY = ReadKey(0); 455*ea0fec79Sdormando return; 456*ea0fec79Sdormando} 457*ea0fec79Sdormando 458*ea0fec79Sdormando# Takes a set of lines, clears screen, dumps header, trims lines, etc 459*ea0fec79Sdormando# MAYBE: mode to wrap lines instead of trim them? 460*ea0fec79Sdormandosub display_lines { 461*ea0fec79Sdormando my $lines = shift; 462*ea0fec79Sdormando 463*ea0fec79Sdormando my $width = $TERM_SIZE[0]; 464*ea0fec79Sdormando my $height_remain = $TERM_SIZE[1]; 465*ea0fec79Sdormando 466*ea0fec79Sdormando unshift @$lines, display_header($width); 467*ea0fec79Sdormando clear_screen() unless $CONF->{no_clear}; 468*ea0fec79Sdormando 469*ea0fec79Sdormando while (--$height_remain && @$lines) { 470*ea0fec79Sdormando # truncate too long lines. 471*ea0fec79Sdormando my $line = shift @$lines; 472*ea0fec79Sdormando $line = substr $line, 0, $width-1; 473*ea0fec79Sdormando print $line, "\n"; 474*ea0fec79Sdormando } 475*ea0fec79Sdormando} 476*ea0fec79Sdormando 477*ea0fec79Sdormandosub display_header { 478*ea0fec79Sdormando my $topbar = 'damemtop: ' . scalar localtime; 479*ea0fec79Sdormando if ($CONF->{mode} eq 't' && $CONF->{top_mode}->{sort_column}) { 480*ea0fec79Sdormando $topbar .= ' [sort: ' . $CONF->{top_mode}->{sort_column} . ']'; 481*ea0fec79Sdormando } 482*ea0fec79Sdormando $topbar .= ' [delay: ' . $CONF->{delay} . 's]'; 483*ea0fec79Sdormando return $topbar; 484*ea0fec79Sdormando} 485*ea0fec79Sdormando 486*ea0fec79Sdormando### Utilities 487*ea0fec79Sdormando 488*ea0fec79Sdormando# find the optimal format spacing for each column, which is: 489*ea0fec79Sdormando# longest length of item in col + 2 (whitespace). 490*ea0fec79Sdormandosub find_optimal_spacing { 491*ea0fec79Sdormando my $rows = shift; 492*ea0fec79Sdormando my @maxes = (); 493*ea0fec79Sdormando 494*ea0fec79Sdormando my $num_cols = @{$rows->[0]}; 495*ea0fec79Sdormando for my $row (@$rows) { 496*ea0fec79Sdormando for my $col (0 .. $num_cols-1) { 497*ea0fec79Sdormando $maxes[$col] = 0 unless $maxes[$col]; 498*ea0fec79Sdormando next unless $row->[$col]; 499*ea0fec79Sdormando $maxes[$col] = length($row->[$col]) 500*ea0fec79Sdormando if length($row->[$col]) > $maxes[$col]; 501*ea0fec79Sdormando } 502*ea0fec79Sdormando } 503*ea0fec79Sdormando for my $col (0 .. $num_cols) { 504*ea0fec79Sdormando $maxes[$col] += 1; 505*ea0fec79Sdormando } 506*ea0fec79Sdormando 507*ea0fec79Sdormando return \@maxes; 508*ea0fec79Sdormando} 509*ea0fec79Sdormando 510*ea0fec79Sdormando# doesn't try too hard to identify numbers... 511*ea0fec79Sdormandosub is_numeric { 512*ea0fec79Sdormando return 0 unless $_[0]; 513*ea0fec79Sdormando return 1 if $_[0] =~ m/^\d+(\.\d*)?(\w+)?$/; 514*ea0fec79Sdormando return 0; 515*ea0fec79Sdormando} 516*ea0fec79Sdormando 517*ea0fec79Sdormandosub format_percent { 518*ea0fec79Sdormando return sprintf("%.2f%%", $_[0] * 100); 519*ea0fec79Sdormando} 520*ea0fec79Sdormando 521*ea0fec79Sdormandosub format_commas { 522*ea0fec79Sdormando my $num = shift; 523*ea0fec79Sdormando $num = int($num); 524*ea0fec79Sdormando $num =~ s/(^[-+]?\d+?(?=(?>(?:\d{3})+)(?!\d))|\G\d{3}(?=\d))/$1,/g; 525*ea0fec79Sdormando return $num; 526*ea0fec79Sdormando} 527*ea0fec79Sdormando 528*ea0fec79Sdormando# Can tick counters/etc here as well. 529*ea0fec79Sdormandosub clear_screen { 530*ea0fec79Sdormando print $CLEAR; 531*ea0fec79Sdormando} 532*ea0fec79Sdormando 533*ea0fec79Sdormando# tries minimally to find a localized config file. 534*ea0fec79Sdormando# TODO: Handle the YAML error and make it prettier. 535*ea0fec79Sdormandosub load_config { 536*ea0fec79Sdormando my $config = $opts{config} if $opts{config}; 537*ea0fec79Sdormando my $homedir = "$ENV{HOME}/.damemtop/damemtop.yaml"; 538*ea0fec79Sdormando if (-e $homedir) { 539*ea0fec79Sdormando $config = $homedir; 540*ea0fec79Sdormando } else { 541*ea0fec79Sdormando $config = '/etc/damemtop.yaml'; 542*ea0fec79Sdormando } 543*ea0fec79Sdormando return LoadFile($config); 544*ea0fec79Sdormando} 545*ea0fec79Sdormando 546*ea0fec79Sdormandosub show_help { 547*ea0fec79Sdormando print <<"ENDHELP"; 548*ea0fec79Sdormandodormando's awesome memcached top utility version v$VERSION 549*ea0fec79Sdormando 550*ea0fec79SdormandoThis program is copyright (c) 2009 Dormando. 551*ea0fec79SdormandoUse and distribution licensed under the BSD license. See 552*ea0fec79Sdormandothe COPYING file for full text. 553*ea0fec79Sdormando 554*ea0fec79Sdormandocontact: dormando\@rydia.net or memcached\@googlegroups.com. 555*ea0fec79Sdormando 556*ea0fec79SdormandoThis early version requires you to edit the ~/.damemtop/damemtop.yaml 557*ea0fec79Sdormando(or /etc/damemtop.yaml) file in order to change options. 558*ea0fec79Sdormando 559*ea0fec79SdormandoYou may display any column that is in the output of 560*ea0fec79Sdormando'stats', 'stats items', or 'stats slabs' from memcached's ASCII protocol. 561*ea0fec79SdormandoStart a column with 'all_' (ie; 'all_get_hits') to display the current stat, 562*ea0fec79Sdormandootherwise the stat is displayed as an average per second. 563*ea0fec79Sdormando 564*ea0fec79SdormandoSpecify a "sort_column" under "top_mode" to sort the output by any column. 565*ea0fec79Sdormando 566*ea0fec79SdormandoSome special "computed" columns exist: 567*ea0fec79Sdormandohit_rate (get/miss hit ratio) 568*ea0fec79Sdormandofill_rate (% bytes used out of the maximum memory limit) 569*ea0fec79SdormandoENDHELP 570*ea0fec79Sdormando exit; 571*ea0fec79Sdormando} 572