#!/usr/bin/python

########################################################################
# File :    DIRACbenchmark.py
# Author :  Andrew McNab
########################################################################

""" DIRAC Benchmark 2012 by Ricardo Graciani, and wrapper functions to
    run multiple copies in parallel by Andrew McNab.

    This file (DIRACbenchmark.py) is intended to be the ultimate upstream
    shared by different users of the DIRAC Benchmark 2012 (DB12). The
    canonical version can be found at https://github.com/DIRACGrid/DB12

    This script can either be imported or run from the command line:

    ./DIRACbenchmark.py NUMBER

    where NUMBER gives the number of benchmark processes to run in parallel.

    Run  ./DIRACbenchmark.py help  to see more options.
"""

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
from past.builtins import long
import os
import sys
import random
import six
from six.moves.urllib.request import urlopen
import multiprocessing

version = "00.04 DB12"


def singleDiracBenchmark(iterations=1, measuredCopies=None):
    """Get Normalized Power of one CPU in DIRAC Benchmark 2012 units (DB12)"""

    # This number of iterations corresponds to 1kHS2k.seconds, i.e. 250 HS06 seconds

    n = int(1000 * 1000 * 12.5)
    calib = 250.0

    m = long(0)
    m2 = long(0)
    p = 0
    p2 = 0
    # Do one iteration extra to allow CPUs with variable speed (we ignore zeroth iteration)
    # Do one or more extra iterations to avoid tail effects when copies run in parallel
    i = 0
    while (i <= iterations) or (measuredCopies is not None and measuredCopies.value > 0):
        if i == 1:
            start = os.times()

        # Now the iterations
        for _j in six.moves.range(n):
            t = random.normalvariate(10, 1)
            m += t
            m2 += t * t
            p += t
            p2 += t * t

        if i == iterations:
            end = os.times()
            if measuredCopies is not None:
                # Reduce the total of running copies by one
                measuredCopies.value -= 1

        i += 1

    cput = sum(end[:4]) - sum(start[:4])
    wall = end[4] - start[4]

    if not cput:
        return None

    # Return DIRAC-compatible values
    return {"CPU": cput, "WALL": wall, "NORM": calib * iterations / cput, "UNIT": "DB12"}


def singleDiracBenchmarkProcess(resultObject, iterations=1, measuredCopies=None):
    """Run singleDiracBenchmark() in a multiprocessing friendly way"""

    benchmarkResult = singleDiracBenchmark(iterations=iterations, measuredCopies=measuredCopies)

    if not benchmarkResult or "NORM" not in benchmarkResult:
        return None

    # This makes it easy to use with multiprocessing.Process
    resultObject.value = benchmarkResult["NORM"]


def multipleDiracBenchmark(copies=1, iterations=1, extraIteration=False):
    """Run multiple copies of the DIRAC Benchmark in parallel"""

    processes = []
    results = []

    if extraIteration:
        # If true, then we run one or more extra iterations in each
        # copy until the number still being meausured is zero.
        measuredCopies = multiprocessing.Value("i", copies)
    else:
        measuredCopies = None

    # Set up all the subprocesses
    for i in range(copies):
        results.append(multiprocessing.Value("d", 0.0))
        processes.append(
            multiprocessing.Process(target=singleDiracBenchmarkProcess, args=(results[i], iterations, measuredCopies))
        )

    # Start them all off at the same time
    for p in processes:
        p.start()

    # Wait for them all to finish
    for p in processes:
        p.join()

    raw = []
    product = 1.0

    for result in results:
        raw.append(result.value)
        product *= result.value

    raw.sort()

    # Return the list of raw results and various averages
    return {
        "raw": raw,
        "copies": copies,
        "sum": sum(raw),
        "arithmetic_mean": sum(raw) / copies,
        "geometric_mean": product ** (1.0 / copies),
        "median": raw[(copies - 1) / 2],
    }


