#!/usr/bin/env python3
"""Machine model and helper functions."""
import os
from datetime import datetime
from distutils.spawn import find_executable
from distutils.version import LooseVersion
import re
from collections import OrderedDict
from copy import deepcopy
import hashlib

import ruamel
import cachesim
from sympy.parsing.sympy_parser import parse_expr

from . import prefixedunit
from . import __version__

MIN_SUPPORTED_VERSION = "0.7.1"

CHANGES_SINCE = OrderedDict([
    ("0.6.6",
     """
     Removed 'cycles per cache line transfer' and replaced it by
     'non-overlap upstream throughput' in cache levels. The new parameter
     takes the following arguments and is now associated with the cache level
     that is read from or written to:
     [$TP B/cy or 'full socket memory bandwidth', 'half-duplex' or 'full-duplex']
     """),
    ("0.7.1",
     """
     The dictionary under 'compiler' needs to be tagged with '!!omap' and formatted
     as a sequence. For example: '- compiler_command: arg u ment s'. Pay attention
     to the leading dash.
     """),
])

def sanitize_symbolname(name):
    """
    Sanitize all characters not matched to a symbol by sympy's parse_expr.

    Based on same rules as used for python variables.
    """
    return re.subn('(^[0-9])|[^0-9a-zA-Z_]', '_', name)[0]

class MachineModel(object):
    """Representation of the hardware and machine architecture."""

    def __init__(self, path_to_yaml=None, machine_yaml=None, args=None):
        """Create machine representation from yaml file."""
        if not path_to_yaml and not machine_yaml:
            raise ValueError('Either path_to_yaml or machine_yaml is required')
        if path_to_yaml and machine_yaml:
            raise ValueError('Only one of path_to_yaml and machine_yaml is allowed')
        self._path = path_to_yaml
        self._data = machine_yaml
        self._args = args
        if path_to_yaml:
            with open(path_to_yaml, 'r') as f:
                # Ignore ruamel unsafe loading warning, by supplying Loader parameter
                self._data = ruamel.yaml.load(f, Loader=ruamel.yaml.Loader)

        assert 'kerncraft version' in self._data, \
            "Machine description requires a 'kerncraft version' entry, containg the kerncraft " \
            "version it was written for."
        file_version = LooseVersion(self._data['kerncraft version'])
        if not (MIN_SUPPORTED_VERSION <= file_version
                <= LooseVersion(__version__)):
            print("Relevant changes to the machine description file format:")
            print('\n'.join(['{}: {}'.format(version, help_text)
                             for version, help_text in CHANGES_SINCE.items()
                             if LooseVersion(version) > file_version]))
            raise ValueError("Machine description is incompatible with this version. "
                             "Supported versions are from {} to {}. Check change logs and examples "
                             "to update your own machine description file format.".format(
                                MIN_SUPPORTED_VERSION, __version__))

    def __getitem__(self, key):
        """Return configuration entry."""
        return self._data[key]

    def __repr__(self):
        """Return object representation."""
        return '{}({})'.format(
            self.__class__.__name__,
            repr(self._path or self._data['model name']),
        )

    def get_identifier(self):
        """Return identifier which is either the machine file name or sha256 checksum of data."""
        if self._path:
            return os.path.basename(self._path)
        else:
            return hashlib.sha256(hashlib.sha256(repr(self._data).encode())).hexdigest()

    def get_last_modified_datetime(self):
        """Return datetime object of modified time of machine file. Return now if not a file."""
        if self._path:
            statbuf = os.stat(self._path)
            return datetime.utcfromtimestamp(statbuf.st_mtime)
        else:
            return datetime.now()

    def get_cachesim(self, cores=1):
        """
        Return a cachesim.CacheSimulator object based on the machine description.

        :param cores: core count (default: 1)
        """
        cache_dict = {}
        for c in self['memory hierarchy']:
            # Skip main memory
            if 'cache per group' not in c:
                continue
            cache_dict[c['level']] = deepcopy(c['cache per group'])
            # Scale size of shared caches according to cores
            if c['cores per group'] > 1:
                cache_dict[c['level']]['sets'] //= cores

        cs, caches, mem = cachesim.CacheSimulator.from_dict(cache_dict)

        return cs

    def get_bandwidth(self, cache_level, read_streams, write_streams, threads_per_core, cores=None):
        """
        Return best fitting bandwidth according to number of threads, read and write streams.

        :param cache_level: integer of cache (0 is L1, 1 is L2 ...)
        :param read_streams: number of read streams expected
        :param write_streams: number of write streams expected
        :param threads_per_core: number of threads that are run on each core
        :param cores: if not given, will choose maximum bandwidth for single NUMA domain
        """
        # try to find best fitting kernel (closest to read/write ratio):
        # write allocate has to be handled in kernel information (all writes are also reads)
        # TODO support for non-write-allocate architectures
        try:
            target_ratio = read_streams/write_streams
        except ZeroDivisionError:
            target_ratio = float('inf')
        measurement_kernel = 'load'
        measurement_kernel_info = self['benchmarks']['kernels'][measurement_kernel]
        measurement_kernel_ratio = float('inf')
        for kernel_name, kernel_info in sorted(self['benchmarks']['kernels'].items()):
            try:
                kernel_ratio = ((kernel_info['read streams']['streams'] +
                                 kernel_info['write streams']['streams'] -
                                 kernel_info['read+write streams']['streams']) /
                                kernel_info['write streams']['streams'])
            except ZeroDivisionError:
                kernel_ratio = float('inf')

            if abs(kernel_ratio - target_ratio) < abs(measurement_kernel_ratio - target_ratio):
                measurement_kernel = kernel_name
                measurement_kernel_info = kernel_info
                measurement_kernel_ratio = kernel_ratio

        # choose smt, and then use max/saturation bw
        bw_level = self['memory hierarchy'][cache_level]['level']
        bw_measurements = \
            self['benchmarks']['measurements'][bw_level][threads_per_core]
        assert threads_per_core == bw_measurements['threads per core'], \
            'malformed measurement dictionary in machine file.'
        if cores is not None:
            # Used by Roofline model
            run_index = bw_measurements['cores'].index(cores)
            bw = bw_measurements['results'][measurement_kernel][run_index]
        else:
            # Used by ECM model
            # Choose maximum number of cores to get bandwidth for
            max_cores = min(self['memory hierarchy'][cache_level]['cores per group'],
                            self['cores per NUMA domain'])
            bw = max(bw_measurements['results'][measurement_kernel][:max_cores])

        # Correct bandwidth due to miss-measurement of write allocation
        # TODO support non-temporal stores and non-write-allocate architectures
        if cache_level == 0:
            # L1 does not have write-allocate, so everything is measured correctly
            factor = 1.0
        else:
            factor = (float(measurement_kernel_info['read streams']['bytes']) +
                      2.0*float(measurement_kernel_info['write streams']['bytes']) -
                      float(measurement_kernel_info['read+write streams']['bytes'])) / \
                     (float(measurement_kernel_info['read streams']['bytes']) +
                      float(measurement_kernel_info['write streams']['bytes']))
        bw = bw * factor

        return bw, measurement_kernel

    def get_compiler(self, compiler=None, flags=None):
        """
        Return tuple of compiler and compiler flags.

        Selects compiler and flags from machine description file, commandline arguments or call
        arguements.
        """
        if self._args:
            compiler = compiler or self._args.compiler
            flags = flags or self._args.compiler_flags
        if compiler is None:
            # Select first available compiler in machine description file's compiler dict
            for c in self['compiler'].keys():
                # Making sure compiler is available:
                if find_executable(c) is not None:
                    compiler = c
                    break
            else:
                raise RuntimeError("No compiler ({}) was found. Add different one in machine file, "
                                   "via --compiler argument or make sure it will be found in "
                                   "$PATH.".format(list(self['compiler'].keys())))
        if flags is None:
            # Select from machine description file
            flags = self['compiler'].get(compiler, '')

        return compiler, flags.split(' ')

    @staticmethod
    def parse_perfctr_event(perfctr):
        """
        Parse events in machine description to tuple representation used in Benchmark module.

        Examples:
        >>> parse_perfctr_event('PERF_EVENT:REG[0-3]')
        ('PERF_EVENT', 'REG[0-3]')
        >>> parse_perfctr_event('PERF_EVENT:REG[0-3]:STAY:FOO=23:BAR=0x23')
        ('PERF_EVENT', 'REG[0-3]', {'STAY': None, 'FOO': 23, 'BAR': 35})

        """
        split_perfctr = perfctr.split(':')
        assert len(split_perfctr) >= 2, "Atleast one colon (:) is required in the event name"
        event_tuple = split_perfctr[:2]
        parameters = {}
        for p in split_perfctr[2:]:
            if '=' in p:
                k, v = p.split('=')
                if v.startswith('0x'):
                    parameters[k] = int(v, 16)
                else:
                    parameters[k] = int(v)
            else:
                parameters[p] = None
        event_tuple.append(parameters)
        return tuple(event_tuple)

    @staticmethod
    def parse_perfmetric(metric):
        """Return (sympy expressions, event names and symbols dict) from performance metric str."""
        # Find all perfs counter references
        perfcounters = re.findall(r'[A-Z0-9_]+:[A-Z0-9\[\]|\-]+(?::[A-Za-z0-9\-_=]+)*', metric)

        # Build a temporary metric, with parser-friendly Symbol names
        temp_metric = metric
        temp_pc_names = {"SYM{}".format(re.sub("[\[\]\-|=:]", "_", pc)): pc
                         for i, pc in enumerate(perfcounters)}
        for var_name, pc in temp_pc_names.items():
            temp_metric = temp_metric.replace(pc, var_name)
        # Parse temporary expression
        expr = parse_expr(temp_metric)

        # Rename symbols to originals
        for s in expr.free_symbols:
            if s.name in temp_pc_names:
                s.name = temp_pc_names[str(s)]

        events = {s: MachineModel.parse_perfctr_event(s.name) for s in expr.free_symbols
                  if s.name in perfcounters}

        return expr, events


