#!/usr/bin/perl
# llstat is a utility that takes stats files as input with optional
# clear-flag. The clear-flag is used to clear the stats file before
# printing stats information. The stats files may be in /proc/fs/lustre
# or /sys/kernel/debug/lustre. This first reads the required statistics
# information from specified stat file, process the information and prints
# the output after every interval specified by user.

# Subroutine for printing usages information
sub usage()
{
	print STDERR "Monitor operation count/rate of a subsystem\n";
	print STDERR "Usage: $pname [-c] [-d] [-g] [-h] [-i <interval>] [-n count] <stats_file>\n";
	print STDERR "       stats_file : Lustre 'stats' file to watch\n";
	print STDERR "       -i interval: polling period in seconds\n";
	print STDERR "       -c         : clear stats file first\n";
	print STDERR "       -d         : debug mode\n";
	print STDERR "       -g         : graphable output format\n";
	print STDERR "       -h         : help, display this information\n";
	print STDERR "       -n count   : number of samples printed\n";
	print STDERR "example: $pname -i 1 ost_io (ost.OSS.ost_io.stats)\n";
	print STDERR "Use CTRL + C to stop statistics printing\n";
	exit 1;
}

#Globals
my $pname = $0;
my $obddev = "";
my $obdstats = "stats";
my $clear = 0;
my $graphable = 0;
my $interval = 0;
my $statspath = "None";
my $statsname = "stats";
my $anysumsquare = 0;
my $printed_once = 0;
my %cumulhash;
my %sumhash;
my $anysum = 0;
my $starttime = 0;
my $width = 120;
my $have_readkey = 0;
my $debug = 0;
my $counter = 999999999;
my $ONE_MB = 1048576;

# Command line parameter parsing
use Getopt::Std;
getopts('cdghi:w:') or usage();
usage() if $opt_h;
$clear = 1 if $opt_c;
$debug = $opt_d if $opt_d;
$graphable = 1 if $opt_g;
$interval = $opt_i if $opt_i;
$counter = $opt_n if $opt_n;
$width = $opt_w if $opt_w;

my $i = 0;
foreach (@ARGV) {
	$obddev = $_;
	$obddev =~ s/\./\//g;
	$i++;
	if ($i > 1) {
		print "ERROR: extra argument $_\n";
		usage();
	}
}
if (!$obddev) {
	print "ERROR: Need to specify stats_file\n";
	usage();
}

# Process arguments
my $procpath = "/sys/kernel/debug/lustre";
foreach my $param ( "$obddev", "$obddev*/$obdstats", "$obddev*/*/$obdstats",
		    "*/$obddev*/$obdstats", "*/*/$obddev*/$obdstats" ) {
	if ($debug) {
		print "trying $procpath/$param\n";
	}
	my $st = glob("$procpath/$param");
	if ($debug) {
		print "glob $procpath/$param = $st\n";
	}
	if (-f "$st") {
		$statspath = $st;
		$statsname = `lctl list_param $param | head -n 1`;
		if ($debug) {
			print "using '$statsname' from $statspath\n"
		}
		last;
	}
}
if ($statspath =~ /^None$/) {
	# some older stats are kept in /proc, but don't look there first
	$procpath = "/proc/fs/lustre";

	foreach my $param ( "$obddev", "$obddev*/$obdstats", "$obddev*/*/$obdstats",
			    "*/$obddev*/$obdstats", "*/*/$obddev*/$obdstats" ) {
		if ($debug) {
			print "trying $procpath/$param\n";
		}
		$st = glob("$procpath/$param");
		if ($debug) {
			print "glob $procpath/$param = $st\n";
		}
		if (-f "$st") {
			$statspath = $st;
			$statsname = `lctl list_param $param | head -n 1`;
			if ($debug) {
				print "using $statspath\n"
			}
			last;
		}
	}
	if ($statspath =~ /^None$/) {
		die "Cannot locate stat file for: $obddev\n";
	}
}

sub print_headings()
{	my $cc = $_[0];
	if ($graphable) {
		if (!$printed_once && $interval) {
			printf "Timestamp [Name Count Rate Total Unit]...";
			printf "\n--------------------------------------------------------------------";
			$printed_once = 1;
		}
		if ($cc && !$starttime) {
			$starttime = $cc
		}
		printf "\n%-5lu", ($cc - $starttime);
	} else {
		printf "$statsname @ $cc\n";
		if ($width <= 90) {
			printf "%-20s %-6s %-9s", "Name", "Rate", "#Events";
		} else {
			printf "%-23s %-6s %-6s %-10s", "Name", "Count", "Rate", "#Events";
		}
		if ($anysum) {
			printf "%-7s %8s %6s %8s %10s", "Unit", "last", "min", "avg", "max";
		}
		if ($anysumsquare && $width >= 100) {
			printf " %8s", "stddev";
		}
		printf "\n";
	}
}

