#!/usr/bin/env python3

import csv
import collections

#----------------------------------------------------------------
# This is a specially modified chart generator that takes many
# repetions of the same test and displays them all on each chart.
#----------------------------------------------------------------

#
#----------------------------------------------------------------
# The files this generator acts upon are the CSV files output
# from one or more runs of test_measure_perf.py.
#
# Data file naming convention: prefix_id_rep.csv
#
# This script takes no arguments. When it's run, it will
# produce an HTML file called "test_perf_charts.html".
#
# Open that file in your browser to see the charts.
#----------------------------------------------------------------

#----------------------------------------------------------------
# The following variables will all need to be edited to match
# the tests that you are charting
#----------------------------------------------------------------
#
# vpx is now vp8
# x264 is now h264
#
# TODO
# -- Move the legend into the chart title area
# -- Abbreviate the app names so they fit on the chart
# -- Update the documentation to describe how to use multiple unique
#    directories containing files with same key, instead of one directory
#    and files with unique keys.
#

# Location of the data files
base_dir = "/home/nickc/xtests/logs"

# Data file prefix
prefix_all = "all_tests_40"
prefix_glx = "h264_glx"

# Result subdirectories
subs_h = ["hv", "h1", "h2", "h3"]
subs_8 = ["0.15.0", "8585_1", "8585_2", "8585_3"]
subs_9 = ["0.15.0", "9612_1", "9612_2", "9612_3"]

# id is the actual id string used in the data file name
# dir is an optional subdirectory within the base_dir where the data is stored
# display is how that parameter should be displayed in the charts
params = [
    {"id": "8585", "dir": subs_8[0], "display": "8585_all_0", "prefix": prefix_all},
    {"id": "8585", "dir": subs_8[1], "display": "8585_all_1", "prefix": prefix_all},
    {"id": "8585", "dir": subs_8[2], "display": "8585_all_2", "prefix": prefix_all},
    {"id": "8585", "dir": subs_8[3], "display": "8585_all_3", "prefix": prefix_all},
    {"id": "8585", "dir": subs_h[0], "display": "8585_glx_0", "prefix": prefix_glx},
    {"id": "8585", "dir": subs_h[1], "display": "8585_glx_1", "prefix": prefix_glx},
    {"id": "8585", "dir": subs_h[2], "display": "8585_glx_2", "prefix": prefix_glx},
    {"id": "8585", "dir": subs_h[3], "display": "8585_glx_3", "prefix": prefix_glx},
    {"id": "9612", "dir": subs_9[0], "display": "9612_all_0", "prefix": prefix_all},
    {"id": "9612", "dir": subs_9[1], "display": "9612_all_1", "prefix": prefix_all},
    {"id": "9612", "dir": subs_9[2], "display": "9612_all_2", "prefix": prefix_all},
    {"id": "9612", "dir": subs_9[3], "display": "9612_all_3", "prefix": prefix_all},
    {"id": "9612", "dir": subs_h[0], "display": "9612_glx_0", "prefix": prefix_glx},
    {"id": "9612", "dir": subs_h[1], "display": "9612_glx_1", "prefix": prefix_glx},
    {"id": "9612", "dir": subs_h[2], "display": "9612_glx_2", "prefix": prefix_glx},
    {"id": "9612", "dir": subs_h[3], "display": "9612_glx_3", "prefix": prefix_glx}
]
#params = [
#    {"id": "8585", "display": "8585"},
#    {"id": "9612", "display": "9612"}
#]

# The description will be shown on the output page
description = 'Results of every test run. The red bars are from the original 2 comparisons.'

# Each file name's 'rep' value is the sequence number of that
# data file, when results of multiple files should be averaged
reps = 9     # Number of data files in each set

# Options
needReallyUniqueId = True
showLegend = "false"

