#!/usr/bin/env python

#####################################################################
# xenmon is a front-end for xenbaked.
# There is a curses interface for live monitoring. XenMon also allows
# logging to a file. For options, run python xenmon.py -h
#
# Copyright (C) 2005 by Hewlett Packard, Palo Alto and Fort Collins
# Authors: Lucy Cherkasova, lucy.cherkasova@hp.com
#          Rob Gardner, rob.gardner@hp.com
#          Diwaker Gupta, diwaker.gupta@hp.com
#####################################################################
#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; under version 2 of the License.
# 
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
# 
#   You should have received a copy of the GNU General Public License
#   along with this program; if not, write to the Free Software
#   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#####################################################################

import mmap
import struct
import os
import time
import optparse as _o
import curses as _c
import math

# constants
NSAMPLES = 100
NDOMAINS = 8

# the struct strings for qos_info
ST_DOM_INFO = "6Q4i32s"
ST_QDATA = "%dQ" % (6*NDOMAINS + 3)

# size of mmaped file
QOS_DATA_SIZE = struct.calcsize(ST_QDATA)*NSAMPLES + struct.calcsize(ST_DOM_INFO)*NDOMAINS + struct.calcsize("3i")

# location of mmaped file, hard coded right now
SHM_FILE = "/tmp/xenq-shm"

# format strings
TOTALS = 15*' ' + "%6.2f%%" + 35*' ' + "%6.2f%%"

ALLOCATED = "Allocated"
GOTTEN = "Gotten"
BLOCKED = "Blocked"
WAITED = "Waited"
IOCOUNT = "I/O Count"
EXCOUNT = "Exec Count"

# globals
# our curses screen
stdscr = None

# parsed options
options, args = None, None

# the optparse module is quite smart
# to see help, just run xenmon -h
def setup_cmdline_parser():
    parser = _o.OptionParser()
    parser.add_option("-l", "--live", dest="live", action="store_true",
                      default=True, help = "show the ncurses live monitoring frontend (default)")
    parser.add_option("-n", "--notlive", dest="live", action="store_false",
                      default="True", help = "write to file instead of live monitoring")
    parser.add_option("-p", "--prefix", dest="prefix",
                      default = "foo", help="prefix to use for output files")
    parser.add_option("-t", "--time", dest="duration",
            action="store", type="int", default=10, 
            help="stop logging to file after this much time has elapsed")
    parser.add_option("--ms_per_sample", dest="mspersample",
            action="store", type="int", default=100,
            help = "determines how many ms worth of data goes in a sample")
    parser.add_option("--cpu_freq", action="store", type="int", dest = "cpufreq",
            default=2660,
            help="the frequency of your processor in MHz. This is used for converting cycles to timestamps")
    return parser

# encapsulate information about a domain
class DomainInfo:
    def __init__(self):
        self.allocated_samples = []
        self.gotten_samples = []
        self.blocked_samples = []
        self.waited_samples = []
        self.execcount_samples = []
        self.iocount_samples = []

    def gotten_stats(self, passed):
        total = sum(self.gotten_samples)
        per = 100*float(total)/passed
        exs = sum(self.execcount_samples)
        if exs > 0:
            avg = float(total)/exs
        else:
            avg = 0
        return [float(total)/(passed/10**9), per, avg]

    def waited_stats(self, passed):
        total = sum(self.waited_samples)
        per = 100*float(total)/passed
        exs = sum(self.execcount_samples)
        if exs > 0:
            avg = float(total)/exs
        else:
            avg = 0
        return [float(total)/(passed/10**9), per, avg]

    def blocked_stats(self, passed):
        total = float(sum(self.blocked_samples))
        per = 100*total/passed
        ios = sum(self.iocount_samples)
        if ios > 0:
            avg = total/float(ios)
        else:
            avg = 0
        return [total/(passed/10**9), per, avg]

    def allocated_stats(self, passed):
        total = sum(self.allocated_samples)
        exs = sum(self.execcount_samples)
        if exs > 0:
            return float(total)/exs
        else:
            return 0

    def ec_stats(self, passed):
        total = float(sum(self.execcount_samples))/(passed / 10**9)
        return total

    def io_stats(self, passed):
        total = float(sum(self.iocount_samples))
        exs = sum(self.execcount_samples)
        if exs > 0:
            avg = total/exs
        else:
            avg = 0
        return [total/(passed / 10**9), avg]

    def stats(self, passed):
        return [self.gotten_stats(passed), self.allocated_stats(passed), self.blocked_stats(passed), 
                self.waited_stats(passed), self.ec_stats(passed), self.io_stats(passed)]

