"""\
Run the benchmark multiple times with a range of settings,
and produce tables / graphs with these results
"""

import argparse
import sys
import logging
import subprocess
import datetime
import os
import matplotlib.pyplot as plt
import matplotlib.ticker as mtick
import numpy as np
import math
import time
import pandas as pd

# Home dir should be continuum/
os.chdir('../')


def enable_logging(verbose):
    """Enable logging
    """
    # Set parameters
    level = logging.INFO
    if verbose:
        level = logging.DEBUG

    format = "[%(asctime)s %(filename)20s:%(lineno)4s - %(funcName)25s() ] %(message)s"
    logging.basicConfig(format=format, 
                        level=level, 
                        datefmt='%Y-%m-%d %H:%M:%S')

    logging.info('Logging has been enabled')


class Experiment():
    """Experiment template / super class
    """
    def __init__(self, resume):
        self.resume = resume
        self.runs = []

    def check_resume(self):
        """If the resume argument is given, get the first x log files >= the resume date,
        and use their output instead of re-running the experiment.
        """
        if self.resume == None:
            return

        log_location = './logs'
        logs = [f for f in os.listdir(log_location) if f.endswith('.log')]
        logs.sort()
        exp_i = 0

        for log in logs:
            splits = log.split('_')
            dt = splits[0] + '_' + splits[1]
            dt = datetime.datetime.strptime(dt, '%Y-%m-%d_%H:%M:%S')

            if dt >= self.resume:
                path = log_location + '/' + log
                logging.info('File %s for experiment run %i' % (path, exp_i))

                f = open(path, 'r')
                output = [line for line in f.readlines()]
                f.close()

                self.runs[exp_i]['output'] = output
                exp_i += 1

                # We have all logs needed
                if exp_i == len(self.runs):
                    break

    def run_commands(self):
        """Execute all generated commands
        """
        for run in self.runs:
            if run['command'] == []:
                continue

            # Skip runs where we got output with --resume
            if run['output'] != None:
                logging.info('Skip command: %s' % (' '.join(run['command'])))
                continue

            output, error = self.execute(run['command'])

            logging.debug('------------------------------------')
            logging.debug('OUTPUT')
            logging.debug('------------------------------------')
            logging.debug('\n' + ''.join(output))

            if error != []:
                logging.debug('------------------------------------')
                logging.debug('ERROR')
                logging.debug('------------------------------------')
                logging.debug('\n' + ''.join(error))
                sys.exit()

            logging.debug('------------------------------------')

            # Get output from log file
            logpath = output[0].rstrip().split('and file at ')[-1]
            f = open(logpath, 'r')
            output = [line for line in f.readlines()]
            run['output'] = output

    def execute(self, command):
        """Execute a process using the subprocess library, and return the output/error or the process

        Args:
            command (list(str)): Command to be executed.

        Returns:
            (list(str), list(str)): Return the output and error generated by this process.
        """
        logging.info(' '.join(command))
        process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        output = [line.decode('utf-8') for line in process.stdout.readlines()]
        error = [line.decode('utf-8') for line in process.stderr.readlines()]
        return output, error