#----------------------------------------------------------------
# Set any of the values in the following lists to 1 in order to
# include that test app, or metric column in the chart page.
#
apps = {"glxgears": 0,
        "glxspheres": 1,
        "moebiusgears": 0,
        "polytopes": 0,
        "x11perf": 0,
        "xterm": 0,
        "gtkperf": 0}

metrics = {"Regions/s": 1,
           "Pixels/s Sent": 1,
           "Encoding Pixels/s": 1,
           "Decoding Pixels/s": 1,
           "Application packets in/s": 1,
           "Application bytes in/s": 1,
           "Application packets out/s": 1,
           "Application bytes out/s": 1,
           "client user cpu_pct": 1,
           "client system cpu pct": 1,
           "client number of threads": 1,
           "client vsize (MB)": 1,
           "client rss (MB)": 1,
           "server user cpu_pct": 1,
           "server system cpu pct": 1,
           "server number of threads": 1,
           "server vsize (MB)": 1,
           "server rss (MB)": 1,
           "Min Batch Delay (ms)": 1,
           "Avg Batch Delay (ms)": 1,
           "Max Batch Delay (ms)": 1,
           "Min Damage Latency (ms)": 1,
           "Avg Damage Latency (ms)": 1,
           "Max Damage Latency (ms)": 1,
           "Min Quality": 1,
           "Avg Quality": 1,
           "Max Quality": 1,
           "Min Speed": 0,
           "Avg Speed": 0,
           "Max Speed": 0}

encodings = {"png": 0,
             "rgb24": 0,
             "jpeg": 0,
             "h264": 1,
             "vp8": 0,
             "vp9": 0,
             "mmap": 0}

header_dupes = []
headers = {}
titles = []
param_ids = []
param_names = []

ENCODING_RGB24 = "rgb24"

def tree():
    return collections.defaultdict(tree)
tests = tree()

def ftree():
    return collections.defaultdict(float)

# Create test map -- schema:
# {metric: {encoding: {id: {app: {rep: avg_value}}}}}
def accumulate_values(file_name, rep, param, uniqueId):
    rownum = 0
    rgb_count = 0
    rgb_values = None

    ifile = open(file_name, "rb")
    for row in csv.reader(ifile, skipinitialspace=True):
        if (rownum == 0):
            if (len(headers) == 0):
                get_headers(row)
        else:
            app = get_value(row, "Test Command")
            if (apps[app] == 1):
                encoding = get_value(row, "Encoding")
                # x264 is now h264
                if (encoding == 'x264'):
                    encoding = 'h264'
                # vpx is now vp8
                if (encoding == 'vpx'):
                    encoding = 'vp8'

                if (encodings[encoding] == 1):
                    if (encoding == ENCODING_RGB24):
                        if (rgb_values is None):
                            rgb_values = ftree()
                            rgb_count = 0

                    for metric in metrics:
                        if (metrics[metric] == 1):
                            row_value = float(get_metric(row, metric))
                            if (encoding == ENCODING_RGB24):
                                if (metric in rgb_values.keys()):
                                    rgb_values[metric] += row_value
                                else:
                                    rgb_values[metric] = row_value
                            else:
                                tests[metric][encoding][uniqueId][app][rep] = row_value

                    if (encoding == ENCODING_RGB24):
                        rgb_count += 1
                        if (rgb_count == 3):
                            for metric in metrics:
                                if (metrics[metric] == 1):
                                    tests[metric][encoding][uniqueId][app][rep] = rgb_values[metric] / 3
                            rgb_count = 0
                            rgb_values = None
        rownum += 1
    ifile.close()

