#!/usr/bin/python

import re
import subprocess
import math
import string

############################################
# Discover devices, and grab sysctl values #
############################################

def mypopen(cmd):
    return subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).stdout

devstats = {}
for line in mypopen("sysctl -d hw.iosched"):
    matchdev = re.match(r"^hw\.iosched\.([^.]+)\.oldest_bio:", line)
    if not matchdev:
        continue
    dev = matchdev.group(1)
    stats = {}
    for statline in mypopen("sysctl hw.iosched." + dev):
        exp = r"^hw\.iosched\." + dev + r"\.([^:]+): (\S+)\s*$"
        matchstat = re.match(exp, statline)
        if not matchstat:
            continue
        name, val = matchstat.group(1, 2)
        stats[name] = val
    devstats[dev] = stats

###########################################################
# Values to output. Each is either the value of a sysctl, #
# or a formula to calculate the value. The formula should #
# return a properly formatted string value.               #
###########################################################

def getval(stats, name):
    return stats[name]

def getsecstomsecs(stats, name):
    return "%.3f" % (float(stats[name]) * 1000)

def getthroughput(stats, dummy):
    nbios = float(stats['stats.latency_avg_bio_count'])
    decay_time = float(stats['stats.latency_avg_timestep']) / 1000000 # usec to sec
    decay_time /= -math.log(1 - 1 / float(stats['stats.latency_avg_denom']))
    ret = None
    # This formula assumes that we're currently halfway
    # between samples for bios completing at an even
    # rate. If the number of bios is small but nonzero,
    # we use a small value approximation to avoid losing
    # the data to the rounding error in the sum inside
    # sqrt(). We do the same thing if the value is large,
    # to avoid taking the log of a number very close to
    # one.
    if nbios <= 0:
        ret = 0
    elif nbios > math.pow(10.0, 4):
        ret = nbios / decay_time
    else:
        fracval = None
        if nbios < math.pow(10.0, -4):
            fracval = nbios
        else:
            fracval = (math.sqrt(4 * nbios * nbios + 1) - 1) / (2 * nbios)
        ret = -1 / (2 * decay_time * math.log(fracval))

    return "%.3f" % (ret)

global toprint
toprint = []
def make_toprint(name, func, data):
    global toprint
    return toprint.append(dict(name=name, func=func, data=data, space=0))

make_toprint('queue length', getval, 'stats.queue_length_avg')
make_toprint('max in progress', getval, 'max_inprog')
make_toprint('mean contig', getval, 'mean_contig_request')
make_toprint('sent', getval, 'num_sent')
make_toprint('completed', getval, 'num_completed')
make_toprint('starved', getval, 'num_starved')
make_toprint('reseeds', getval, 'num_reseeds')
make_toprint('latency (msec)', getsecstomsecs, 'stats.latency_avg')
make_toprint('oldest bio (msec)', getsecstomsecs, 'stats.oldest_bio_avg')
make_toprint('bios/sec', getthroughput, '')

################################################
# Calculate values and determine column widths #
################################################

devspace = 0
devvals = {}
for dev, inputs in devstats.iteritems():
    devspace = max(devspace, len(dev))
    vals = {}
    for stat in toprint:
        val = stat['func'](inputs, stat['data'])
        stat['space'] = max(stat['space'], len(val))
        vals[stat['name']] = val
    devvals[dev] = vals

####################
# Render the table #
####################

def devcmp(a, b):
    exp = r"^(.*[^0-9])([0-9]+)$"
    ma = re.match(exp, a)
    mb = re.match(exp, b)
    if ma.group(1) == mb.group(1):
        return cmp(int(ma.group(2)), int(mb.group(2)))
    else:
        return cmp(ma.group(1), mb.group(1))

sep = 2 # column separation
global header
header = list()
global last_header
last_header = None

def add_column_header(str, pos):
    global header
    global last_header
    if last_header != None:
        try_header = last_header + 1
        while True:
            if try_header == len(header):
                try_header = 0
            row = header[try_header]
            if pos >= sep + len(row):
                header[try_header] = string.ljust(row, pos) + str
                last_header = try_header
                return
            if try_header == last_header:
                break
            try_header = try_header + 1
    # Need another row
    header.append(string.ljust('', pos) + str)
    last_header = len(header) - 1


add_column_header('device', 0)
indent = devspace + sep
for stat in toprint:
    add_column_header(stat['name'], indent)
    indent += stat['space'] + sep

print
for line in header:
    print line
print

keys = devvals.keys()
keys.sort(devcmp)
for dev in keys:
    print string.ljust(dev, devspace),
    vals = devvals[dev]
    for stat in toprint:
        print string.rjust(vals[stat['name']], stat['space'] + sep - 1),
    print

print