class Figure4(Experiment):
    """Replicate figure 4:
    System load with an increasing number of endpoints connected to a singleworker

    So:
    - Run with all 3 deployment modes
    - Vary number of endpoints connected to a single worker
    """
    def __init__(self, resume):
        Experiment.__init__(self, resume)

        self.modes = ['cloud', 'edge', 'endpoint']
        self.cores = [4, 2, 1]
        self.endpoints = [1, 2, 4, 8]

        self.y = None

    def __repr__(self):
        """Returns this string when called as print(object)
        """
        return '''
APP                     image-classification
MODES                   %s
WORKERS                 1
CLOUD_CORES             %i
EDGE_CORES              %i
ENDPOINT_CORES          %i
ENDPOINTS/WORKER        %s''' % (
            ','.join(self.modes), self.cores[0], self.cores[1], self.cores[2],
            ','.join([str(endpoint) for endpoint in self.endpoints]))

    def generate(self):
        """Generate commands to run the benchmark based on the current settings
        """
        # Differ in deployment modes
        for mode in self.modes:
            if mode == 'cloud':
                config = 'cloud_endpoint'
                cores = self.cores[0]
            elif mode == 'edge':
                config = 'edge_endpoint'
                cores = self.cores[1]
            else:
                config = 'endpoint'
                cores = self.cores[2]

            # Differ in #endpoints per worker
            for endpoint in self.endpoints:
                # No sense to use more than 1 endpoint in endpoint-only deployment mode
                if mode == 'endpoint' and endpoint > 1:
                    continue

                command = ['python3', 'main.py', 'configuration/fig4/' + config + str(endpoint) + '.cfg']
                command = [str(c) for c in command]

                run = {'mode': mode, 
                       'cores': cores,
                       'endpoints': endpoint, 
                       'command': command, 
                       'output': None, 
                       'worker_time': None}
                self.runs.append(run)

    def parse_output(self):
        """For all runs, get the worker runtime
        """
        # Get the line containing the metrics
        for run in self.runs:
            input = run['output'][-7][1:-2]
            if 'Output in csv' in input:
                input = run['output'][-6][1:-2]

            # Split string into list
            l = [x.split(',') for x in input.split('\\n')]
            l = l[:-1]
            l = [sub[1:] for sub in l]

            # Split into header and data
            header = l[0]
            data = l[1:]

            # Convert into dataframe
            df = pd.DataFrame(data, columns=header)
            df['proc/data (ms)'] = pd.to_numeric(df['proc/data (ms)'], downcast='float')

            # Calculate num. of images processed per second by the cloud/edge/endpoint
            processed_rate = df['proc/data (ms)'].mean()
            processed_rate = 1000.0 / processed_rate
            processed_rate *= run['cores']

            # Calculate number of images generated per second
            frequency = 5
            requested_rate = float(frequency * run['endpoints'])

            # Calculate usage of processors
            run['usage'] = int((requested_rate / processed_rate) * 100)

    def plot(self):
        # set width of bar
        plt.rcParams.update({'font.size': 22})
        fig = plt.subplots(figsize =(12, 6))

        barWidth = 0.2
        bars = np.arange(len(self.modes))
        
        colors = ['dimgray', 'gray', 'darkgray', 'lightgray']

        y_total = []
        for endpoint, color in zip(self.endpoints, colors):
            # Get the x and y data
            y = [run['usage'] for run in self.runs if run['endpoints'] == endpoint]
            x = [x + math.log2(endpoint) * barWidth for x in bars]

            # mode=endpoint only uses 1 endpoint
            if endpoint > 1:
                x = x[:-1]

            # Plot the bar
            plt.bar(x, y, color=color, width=barWidth*0.9, label='Endpoints: %s' % (endpoint))
            y_total += y

        # Add horizontal lines every 100 percent
        plt.axhline(y=100, color='k', linestyle='-', linewidth=3)
        plt.axhline(y=200, color='k', linestyle='-', linewidth=1, alpha=0.5)
        plt.axhline(y=300, color='k', linestyle='-', linewidth=1, alpha=0.5)

        # Adding titles
        plt.xlabel('Deployment Mode')
        plt.ylabel('System Load')

        # Adding Xticks
        label_ticks = [r + (len(self.modes) / 2) * barWidth for r in range(len(self.modes))]
        label_ticks[-1] -= (len([endpoint for endpoint in self.endpoints if endpoint > 1]) / 2) * barWidth
        plt.xticks(label_ticks, [mode.capitalize() for mode in self.modes])

        plt.legend(loc='upper left', framealpha=1.0)
        plt.gca().yaxis.set_major_formatter(mtick.PercentFormatter())
        plt.ylim(0, 400)
        plt.yticks(np.arange(0, 500, 100))

        t = time.strftime("%Y-%m-%d_%H:%M:%S", time.gmtime())
        plt.savefig('./logs/fig4_%s.png' % (t), bbox_inches='tight')

        self.y = y_total

    def print_result(self):
        i = 0
        for endpoint in self.endpoints:
            for mode in self.modes:
                if mode == 'endpoint' and endpoint > 1:
                    break

                logging.info('Mode: %10s | Endpoints: %3s | System Load: %i%%' % (mode, endpoint, self.y[i]))
                i += 1


