#!python
# -*- coding: latin-1 -*-

#
# time_average
#
# Exécute une (ou plusieurs) commande plusieurs fois
# et affiche une moyenne du temps d'exécution.
# Peut aussi créer une page HTML récapitulative.
#
# (Utilise la commande Unix time, qui doit être accessible dans le PATH.)
#

# (c) Olivier Pirson --- olivier_pirson_opi@yahoo.fr
# DragonSoft DS --- http://www.opimedia.be/DS/
# Débuté le 8 novembre 2010
# v.01.00.00 --- 10 novembre 2010
# v.01.01.00 --- 11 septembre 2011
#   Ajouts :
#   - Indication de la nécessité de la commande time
#   - Assertions sur les paramètres des fonctions
#   - Récupère le code d'erreur de l'exécution des commandes
#   - Exclu les commandes sans résultats
#   - Sortie HTML : affiche la ligne de commandes
#   - Sortie HTML : affiche les résultats individuels
#############################################################
from __future__ import print_function

VERSION = '01.01.00 --- 2011 September 11'

import random
import subprocess
import sys
import tempfile



#####################
# Constante globale #
#####################
TIME = 'time -p'



#####################
# Fonctions privées #
#####################
def _class_min_max(x, min, max, void=False):
    """Renvoie ' class="void"', ' class="min"', ' class="max"' ou ''

    Pre: x: float
         min: float
         max: float
         void: booléen

    Result: string"""
    assert isinstance(x, float), type(x)
    assert isinstance(min, float), type(min)
    assert isinstance(max, float), type(max)

    return (' class="void"' if void
            else (' class="min"' if x == min
                  else (' class="max"' if x == max
                        else '')))


def _round(x, nb=2):
    """Renvoie x arrondi à nb chiffres après la virgule

    Pre: x: float
         nb: int >= 0

    Result: string"""
    assert isinstance(x, float), type(x)
    assert isinstance(nb, int), type(nb)
    assert nb >= 0, nb

    p = 10**nb
    x = round(x*p)/p

    return str(x if x != round(x)
               else int(x))



#############
# Fonctions #
#############
def help_msg():
    """Affiche le message d'aide sur la sortie des erreurs
    et termine le programme par un code d'erreur 1"""
    print("""time_average [-H number] [-h file] [-n number] command [command [...]]

  (c) Olivier Pirson --- olivier_pirson_opi@yahoo.fr
       DragonSoft DS --- http://www.opimedia.be/DS/
          v.{0}

  command: String (if '' then separator).

  Options:
    -H number>0   Maximal height (in pixel) of graph in HTML output
                    (20 by default).
    -h file       Filename of HTML output (by default, don't build HTML file).
    -n number>0   Number of repetitions (10 by default).

    --help        Print this message on error output and exit by error code 1.

  Require:
    The '{1}' command is require (see http://directory.fsf.org/project/time/)""".format(VERSION, TIME), file=sys.stderr)
    exit(1)