#!/usr/bin/env python3
"""A simple interval implementation."""

class Intervals(object):
    """Very simple interval implementation for integers (might also work on floats)."""

    def __init__(self, *args, **kwargs):
        """If keywords *sane* is True (default: False), checks will not be done on given data."""
        self.data = list(args)
        if not kwargs.get('sane', False):
            self.data = [d for d in self.data if d[1] > d[0]]
            self._enforce_order()
            self._enforce_no_overlap()

    def _enforce_order(self):
        """Enforce the order of all entries in internal storage."""
        self.data.sort(key=lambda d: d[0])

    def _enforce_no_overlap(self, start_at=0):
        """Enforce that no ranges overlap in internal storage."""
        i = start_at
        while i+1 < len(self.data):
            if self.data[i][1] >= self.data[i+1][0]:
                # beginning of i+1-th range is contained in i-th range
                if self.data[i][1] < self.data[i+1][1]:
                    # i+1-th range is longer, thus enlarge i-th range
                    self.data[i][1] = self.data[i+1][1]

                # removed contained range
                del self.data[i+1]
            i += 1

    def __and__(self, other):
        """Combine two intervals, under the assumption that they are sane."""
        return Intervals(*(self.data+other.data))

    def __len__(self):
        """Return sum of range lengths."""
        return int(sum(upper-lower for (lower, upper) in self.data))

    def __contains__(self, needle):
        """Return True if needle is contained in intervals."""
        return any(lower <= needle < upper for (lower, upper) in self.data)

    def __repr__(self):
        """Return string representation of object."""
        return str(self.__class__) + '(' + ', '.join([list.__repr__(d) for d in self.data]) + ')'

    def __eq__(self, other):
        """Return True if other contains exactly the same interval regions."""
        return self.data == other.data


"""Kerncraft static analytical performance modeling framework and tool."""
__version__ = '0.8.0'

# To trigger travis deployment to pypi, do the following:
# 1. Increment __version___
# 2. commit to RRZE-HPC/kerncraft's master branch
# 3. wait for travis to complete successful (unless already tested)
# 4. tag commit with 'v{}'.format(__version__) (`git tag vX.Y.Z`)
# 5. push tag to github (`git push origin vX.Y.Z` or push all tags with `git push --tags`)

def get_header_path() -> str:
    """Return local folder path of header files."""
    import os
    return os.path.abspath(os.path.dirname(os.path.realpath(__file__))) + '/headers/'


#!/usr/bin/env python3
"""Cache prediction interface classes are gathered in this module."""
from itertools import chain
import sys
from pprint import pprint

import sympy
import numpy as np

from kerncraft.kernel import symbol_pos_int