class Figure5(Experiment):
    """Replicate figure 5:
    System load with an increasing number of CPU cores per worker

    So:
    - Run with all 3 deployment modes
    - Vary number of CPU cores per worker
    """
    def __init__(self, resume):
        Experiment.__init__(self, resume)

        self.modes = ['cloud', 'edge', 'endpoint']
        self.cores = [1, 2, 4]

        self.y = None

    def __repr__(self):
        """Returns this string when called as print(object)
        """
        return '''
APP                     image-classificatiom
MODES                   %s
WORKERS                 1
CORES                   %s
ENDPOINTS/WORKER        1''' % (
            ','.join(self.modes), 
            ','.join([str(x) for x in self.cores]))

    def generate(self):
        """Generate commands to run the benchmark based on the current settings
        """
        # Differ in deployment modes
        for mode in self.modes:
            # Differ in #cores per worker
            for core in self.cores:
                command = []
                if not (mode == 'cloud' and core == 1):
                    command = ['python3', 'main.py', 'configuration/fig5/%s_cores%i.cfg' % (mode, core)]
                    command = [str(c) for c in command]

                run = {'mode': mode, 
                       'cores': core,
                       'endpoints': 1, 
                       'command': command, 
                       'output': None, 
                       'worker_time': None}
                self.runs.append(run)

    def parse_output(self):
        """For all runs, get the worker runtime
        """
        # Get the line containing the metrics
        for run in self.runs:
            # Kubernetes does not work with only 1 core
            if run['mode'] == 'cloud' and run['cores'] == 1:
                run['usage'] = 0
                continue

            input = run['output'][-7][1:-2]
            if 'Output in csv' in input:
                input = run['output'][-6][1:-2]

            # Split string into list
            l = [x.split(',') for x in input.split('\\n')]
            l = l[:-1]
            l = [sub[1:] for sub in l]

            # Split into header and data
            header = l[0]
            data = l[1:]

            # Convert into dataframe
            df = pd.DataFrame(data, columns=header)
            df['proc/data (ms)'] = pd.to_numeric(df['proc/data (ms)'], downcast='float')

            # Calculate num. of images processed per second by the cloud/edge/endpoint
            processed_rate = df['proc/data (ms)'].mean()
            processed_rate = 1000.0 / processed_rate
            processed_rate *= run['cores']

            # Calculate number of images generated per second
            frequency = 5
            requested_rate = float(frequency * run['endpoints'])

            # Calculate usage of processors
            run['usage'] = int((requested_rate / processed_rate) * 100)

    def plot(self):
        # set width of bar
        plt.rcParams.update({'font.size': 22})
        fig = plt.subplots(figsize =(12, 6))

        barWidth = 0.2
        bars = np.arange(len(self.modes))
        
        colors = ['dimgray', 'gray', 'darkgray']

        y_total = []
        for core, color in zip(self.cores, colors):
            # Get the x and y data
            y = [run['usage'] for run in self.runs if run['cores'] == core]
            x = [x + math.log2(core) * barWidth * 1.2 for x in bars]

            # Plot the bar
            plt.bar(x, y, color=color, width=barWidth * 1.1, label='CPU Cores: %s' % (core))

            y_total += y

        # Add horizontal lines every 100 percent
        plt.axhline(y=100, color='k', linestyle='-', linewidth=3)
        plt.axhline(y=200, color='k', linestyle='-', linewidth=1, alpha=0.5)
        plt.axhline(y=300, color='k', linestyle='-', linewidth=1, alpha=0.5)

        # Adding titles
        plt.xlabel('Deployment Mode')
        plt.ylabel('System Load')

        # Adding Xticks
        label_ticks = [r + (len(self.modes) / 2) * barWidth for r in range(len(self.modes))]
        plt.xticks(label_ticks, [mode.capitalize() for mode in self.modes])

        plt.legend(loc='upper left', framealpha=1.0)
        plt.gca().yaxis.set_major_formatter(mtick.PercentFormatter())
        plt.ylim(0, 400)
        plt.yticks(np.arange(0, 500, 100))

        t = time.strftime("%Y-%m-%d_%H:%M:%S", time.gmtime())
        plt.savefig('./logs/fig5_%s.png' % (t), bbox_inches='tight')

        self.y = y_total

    def print_result(self):
        i = 0
        for core in self.cores:
            for mode in self.modes:
                logging.info('Mode: %10s | Cores: %3s | System Load: %i%%' % (mode, core, self.y[i]))
                i += 1