def print_html(command_line, results, filename, height):
    """Crée un fichier HTML de nom filename
    avec les résultats contenu dans results
    de command_line

    height spécifie la hauteur maximale des colonnes pour les graphiques

    Pre: command_line: string
         results: list
         filename: string
         height: int > 0"""
    assert isinstance(command_line, str), type(command_line)
    assert isinstance(results, list), type(results)
    assert isinstance(filename, str), type(filename)
    assert isinstance(height, int), type(height)
    assert height > 0, height

    f = open(filename, mode='w')

    print("""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
  <head>
    <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
    <title>Results &ndash; time_average &ndash; v.{0}</title>

    <style type="text/css">
body {{ font-family: Arial, sans-serif; }}

div.graph {{
    height: {1}px;
    margin-bottom: 50px;
    position: relative;
    top: {1}px;
}}

div.graph span {{
    height: {1}px;
    position: relative;
}}

div.graph span span {{
    background-color: silver;
    border: solid 1px;
    bottom: 0px;
    font-size: smaller;
    position: absolute;
    text-align: center;
    width: 14px;
}}

div.graph span span.max {{
    background-color: red;
    color: black;
}}

div.graph span span.min {{
    background-color: green;
    color: black;
}}

h1 {{
    margin: 0px;
    text-align: center;
}}

h2 {{ margin: 0px; }}

table {{
    border-collapse: collapse;
    white-space: nowrap;
}}

td {{
    border: solid 1px black;
    padding: 0px .5ex;
    text-align: right;
}}

th {{
    border-right: solid 1px black;
    padding: 0px .5ex;
    text-align: right;
}}

tt {{ font-family: Dina, monospace; }}

.left {{ text-align: left; }}

.max {{ color: red; }}
.min {{ color: green; }}
.void {{ color: gray; }}
    </style>
  </head>

  <body>
    <h1>Results &ndash; time_average</h1>
    <tt>{2}</tt>""".format(VERSION.replace('---', '&ndash;'), height, command_line), file=f)

    s = tuple([r[3] for r in results if r[0] and r[4]])  # pour les vraies commandes, qui ont des résultats
    time_max = max(s)  # plus grande durée rencontrée
    time_min = min(s)  # plus petite durée rencontrée


    print("""    <h2 id="graph">Average graph</h2>
<div class="graph">""", file=f)
    for i in range(len(results)):
        r = results[i]
        if r[0]:
            print('  <span><span{3} title="{2}" style="height:{1}px; left:{0}px">{4}</span></span>'
                  .format(15*i, (round(r[3]*height/time_max) if time_max
                                 else 0),
                          "'{0}': {1} s  {2} % of min  {3} % of max".format(r[0], _round(r[3]),
                                                                            (_round(r[3]*100/time_min) if time_min
                                                                             else ''),
                                                                            (_round(r[3]*100/time_max) if time_max
                                                                             else '')),
                          _class_min_max(r[3], time_min, time_max), '<br>'.join(list(str(i + 1)))),
                  file=f)
    print('</div>', file=f)


    total_time = 0
    total_nb = 0
    print("""    <h2 id="results">Average results</h2>
    <table>
      <tr><th></th><th class="left">Command</th><th>Time/Number</th><th>Average (in seconds)</th><th>% of min</th><th>% of max</th></tr>""", file=f)
    for i in range(len(results)):
        r = results[i]
        if r[0]:
            print('<tr{7}><td>{0}</td><td class="left"><tt>{1}</tt></td><td>{3} s/{2}</td><td>{4} s</td><td>{5} %</td><td>{6} %</td></tr>'
                  .format(i + 1, r[0], r[1], _round(r[2]), _round(r[3]),
                          (_round(r[3]*100/time_min) if time_min
                           else ''),
                          (_round(r[3]*100/time_max) if time_max
                           else ''),
                          _class_min_max(r[3], time_min, time_max, not r[4])),
                  file=f)
            total_time += r[2]
            total_nb += r[1]
        else:
            print('<tr style="height:1.5ex"><td colspan="6"></td></tr>', file=f)
    print("""<tr><th></th><th class="left">Total</th><th>{3} s/{2}</th><th>{4} s</th><th>{5} %</th><th>{6} %</th></tr>
    </table>""".format(i + 1, r[0], total_nb, _round(total_time), _round(total_time/total_nb),
                       (_round(total_time/total_nb*100/time_min) if time_min
                        else ''),
                       (_round(total_time/total_nb*100/time_max) if time_max
                        else '')),
          file=f)


    indexes_sorted = sorted(range(len(results)), key=lambda i: results[i][3])
    print("""    <h2 id="sorted">Average sorted results</h2>
    <table>
      <tr><th></th><th class="left">Command</th><th>Time/Number</th><th>Average (in seconds)</th><th>% of min</th><th>% of max</th></tr>""", file=f)
    for i in range(len(indexes_sorted)):
        r = results[indexes_sorted[i]]
        if r[0]:
            print('<tr{7}><td>{0}</td><td class="left"><tt>{1}</tt></td><td>{3} s/{2}</td><td>{4} s</td><td>{5} %</td><td>{6} %</td></tr>'
                  .format(indexes_sorted[i] + 1, r[0], r[1], _round(r[2]), _round(r[3]),
                          (_round(r[3]*100/time_min) if time_min
                           else ''),
                          (_round(r[3]*100/time_max) if time_max
                           else ''),
                          _class_min_max(r[3], time_min, time_max, not r[4])),
                  file=f)
    print('    </table>', file=f)


    # Les résultats individuels, par commande
    for i in range(len(results)):
        print('   <hr>', file=f)
        if not results[i][0]:
            continue

        print("""    <h2{0} id="command-{1}">Command {1}. '{2}'</h2>""".format(_class_min_max(results[i][3], time_min, time_max, not results[i][4]),
                                                                               i + 1, results[i][0]),
              file=f)
        each_result = results[i][4]
        if not each_result:
            print('<span class="void">No result</span>', file=f)

            continue

        each_time_max = max(each_result)  # plus grande durée rencontrée
        each_time_min = min(each_result)  # plus petite durée rencontrée

        print('<div class="graph">', file=f)
        for j in range(len(each_result)):
            t = each_result[j]
            if t:
                print('  <span><span{3} title="{2}" style="height:{1}px; left:{0}px">{4}</span></span>'
                      .format(15*j, (round(t*height/each_time_max) if each_time_max
                                     else 0),
                              "{0} s  {1} % of min  {2} % of max".format(_round(t),
                                                                         (_round(t*100/each_time_min) if each_time_min
                                                                          else ''),
                                                                         (_round(t*100/each_time_max) if each_time_max
                                                                          else '')),
                              _class_min_max(t, each_time_min, each_time_max), '<br>'.join(list(str(j + 1)))),
                      file=f)
        print('</div>', file=f)

        print("""    <table>
          <tr><th></th><th>Time</th><th>% of min</th><th>% of max</th></tr>""", file=f)
        for j in range(len(each_result)):
            t = each_result[j]
            print('<tr{4}><td>{0}</td><td>{1} s</td><td>{2} %</td><td>{3} %</td></tr>'
                  .format(j + 1, _round(t),
                          (_round(t*100/each_time_min) if each_time_min
                           else ''),
                          (_round(t*100/each_time_max) if each_time_max
                           else ''),
                          _class_min_max(t, each_time_min, each_time_max)),
                  file=f)
        print('    </table>', file=f)


    print("""  </body>
</html>""", file=f)

    f.close()