# report values over desired interval
def summarize(startat, endat, duration, samples):
    dominfos = {}
    for i in range(0, NDOMAINS):
        dominfos[i] = DomainInfo()
        
    passed = 0
    curid = startat
    numbuckets = 0
    lost_samples = []
    
    while passed < duration:
        for i in range(0, NDOMAINS):
            dominfos[i].gotten_samples.append(samples[curid][0*NDOMAINS + i])
            dominfos[i].allocated_samples.append(samples[curid][1*NDOMAINS + i])
            dominfos[i].waited_samples.append(samples[curid][2*NDOMAINS + i])
            dominfos[i].blocked_samples.append(samples[curid][3*NDOMAINS + i])
            dominfos[i].execcount_samples.append(samples[curid][4*NDOMAINS + i])
            dominfos[i].iocount_samples.append(samples[curid][5*NDOMAINS + i])
    
        passed += samples[curid][6*NDOMAINS]
        lost_samples.append(samples[curid][6*NDOMAINS + 2])

        numbuckets += 1

        if curid > 0:
            curid -= 1
        else:
            curid = NSAMPLES - 1
        if curid == endat:
            break

    lostinfo = [min(lost_samples), sum(lost_samples), max(lost_samples)]

    ldoms = map(lambda x: dominfos[x].stats(passed), range(0, NDOMAINS))

    return [ldoms, lostinfo]

# scale microseconds to milliseconds or seconds as necessary
def time_scale(ns):
    if ns < 1000:
        return "%4.2f ns" % float(ns)
    elif ns < 1000*1000:
        return "%4.2f us" % (float(ns)/10**3)
    elif ns < 10**9:
	    return "%4.2f ms" % (float(ns)/10**6)
    else:
        return "%4.2f s" % (float(ns)/10**9)