def main(args):
    """Main function

    Args:
        args (Namespace): Argparse object
    """
    if args.figure == 'figure4':
        logging.info('Replicate figure 4')
        fig = Figure4(args.resume)
    elif args.figure == 'figure5':
        logging.info('Replicate figure 5')
        fig = Figure5(args.resume)
    else:
        logging.error('Invalid figure: %s' % (args.figure))
        sys.exit()

    logging.info(fig)
    fig.generate()
    fig.check_resume()
    fig.run_commands()
    fig.parse_output()
    fig.plot()
    fig.print_result()


if __name__ == '__main__':
    """Get input arguments, and validate those arguments
    """
    parser = argparse.ArgumentParser()

    parser.add_argument('figure', choices=['figure4', 'figure5'],
        help='Figure to replicate')
    parser.add_argument('-v', '--verbose', action='store_true',
        help='increase verbosity level')
    parser.add_argument('-r', '--resume', type=lambda s: datetime.datetime.strptime(s, '%Y-%m-%d_%H:%M:%S'),
        help='Resume a previous figure replication from datetime "YYYY-MM-DD_HH:mm:ss"')
    args = parser.parse_args()

    enable_logging(args.verbose)
    main(args)

"""\
Run the benchmark multiple times with a range of settings,
and produce tables / graphs with these results
"""

import argparse
import sys
import logging
import subprocess
import datetime
import os
import matplotlib.pyplot as plt
import matplotlib.ticker as mtick
import numpy as np
import math
import time
import pandas as pd

# Home dir should be continuum/
os.chdir('../')


def enable_logging(verbose):
    """Enable logging
    """
    # Set parameters
    level = logging.INFO
    if verbose:
        level = logging.DEBUG

    format = "[%(asctime)s %(filename)20s:%(lineno)4s - %(funcName)25s() ] %(message)s"
    logging.basicConfig(format=format, 
                        level=level, 
                        datefmt='%Y-%m-%d %H:%M:%S')

    logging.info('Logging has been enabled')


class Experiment():
    """Experiment template / super class
    """
    def __init__(self, resume):
        self.resume = resume
        self.runs = []

    def check_resume(self):
        """If the resume argument is given, get the first x log files >= the resume date,
        and use their output instead of re-running the experiment.
        """
        if self.resume == None:
            return

        log_location = './logs'
        logs = [f for f in os.listdir(log_location) if f.endswith('.log')]
        logs.sort()
        exp_i = 0

        for log in logs:
            splits = log.split('_')
            dt = splits[0] + '_' + splits[1]
            dt = datetime.datetime.strptime(dt, '%Y-%m-%d_%H:%M:%S')

            if dt >= self.resume:
                path = log_location + '/' + log
                logging.info('File %s for experiment run %i' % (path, exp_i))

                f = open(path, 'r')
                output = [line for line in f.readlines()]
                f.close()

                self.runs[exp_i]['output'] = output
                exp_i += 1

                # We have all logs needed
                if exp_i == len(self.runs):
                    break

    def run_commands(self):
        """Execute all generated commands
        """
        for run in self.runs:
            if run['command'] == []:
                continue

            # Skip runs where we got output with --resume
            if run['output'] != None:
                logging.info('Skip command: %s' % (' '.join(run['command'])))
                continue

            output, error = self.execute(run['command'])

            logging.debug('------------------------------------')
            logging.debug('OUTPUT')
            logging.debug('------------------------------------')
            logging.debug('\n' + ''.join(output))

            if error != []:
                logging.debug('------------------------------------')
                logging.debug('ERROR')
                logging.debug('------------------------------------')
                logging.debug('\n' + ''.join(error))
                sys.exit()

            logging.debug('------------------------------------')

            # Get output from log file
            logpath = output[0].rstrip().split('and file at ')[-1]
            f = open(logpath, 'r')
            output = [line for line in f.readlines()]
            run['output'] = output

    def execute(self, command):
        """Execute a process using the subprocess library, and return the output/error or the process

        Args:
            command (list(str)): Command to be executed.

        Returns:
            (list(str), list(str)): Return the output and error generated by this process.
        """
        logging.info(' '.join(command))
        process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        output = [line.decode('utf-8') for line in process.stdout.readlines()]
        error = [line.decode('utf-8') for line in process.stderr.readlines()]
        return output, error


