// SPDX-License-Identifier: BSD-2-Clause
/* Copyright (C) 2014 - 2021 Intel Corporation. */

#pragma once

#include <memkind.h>

#include <condition_variable>
#include <mutex>
#include <thread>
#include <vector>
// Malloc, jemalloc, memkind jemalloc and memkind memory operations definitions
#include "operations.hpp"

/* Framework for testing memory allocators pefromance */
namespace performance_tests
{
// Nanoseconds in second
const uint32_t NanoSecInSec = 1e9;

// Simple barrier implementation
class Barrier
{
    // Barrier mutex
    std::mutex m_barrierMutex;
    // Contitional variable
    std::condition_variable m_cVar;
    // Number of threads expected to enter the barrier
    size_t m_waiting;
    timespec m_releasedAt;

public:
    // Called by each thread entering the barrier; returns control to caller
    // only after been called from the last thread expected at the barrier
    void wait();

    // (Re)Initializes the barrier
    void reset(unsigned waiting)
    {
        m_releasedAt.tv_sec = m_releasedAt.tv_nsec = 0;
        m_waiting = waiting;
    }

    // Get time when barrier was released
    timespec &releasedAt()
    {
        return m_releasedAt;
    }

    // Singleton
    static Barrier &GetInstance()
    {
        // Automatically created and deleted one and only instance
        static Barrier instance;
        return instance;
    }

private:
    Barrier()
    {
        reset(0);
    }
    // Cannot be used with singleton, so prevent compiler from creating them
    // automatically
    Barrier(Barrier const &) = delete;
    void operator=(Barrier const &) = delete;
};

// Data of a single test action, that is, memory operation (malloc, calloc,
// etc.) to perform and its parameters (size, alignment etc.)
class Action
{
protected:
    Operation *m_operation;
    Operation *m_freeOperation;
    const memkind_t m_kind;
    void *m_allocation;
    const size_t m_size;
    const size_t m_offset;
    const size_t m_alignment;

public:
    Action(Operation *operation, Operation *freeOperation, const memkind_t kind,
           const size_t size, const size_t offset, const size_t alignment)
        : m_operation(operation),
          m_freeOperation(freeOperation),
          m_kind(kind),
          m_allocation(nullptr),
          m_size(size),
          m_offset(offset),
          m_alignment(alignment)
    {}

    Action(Operation *operation, Operation *freeOperation, const memkind_t kind)
        : Action(operation, freeOperation, kind, 0, 0, 0)
    {}

    void alloc()
    {
        m_operation->perform(m_kind, m_allocation, m_size, m_offset,
                             m_alignment);
    }

    void free()
    {
        m_freeOperation->perform(m_kind, m_allocation);
    }
};

// Performs and tracks requested memory operations in a separate thread
class Worker
{
protected:
#ifdef __DEBUG
    uint16_t m_threadId;
#endif
    // Requested number of test actions
    const uint32_t m_actionsCount;
    // List of memory block sizes - for each memory allocation operation actual
    // value is chosen randomly
    const vector<size_t> &m_allocationSizes;
    // List of test actions
    vector<Action *> m_actions;
    // Memory free action
    Action *m_freeAction;
    // Operation kind (useful for memkind only)
    memkind_t m_kind;
    // Working thread
    thread *m_thread;

public:
    Worker(uint32_t actionsCount, const vector<size_t> &allocationSizes,
           Operation *freeOperation, memkind_t kind);

    ~Worker();

    // Set operations list for the worker
    void init(const vector<Operation *> &testOperations,
              Operation *&freeOperation);

    // Create & start thread
    void run();

#ifdef __DEBUG
    // Get thread id
    uint16_t getId();

    // Set thread id
    void setId(uint16_t threadId);
#endif

    // Finish thread and free all allocations made
    void finish();

    // Free allocated memory
    virtual void clean();

private:
    // Actual thread function (allow inheritance)
    virtual void work();
};

enum ExecutionMode
{
    SingleInteration, // Single iteration, operations listS will be distributed
                      // among threads sequentially
    ManyIterations // Each operations list will be run in separate iteration by
                   // each thread
};

struct Metrics {
    uint64_t executedOperations;
    uint64_t totalDuration;
    double operationsPerSecond;
    double avgOperationDuration;
    double iterationDuration;
    double repeatDuration;
};

// Performance test parameters class
class PerformanceTest
{
protected:
    // empirically determined % of worst results needed to be discarded
    // to eliminate malloc() performance results skewness
    static constexpr double distardPercent = 20.0;

protected:
    // Number of test repeats
    size_t m_repeatsCount;
    // Number of test repeats with worst results to be discarded
    size_t m_discardCount;
    // Number of threads
    size_t m_threadsCount;
    // Number of memory operations in each thread
    uint32_t m_operationsCount;
    // List of allocation sizes
    vector<size_t> m_allocationSizes;
    // List of list of allocation operations, utlization depends on execution
    // mode
    vector<vector<Operation *>> m_testOperations;
    // Free operation
    Operation *m_freeOperation;
    // List of memory kinds (for memkind allocation only)
    // distributed among threads sequentially
    vector<memkind_t> m_kinds;
    // List of thread workers
    vector<Worker *> m_workers;
    // Time measurement
    vector<uint64_t> m_durations;
    // Execution mode
    ExecutionMode m_executionMode;

public:
    // Create test
    PerformanceTest(size_t repeatsCount, size_t threadsCount,
                    size_t operationsCount);

    virtual ~PerformanceTest()
    {}

    // Set list of block sizes
    void setAllocationSizes(const vector<size_t> &allocationSizes);

    // Set list of operations per thread/per iteration (depending on execution
    // mode)
    void setOperations(const vector<vector<Operation *>> &testOperations,
                       Operation *freeOperation);

    // Set per-thread list of memory kinds
    void setKind(const vector<memkind_t> &kinds);

    // Set execution mode (different operations per each thread/same operations
    // for each thread, but many iterations)
    void setExecutionMode(ExecutionMode operationMode);

    // Execute test
    int run();

    // Print test parameters
    virtual void showInfo();

    // Write test metrics
    void writeMetrics(const string &suiteName, const string &caseName,
                      const string &fileName = "");

    Metrics getMetrics();

private:
    // Run single iteration
    void runIteration();

    // Setup thread workers
    void prepareWorkers();
};
} // namespace performance_tests