# the live monitoring code
def show_livestats():
    # mmap the file
    shmf = open(SHM_FILE, "r+")
    shm = mmap.mmap(shmf.fileno(), QOS_DATA_SIZE)

    samples = []
    doms = []

    # initialize curses
    stdscr = _c.initscr()
    _c.noecho()
    _c.cbreak()

    stdscr.keypad(1)
    stdscr.timeout(1000)
    [maxy, maxx] = stdscr.getmaxyx()

    # display in a loop
    while True:
        samples = []
        doms = []

        # read in data
        idx = 0    
        for i in range(0, NSAMPLES):
            len = struct.calcsize(ST_QDATA)
            sample = struct.unpack(ST_QDATA, shm[idx:idx+len])
            samples.append(sample)
            idx += len

        for i in range(0, NDOMAINS):
            len = struct.calcsize(ST_DOM_INFO)
            dom = struct.unpack(ST_DOM_INFO, shm[idx:idx+len])
            doms.append(dom)
            idx += len

        (next, freq, dom0ctr) = struct.unpack("3i", shm[idx:])

        startat = next - 1
        if next + 10 < NSAMPLES:
            endat = next + 10
        else:
            endat = 10

        # get summary over desired interval
        [h1, l1] = summarize(startat, endat, 10**9, samples)
        [h2, l2] = summarize(startat, endat, 10 * 10**9, samples)

        # the actual display code
        stdscr.addstr(1, 1, "%sLast 10 seconds%sLast 1 second" % (20*' ', 30*' '), _c.A_BOLD)
        stdscr.hline(2, 1, "-", maxx-2)
        index = 2

        total_h1_cpu = 0
        total_h2_cpu = 0

        for dom in range(0, NDOMAINS):
            row = index
            col = 2
            if h1[dom][0][1] > 0 or dom == NDOMAINS - 1:
                # display gotten
                row += 1
                col = 2
                stdscr.addstr(row, col, "%d" % dom)
                col += 4
                stdscr.addstr(row, col, "%s" % time_scale(h2[dom][0][0]))
                col += 12
                stdscr.addstr(row, col, "%3.2f%%" % h2[dom][0][1])
                col += 12
                stdscr.addstr(row, col, "%s/ex" % time_scale(h2[dom][0][2]))
                col += 18
                stdscr.addstr(row, col, "%s" % time_scale(h1[dom][0][0]))
                col += 12
                stdscr.addstr(row, col, "%3.2f%%" % h1[dom][0][1])
                col += 12
                stdscr.addstr(row, col, "%s/ex" % time_scale(h1[dom][0][2]))
                col += 18
                stdscr.addstr(row, col, "Gotten")
    
                # display allocated
                row += 1
                col = 2
                stdscr.addstr(row, col, "%d" % dom)
                col += 28
                stdscr.addstr(row, col, "%s/ex" % time_scale(h2[dom][1]))
                col += 42
                stdscr.addstr(row, col, "%s/ex" % time_scale(h1[dom][1]))
                col += 18
                stdscr.addstr(row, col, "Allocated")

                # display blocked
                row += 1
                col = 2
                stdscr.addstr(row, col, "%d" % dom)
                col += 4
                stdscr.addstr(row, col, "%s" % time_scale(h2[dom][2][0]))
                col += 12
                stdscr.addstr(row, col, "%3.2f%%" % h2[dom][2][1])
                col += 12
                stdscr.addstr(row, col, "%s/io" % time_scale(h2[dom][2][2]))
                col += 18
                stdscr.addstr(row, col, "%s" % time_scale(h1[dom][2][0]))
                col += 12
                stdscr.addstr(row, col, "%3.2f%%" % h1[dom][2][1])
                col += 12
                stdscr.addstr(row, col, "%s/io" % time_scale(h1[dom][2][2]))
                col += 18
                stdscr.addstr(row, col, "Blocked")

                # display waited
                row += 1
                col = 2
                stdscr.addstr(row, col, "%d" % dom)
                col += 4
                stdscr.addstr(row, col, "%s" % time_scale(h2[dom][3][0]))
                col += 12
                stdscr.addstr(row, col, "%3.2f%%" % h2[dom][3][1])
                col += 12
                stdscr.addstr(row, col, "%s/ex" % time_scale(h2[dom][3][2]))
                col += 18
                stdscr.addstr(row, col, "%s" % time_scale(h1[dom][3][0]))
                col += 12
                stdscr.addstr(row, col, "%3.2f%%" % h1[dom][3][1])
                col += 12
                stdscr.addstr(row, col, "%s/ex" % time_scale(h1[dom][3][2]))
                col += 18
                stdscr.addstr(row, col, "Waited")

                # display ex count
                row += 1
                col = 2
                stdscr.addstr(row, col, "%d" % dom)
                col += 28
                stdscr.addstr(row, col, "%d/s" % h2[dom][4])
                col += 42
                stdscr.addstr(row, col, "%d" % h1[dom][4])
                col += 18
                stdscr.addstr(row, col, "Execution count")

                # display io count
                row += 1
                col = 2
                stdscr.addstr(row, col, "%d" % dom)
                col += 4
                stdscr.addstr(row, col, "%d/s" % h2[dom][5][0])
                col += 24
                stdscr.addstr(row, col, "%d/ex" % h2[dom][5][1])
                col += 18
                stdscr.addstr(row, col, "%d" % h1[dom][5][0])
                col += 24
                stdscr.addstr(row, col, "%3.2f/ex" % h1[dom][5][1])
                col += 18
                stdscr.addstr(row, col, "I/O Count")
                stdscr.hline(index + 7, 1, '-', maxx - 2)

                index += 7
                total_h1_cpu += h1[dom][0][1]
                total_h2_cpu += h2[dom][0][1]
        stdscr.addstr(index + 1, 2, TOTALS % (total_h2_cpu, total_h1_cpu))

        if l1[1] > 1 :
            stdscr.addstr(index + 4, 2, 
                "\tRecords lost: %d (Min: %d, Max: %d)\t\t\tRecords lost: %d (Min: %d, Max %d)" % 
                (math.ceil(l2[1]), l2[0], l2[2], math.ceil(l1[1]), l1[0], l1[2]), _c.A_BOLD)

        c = stdscr.getch()
        if c == ord('q'):
            break
    
        stdscr.erase()

    _c.nocbreak()
    stdscr.keypad(0)
    _c.echo()
    _c.endwin()
    shm.close()
    shmf.close()