class Figure4(Experiment):
    """Replicate figure 4:
    System load with an increasing number of endpoints connected to a singleworker

    So:
    - Run with all 3 deployment modes
    - Vary number of endpoints connected to a single worker
    """
    def __init__(self, resume):
        Experiment.__init__(self, resume)

        self.modes = ['cloud', 'edge', 'endpoint']
        self.cores = [4, 2, 1]
        self.endpoints = [1, 2, 4, 8]

        self.y = None

    def __repr__(self):
        """Returns this string when called as print(object)
        """
        return '''
APP                     image-classification
MODES                   %s
WORKERS                 1
CLOUD_CORES             %i
EDGE_CORES              %i
ENDPOINT_CORES          %i
ENDPOINTS/WORKER        %s''' % (
            ','.join(self.modes), self.cores[0], self.cores[1], self.cores[2],
            ','.join([str(endpoint) for endpoint in self.endpoints]))

    def generate(self):
        """Generate commands to run the benchmark based on the current settings
        """
        # Differ in deployment modes
        for mode in self.modes:
            if mode == 'cloud':
                config = 'cloud_endpoint'
                cores = self.cores[0]
            elif mode == 'edge':
                config = 'edge_endpoint'
                cores = self.cores[1]
            else:
                config = 'endpoint'
                cores = self.cores[2]

            # Differ in #endpoints per worker
            for endpoint in self.endpoints:
                # No sense to use more than 1 endpoint in endpoint-only deployment mode
                if mode == 'endpoint' and endpoint > 1:
                    continue

                command = ['python3', 'main.py', 'configuration/fig4/' + config + str(endpoint) + '.cfg']
                command = [str(c) for c in command]

                run = {'mode': mode, 
                       'cores': cores,
                       'endpoints': endpoint, 
                       'command': command, 
                       'output': None, 
                       'worker_time': None}
                self.runs.append(run)

    def parse_output(self):
        """For all runs, get the worker runtime
        """
        # Get the line containing the metrics
        for run in self.runs:
            input = run['output'][-7][1:-2]
            if 'Output in csv' in input:
                input = run['output'][-6][1:-2]

            # Split string into list
            l = [x.split(',') for x in input.split('\\n')]
            l = l[:-1]
            l = [sub[1:] for sub in l]

            # Split into header and data
            header = l[0]
            data = l[1:]

            # Convert into dataframe
            df = pd.DataFrame(data, columns=header)
            df['proc/data (ms)'] = pd.to_numeric(df['proc/data (ms)'], downcast='float')

            # Calculate num. of images processed per second by the cloud/edge/endpoint
            processed_rate = df['proc/data (ms)'].mean()
            processed_rate = 1000.0 / processed_rate
            processed_rate *= run['cores']

            # Calculate number of images generated per second
            frequency = 5
            requested_rate = float(frequency * run['endpoints'])

            # Calculate usage of processors
            run['usage'] = int((requested_rate / processed_rate) * 100)

    def plot(self):
        # set width of bar
        plt.rcParams.update({'font.size': 22})
        fig = plt.subplots(figsize =(12, 6))

        barWidth = 0.2
        bars = np.arange(len(self.modes))
        
        colors = ['dimgray', 'gray', 'darkgray', 'lightgray']

        y_total = []
        for endpoint, color in zip(self.endpoints, colors):
            # Get the x and y data
            y = [run['usage'] for run in self.runs if run['endpoints'] == endpoint]
            x = [x + math.log2(endpoint) * barWidth for x in bars]

            # mode=endpoint only uses 1 endpoint
            if endpoint > 1:
                x = x[:-1]

            # Plot the bar
            plt.bar(x, y, color=color, width=barWidth*0.9, label='Endpoints: %s' % (endpoint))
            y_total += y

        # Add horizontal lines every 100 percent
        plt.axhline(y=100, color='k', linestyle='-', linewidth=3)
        plt.axhline(y=200, color='k', linestyle='-', linewidth=1, alpha=0.5)
        plt.axhline(y=300, color='k', linestyle='-', linewidth=1, alpha=0.5)

        # Adding titles
        plt.xlabel('Deployment Mode')
        plt.ylabel('System Load')

        # Adding Xticks
        label_ticks = [r + (len(self.modes) / 2) * barWidth for r in range(len(self.modes))]
        label_ticks[-1] -= (len([endpoint for endpoint in self.endpoints if endpoint > 1]) / 2) * barWidth
        plt.xticks(label_ticks, [mode.capitalize() for mode in self.modes])

        plt.legend(loc='upper left', framealpha=1.0)
        plt.gca().yaxis.set_major_formatter(mtick.PercentFormatter())
        plt.ylim(0, 400)
        plt.yticks(np.arange(0, 500, 100))

        t = time.strftime("%Y-%m-%d_%H:%M:%S", time.gmtime())
        plt.savefig('./logs/fig4_%s.png' % (t), bbox_inches='tight')

        self.y = y_total

    def print_result(self):
        i = 0
        for endpoint in self.endpoints:
            for mode in self.modes:
                if mode == 'endpoint' and endpoint > 1:
                    break

                logging.info('Mode: %10s | Endpoints: %3s | System Load: %i%%' % (mode, endpoint, self.y[i]))
                i += 1