def time_cmd(cmd):
    """Exécute la commande externe cmd
    et renvoie le nombre de secondes nécessaire à son exécution
    (ou None si il y a eu un problème)

    Pre: cmd: string

    Result: float ou None"""
    assert isinstance(cmd, str), type(cmd)

    tmp_f_out = tempfile.TemporaryFile(mode='w', suffix='time_average_tmp')  # fichier temporaire pour récupérer la sortie de la commande
    tmp_f_result = tempfile.TemporaryFile(mode='w+', suffix='time_average_tmp')  # fichier temporaire pour récupérer les résultats de time

    try:
        p = subprocess.Popen(TIME + ' ' + cmd, stdout=tmp_f_out, stderr=tmp_f_result, universal_newlines=True)
    except:
        return None
    return_code = p.wait()
    tmp_f_out.close()

    if return_code:  # la commande TIME a retourné un code d'erreur
        tmp_f_result.close()

        return None

    tmp_f_result.seek(0)
    time_real = None  # durée renvoyée par la commande TIME à récupérer
    time_user = None  # durée renvoyée par la commande TIME à ignorer
    time_sys = None  # durée renvoyée par la commande TIME à ignorer
    for s in tmp_f_result:
        time_real, time_user, time_sys = time_user, time_sys, s
    tmp_f_result.close()

    if (time_real != None) and (time_real[:5] == 'real ') and (time_user[:5] == 'user ') and (time_sys[:4] == 'sys '):
        try:
            time_real = float(time_real[5:])

            return time_real
        except ValueError:
            pass

    return None



########
# Main #
########
if __name__ == '__main__':
    cmds = []  # liste de [commande ou None, nombre de fois exécutée, durée total en secondes, durée moyenne, liste des durées]
    cmd_indexes = []  # liste des indices des éléments de cmds qui ne sont pas nuls
    height = 200  # hauteur maximale (en pixel) du graphique HTML
    html_output = None  # None ou nom du fichier pour la page de résultat HTML
    nb_repeat = 10  # nombre de répétition pour chaque commande


    # Parcourt les paramètres de la ligne de commande
    i = 0
    while i + 1 < len(sys.argv):
        i += 1
        s = sys.argv[i]

        if s and (s[0] == '-'):  # option
            if s == '-H':
                i += 1
                try:
                    height = int(sys.argv[i])
                except:
                    help_msg()
                if height <= 0:
                    help_msg()
                sys.argv[i] = height
            elif s == '-h':
                i += 1
                if i >= len(sys.argv):
                    help_msg()
                html_output = sys.argv[i]
                sys.argv[i] = '"' + sys.argv[i] + '"'
            elif s == '-n':
                i += 1
                try:
                    nb_repeat = int(sys.argv[i])
                except:
                    help_msg()
                if nb_repeat <= 0:
                    help_msg()
                sys.argv[i] = nb_repeat
            else:
                help_msg()
        else:                    # commande à tester
            if s:
                cmds.append([s, 0, 0.0, 0.0, []])
                cmd_indexes.append(len(cmds) - 1)
            else:
                cmds.append([None, 0, 0, 0, []])
            sys.argv[i] = '"' + sys.argv[i] + '"'
    if not cmd_indexes:
        help_msg()


    print('time_average BEGIN')

    while cmd_indexes:  # tant qu'il reste des commandes qui n'ont pas été exécutées nb_repeat fois (les parcourt aléatoirement)
        r = random.randrange(len(cmd_indexes))  # indice de cmd_indexes
        i = cmd_indexes[r]  # indice de cmds

        assert cmds[i][0], cmds[i][0]

        cmds[i][1] += 1
        if cmds[i][1] == nb_repeat:
            del cmd_indexes[r]

        sys.stderr.flush()
        print("'{0}': {1}... ".format(cmds[i][0], cmds[i][1]), end='')
        sys.stdout.flush()

        time = time_cmd(cmds[i][0])
        if time != None:
            cmds[i][2] += time
            cmds[i][4].append(time)
            print(_round(time), 's')
        else:
            print('KO!')
        sys.stdout.flush()

    print("""END
""")


    for r in cmds:
        r[1] = len(r[4])
        if r[1]:
            r[3] = r[2]/r[1]
        if r[0]:
            print("'{0}': {2} s/{1} = {3} s".format(r[0], r[1], _round(r[2]), _round(r[3])))

    if html_output:
        print_html(' '.join([str(x) for x in sys.argv]), cmds, html_output, height=height)