def write_html():
    app_count = 0
    for app in apps.keys():
        if (apps[app] == 1):
            app_count += 1

    chart_count = 0
    for encoding in encodings.keys():
        if (encodings[encoding] == 1):
            chart_count += 1
    box_height = 0
    if (chart_count < 4):
        box_height = 400
    elif (chart_count < 7):
        box_height = 800
    elif (chart_count < 10):
        box_height = 1200

    ofile = open("charts.html", "w")
    ofile.write('<!DOCTYPE html>\n')
    ofile.write('<html>\n')
    ofile.write('<head>\n')
    ofile.write('  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">\n')
    ofile.write('  <title>Xpra Performance Results</title>\n')
    ofile.write('  <link href="css/xpra.css" rel="stylesheet" type="text/css">\n')
    ofile.write('  <script language="javascript" type="text/javascript" src="js/jquery.js"></script>\n')
    ofile.write('  <script language="javascript" type="text/javascript" src="js/jquery.flot.js"></script>\n')
    ofile.write('  <script language="javascript" type="text/javascript" src="js/jquery.flot.categories.js"></script>\n')
    ofile.write('  <script language="javascript" type="text/javascript" src="js/jquery.flot.orderbars_mod.js"></script>\n')
    ofile.write('  <script language="javascript" type="text/javascript" src="js/xpra.js"></script>\n')
    ofile.write('  <script language="javascript" type="text/javascript">\n')
    ofile.write('    var options = {canvas:true, grid: {margin: {top:50}, hoverable: true}, series: {bars: {show: true, barWidth: 0.5}}, legend: {show: ' + showLegend + '}, '
                #' xaxis: {mode: "categories", tickLength: 0, min: -0.3, max: ' + str(app_count) +'}, '
                ' xaxis: {mode: "categories", tickLength: 0, min: -6, max: 6}, '
                ' colors: ["#cc0000", "#787A40", "#9FBF8C", "#C8AB65", "#cc0000", "#787A40", "#9FBF8C", "#C8AB65", "#cc0000", "#787A40", "#9FBF8C", "#C8AB65", "#cc0000", "#787A40", "#9FBF8C", "#C8AB65"]};\n')

    m_index = 0
    m_names = []
    for metric in sorted(tests.keys()):
        m_names.append(metric)
        e_names = []
        for encoding in sorted(tests[metric].keys()):
            e_names.append(encoding)
            titles.append(metric + ' ( ' + encoding + ' )')
            for param in sorted(tests[metric][encoding].keys()):
                ofile.write('    var e' + str(m_index) + '_' + encoding + '_' + param + ' = [')
                for app in sorted(tests[metric][encoding][param].keys()):
                    value = 0
                    actual_reps = 0
                    for rep in sorted(tests[metric][encoding][param][app].keys()):
                        value += float(tests[metric][encoding][param][app][rep])
                        actual_reps += 1
                    value = value / actual_reps
                    ofile.write('["' + app + '", ' + str(value) + '], ')
                ofile.write('];' + '\n')
            varStr = '    var d' + str(m_index) + '_' + encoding + ' = ['
            for i in range(0, len(param_ids)):
                if (i > 0):
                    varStr += ','
                varStr += '{label: "' + param_names[i] + '", data: e' + str(m_index) + '_' + encoding + '_' + param_ids[i] + ', bars:{order:' + str(i) + '}}'
            varStr += '];\n'
            ofile.write(varStr)
        m_index += 1

    chart_index = 0
    m_index = 0
    ofile.write('    $(function() {\n')
    for metric in sorted(tests.keys()):
        e_index = 0
        for encoding in sorted(tests[metric].keys()):
            ofile.write('        var plot' +str(chart_index)+ ' = $.plot($("#placeholder_' + str(m_index) + '_' + str(e_index) + '"), d' + str(m_index) + '_' + e_names[e_index] + ', options);\n')
            e_index += 1
            chart_index += 1
        m_index += 1
    title_index = 0

    for metric in sorted(tests.keys()):
        for encoding in sorted(tests[metric].keys()):
            ofile.write('        set_title(' + str(title_index) + ', "' + titles[title_index] + '");\n')
            title_index += 1

    for mx in range(0, m_index):
        ofile.write('$("#metric_link_'+str(mx)+'").click(function() {$("#metric_list").scrollTop(' + str(box_height) + '*'+str(mx)+');});')
    ofile.write('    });\n')
    ofile.write('  </script>\n')
    ofile.write('  <style>.metric_box {height: ' + str(box_height) + 'px}</style>\n')
    ofile.write('</head>\n')
    ofile.write('<body>\n')
    ofile.write('  <div id="page">\n')
    ofile.write('    <div id="header_box">\n')
    ofile.write('      <div id="header">\n')
    ofile.write('        <h2>Xpra Performance Results</h2>\n')
    ofile.write('        <h3>' + description + '</h3>\n')
    ofile.write('        <div id="help_text">Click a metric to locate it in the results.</div>\n')
    ofile.write('      </div>\n')

    ofile.write('      <div id="select_box">\n')
    m_index = 0
    for metric in sorted(tests.keys()):
        ofile.write('        <div id="metric_link_' + str(m_index) + '" style="float:left;height:20px;width:200px"><a href="#">' + metric + '</a></div>\n')
        m_index += 1
    ofile.write('      </div>\n')
    ofile.write('    </div>\n')

    ofile.write('    <div style="clear:both"></div>\n')
    ofile.write('    <div id="metric_list">\n')
    m_index = 0
    for metric in sorted(tests.keys()):
        ofile.write('      <div class="metric_box" id="metric_box_' + str(m_index) + '">\n')
        ofile.write('        <div class="metric_label">' + metric + '</div>\n')
        e_index = 0
        for encoding in sorted(tests[metric].keys()):
            ofile.write('        <div class="container">\n')
            ofile.write('          <div id="placeholder_' + str(m_index) + '_' + str(e_index) + '" class="placeholder"></div>\n')
            ofile.write('        </div>\n')
            e_index += 1

        ofile.write('      </div>\n')
        m_index += 1
    ofile.write('      <div class="metric_box"></div>\n')
    ofile.write('    </div>\n')
    ofile.write('  </div>\n')
    ofile.write('</body>\n')
    ofile.write('</html>\n')
    ofile.close()