class Figure5(Experiment):
    """Replicate figure 5:
    System load with an increasing number of CPU cores per worker

    So:
    - Run with all 3 deployment modes
    - Vary number of CPU cores per worker
    """
    def __init__(self, resume):
        Experiment.__init__(self, resume)

        self.modes = ['cloud', 'edge', 'endpoint']
        self.cores = [1, 2, 4]

        self.y = None

    def __repr__(self):
        """Returns this string when called as print(object)
        """
        return '''
APP                     image-classificatiom
MODES                   %s
WORKERS                 1
CORES                   %s
ENDPOINTS/WORKER        1''' % (
            ','.join(self.modes), 
            ','.join([str(x) for x in self.cores]))

    def generate(self):
        """Generate commands to run the benchmark based on the current settings
        """
        # Differ in deployment modes
        for mode in self.modes:
            # Differ in #cores per worker
            for core in self.cores:
                command = []
                if not (mode == 'cloud' and core == 1):
                    command = ['python3', 'main.py', 'configuration/fig5/%s_cores%i.cfg' % (mode, core)]
                    command = [str(c) for c in command]

                run = {'mode': mode, 
                       'cores': core,
                       'endpoints': 1, 
                       'command': command, 
                       'output': None, 
                       'worker_time': None}
                self.runs.append(run)

    def parse_output(self):
        """For all runs, get the worker runtime
        """
        # Get the line containing the metrics
        for run in self.runs:
            # Kubernetes does not work with only 1 core
            if run['mode'] == 'cloud' and run['cores'] == 1:
                run['usage'] = 0
                continue

            input = run['output'][-7][1:-2]
            if 'Output in csv' in input:
                input = run['output'][-6][1:-2]

            # Split string into list
            l = [x.split(',') for x in input.split('\\n')]
            l = l[:-1]
            l = [sub[1:] for sub in l]

            # Split into header and data
            header = l[0]
            data = l[1:]

            # Convert into dataframe
            df = pd.DataFrame(data, columns=header)
            df['proc/data (ms)'] = pd.to_numeric(df['proc/data (ms)'], downcast='float')

            # Calculate num. of images processed per second by the cloud/edge/endpoint
            processed_rate = df['proc/data (ms)'].mean()
            processed_rate = 1000.0 / processed_rate
            processed_rate *= run['cores']

            # Calculate number of images generated per second
            frequency = 5
            requested_rate = float(frequency * run['endpoints'])

            # Calculate usage of processors
            run['usage'] = int((requested_rate / processed_rate) * 100)

    def plot(self):
        # set width of bar
        plt.rcParams.update({'font.size': 22})
        fig = plt.subplots(figsize =(12, 6))

        barWidth = 0.2
        bars = np.arange(len(self.modes))
        
        colors = ['dimgray', 'gray', 'darkgray']

        y_total = []
        for core, color in zip(self.cores, colors):
            # Get the x and y data
            y = [run['usage'] for run in self.runs if run['cores'] == core]
            x = [x + math.log2(core) * barWidth * 1.2 for x in bars]

            # Plot the bar
            plt.bar(x, y, color=color, width=barWidth * 1.1, label='CPU Cores: %s' % (core))

            y_total += y

        # Add horizontal lines every 100 percent
        plt.axhline(y=100, color='k', linestyle='-', linewidth=3)
        plt.axhline(y=200, color='k', linestyle='-', linewidth=1, alpha=0.5)
        plt.axhline(y=300, color='k', linestyle='-', linewidth=1, alpha=0.5)

        # Adding titles
        plt.xlabel('Deployment Mode')
        plt.ylabel('System Load')

        # Adding Xticks
        label_ticks = [r + (len(self.modes) / 2) * barWidth for r in range(len(self.modes))]
        plt.xticks(label_ticks, [mode.capitalize() for mode in self.modes])

        plt.legend(loc='upper left', framealpha=1.0)
        plt.gca().yaxis.set_major_formatter(mtick.PercentFormatter())
        plt.ylim(0, 400)
        plt.yticks(np.arange(0, 500, 100))

        t = time.strftime("%Y-%m-%d_%H:%M:%S", time.gmtime())
        plt.savefig('./logs/fig5_%s.png' % (t), bbox_inches='tight')

        self.y = y_total

    def print_result(self):
        i = 0
        for core in self.cores:
            for mode in self.modes:
                logging.info('Mode: %10s | Cores: %3s | System Load: %i%%' % (mode, core, self.y[i]))
                i += 1