def writelog():
    shmf = open(SHM_FILE, "r+")
    shm = mmap.mmap(shmf.fileno(), QOS_DATA_SIZE)

    interval = 0
    outfiles = {}
    for dom in range(0, NDOMAINS):
        outfiles[dom] = open("%s-dom%d.log" % (options.prefix, dom), 'w')
        outfiles[dom].write("# passed dom cpu(tot) cpu(%) cpu/ex allocated/ex blocked(tot) blocked(%) blocked/io waited(tot) waited(%) waited/ex ex/s io(tot) io/ex\n")

    while interval < options.duration:
        samples = []
        doms = []

        idx = 0    
        for i in range(0, NSAMPLES):
            len = struct.calcsize(ST_QDATA)
            sample = struct.unpack(ST_QDATA, shm[idx:idx+len])
            samples.append(sample)
            idx += len

        for i in range(0, NDOMAINS):
            len = struct.calcsize(ST_DOM_INFO)
            dom = struct.unpack(ST_DOM_INFO, shm[idx:idx+len])
            doms.append(dom)
            idx += len

        (next, freq, dom0ctr) = struct.unpack("3i", shm[idx:])

        startat = next - 1
        if next + 10 < NSAMPLES:
            endat = next + 10
        else:
            endat = 10

        [h1,l1] = summarize(startat, endat, 10**9, samples)
        for dom in range(0, NDOMAINS):
            if h1[dom][0][1] > 0 or dom == NDOMAINS - 1:
                outfiles[dom].write("%d %d %.3f %.3f %.3f %.3f %.3f %.3f %.3f %.3f %.3f %.3f %.3f %.3f %.3f\n" %
                                    (interval, dom,
                                     h1[dom][0][0], h1[dom][0][1], h1[dom][0][2],
                                     h1[dom][1],
                                     h1[dom][2][0], h1[dom][2][1], h1[dom][2][2],
                                     h1[dom][3][0], h1[dom][3][1], h1[dom][3][2],
                                     h1[dom][4], 
                                     h1[dom][5][0], h1[dom][5][1]))

        interval += 1
        time.sleep(1)

    for dom in range(0, NDOMAINS):
        outfiles[dom].close()

# start xenbaked
def start_xenbaked():
    global options
    global args
    
    os.system("killall -9 xenbaked")
    # assumes that xenbaked is in your path
    os.system("xenbaked --cpu_freq=%d --ms_per_sample=%d &" %
              (options.cpufreq, options.mspersample))
    time.sleep(1)

# stop xenbaked
def stop_xenbaked():
    os.system("killall -s INT xenbaked")

def main():
    global options
    global args
    global domains

    parser = setup_cmdline_parser()
    (options, args) = parser.parse_args()
    
    start_xenbaked()
    if options.live:
        show_livestats()
    else:
        writelog()
    stop_xenbaked()

if __name__ == "__main__":
    main()