def col_index(label):
    return headers[label]

def get_value(row, label):
    return row[col_index(label)].strip()

def get_metric(row, label):
    cell = row[col_index(label)]
    if cell is None or cell is '':
        cell = '0'
    return cell.strip()

def sanitize(dirName):
    # Make the directory name valid as a javascript variable
    newName = dirName.replace('.', '_')
    return newName

def get_headers(row):
    index = 0
    for column in row:
        col = column.strip()
        if col in headers:
            header_dupes.append(col)
        headers[col] = index
        index += 1

def print_headers():
    for entry in headers:
        print(entry + " " + str(headers[entry]))
    for entry in header_dupes:
        print("Found dupe: %s" % entry)

def main():
    for param in params:
        param_id = param_name = param['id']
        if ('dir' in param.keys()):
            param_id = sanitize(param['dir'])
        if (needReallyUniqueId):
            param_id = param['id'] + '_' + sanitize(param['dir']);

        if ('display' in param.keys()):
            param_name = param['display']
        param_ids.append(param_id)
        param_names.append(param_name)

    for param in params:
        uniqueId = param['id']
        if ('dir' in param.keys()):
            uniqueId = sanitize(param['dir'])

        if (needReallyUniqueId):
            uniqueId = param['id'] + '_' + sanitize(param['dir']);

        #file_name = base_dir + '/' + param['dir'] + '/' + param['prefix'] + '_' + param['id'] + '_x.csv'
        #print file_name

        for rep in range(0, reps):
            if ('dir' in param.keys()):
                file_name = base_dir + '/' + param['dir'] + '/' + param['prefix'] + '_' + param['id'] + '_' + str(rep+1) + '.csv'
            else:
                file_name = base_dir + '/' + param['prefix'] + '_' + param['id'] + '_' + str(rep+1) + '.csv'
            accumulate_values(file_name, rep, param, uniqueId)
    write_html()
    print('\nCreated: charts.html\n')

if __name__ == "__main__":
    main()