def main(args):
    """Main function

    Args:
        args (Namespace): Argparse object
    """
    if args.figure == 'figure4':
        logging.info('Replicate figure 4')
        fig = Figure4(args.resume)
    elif args.figure == 'figure5':
        logging.info('Replicate figure 5')
        fig = Figure5(args.resume)
    else:
        logging.error('Invalid figure: %s' % (args.figure))
        sys.exit()

    logging.info(fig)
    fig.generate()
    fig.check_resume()
    fig.run_commands()
    fig.parse_output()
    fig.plot()
    fig.print_result()


if __name__ == '__main__':
    """Get input arguments, and validate those arguments
    """
    parser = argparse.ArgumentParser()

    parser.add_argument('figure', choices=['figure4', 'figure5'],
        help='Figure to replicate')
    parser.add_argument('-v', '--verbose', action='store_true',
        help='increase verbosity level')
    parser.add_argument('-r', '--resume', type=lambda s: datetime.datetime.strptime(s, '%Y-%m-%d_%H:%M:%S'),
        help='Resume a previous figure replication from datetime "YYYY-MM-DD_HH:mm:ss"')
    args = parser.parse_args()

    enable_logging(args.verbose)
    main(args)

\ Run the benchmark multiple times with a range of settings, and produce tables / graphs with these results # Home dir should be continuum/ Enable logging # Set parameters Experiment template / super class If the resume argument is given, get the first x log files >= the resume date,         and use their output instead of re-running the experiment. # We have all logs needed Execute all generated commands # Skip runs where we got output with --resume # Get output from log file Execute a process using the subprocess library, and return the output/error or the process          Args:             command (list(str)): Command to be executed.          Returns:             (list(str), list(str)): Return the output and error generated by this process. Replicate figure 4:     System load with an increasing number of endpoints connected to a singleworker      So:     - Run with all 3 deployment modes     - Vary number of endpoints connected to a single worker Returns this string when called as print(object) APP                     image-classification MODES                   %s WORKERS                 1 CLOUD_CORES             %i EDGE_CORES              %i ENDPOINT_CORES          %i ENDPOINTS/WORKER        %s Generate commands to run the benchmark based on the current settings # Differ in deployment modes # Differ in #endpoints per worker # No sense to use more than 1 endpoint in endpoint-only deployment mode For all runs, get the worker runtime # Get the line containing the metrics # Split string into list # Split into header and data # Convert into dataframe # Calculate num. of images processed per second by the cloud/edge/endpoint # Calculate number of images generated per second # Calculate usage of processors # set width of bar # Get the x and y data # mode=endpoint only uses 1 endpoint # Plot the bar # Add horizontal lines every 100 percent # Adding titles # Adding Xticks Replicate figure 5:     System load with an increasing number of CPU cores per worker      So:     - Run with all 3 deployment modes     - Vary number of CPU cores per worker Returns this string when called as print(object) APP                     image-classificatiom MODES                   %s WORKERS                 1 CORES                   %s ENDPOINTS/WORKER        1 Generate commands to run the benchmark based on the current settings # Differ in deployment modes # Differ in #cores per worker For all runs, get the worker runtime # Get the line containing the metrics # Kubernetes does not work with only 1 core # Split string into list # Split into header and data # Convert into dataframe # Calculate num. of images processed per second by the cloud/edge/endpoint # Calculate number of images generated per second # Calculate usage of processors # set width of bar # Get the x and y data # Plot the bar # Add horizontal lines every 100 percent # Adding titles # Adding Xticks Main function      Args:         args (Namespace): Argparse object Get input arguments, and validate those arguments