# From https://stackoverflow.com/a/17511341 by dlitz
def ceildiv(a: int, b: int) -> object:
    return -(-a // b)

class CachePredictor(object):
    """
    Predictor class used to interface LayerCondition and CacheSimulation with model classes.

    It's goal is to predict the amount of hits and misses it takes to process one cache line worth
    of work under a steady state assumption.

    Only stubs here.
    """

    def __init__(self, kernel, machine, cores=1):
        """Initialize cache predictor."""
        self.kernel = kernel
        self.machine = machine
        self.cores = cores

    def get_loads(self):
        """Return a list with number of loaded cache lines per memory hierarchy level."""
        raise NotImplementedError("CachePredictor should only be used as a base class.")

    def get_hits(self):
        """Return a list with number of hit cache lines per memory hierarchy level."""
        raise NotImplementedError("CachePredictor should only be used as a base class.")

    def get_misses(self):
        """Return a list with number of missed cache lines per memory hierarchy level."""
        raise NotImplementedError("CachePredictor should only be used as a base class.")

    def get_stores(self):
        """Return a list with number of stored cache lines per memory hierarchy level."""
        raise NotImplementedError("CachePredictor should only be used as a base class.")

    def get_evicts(self):
        """Return a list with number of evicted cache lines per memory hierarchy level."""
        raise NotImplementedError("CachePredictor should only be used as a base class.")

    def get_infos(self):
        """Return verbose information about the predictor."""
        raise NotImplementedError("CachePredictor should only be used as a base class.")

class LayerConditionPredictor(CachePredictor):
    """Predictor class based on layer condition analysis."""

    def __init__(self, kernel, machine, cores=1):
        """Initialize layer condition based predictor from kernel and machine object."""
        CachePredictor.__init__(self, kernel, machine, cores=cores)

        # check that layer conditions can be applied on this kernel:
        # 1. All iterations may only have a step width of 1
        loop_stack = list(self.kernel.get_loop_stack())
        if any([l['increment'] != 1 for l in loop_stack]):
            raise ValueError("Can not apply layer condition, since not all loops are of step "
                             "length 1.")

        # 2. The order of iterations must be reflected in the order of indices in all array
        #    references containing the inner loop index. If the inner loop index is not part of the
        #    reference, the reference is simply ignored
        index_order = [symbol_pos_int(l['index']) for l in loop_stack]
        for var_name, arefs in chain(self.kernel.sources.items(), self.kernel.destinations.items()):
            if next(iter(arefs)) is None:
                # Anything that is a scalar may be ignored
                continue
            for a in [self.kernel.access_to_sympy(var_name, a) for a in arefs]:
                for t in a.expand().as_ordered_terms():
                    # Check each and every term if they are valid according to loop order and array
                    # initialization
                    idx = t.free_symbols.intersection(index_order)

                    # Terms without any indices can be treat as constant offsets and are acceptable
                    if not idx:
                        continue

                    if len(idx) != 1:
                        raise ValueError("Only one loop counter may appear per term. "
                                         "Problematic term: {}.".format(t))
                    else:  # len(idx) == 1
                        idx = idx.pop()
                        # Check that number of multiplication match access order of iterator
                        pow_dict = {k: v for k, v in t.as_powers_dict().items()
                                    if k != idx}
                        stride_dim = sum(pow_dict.values())
                        error = False
                        try:
                            if loop_stack[-stride_dim-1]['index'] != idx.name:
                                error = True
                        except IndexError:
                            error = True
                        if error:
                            raise ValueError("Number of multiplications in index term does not "
                                             "match loop counter order. "
                                             "Problematic term: {}.".format(t))

        # 3. Indices may only increase with one
        inner_index = symbol_pos_int(loop_stack[-1]['index'])
        inner_increment = loop_stack[-1]['increment']
        # TODO use a public interface, not self.kernel._*
        for arefs in chain(chain(*self.kernel.sources.values()),
                           chain(*self.kernel.destinations.values())):
            if arefs is None:
                continue
            for expr in arefs:
                diff = expr.subs(inner_index, 1+inner_increment) - expr.subs(inner_index, 1)
                if diff != 0 and diff != 1:
                    # TODO support -1 aswell
                    raise ValueError("Can not apply layer condition, array references may not "
                                     "increment more then one per iteration.")
        # FIXME handle multiple datatypes
        element_size = self.kernel.datatypes_size[self.kernel.datatype]

        accesses = {}
        destinations = set()
        distances = []
        for var_name in self.kernel.variables:
            # Gather all access to current variable/array
            accesses[var_name] = self.kernel.sources.get(var_name, set()).union(
                                    self.kernel.destinations.get(var_name, set()))
            # Skip non-variable offsets, where acs is [None, None, None] (or similar) or only made
            # up from constant offsets
            if not any(accesses[var_name]) or not any(
                    [a == inner_index or a.coeff(inner_index) != 0
                     for a in chain.from_iterable(accesses[var_name])]):
                continue
            destinations.update(
                [(var_name, tuple(r)) for r in self.kernel.destinations.get(var_name, [])])
            acs = accesses[var_name]
            # Transform them into sympy expressions
            acs = [self.kernel.access_to_sympy(var_name, r) for r in acs]
            # Replace constants with their integer counter parts, to make the entries sortable
            acs = [self.kernel.subs_consts(e) for e in acs]
            # Sort accesses by decreasing order
            acs.sort(reverse=True)

            # Create reuse distances by substracting accesses pairwise in decreasing order
            distances += [(acs[i-1]-acs[i]).simplify() for i in range(1, len(acs))]
            # Add infinity for each array
            distances.append(sympy.oo)

        # Sort distances by decreasing order
        distances.sort(reverse=True)
        # Create copy of distances in bytes:
        distances_bytes = [d*element_size for d in distances]
        # CAREFUL! From here on we are working in byte offsets and not in indices anymore.

        # converting access sets to lists, otherwise pprint will fail during obligatory sorting step
        results = {'accesses': {k: list(v) for k,v in accesses.items()},
                   'distances': distances,
                   'destinations': destinations,
                   'distances_bytes': distances_bytes,
                   'cache': []}

        sum_array_sizes = sum(self.kernel.array_sizes(in_bytes=True, subs_consts=True).values())

        for c in self.machine.get_cachesim(self.cores).levels(with_mem=False):
            # Assuming increasing order of cache sizes
            hits = 0
            misses = len(distances_bytes)
            cache_requirement = 0

            tail = 0
            # Test for full caching
            if c.size() > sum_array_sizes:
                hits = misses
                misses = 0
                cache_requirement = sum_array_sizes
            else:
                for tail in sorted(set(distances_bytes), reverse=True):
                    # Assuming decreasing order of tails
                    # Ignoring infinity tail:
                    if tail is sympy.oo:
                        continue
                    cache_requirement = (
                        # Sum of inter-access caches
                        sum([d for d in distances_bytes if d <= tail]) +
                        # Tails
                        tail*len([d for d in distances_bytes if d > tail]))

                    if cache_requirement <= c.size():
                        # If we found a tail that fits into our available cache size
                        # note hits and misses and break
                        hits = len([d for d in distances_bytes if d <= tail])
                        misses = len([d for d in distances_bytes if d > tail])
                        break

            # Resulting analysis for current cache level
            # TODO include loads and stores
            results['cache'].append({
                'name': c.name,
                'hits': hits,
                'misses': misses,
                'evicts': len(destinations) if c.size() < sum_array_sizes else 0,
                'requirement': cache_requirement,
                'tail': tail})

        self.results = results

    def get_loads(self):
        """Return a list with number of loaded cache lines per memory hierarchy level."""
        # TODO FIXME L1 loads need to be derived from accesses
        return [0]+[c['misses'] for c in self.results['cache']]

    def get_hits(self):
        """Return a list with number of hit cache lines per memory hierarchy level."""
        # At last level, all previous misses are hits
        return [c['hits'] for c in self.results['cache']]+[self.results['cache'][-1]['misses']]

    def get_misses(self):
        """Return a list with number of missed cache lines per memory hierarchy level."""
        # At last level, there are no misses
        return [c['misses'] for c in self.results['cache']]+[0]

    def get_stores(self):
        """Return a list with number of stored cache lines per memory hierarchy level."""
        # TODO FIXME L1 stores need to be derived from accesses
        return [0]+[c['evicts'] for c in self.results['cache']]

    def get_evicts(self):
        """Return a list with number of evicted cache lines per memory hierarchy level."""
        # At last level, there are no evicts
        return [c['evicts'] for c in self.results['cache']]+[0]

    def get_infos(self):
        """Return verbose information about the predictor."""
        return self.results

class CacheSimulationPredictor(CachePredictor):
    """Predictor class based on layer condition analysis."""

    def __init__(self, kernel, machine, cores=1):
        """Initialize cache simulation based predictor from kernel and machine object."""
        CachePredictor.__init__(self, kernel, machine, cores)
        # Get the machine's cache model and simulator
        self.csim = self.machine.get_cachesim(self.cores)

        # FIXME handle multiple datatypes
        element_size = self.kernel.datatypes_size[self.kernel.datatype]
        cacheline_size = self.machine['cacheline size']
        elements_per_cacheline = int(cacheline_size // element_size)
        iterations_per_cacheline = (sympy.Integer(self.machine['cacheline size']) /
                                    sympy.Integer(self.kernel.bytes_per_iteration))

        # Gathering some loop information:
        inner_loop = list(self.kernel.get_loop_stack(subs_consts=True))[-1]
        inner_index = symbol_pos_int(inner_loop['index'])
        inner_increment = inner_loop['increment']  # Calculate the number of iterations for warm-up
        total_length = self.kernel.iteration_length()
        max_iterations = self.kernel.subs_consts(total_length)
        max_cache_size = sum([c.size() for c in self.csim.levels(with_mem=False)])

        # Warmup
        # Phase 1:
        # define warmup interval boundaries
        max_steps = 100
        warmup_increment = ceildiv(max_cache_size // element_size, max_steps // 2)
        invalid_entries = self.csim.count_invalid_entries()
        step = 0
        warmup_iteration = 0
        complete_sweep = False
        while invalid_entries > 0 and step < max_steps and not complete_sweep:
            prev_warmup_iteration = warmup_iteration
            warmup_iteration = warmup_iteration + warmup_increment
            if warmup_iteration > max_iterations:
                warmup_iteration = max_iterations
                complete_sweep = True

            # print("warmup_iteration1", warmup_iteration)
            offsets = self.kernel.compile_global_offsets(
                iteration=range(prev_warmup_iteration, warmup_iteration))
            self.csim.loadstore(offsets, length=element_size)
            invalid_entries = self.csim.count_invalid_entries()
            # TODO more intelligent break criteria based on change of invalid entries might be
            #      useful for early termination.
            # print("invalid_entries", invalid_entries)

            step += 1

        # Phase 2:
        # Check that there is enough space left for benchmarking
        if complete_sweep:
            warmup_iteration = 0
        else:
            # Are we far away from max_iterations?
            # print("max_iterations", max_iterations)
            if warmup_iteration > max_iterations - 100000:
                # To close to end, need to complete sweep
                complete_sweep = True
                prev_warmup_iteration = warmup_iteration
                warmup_iteration = max_iterations
                # print("warmup_iteration2", warmup_iteration, end="; ")
                offsets = self.kernel.compile_global_offsets(
                    iteration=range(prev_warmup_iteration, warmup_iteration))
                self.csim.loadstore(offsets, length=element_size)
                warmup_iteration = 0
            if not complete_sweep and invalid_entries > 0:
                print("Warning: Unable to perform complete sweep nor initialize cache completely. "
                      "This might introduce inaccuracies (additional cache misses) in the cache "
                      "prediction.")

        # Phase 3:
        # Iterate to safe handover point
        prev_warmup_iteration = warmup_iteration
        warmup_iteration = self._align_iteration_with_cl_boundary(warmup_iteration, subtract=False)
        if warmup_iteration != prev_warmup_iteration:
            # print("warmup_iteration3", warmup_iteration)
            offsets = self.kernel.compile_global_offsets(
                iteration=range(prev_warmup_iteration, warmup_iteration))
            self.csim.loadstore(offsets, length=element_size)

        # Reset stats to conclude warm-up phase
        self.csim.reset_stats()

        # Benchmark
        bench_iteration = self._align_iteration_with_cl_boundary(min(
            warmup_iteration + 100000, max_iterations - 1))
        # print("bench_iteration", bench_iteration)
        first_dim_factor = float((bench_iteration - warmup_iteration) / iterations_per_cacheline)
        # If end point is less than 100 cacheline away, warn user of inaccuracy
        if first_dim_factor < 1000:
            print("Warning: benchmark iterations are very low ({} CL). This may lead to inaccurate "
                  "cache predictions.".format(first_dim_factor))

        # Compile access needed for one cache-line
        offsets = self.kernel.compile_global_offsets(
            iteration=range(warmup_iteration, bench_iteration))
        # Run cache simulation
        self.csim.loadstore(offsets, length=element_size)
        # FIXME compile_global_offsets should already expand to element_size

        # use stats to build results
        self.stats = list(self.csim.stats())
        self.first_dim_factor = first_dim_factor

    def _align_iteration_with_cl_boundary(self, iteration, subtract=True):
        """Align iteration with cacheline boundary."""
        # FIXME handle multiple datatypes
        element_size = self.kernel.datatypes_size[self.kernel.datatype]
        cacheline_size = self.machine['cacheline size']
        elements_per_cacheline = int(cacheline_size // element_size)

        # Gathering some loop information:
        inner_loop = list(self.kernel.get_loop_stack(subs_consts=True))[-1]
        inner_increment = inner_loop['increment']

        # do this by aligning either writes (preferred) or reads
        # Assumption: writes (and reads) increase linearly
        o = self.kernel.compile_global_offsets(iteration=iteration)[0]
        if len(o[1]):
            # we have a write to work with:
            first_offset = min(o[1])
        else:
            # we use reads
            first_offset = min(o[0])

        diff = first_offset - \
               (int(first_offset) >> self.csim.first_level.cl_bits << self.csim.first_level.cl_bits)
        if diff == 0:
            return iteration
        elif subtract:
            return iteration - (diff // element_size) // inner_increment
        else:
            return iteration + (elements_per_cacheline - diff // element_size) // inner_increment

    def __init__old(self, kernel, machine, cores=1):
        """Initialize cache simulation based predictor from kernel and machine object."""
        CachePredictor.__init__(self, kernel, machine, cores)
        # Get the machine's cache model and simulator
        csim = self.machine.get_cachesim(self.cores)

        # FIXME handle multiple datatypes
        element_size = self.kernel.datatypes_size[self.kernel.datatype]
        cacheline_size = self.machine['cacheline size']
        elements_per_cacheline = int(cacheline_size // element_size)

        # Gathering some loop information:
        inner_loop = list(self.kernel.get_loop_stack(subs_consts=True))[-1]
        inner_index = symbol_pos_int(inner_loop['index'])
        inner_increment = inner_loop['increment']  # Calculate the number of iterations for warm-up
        max_cache_size = max(map(lambda c: c.size(), csim.levels(with_mem=False)))
        max_array_size = max(self.kernel.array_sizes(in_bytes=True, subs_consts=True).values())

        # Regular Initialization
        warmup_indices = {
            symbol_pos_int(l['index']): ((l['stop']-l['start'])//l['increment'])//3
            for l in self.kernel.get_loop_stack(subs_consts=True)}
        warmup_iteration_count = self.kernel.indices_to_global_iterator(warmup_indices)
        # Make sure we are not handeling gigabytes of data, but 1.5x the maximum cache size
        while warmup_iteration_count * element_size > max_cache_size*1.5:
            # Decreasing indices (starting from outer), until total size is small enough
            for l in self.kernel.get_loop_stack():
                index = symbol_pos_int(l['index'])
                if warmup_indices[index] > l['start']:
                    warmup_indices[index] -= 1
                    # get offset in iterations and size that a change on this level provokes
                    diff_iterations = (warmup_iteration_count -
                                       self.kernel.indices_to_global_iterator(warmup_indices))
                    diff_size = diff_iterations*element_size
                    warmup_indices[index] = max(
                        l['start'],
                        (warmup_iteration_count - (max_cache_size*1.5) // element_size) // diff_size
                    )
                    break
            warmup_iteration_count = self.kernel.indices_to_global_iterator(warmup_indices)

        # Align iteration count with cachelines
        # do this by aligning either writes (preferred) or reads
        # Assumption: writes (and reads) increase linearly
        o = self.kernel.compile_global_offsets(iteration=warmup_iteration_count)[0]
        if len(o[1]):
            # we have a write to work with:
            first_offset = min(o[1])
        else:
            # we use reads
            first_offset = min(o[0])

        # Distance from cacheline boundary (in bytes)
        diff = first_offset - \
            (int(first_offset) >> csim.first_level.cl_bits << csim.first_level.cl_bits)
        warmup_iteration_count -= (diff//element_size)//inner_increment
        warmup_indices = self.kernel.global_iterator_to_indices(warmup_iteration_count)
        offsets = self.kernel.compile_global_offsets(
            iteration=range(0, warmup_iteration_count))

        if max_array_size < 2*max_cache_size:
            # Full caching possible, go through all itreration before actual initialization
            offsets = np.concatenate((self.kernel.compile_global_offsets(
                                         iteration=range(0, self.kernel.iteration_length())),
                                      offsets))

        # Do the warm-up
        csim.loadstore(offsets, length=element_size)
        # FIXME compile_global_offsets should already expand to element_size

        # Reset stats to conclude warm-up phase
        csim.reset_stats()

        # Benchmark iterations:
        # Starting point is one past the last warmup element
        bench_iteration_start = warmup_iteration_count
        # End point is the end of the current dimension (cacheline alligned)
        first_dim_factor = int((inner_loop['stop'] - warmup_indices[inner_index] - 1)
                               // (elements_per_cacheline // inner_increment))
        # If end point is less than one cacheline away, go beyond for 100 cachelines and
        # warn user of potentially inaccurate results
        if first_dim_factor == 0:
            # TODO a nicer solution would be to do less warmup iterations to select a
            # cacheline within a first dimension, if possible
            print('Warning: (automatic) warmup vs benchmark iteration choice was not perfect '
                  'and may lead to inaccurate cache miss predictions. This is most likely the '
                  'result of too few inner loop iterations ({} from {} to {}).'.format(
                      inner_loop['index'], inner_loop['start'], inner_loop['stop']
                  ))
            first_dim_factor = 100
        bench_iteration_end = (bench_iteration_start +
                               elements_per_cacheline * inner_increment * first_dim_factor)

        # compile access needed for one cache-line
        offsets = self.kernel.compile_global_offsets(
            iteration=range(bench_iteration_start, bench_iteration_end))
        # simulate
        csim.loadstore(offsets, length=element_size)
        # FIXME compile_global_offsets should already expand to element_size

        # use stats to build results
        self.stats = list(csim.stats())
        self.first_dim_factor = first_dim_factor

    def get_loads(self):
        """Return a list with number of loaded cache lines per memory hierarchy level."""
        return [self.stats[cache_level]['LOAD_count'] / self.first_dim_factor
                for cache_level in range(len(self.machine['memory hierarchy']))]

    def get_hits(self):
        """Return a list with number of hit cache lines per memory hierarchy level."""
        return [self.stats[cache_level]['HIT_count']/self.first_dim_factor
                for cache_level in range(len(self.machine['memory hierarchy']))]

    def get_misses(self):
        """Return a list with number of missed cache lines per memory hierarchy level."""
        return [self.stats[cache_level]['MISS_count']/self.first_dim_factor
                for cache_level in range(len(self.machine['memory hierarchy']))]

    def get_stores(self):
        """Return a list with number of stored cache lines per memory hierarchy level."""
        return [self.stats[cache_level]['STORE_count']/self.first_dim_factor
                for cache_level in range(len(self.machine['memory hierarchy']))]

    def get_evicts(self):
        """Return a list with number of evicted cache lines per memory hierarchy level."""
        return [self.stats[cache_level]['EVICT_count']/self.first_dim_factor
                for cache_level in range(len(self.machine['memory hierarchy']))]

    def get_infos(self):
        """Return verbose information about the predictor."""
        first_dim_factor = self.first_dim_factor
        infos = {'memory hierarchy': [], 'cache stats': self.stats,
                 'cachelines in stats': first_dim_factor}
        for cache_level, cache_info in list(enumerate(self.machine['memory hierarchy'])):
            infos['memory hierarchy'].append({
                'index': len(infos['memory hierarchy']),
                'level': '{}'.format(cache_info['level']),
                'total loads': self.stats[cache_level]['LOAD_byte']/first_dim_factor,
                'total misses': self.stats[cache_level]['MISS_byte']/first_dim_factor,
                'total hits': self.stats[cache_level]['HIT_byte']/first_dim_factor,
                'total stores': self.stats[cache_level]['STORE_byte']/first_dim_factor,
                'total evicts': self.stats[cache_level]['EVICT_byte']/first_dim_factor,
                'total lines load': self.stats[cache_level]['LOAD_count']/first_dim_factor,
                'total lines misses': self.stats[cache_level]['MISS_count']/first_dim_factor,
                'total lines hits': self.stats[cache_level]['HIT_count']/first_dim_factor,
                'total lines stores': self.stats[cache_level]['STORE_count']/first_dim_factor,
                'total lines evicts': self.stats[cache_level]['EVICT_count']/first_dim_factor,
                'cycles': None})
        return infos


#!/usr/bin/env python3
import collections
import sys
import subprocess
import re
from copy import copy
from pprint import pprint

from ruamel import yaml

from .prefixedunit import PrefixedUnit
from . import __version__

def get_match_or_break(regex, haystack, flags=re.MULTILINE):
    m = re.search(regex, haystack, flags)
    if not m:
        raise ValueError("could not find " + repr(regex) + " in " + repr(haystack))
    return m.groups()

def get_likwid_topology(cmd: str='likwid-topology') -> str:
    try:
        topo = subprocess.Popen(
            ['likwid-topology'], stdout=subprocess.PIPE
        ).communicate()[0].decode("utf-8")
    except OSError as e:
        print('likwid-topology execution failed ({}), is it installed and loaded?'.format(e),
              file=sys.stderr)
        sys.exit(1)
    return topo

def read_cpuinfo(cpuinfo_path: str='/proc/cpuinfo') -> str:
    with open(cpuinfo_path, 'r') as f:
        cpuinfo = f.read()
    return cpuinfo

def get_machine_topology(cpuinfo_path: str='/proc/cpuinfo') -> dict:
    topo = get_likwid_topology()
    cpuinfo = read_cpuinfo(cpuinfo_path)

    sockets = int(get_match_or_break(r'^Sockets:\s+([0-9]+)\s*$', topo)[0])
    cores_per_socket = int(get_match_or_break(r'^Cores per socket:\s+([0-9]+)\s*$', topo)[0])
    numa_domains_per_socket = \
        int(get_match_or_break(r'^NUMA domains:\s+([0-9]+)\s*$', topo)[0]) // sockets
    cores_per_numa_domain = cores_per_socket // numa_domains_per_socket
    machine = {
        'kerncraft version': __version__,
        'model type': get_match_or_break(r'^CPU type:\s+(.+?)\s*$', topo)[0],
        'model name': get_match_or_break(r'^model name\s+:\s+(.+?)\s*$', cpuinfo)[0],
        'sockets': sockets,
        'cores per socket': cores_per_socket,
        'threads per core': int(get_match_or_break(r'^Threads per core:\s+([0-9]+)\s*$', topo)[0]),
        'NUMA domains per socket': numa_domains_per_socket,
        'cores per NUMA domain': cores_per_numa_domain,
        'clock': 'INFORMATION_REQUIRED (e.g., 2.7 GHz)',
        'FLOPs per cycle': {'SP': {'total': 'INFORMATION_REQUIRED',
                                   'FMA': 'INFORMATION_REQUIRED',
                                   'ADD': 'INFORMATION_REQUIRED',
                                   'MUL': 'INFORMATION_REQUIRED'},
                            'DP': {'total': 'INFORMATION_REQUIRED',
                                   'FMA': 'INFORMATION_REQUIRED',
                                   'ADD': 'INFORMATION_REQUIRED',
                                   'MUL': 'INFORMATION_REQUIRED'}},
        'micro-architecture': 'INFORMATION_REQUIRED (options: NHM, WSM, SNB, IVB, HSW, BDW, SKL, SKX)',
        # TODO retrive flags automatically from compiler with -march=native
        'compiler': collections.OrderedDict([
                    ('icc', ['INFORMATION_REQUIRED (e.g., -O3 -fno-alias -xAVX)']),
                    ('clang', ['INFORMATION_REQUIRED (e.g., -O3 -mavx, -D_POSIX_C_SOURCE=200112L']),
                    ('gcc', ['INFORMATION_REQUIRED (e.g., -O3 -march=ivybridge)'])]),
        'cacheline size': 'INFORMATION_REQUIRED (in bytes, e.g. 64 B)',
        'overlapping model': {
            'ports': 'INFORAMTION_REQUIRED (list of ports as they appear in IACA, e.g.)'
                     ', ["0", "0DV", "1", "2", "2D", "3", "3D", "4", "5", "6", "7"])',
            'performance counter metric':
                'INFORAMTION_REQUIRED Example:'
                'max(UOPS_DISPATCHED_PORT_PORT_0__PMC2, UOPS_DISPATCHED_PORT_PORT_1__PMC3,'
                '    UOPS_DISPATCHED_PORT_PORT_4__PMC0, UOPS_DISPATCHED_PORT_PORT_5__PMC1)'
        },
        'non-overlapping model': {
            'ports': 'INFORAMTION_REQUIRED (list of ports as they appear in IACA, e.g.)'
                     ', ["0", "0DV", "1", "2", "2D", "3", "3D", "4", "5", "6", "7"])',
            'performance counter metric':
                'INFORAMTION_REQUIRED Example:'
                'max(UOPS_DISPATCHED_PORT_PORT_0__PMC2, UOPS_DISPATCHED_PORT_PORT_1__PMC3,'
                '    UOPS_DISPATCHED_PORT_PORT_4__PMC0, UOPS_DISPATCHED_PORT_PORT_5__PMC1)'
        }
    }

    threads_start = topo.find('HWThread')
    threads_end = topo.find('Cache Topology')
    threads = {}
    for line in topo[threads_start:threads_end].split('\n'):
        m = re.match(r'([0-9]+)\s+([0-9]+)\s+([0-9]+)\s+([0-9]+)', line)
        if m:
            threads[m.groups()[0]] = (m.groups()[1:])

    cache_start = topo.find('Cache Topology')
    cache_end = topo.find('NUMA Topology')
    machine['memory hierarchy'] = []
    mem_level = {}
    for line in topo[cache_start:cache_end].split('\n'):
        if line.startswith('Level:'):
            mem_level = {'level': 'L' + line.split(':')[1].strip()}
            machine['memory hierarchy'].append(mem_level)
            if mem_level['level'] != 'L1':
                mem_level['non-overlap upstream throughput'] = [
                    'INFORMATION_REQUIRED (e.g. 24 B/cy)',
                    'INFORMATION_REQUIRED (e.g. "half-duplex" or "full-duplex")']
        elif line.startswith('Size:'):
            size = PrefixedUnit(line.split(':')[1].strip())
            mem_level['cache per group'] = {
                'sets': 'INFORMATION_REQUIRED (sets*ways*cl_size=' + str(size) + ')',
                'ways': 'INFORMATION_REQUIRED (sets*ways*cl_size=' + str(size) + ')',
                'cl_size': 'INFORMATION_REQUIRED (sets*ways*cl_size=' + str(size) + ')',
                'replacement_policy': 'INFORMATION_REQUIRED (options: LRU, FIFO, MRU, RR)',
                'write_allocate': 'INFORMATION_REQUIRED (True/False)',
                'write_back': 'INFORMATION_REQUIRED (True/False)',
                'load_from': 'L' + str(int(mem_level['level'][1:]) + 1),
                'store_to': 'L' + str(int(mem_level['level'][1:]) + 1)}
            mem_level['size per group'] = size
        elif line.startswith('Cache groups:'):
            mem_level['groups'] = line.count('(')
            mem_level['cores per group'] = \
                (machine['cores per socket'] * machine['sockets']) // mem_level['groups']
            mem_level['threads per group'] = \
                int(mem_level['cores per group'] * machine['threads per core'])
        mem_level['performance counter metrics'] = {
            'accesses': 'INFORMATION_REQUIRED (e.g., L1D_REPLACEMENT__PMC0)',
            'misses': 'INFORMATION_REQUIRED (e.g., L2_LINES_IN_ALL__PMC1)',
            'evicts': 'INFORMATION_REQUIRED (e.g., L2_LINES_OUT_DIRTY_ALL__PMC2)'
        }

    # Remove last caches load_from and store_to:
    del machine['memory hierarchy'][-1]['cache per group']['load_from']
    del machine['memory hierarchy'][-1]['cache per group']['store_to']

    machine['memory hierarchy'].append({
        'level': 'MEM',
        'cores per group': int(machine['cores per socket']),
        'threads per group': int(machine['threads per core'] * machine['cores per socket']),
        'non-overlap upstream throughput':
            ['full socket memory bandwidth',
             'INFORMATION_REQUIRED (e.g. "half-duplex" or "full-duplex")'],
        'penalty cycles per read stream': 0,
        'size per group': None
    })

    return machine

def measure_bw(type_, total_size, threads_per_core, max_threads_per_core, cores_per_socket,
               sockets):
    """*size* is given in kilo bytes"""
    groups = []
    for s in range(sockets):
        groups += [
            '-w',
            'S' + str(s) + ':' + str(total_size) + 'kB:' +
            str(threads_per_core * cores_per_socket) +
            ':1:' + str(int(max_threads_per_core / threads_per_core))]
    # for older likwid versions add ['-g', str(sockets), '-i', str(iterations)] to cmd
    cmd = ['likwid-bench', '-t', type_] + groups
    sys.stderr.write(' '.join(cmd))
    output = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0].decode('utf-8')
    if not output:
        print(' '.join(cmd) + ' returned no output, possibly wrong version installed '
                              '(requires 4.0 or later)', file=sys.stderr)
        sys.exit(1)
    bw = float(get_match_or_break(r'^MByte/s:\s+([0-9]+(?:\.[0-9]+)?)\s*$', output)[0])
    print(' ', PrefixedUnit(bw, 'MB/s'), file=sys.stderr)
    return PrefixedUnit(bw, 'MB/s')

def cli():
    # TODO support everything described here
    if '-h' in sys.argv[1:] or '--help' in sys.argv[1:]:
        print("""Usage:', sys.argv[0], '[-h] {collect|measure} [machinefile] | upgrade machinefile

        collect will retrieve as much hardware information as possible, without benchmarking
        measure will do the same as collect, but also include memory benchmarks

        If machinefile already exists the CPU name will be compared. If they matche, measurements
        will proceed and the file is updated accordingly. All other information in the file
        (typically manually inserted) will be left alone.

        If no machinefile is given, the information will be printed to stdout.

        upgrade will transform machinefile to the most up-to-date machine file version.
        """)

def main():
    machine = get_machine_topology()

    machine['benchmarks'] = {
        'kernels': {
            'load': {
                'read streams': {'streams': 1, 'bytes': PrefixedUnit(8, 'B')},
                'read+write streams': {'streams': 0, 'bytes': PrefixedUnit(0, 'B')},
                'write streams': {'streams': 0, 'bytes': PrefixedUnit(0, 'B')},
                'FLOPs per iteration': 0},
            'copy': {
                'read streams': {'streams': 1, 'bytes': PrefixedUnit(8, 'B')},
                'read+write streams': {'streams': 0, 'bytes': PrefixedUnit(0, 'B')},
                'write streams': {'streams': 1, 'bytes': PrefixedUnit(8, 'B')},
                'FLOPs per iteration': 0},
            'update': {
                'read streams': {'streams': 1, 'bytes': PrefixedUnit(8, 'B')},
                'read+write streams': {'streams': 1, 'bytes': PrefixedUnit(8, 'B')},
                'write streams': {'streams': 1, 'bytes': PrefixedUnit(8, 'B')},
                'FLOPs per iteration': 0},
            'triad': {
                'read streams': {'streams': 3, 'bytes': PrefixedUnit(24, 'B')},
                'read+write streams': {'streams': 0, 'bytes': PrefixedUnit(0, 'B')},
                'write streams': {'streams': 1, 'bytes': PrefixedUnit(8, 'B')},
                'FLOPs per iteration': 2},
            'daxpy': {
                'read streams': {'streams': 2, 'bytes': PrefixedUnit(16, 'B')},
                'read+write streams': {'streams': 1, 'bytes': PrefixedUnit(8, 'B')},
                'write streams': {'streams': 1, 'bytes': PrefixedUnit(8, 'B')},
                'FLOPs per iteration': 2}, },
        'measurements': {}}

    USAGE_FACTOR = 0.66
    MEM_FACTOR = 15.0

    cores = list(range(1, machine['cores per socket'] + 1))
    for mem in machine['memory hierarchy']:
        measurement = {}
        machine['benchmarks']['measurements'][mem['level']] = measurement

        for threads_per_core in range(1, machine['threads per core'] + 1):
            threads = [c * threads_per_core for c in cores]
            if mem['size per group'] is not None:
                total_sizes = [
                    PrefixedUnit(max(int(mem['size per group']) * c / mem['cores per group'],
                                     int(mem['size per group'])) * USAGE_FACTOR, 'B')
                    for c in cores]
            else:
                last_mem = machine['memory hierarchy'][-2]
                total_sizes = [last_mem['size per group'] * MEM_FACTOR for c in cores]
            sizes_per_core = [t / cores[i] for i, t in enumerate(total_sizes)]
            sizes_per_thread = [t / threads[i] for i, t in enumerate(total_sizes)]

            measurement[threads_per_core] = {
                'threads per core': threads_per_core,
                'cores': copy(cores),
                'threads': threads,
                'size per core': sizes_per_core,
                'size per thread': sizes_per_thread,
                'total size': total_sizes,
                'results': {}, }
    print('Progress: ', end='', file=sys.stderr)
    sys.stderr.flush()
    for mem_level in list(machine['benchmarks']['measurements'].keys()):
        for threads_per_core in list(machine['benchmarks']['measurements'][mem_level].keys()):
            measurement = machine['benchmarks']['measurements'][mem_level][threads_per_core]
            measurement['results'] = {}
            for kernel in list(machine['benchmarks']['kernels'].keys()):
                measurement['results'][kernel] = []
                for i, total_size in enumerate(measurement['total size']):
                    measurement['results'][kernel].append(measure_bw(
                        kernel,
                        int(float(total_size) / 1000),
                        threads_per_core,
                        machine['threads per core'],
                        measurement['cores'][i],
                        sockets=1))

                    print('.', end='', file=sys.stderr)
                    sys.stderr.flush()

    print(yaml.dump(machine, Dumper=yaml.Dumper))

if __name__ == '__main__':
    main()


#!/usr/bin/env python3
"""Benchmark model and helper functions."""
import os
import subprocess
from functools import reduce
import operator
import sys
from distutils.spawn import find_executable
import re
from collections import defaultdict
import string
import pprint
import contextlib

from kerncraft.prefixedunit import PrefixedUnit
from .base import PerformanceModel

class sympy_safe_key:
    """Replacement for pprint._safe_key to be sympy-safe"""

    __slots__ = ['obj']

    def __init__(self, obj):
        self.obj = obj

    def __lt__(self, other):
        return ((str(type(self.obj)), id(self.obj)) < \
                (str(type(other.obj)), id(other.obj)))

@contextlib.contextmanager
def pprint_nosort():
    orig, pprint._safe_key = pprint._safe_key, sympy_safe_key
    try:
        yield
    finally:
        pprint._safe_key = orig

@contextlib.contextmanager
def fix_env_variable(name, value):
    """Fix environment variable to a value within context. Unset if value is None."""
    orig = os.environ.get(name, None)
    if value is not None:
        # Set if value is not None
        os.environ[name] = value
    elif name in os.environ:
        # Unset if value is None
        del os.environ[name]
    try:
        yield
    finally:
        if orig is not None:
            # Restore original value
            os.environ[name] = orig
        elif name in os.environ:
            # Unset
            del os.environ[name]

def group_iterator(group):
    """
    Iterate over simple regex-like groups.

    The only special character is a dash (-), which take the preceding and the following chars to
    compute a range. If the range is non-sensical (e.g., b-a) it will be empty

    Example:
    >>> list(group_iterator('a-f'))
    ['a', 'b', 'c', 'd', 'e', 'f']
    >>> list(group_iterator('148'))
    ['1', '4', '8']
    >>> list(group_iterator('7-9ab'))
    ['7', '8', '9', 'a', 'b']
    >>> list(group_iterator('0B-A1'))
    ['0', '1']
    """
    ordered_chars = string.ascii_letters + string.digits
    tokenizer = ('(?P<seq>[a-zA-Z0-9]-[a-zA-Z0-9])|'
                 '(?P<chr>.)')
    for m in re.finditer(tokenizer, group):
        if m.group('seq'):
            start, sep, end = m.group('seq')
            for i in range(ordered_chars.index(start), ordered_chars.index(end) + 1):
                yield ordered_chars[i]
        else:
            yield m.group('chr')

def register_options(regdescr):
    """
    Very reduced regular expressions for describing a group of registers.

    Only groups in square bracktes and unions with pipes (|) are supported.

    Examples:
    >>> list(register_options('PMC[0-3]'))
    ['PMC0', 'PMC1', 'PMC2', 'PMC3']
    >>> list(register_options('MBOX0C[01]'))
    ['MBOX0C0', 'MBOX0C1']
    >>> list(register_options('CBOX2C1'))
    ['CBOX2C1']
    >>> list(register_options('CBOX[0-3]C[01]'))
    ['CBOX0C0', 'CBOX0C1', 'CBOX1C0', 'CBOX1C1', 'CBOX2C0', 'CBOX2C1', 'CBOX3C0', 'CBOX3C1']
    >>> list(register_options('PMC[0-1]|PMC[23]'))
    ['PMC0', 'PMC1', 'PMC2', 'PMC3']
    """
    if not regdescr:
        yield None
    tokenizer = ('\[(?P<grp>[^]]+)\]|'
                 '(?P<chr>.)')
    for u in regdescr.split('|'):
        m = re.match(tokenizer, u)

        if m.group('grp'):
            current = group_iterator(m.group('grp'))
        else:
            current = [m.group('chr')]

        for c in current:
            if u[m.end():]:
                for r in register_options(u[m.end():]):
                    yield c + r
            else:
                yield c

def eventstr(event_tuple=None, event=None, register=None, parameters=None):
    """
    Return a LIKWID event string from an event tuple or keyword arguments.

    *event_tuple* may have two or three arguments: (event, register) or
    (event, register, parameters)

    Keyword arguments will be overwritten by *event_tuple*.

    >>> eventstr(('L1D_RE'+'PLACEMENT', 'PMC0', None))
    'L1D_RE'+'PLACEMENT:PMC0'
    >>> eventstr(('L1D_RE'+'PLACEMENT', 'PMC0'))
    'L1D_RE'+'PLACEMENT:PMC0'
    >>> eventstr(('MEM_UOPS_RETIRED_LOADS', 'PMC3', {'EDGEDETECT': None, 'THRESHOLD': 2342}))
    'MEM_UOPS_RETIRED_LOADS:PMC3:EDGEDETECT:THRESHOLD=0x926'
    >>> eventstr(event='DTLB_LOAD_MISSES_WALK_DURATION', register='PMC3')
    'DTLB_LOAD_MISSES_WALK_DURATION:PMC3'
    """
    if len(event_tuple) == 3:
        event, register, parameters = event_tuple
    elif len(event_tuple) == 2:
        event, register = event_tuple
    event_dscr = [event, register]

    if parameters:
        for k, v in sorted(event_tuple[2].items()):  # sorted for reproducability
            if type(v) is int:
                k += "={}".format(hex(v))
            event_dscr.append(k)
    return ":".join(event_dscr)

def build_minimal_runs(events):
    """Compile list of minimal runs for given events."""
    # Eliminate multiples
    events = [e for i, e in enumerate(events) if events.index(e) == i]

    # Build list of runs per register group
    scheduled_runs = {}
    scheduled_events = []
    cur_run = 0
    while len(scheduled_events) != len(events):
        for event_tpl in events:
            event, registers, parameters = event_tpl
            # Skip allready scheduled events
            if event_tpl in scheduled_events:
                continue
            # Compile explicit list of possible register locations
            for possible_reg in register_options(registers):
                # Schedule in current run, if register is not yet in use
                s = scheduled_runs.setdefault(cur_run, {})
                if possible_reg not in s:
                    s[possible_reg] = (event, possible_reg, parameters)
                    # ban from further scheduling attempts
                    scheduled_events.append(event_tpl)
                    break
        cur_run += 1

    # Collaps all register dicts to single runs
    runs = [list(v.values()) for v in scheduled_runs.values()]

    return runs

class Benchmark(PerformanceModel):
    """Produce a benchmarkable binary to be used with likwid."""

    name = "benchmark"

    @classmethod
    def configure_arggroup(cls, parser):
        """Configure argument parser."""
        parser.add_argument(
            '--no-phenoecm', action='store_true',
            help='Disables the phenomenological ECM model building.')
        parser.add_argument(
            '--iterations', type=int, default=10,
            help='Number of outer-loop iterations (e.g. time loop) during benchmarking. '
                 'Default is 10, but actual number will be adapted to at least 0.2s runtime.')
        parser.add_argument(
            '--ignore-warnings', action='store_true',
            help='Ignore warnings about missmatched CPU model and frequency.')

    def __init__(self, kernel, machine, args=None, parser=None, no_phenoecm=False, verbose=0):
        """
        Create Benchmark model from kernel and machine objects.

        *kernel* is a Kernel object
        *machine* describes the machine (cpu, cache and memory) characteristics
        *args* (optional) are the parsed arguments from the command line

        If *args* is None, *no_phenoecm* and *verbose* are used.
        """
        self.kernel = kernel
        self.machine = machine
        self._args = args
        self._parser = parser
        self.results = None

        if args:
            self.no_phenoecm = args.no_phenoecm
            self.verbose = args.verbose
            self.iterations = args.iterations
        else:
            self.no_phenoecm = no_phenoecm
            self.verbose = verbose
            self.iterations = 10

        if self._args.cores > 1 and not self.no_phenoecm:
            print("Info: phenological ECM model can only be created with a single core benchmark.")
            self.no_phenoecm = True
        elif not self.no_phenoecm:
            print("Info: If this takes too long and a phenological ECM model is not required, run "
                  "with --no-phenoecm.", file=sys.stderr)

        warning = False
        cpuinfo = ''
        try:
            with open('/proc/cpuinfo') as f:
                cpuinfo = f.read()
        except FileNotFoundError:
            pass

        try:
            current_cpu_model = re.search(r'^model name\s+:\s+(.+?)\s*$',
                                          cpuinfo,
                                          flags=re.MULTILINE).groups()[0]
        except AttributeError:
            current_cpu_model = None
        if self.machine['model name'] != current_cpu_model:
            print("WARNING: current CPU model and machine description do not "
                  "match. ({!r} vs {!r})".format(self.machine['model name'],
                                                 current_cpu_model))
            warning = True
        try:
            current_cpu_freq = re.search(r'^cpu MHz\s+:\s+'
                                         r'([0-9]+(?:\.[0-9]+)?)\s*$',
                                         cpuinfo,
                                         flags=re.MULTILINE).groups()[0]
            current_cpu_freq = float(current_cpu_freq) * 1e6
        except AttributeError:
            current_cpu_freq = None
        if float(self.machine['clock']) != current_cpu_freq:
            print("WARNING: current CPU frequency and machine description do "
                  "not match. ({!r} vs {!r})".format(float(self.machine['clock']),
                                                     current_cpu_freq))
            warning = True
        if warning and not args.ignore_warnings:
            print("You may ignore warnings by adding --ignore-warnings to the command line.")
            sys.exit(1)

    def perfctr(self, cmd, group='MEM', code_markers=True):
        """
        Run *cmd* with likwid-perfctr and returns result as dict.

        *group* may be a performance group known to likwid-perfctr or an event string.

        if CLI argument cores > 1, running with multi-core, otherwise single-core
        """
        # Making sure likwid-perfctr is available:
        if find_executable('likwid-perfctr') is None:
            print("likwid-perfctr was not found. Make sure likwid is installed and found in PATH.",
                  file=sys.stderr)
            sys.exit(1)

        # FIXME currently only single core measurements support!
        perf_cmd = ['likwid-perfctr', '-f', '-O', '-g', group]

        cpu = 'S0:0'
        if self._args.cores > 1:
            cpu += '-'+str(self._args.cores-1)

        # Pinned and measured on cpu
        perf_cmd += ['-C', cpu]

        # code must be marked using likwid markers
        perf_cmd.append('-m')

        perf_cmd += cmd
        if self.verbose > 1:
            print(' '.join(perf_cmd))
        try:
            with fix_env_variable('OMP_NUM_THREADS', None):
                output = subprocess.check_output(perf_cmd).decode('utf-8').split('\n')
        except subprocess.CalledProcessError as e:
            print("Executing benchmark failed: {!s}".format(e), file=sys.stderr)
            sys.exit(1)

        # TODO multicore output is different and needs to be considered here!
        results = {}
        for line in output:
            line = line.split(',')
            try:
                # Metrics
                results[line[0]] = float(line[1])
            except ValueError:
                # Would not convert to float
                pass
            except IndexError:
                # Not a parable line (did not contain any commas)
                continue
            try:
                # Event counters
                if line[2] == '-' or line[2] == 'nan':
                    counter_value = 0
                else:
                    counter_value = int(line[2])
                if re.fullmatch(r'[A-Z0-9_]+', line[0]) and re.fullmatch(r'[A-Z0-9]+', line[1]):
                    results.setdefault(line[0], {})
                    results[line[0]][line[1]] = counter_value
            except (IndexError, ValueError):
                pass

        return results

    def analyze(self):
        """Run analysis."""
        bench = self.kernel.build_executable(verbose=self.verbose > 1, openmp=self._args.cores > 1)
        element_size = self.kernel.datatypes_size[self.kernel.datatype]

        # Build arguments to pass to command:
        args = [str(s) for s in list(self.kernel.constants.values())]

        # Determine base runtime with 10 iterations
        runtime = 0.0
        time_per_repetition = 2.0 / 10.0
        repetitions = self.iterations // 10
        mem_results = {}

        # TODO if cores > 1, results are for openmp run. Things might need to be changed here!

        while runtime < 1.5:
            # Interpolate to a 2.0s run
            if time_per_repetition != 0.0:
                repetitions = 2.0 // time_per_repetition
            else:
                repetitions = int(repetitions * 10)

            mem_results = self.perfctr([bench] + [str(repetitions)] + args, group="MEM")
            runtime = mem_results['Runtime (RDTSC) [s]']
            time_per_repetition = runtime / float(repetitions)
        raw_results = [mem_results]

        # Base metrics for further metric computations:
        # An iteration is equal to one high-level code inner-most-loop iteration
        iterations_per_repetition = reduce(
            operator.mul,
            [self.kernel.subs_consts(max_ - min_) / self.kernel.subs_consts(step)
             for idx, min_, max_, step in self.kernel._loop_stack],
            1)
        iterations_per_cacheline = (float(self.machine['cacheline size']) /
                                    self.kernel.bytes_per_iteration)
        cys_per_repetition = time_per_repetition * float(self.machine['clock'])

        # Gather remaining counters
        if not self.no_phenoecm:
            # Build events and sympy expressions for all model metrics
            T_OL, event_counters = self.machine.parse_perfmetric(
                self.machine['overlapping model']['performance counter metric'])
            T_data, event_dict = self.machine.parse_perfmetric(
                self.machine['non-overlapping model']['performance counter metric'])
            event_counters.update(event_dict)
            cache_metrics = defaultdict(dict)
            for i in range(len(self.machine['memory hierarchy']) - 1):
                cache_info = self.machine['memory hierarchy'][i]
                name = cache_info['level']
                for k, v in cache_info['performance counter metrics'].items():
                    cache_metrics[name][k], event_dict = self.machine.parse_perfmetric(v)
                    event_counters.update(event_dict)

            # Compile minimal runs to gather all required events
            minimal_runs = build_minimal_runs(list(event_counters.values()))
            measured_ctrs = {}
            for run in minimal_runs:
                ctrs = ','.join([eventstr(e) for e in run])
                r = self.perfctr([bench] + [str(repetitions)] + args, group=ctrs)
                raw_results.append(r)
                measured_ctrs.update(r)
            # Match measured counters to symbols
            event_counter_results = {}
            for sym, ctr in event_counters.items():
                event, regs, parameter = ctr[0], register_options(ctr[1]), ctr[2]
                for r in regs:
                    if r in measured_ctrs[event]:
                        event_counter_results[sym] = measured_ctrs[event][r]

            # Analytical metrics needed for futher calculation
            cl_size = float(self.machine['cacheline size'])
            total_iterations = iterations_per_repetition * repetitions
            total_cachelines = total_iterations / iterations_per_cacheline

            T_OL_result = T_OL.subs(event_counter_results) / total_cachelines
            cache_metric_results = defaultdict(dict)
            for cache, mtrcs in cache_metrics.items():
                for m, e in mtrcs.items():
                    cache_metric_results[cache][m] = e.subs(event_counter_results)

            # Inter-cache transfers per CL
            cache_transfers_per_cl = {cache: {k: PrefixedUnit(v / total_cachelines, 'CL/CL')
                                              for k, v in d.items()}
                                      for cache, d in cache_metric_results.items()}
            cache_transfers_per_cl['L1']['accesses'].unit = 'LOAD/CL'

            # Select appropriate bandwidth
            mem_bw, mem_bw_kernel = self.machine.get_bandwidth(
                -1,  # mem
                cache_metric_results['L3']['misses'],  # load_streams
                cache_metric_results['L3']['evicts'],  # store_streams
                1)

            data_transfers = {
                # Assuming 0.5 cy / LOAD (SSE on SNB or IVB; AVX on HSW, BDW, SKL or SKX)
                'T_nOL': (cache_metric_results['L1']['accesses'] / total_cachelines * 0.5),
                'T_L1L2': ((cache_metric_results['L1']['misses'] +
                            cache_metric_results['L1']['evicts']) /
                           total_cachelines * cl_size /
                           self.machine['memory hierarchy'][1]['non-overlap upstream throughput'][0]),
                'T_L2L3': ((cache_metric_results['L2']['misses'] +
                            cache_metric_results['L2']['evicts']) /
                           total_cachelines * cl_size /
                           self.machine['memory hierarchy'][2]['non-overlap upstream throughput'][0]),
                'T_L3MEM': ((cache_metric_results['L3']['misses'] +
                             cache_metric_results['L3']['evicts']) *
                            float(self.machine['cacheline size']) /
                            total_cachelines / mem_bw *
                            float(self.machine['clock']))
            }

            # Build phenomenological ECM model:
            ecm_model = {'T_OL': T_OL_result}
            ecm_model.update(data_transfers)
        else:
            event_counters = {}
            ecm_model = None
            cache_transfers_per_cl = None

        self.results = {'raw output': raw_results, 'ECM': ecm_model,
                        'data transfers': cache_transfers_per_cl,
                        'Runtime (per repetition) [s]': time_per_repetition,
                        'event counters': event_counters,
                        'Iterations per repetition': iterations_per_repetition,
                        'Iterations per cacheline': iterations_per_cacheline}

        # TODO make more generic to support other (and multiple) constant names
        self.results['Runtime (per cacheline update) [cy/CL]'] = \
            (cys_per_repetition / iterations_per_repetition) * iterations_per_cacheline
        self.results['MEM volume (per repetition) [B]'] = \
            mem_results['Memory data volume [GBytes]'] * 1e9 / repetitions
        self.results['Performance [MFLOP/s]'] = \
            sum(self.kernel._flops.values()) / (
            time_per_repetition / iterations_per_repetition) / 1e6
        if 'Memory bandwidth [MBytes/s]' in mem_results:
            self.results['MEM BW [MByte/s]'] = mem_results['Memory bandwidth [MBytes/s]']
        else:
            self.results['MEM BW [MByte/s]'] = mem_results['Memory BW [MBytes/s]']
        self.results['Performance [MLUP/s]'] = (
                                               iterations_per_repetition / time_per_repetition) / 1e6
        self.results['Performance [MIt/s]'] = (
                                              iterations_per_repetition / time_per_repetition) / 1e6

    def report(self, output_file=sys.stdout):
        """Report gathered analysis data in human readable form."""
        if self.verbose > 1:
            with pprint_nosort():
                pprint.pprint(self.results)

        if self.verbose > 0:
            print('Runtime (per repetition): {:.2g} s'.format(
                self.results['Runtime (per repetition) [s]']),
                file=output_file)
        if self.verbose > 0:
            print('Iterations per repetition: {!s}'.format(
                self.results['Iterations per repetition']),
                file=output_file)
        print('Runtime (per cacheline update): {:.2f} cy/CL'.format(
            self.results['Runtime (per cacheline update) [cy/CL]']),
            file=output_file)
        print('MEM volume (per repetition): {:.0f} Byte'.format(
            self.results['MEM volume (per repetition) [B]']),
            file=output_file)
        print('Performance: {:.2f} MFLOP/s'.format(self.results['Performance [MFLOP/s]']),
              file=output_file)
        print('Performance: {:.2f} MLUP/s'.format(self.results['Performance [MLUP/s]']),
              file=output_file)
        print('Performance: {:.2f} It/s'.format(self.results['Performance [MIt/s]']),
              file=output_file)
        if self.verbose > 0:
            print('MEM bandwidth: {:.2f} MByte/s'.format(self.results['MEM BW [MByte/s]']),
                  file=output_file)
        print('', file=output_file)

        if not self.no_phenoecm:
            print("Data Transfers:")
            print("{:^8} |".format("cache"), end='')
            for metrics in self.results['data transfers'].values():
                for metric_name in sorted(metrics):
                    print(" {:^14}".format(metric_name), end='')
                print()
                break
            for cache, metrics in sorted(self.results['data transfers'].items()):
                print("{!s:^8} |".format(cache), end='')
                for k, v in sorted(metrics.items()):
                    print(" {!s:^14}".format(v), end='')
                print()
            print()

            print('Phenomenological ECM model: {{ {T_OL:.1f} || {T_nOL:.1f} | {T_L1L2:.1f} | '
                  '{T_L2L3:.1f} | {T_L3MEM:.1f} }} cy/CL'.format(
                **{k: float(v) for k, v in self.results['ECM'].items()}),
                file=output_file)
            print('T_OL assumes that two loads per cycle may be retiered, which is true for '
                  '128bit SSE/half-AVX loads on SNB and IVY, and 256bit full-AVX loads on HSW, '
                  'BDW, SKL and SKX, but it also depends on AGU availability.',
                  file=output_file)


#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Installation:
    pip install git+https://github.com/Erotemic/progiter.git

Developing:
    git clone https://github.com/Erotemic/progiter.git
    pip install -e progiter

Pypi:
     # Presetup
     pip install twine

     # First tag the source-code
     VERSION=$(python -c "import setup; print(setup.version)")
     echo $VERSION
     git tag $VERSION -m "tarball tag $VERSION"
     git push --tags origin master

     # NEW API TO UPLOAD TO PYPI
     # https://packaging.python.org/tutorials/distributing-packages/

     # Build wheel or source distribution
     python setup.py bdist_wheel --universal

     # Use twine to upload. This will prompt for username and password
     # If you get an error:
     #   403 Client Error: Invalid or non-existent authentication information.
     # simply try typing your password slower.
     twine upload --username erotemic --skip-existing dist/*

     # Check the url to make sure everything worked
     https://pypi.org/project/progiter/

     # ---------- OLD ----------------
     # Check the url to make sure everything worked
     https://pypi.python.org/pypi?:action=display&name=progiter

Update Requirments:
    # Requirements are broken down by type in the `requirements` folder, and
    # `requirments.txt` lists them all. Thus we autogenerate via:
    cat requirements/*.txt > requirements.txt
"""
from setuptools import setup

def parse_version(package):
    """
    Statically parse the version number from __init__.py

    CommandLine:
        python -c "import setup; print(setup.parse_version('progiter'))"
    """
    from os.path import dirname, join
    import ast
    init_fpath = join(dirname(__file__), package, '__init__.py')
    with open(init_fpath) as file_:
        sourcecode = file_.read()
    pt = ast.parse(sourcecode)
    class VersionVisitor(ast.NodeVisitor):
        def visit_Assign(self, node):
            for target in node.targets:
                if target.id == '__version__':
                    self.version = node.value.s
    visitor = VersionVisitor()
    visitor.visit(pt)
    return visitor.version

def parse_description():
    """
    Parse the description in the README file

    CommandLine:
        python -c "import setup; print(setup.parse_description())"
    """
    from os.path import dirname, join, exists
    readme_fpath = join(dirname(__file__), 'README.md')
    # print('readme_fpath = %r' % (readme_fpath,))
    # This breaks on pip install, so check that it exists.
    if exists(readme_fpath):
        # try:
        #     # convert markdown to rst for pypi
        #     import pypandoc
        #     return pypandoc.convert(readme_fpath, 'rst')
        # except Exception as ex:
            # strip out markdown to make a clean readme for pypi
            textlines = []
            with open(readme_fpath, 'r') as f:
                capture = False
                for line in f.readlines():
                    if '# Purpose' in line:
                        capture = True
                    elif line.startswith('##'):
                        break
                    elif capture:
                        textlines += [line]
            text = ''.join(textlines).strip()
            text = text.replace('\n\n', '_NLHACK_')
            text = text.replace('\n', ' ')
            text = text.replace('_NLHACK_', '\n\n')
            return text
    return ''

def parse_requirements(fname='requirements.txt'):
    """
    Parse the package dependencies listed in a requirements file but strips
    specific versioning information.

    CommandLine:
        python -c "import setup; print(setup.parse_requirements())"
    """
    from os.path import dirname, join, exists
    import re
    require_fpath = join(dirname(__file__), fname)
    # This breaks on pip install, so check that it exists.
    if exists(require_fpath):
        with open(require_fpath, 'r') as f:
            packages = []
            for line in f.readlines():
                line = line.strip()
                if line and not line.startswith('#'):
                    if line.startswith('-e '):
                        package = line.split('#egg=')[1]
                        packages.append(package)
                    else:
                        pat = '|'.join(['>', '>=', '=='])
                        package = re.split(pat, line)[0]
                        packages.append(package)
            return packages
    return []

version = parse_version('progiter')  # needs to be a global var for git tags

if __name__ == '__main__':
    setup(
        name='progiter',
        version=version,
        author='Jon Crall',
        description='A single-threaded alternative to tqdm.',
        long_description=parse_description(),
        install_requires=parse_requirements('requirements/runtime.txt'),
        extras_require={
            'all': parse_requirements('requirements.txt'),
            'tests': parse_requirements('requirements/tests.txt'),
            'docs': parse_requirements('docs/requirements.txt'),
        },
        author_email='erotemic@gmail.com',
        url='https://github.com/Erotemic/progiter',
        license='Apache 2',
        packages=['progiter'],
        classifiers=[
            # List of classifiers available at:
            # https://pypi.python.org/pypi?%3Aaction=list_classifiers
            'Development Status :: 3 - Alpha',
            'Intended Audience :: Developers',
            'Topic :: Software Development :: Libraries :: Python Modules',
            'Topic :: Utilities',
            # This should be interpreted as Apache License v2.0
            'License :: OSI Approved :: Apache Software License',
            # Supported Python versions
            'Programming Language :: Python :: 2.7',
            'Programming Language :: Python :: 3',
        ],
    )