# known units are: reqs, bytes, usec, bufs, regs, pages
# readstat subroutine reads and processes statistics from stats file.
# This subroutine gets called after every interval specified by user.
sub readstat()
{
	seek STATS, 0, 0;
	while (<STATS>) {
	chop;
	($name, $cumulcount, $samples, $unit, $min, $max, $sum, $sumsquare) =
		split(/\s+/, $_);
	$prevcount = $cumulhash->{$name};
	if (defined($prevcount)) {
		$diff = $cumulcount - $prevcount;
		if ($name =~ /^snapshot_time/) {
			$tdiff = int($diff);
			&print_headings($cumulcount);
			$| = 1;
			if ($tdiff == 0) {
				$tdiff = 1; # avoid division by zero
			}
		}
		elsif ($cumulcount!=0) {
			if ($graphable) {
				my $myunit = $unit;
				my $myname = $name;
				if (defined($sum)) {
					$myunit = "[reqs]";
					$myname = $myname . "_rq";
				}
				printf "   %s %lu %lu %lu %s", $myname, $diff,
					($diff/$tdiff), $cumulcount, $myunit;
			} else {
				if ($width <= 90) {
					printf "%-20.20s %-6lu %-9lu",
						$name, ($diff/$tdiff), $cumulcount;
				} else {
					printf "%-23.23s %-6lu %-6lu %-10lu", $name,
						$diff, ($diff/$tdiff), $cumulcount;
				}
			}
			if (defined($sum)) {
				my $sum_orig = $sum;
				my $sum_diff = $sum - $sumhash->{$name};
				if ($graphable) {
					printf "   %s %lu %lu %lu %s", $name,
						$diff ? ($sum_diff / $diff) : 0,
						($sum_diff/$tdiff), $sum, $unit;
				} else {
					printf "%-7s %8lu %6lu %8lu %10lu", $unit,
						$diff ? ($sum_diff / $diff) : 0, $min,
						($sum/$cumulcount), $max;
				}
				if (defined($sumsquare) && $width >= 100) {
					my $s = $sumsquare - (($sum_orig * $sum_orig) /
							      $cumulcount);
					if ($s >= 0) {
						my $cnt = ($cumulcount >= 2) ?
							  $cumulcount : 2 ;
						my $stddev = sqrt($s/($cnt - 1));
						if (!$graphable) {
							printf " %8lu", $stddev;
						}
					}
				}
			}
			if (!$graphable) {
				printf "\n";
			}
			$| = 1;
		}
	} else {
		if ($cumulcount != 0) {
			# print info when interval is not specified.
			printf "%-25s $cumulcount\n", $name
		}
		if (defined($sum)) {
			$anysum = 1;
		}
		if (defined($sumsquare)) {
			$anysumsquare = 1;
		}
	}
	$cumulhash->{$name} = $cumulcount;
	$sumhash->{$name} = $sum;
	} #end of while

	if (!$graphable) {
		printf "\n";
	}
}

# check if Term::ReadKey is installed for efficient tty size, but OK if missing
eval "require Term::ReadKey" or $have_readkey = 0;
if ($debug) {
	print "have_readkey=$have_readkey\n";
}
if ($have_readkey) {
	eval "use Term::ReadKey";
}

# Clears stats file before printing information in intervals
if ($clear) {
	open(STATS, "> $statspath") || die "Cannot clear $statspath: $!\n";
	print STATS " ";
	close STATS;
	sleep($interval);
}

$hostname = `lctl list_nids | head -1 2> /dev/null`;
chop($hostname);
printf "%s: %s at %s on %s\n", $pname, $statsname, `date`, $hostname;
open(STATS, $statspath) || die "Cannot open $statspath: $!\n";
do {
	# find the terminal width
	if (!$opt_w) {
		if ($have_readkey) {
			($width, $height, $wpixels, $hpixels) = GetTerminalSize();
		} else {
			($height, $width) = split / /, `stty size 2> /dev/null`;
			#$width = 120 if ! $width
		}
	}

	readstat();
	if ($interval) {
		sleep($interval);
	}
} while ($interval && $counter-- > 0);
close STATS;
