xref: /memcached-1.4.29/scripts/damemtop (revision ea0fec79)
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