def wholenodeDiracBenchmark(copies=None, iterations=1, extraIteration=False):
    """Run as many copies as needed to occupy the whole machine"""

    # Try $MACHINEFEATURES first if not given by caller
    if copies is None and "MACHINEFEATURES" in os.environ:
        try:
            copies = int(urlopen(os.environ["MACHINEFEATURES"] + "/total_cpu").read())
        except Exception:
            pass

    # If not given by caller or $MACHINEFEATURES/total_cpu then just count CPUs
    if copies is None:
        try:
            copies = multiprocessing.cpu_count()
        except Exception:
            copies = 1

    return multipleDiracBenchmark(copies=copies, iterations=iterations, extraIteration=extraIteration)


def jobslotDiracBenchmark(copies=None, iterations=1, extraIteration=False):
    """Run as many copies as needed to occupy the job slot"""

    # Try $JOBFEATURES first if not given by caller
    if copies is None and "JOBFEATURES" in os.environ:
        try:
            copies = int(urlopen(os.environ["JOBFEATURES"] + "/allocated_cpu").read())
        except Exception:
            pass

    # If not given by caller or $JOBFEATURES/allocated_cpu then just run one copy
    if copies is None:
        copies = 1

    return multipleDiracBenchmark(copies=copies, iterations=iterations, extraIteration=extraIteration)


#
# If we run as a command
#
if __name__ == "__main__":

    helpString = """DIRACbenchmark.py [--iterations ITERATIONS] [--extra-iteration]
                  [COPIES|single|wholenode|jobslot|version|help]

Uses the functions within DIRACbenchmark.py to run the DB12 benchmark from the
command line.

By default one benchmarking iteration is run, in addition to the initial
iteration which DB12 runs and ignores to avoid ramp-up effects at the start.
The number of benchmarking iterations can be increased using the --iterations
option. Additional iterations which are also ignored can be added with the
--extra-iteration option  to avoid tail effects. In this case copies which
finish early run additional iterations until all the measurements finish.

The COPIES (ie an integer) argument causes multiple copies of the benchmark to
be run in parallel. The tokens "wholenode", "jobslot" and "single" can be
given instead to use $MACHINEFEATURES/total_cpu, $JOBFEATURES/allocated_cpu,
or 1 as the number of copies respectively. If $MACHINEFEATURES/total_cpu is
not available, then the number of (logical) processors visible to the
operating system is used.

Unless the token "single" is used, the script prints the following results to
two lines on stdout:

COPIES SUM ARITHMETIC-MEAN GEOMETRIC-MEAN MEDIAN
RAW-RESULTS

The tokens "version" and "help" print information about the script.

The source code of DIRACbenchmark.py provides examples of how the functions
within DIRACbenchmark.py can be used by other Python programs.

DIRACbenchmark.py is distributed from  https://github.com/DIRACGrid/DB12
"""

    copies = None
    iterations = 1
    extraIteration = False

    for arg in sys.argv[1:]:
        if arg.startswith("--iterations="):
            iterations = int(arg[13:])
        elif arg == "--extra-iteration":
            extraIteration = True
        elif arg == "--help" or arg == "help":
            print(helpString)
            sys.exit(0)
        elif not arg.startswith("--"):
            copies = arg

    if copies == "version":
        print(version)
        sys.exit(0)

    if copies is None or copies == "single":
        print(singleDiracBenchmark()["NORM"])
        sys.exit(0)

    if copies == "wholenode":
        result = wholenodeDiracBenchmark(iterations=iterations, extraIteration=extraIteration)
        print(result["copies"], result["sum"], result["arithmetic_mean"], result["geometric_mean"], result["median"])
        print(" ".join([str(i) for i in result["raw"]]))
        sys.exit(0)

    if copies == "jobslot":
        result = jobslotDiracBenchmark(iterations=iterations, extraIteration=extraIteration)
        print(result["copies"], result["sum"], result["arithmetic_mean"], result["geometric_mean"], result["median"])
        print(" ".join([str(i) for i in result["raw"]]))
        sys.exit(0)

    result = multipleDiracBenchmark(copies=int(copies), iterations=iterations, extraIteration=extraIteration)
    print(result["copies"], result["sum"], result["arithmetic_mean"], result["geometric_mean"], result["median"])
    print(" ".join([str(i) for i in result["raw"]]))
    sys.exit(0)
