/*========================== begin_copyright_notice ============================
Copyright (C) 2021-2024 Intel Corporation

SPDX-License-Identifier: MIT
============================= end_copyright_notice ===========================*/

/*!
 * @file Helper class that facilitates data collection in a distributed array of profile records
 */

#ifndef GT_PROFILE_ARRAY_H_
#define GT_PROFILE_ARRAY_H_

#include "gt_basic_defs.h"
#include "gt_basic_utils.h"
#include "igt_profile_buffer.h"
#include "igt_gen_coder.h"
#include "igt_gen_procedure.h"

namespace gtpin
{
#pragma pack(push, 8)

/*! @addtogroup GTPIN_PROFILE_BUFFER 
@{
*/

/* ===================================================================================================== */
// Class GtProfileArray
/* ===================================================================================================== */
/*!
 * @brief Class that represents an array of data records in the profile buffer.
 * 
 * The data can be collected in a single global array or distributed among multiple thread buckets each
 * of which stores profile data in its own array (chunk)
 *
 * Terminology:
 *
 * - GTPin groups threads in buckets to minimize contention of concurrent memory accesses. Thread bucket is a group
 *   of threads that store their profile data in the same, shared memory block of the profile buffer. Threads that
 *   belong to different buckets use separate, not interleaving memory areas of the profile buffer, called chunks.\n
 *   The number of thread buckets (memory chunks) is a configurable value, determined by the tool. The valid
 *   range of this value is [1, max. number of HW threads]. To simplify code generation, GTPin requires
 *   number of thread buckets to be power-of-two.\n
 *   To map a thread into the corresponding thread bucket, GTPin applies some hash function to the thread ID.
 * 
 * @par Availability:
 * - IGtTool::OnKernelBuild
 * - IGtTool::OnKernelRun
 * - IGtTool::OnKernelComplete
 */
class GtProfileArray
{
public:
    /// Default constructor
    inline GtProfileArray();

    /*!
     * @brief Constructor
     * @param[in]   recordSize          Size of the record in bytes
     * @param[in]   numRecords          Number of data records in the array
     * @param[in]   numThreadBuckets    Number of thread buckets each of which will collect profile data
     *                                  in its own array, called chunk (see 'Terminology' section).
     *                                  The value of this parameter always rounded up to the next power of two.
     * @param[in]   alignment           Alignment of the array in the profile buffer; must be power of two
     */
    inline GtProfileArray(uint32_t recordSize, uint32_t numRecords, uint32_t numThreadBuckets = 1, uint32_t alignment = 64);

    /*!
     * @brief Reserve space required to store all chunks of this array in the profile buffer
     * @param[in]   allocator           Allocator to be used to reserve space
     * @return true - success, false - failure
     *
     * @pre !IsAllocated()
     */
    inline bool Allocate(IGtProfileBufferAllocator& allocator);

    /*!
     * @brief Read data from the specified records of this array in the profile buffer
     * @param[in]   profileBuffer       Profile buffer to read records from
     * @param[in]   records             Pointer to an array that receives records from the profile buffer.
     *                                  The size of this array should be at least (numRecords * RecordSize()) bytes.
     * @param[in]   threadBucketNum     Ordinal number of the thread bucket (memory chunk) to read records from
     * @param[in]   fistRecordNum       Ordinal number of the first record to read
     * @param[in]   numRecords          Number of records to read
     * @return true - success, false - failure
     *
     * @pre IsAllocated() && (threadBucketNum < NumThreadBuckets()) && (fistRecordNum + numRecords <= NumRecords())
     */
    inline bool Read(const IGtProfileBuffer& profileBuffer, void* records,
                     uint32_t fistRecordNum, uint32_t numRecords = 1, uint32_t threadBucketNum = 0) const;

    /*!
     * @brief Write data into specified records of this array in the profile buffer (see the  Read() parameters descriptions)
     * @return true - success, false - failure
     *
     * @pre IsAllocated() && (threadBucketNum < NumThreadBuckets()) && (fistRecordNum + numRecords <= NumRecords())
     */
    inline bool Write(IGtProfileBuffer& profileBuffer, const void* records,
                      uint32_t fistRecordNum, uint32_t numRecords = 1, uint32_t threadBucketNum = 0) const;

    /*!
     * @brief Zero-initialize memory area allocated for this array in the specified profile buffer
     * @param[in]   profileBuffer       Profile buffer in which this array is allocated
     * @return true - success, false - failure
     *
     * @pre IsAllocated()
     */
    inline bool Initialize(IGtProfileBuffer& profileBuffer) const;

    /*!
     * @brief Generate code that computes offset/address within the specified record
     * 
     * Generate code that computes offset/address within a record that resides in the memory chunk of the currently
     * running HW thread. The offset is relative to the profile buffer base.
     * 
     * @note If both recordNum = 0 and offsetInRecord = 0, the generated code computes offset/address of the
     * memory chunk associated with the currently running HW thread.
     *
     * @param[in]   coder               GEN code generator
     * @param[in]   proc                Procedure, the generated code is appended to
     * @param[in]   offsetReg           Register that receives the calculated offset within the profile buffer
     * @param[in]   addrReg             Register that receives the calculated address
     * @param[in]   recordNum           Ordinal number of the record
     * @param[in]   offsetInChunkReg    32-bit register that holds offset (of the record) relative to the chunk base
     * @param[in]   offsetInRecord      Offset within the record
     *
     * @pre IsAllocated() && (recordNum < NumRecords()) && (offsetInRecord < RecordSize())
     */
    inline void ComputeOffset(const IGtGenCoder& coder, IGtGenProcedure& proc,
                              const GtReg& offsetReg, uint32_t recordNum = 0, uint32_t offsetInRecord = 0) const;

    /*! @copydoc ComputeOffset */
    inline void ComputeAddress(const IGtGenCoder& coder, IGtGenProcedure& proc,
                               const GtReg& addrReg, uint32_t recordNum = 0, uint32_t offsetInRecord = 0) const;

    /*! @copydoc ComputeAddress */
    inline void ComputeOffset(const IGtGenCoder& coder, IGtGenProcedure& proc,
                              const GtReg& offsetReg, const GtReg& offsetInChunkReg, uint32_t offsetInRecord = 0) const;

    /*! @copydoc ComputeOffset */
    inline void ComputeAddress(const IGtGenCoder& coder, IGtGenProcedure& proc,
                               const GtReg& addrReg, const GtReg& offsetInChunkReg, uint32_t offsetInRecord = 0) const;

    /*!
     * @brief Generate code that computes address of a (field) location within a record relative to the
     * specified base address within the same record
     * @param[in]   coder               GEN code generator
     * @param[in]   proc                Procedure, the generated code is appended to
     * @param[in]   addrReg             Register that receives the calculated address
     * @param[in]   baseReg             Register that holds an address within the record
     * @param[in]   offset              Offset within the record, relative to 'baseReg'
     */
    inline void ComputeRelAddress(const IGtGenCoder& coder, IGtGenProcedure& proc,
                                  const GtReg& addrReg, const GtReg& baseReg, int32_t offset) const;

    // Accessors
    // =========

    /*!
     * @brief Get offset of the specified record in the specified thread bucket, relative to the profile buffer base
     * @param[in]   recordNum           Ordinal number of the record
     * @param[in]   threadBucketNum     Ordinal number of the thread bucket
     * @return Offset of the specified record
     * 
     * @pre IsAllocated() && (threadBucketNum < NumThreadBuckets()) && (recordNum < NumRecords())
     */
    inline uint32_t RecordOffset(uint32_t recordNum, uint32_t threadBucketNum) const;

    inline uint32_t              Size()   const { return _size; }   ///< Return size of the memory block, in bytes
    inline GtProfileBufferOffset Offset() const { return _offset; } ///< Return offset of the allocated memory block, or -1

    inline bool     IsAllocated()         const { return _offset.IsValid(); } ///< Return true if this array has been allocated
    inline uint32_t RecordSize()          const { return _recordSize; }       ///< Return size of the record, in bytes
    inline uint32_t NumRecords()          const { return _numRecords; }       ///< Return number of data records in the array
    inline uint32_t NumThreadBuckets()    const { return _numChunks; }        ///< Return number of thread buckets (chunks)
    inline uint32_t ChunkSize()           const { return _chunkSize; }        ///< Return size of the per-bucket chunk, in bytes

private:
    uint32_t              _recordSize;  ///< Size of the record, in bytes
    uint32_t              _numRecords;  ///< Number of data records in the array
    uint32_t              _alignment;   ///< Alignment of the array in the profile buffer
    uint32_t              _numChunks;   ///< Number of thread buckets (chunks)
    uint32_t              _chunkSize;   ///< Size of the per-bucket chunk, in bytes
    uint32_t              _size;        ///< Size of the memory block, in bytes
    GtProfileBufferOffset _offset;      ///< Offset of the allocated memory block, or -1
};
/*! @} */

/* ===================================================================================================== */
// GtProfileArray implementation
/* ===================================================================================================== */
GtProfileArray::GtProfileArray() : _recordSize(0), _numRecords(0), _alignment(0), _numChunks(0), _chunkSize(0), _size(0) {}

GtProfileArray::GtProfileArray(uint32_t recordSize, uint32_t numRecords, uint32_t numThreadBuckets, uint32_t alignment) :
    _recordSize(recordSize), _numRecords(numRecords), _alignment(alignment)
{
    GTPIN_ASSERT(IsPower2(_alignment));
    _numChunks = AlignPower2Up(numThreadBuckets);
    _chunkSize = AlignPower2Up(_numRecords * RecordSize());
    _size = _numChunks * _chunkSize;
}

bool GtProfileArray::Allocate(IGtProfileBufferAllocator& allocator)
{
    GTPIN_ASSERT(!IsAllocated());
    if (Size() > 0)
    {
        _offset = allocator.Allocate(Size(), _alignment);
    }
    return _offset.IsValid();
}

uint32_t GtProfileArray::RecordOffset(uint32_t recordNum, uint32_t threadBucketNum) const
{
    GTPIN_ASSERT(IsAllocated());
    GTPIN_ASSERT(threadBucketNum < _numChunks);
    GTPIN_ASSERT(recordNum < _numRecords);
    return _offset + (threadBucketNum * _chunkSize) + (recordNum * RecordSize());
}

bool GtProfileArray::Read(const IGtProfileBuffer& profileBuffer, void* records,
                          uint32_t fistRecordNum, uint32_t numRecords, uint32_t threadBucketNum) const
{
    GTPIN_ASSERT(fistRecordNum + numRecords <= _numRecords);

    uint32_t size = numRecords * RecordSize();
    return profileBuffer.Read(RecordOffset(fistRecordNum, threadBucketNum), records, size);
}

bool GtProfileArray::Write(IGtProfileBuffer& profileBuffer, const void* records,
                           uint32_t fistRecordNum, uint32_t numRecords, uint32_t threadBucketNum) const
{
    GTPIN_ASSERT(fistRecordNum + numRecords <= _numRecords);

    uint32_t size = numRecords * RecordSize();
    return profileBuffer.Write(RecordOffset(fistRecordNum, threadBucketNum), records, size);
}

bool GtProfileArray::Initialize(IGtProfileBuffer& profileBuffer)  const
{
    if (Size() != 0)
    {
        GTPIN_ASSERT(IsAllocated());
        return profileBuffer.Fill(_offset, Size(), 0);
    }
    return true;
}

void GtProfileArray::ComputeOffset(const IGtGenCoder& coder, IGtGenProcedure& proc,
                                   const GtReg& offsetReg, uint32_t recordNum, uint32_t offsetInRecord) const
{
    GTPIN_ASSERT(recordNum < _numRecords);
    GTPIN_ASSERT(offsetInRecord < RecordSize());

    uint32_t offsetInChunk = (recordNum * RecordSize()) + offsetInRecord;
    coder.ComputeThreadChunkOffset(proc, offsetReg, _numChunks, _chunkSize, _offset, offsetInChunk);
}

void GtProfileArray::ComputeAddress(const IGtGenCoder& coder, IGtGenProcedure& proc,
                                    const GtReg& addrReg, uint32_t recordNum, uint32_t offsetInRecord) const
{
    GTPIN_ASSERT(recordNum < _numRecords);
    GTPIN_ASSERT(offsetInRecord < RecordSize());

    uint32_t offsetInChunk = (recordNum * RecordSize()) + offsetInRecord;
    coder.ComputeThreadChunkAddress(proc, addrReg, _numChunks, _chunkSize, _offset, offsetInChunk);
}

void GtProfileArray::ComputeOffset(const IGtGenCoder& coder, IGtGenProcedure& proc,
                                   const GtReg& offsetReg, const GtReg& offsetInChunkReg, uint32_t offsetInRecord) const
{
    GTPIN_ASSERT(offsetInRecord < RecordSize());

    coder.ComputeThreadChunkOffset(proc, offsetReg, _numChunks, _chunkSize, _offset, offsetInRecord);
    proc += coder.InstructionFactory().MakeAdd(offsetReg, offsetReg, offsetInChunkReg);
}

void GtProfileArray::ComputeAddress(const IGtGenCoder& coder, IGtGenProcedure& proc,
                                    const GtReg& addrReg, const GtReg& offsetInChunkReg, uint32_t offsetInRecord) const
{
    GTPIN_ASSERT(offsetInRecord < RecordSize());
    GTPIN_ASSERT(addrReg.ElementSize() >= sizeof(uint32_t));
    GtReg offsetReg(addrReg, sizeof(uint32_t), 0);

    ComputeOffset(coder, proc, offsetReg, offsetInChunkReg, offsetInRecord);
    coder.ComputeAddress(proc, addrReg, offsetReg);
}

void GtProfileArray::ComputeRelAddress(const IGtGenCoder& coder, IGtGenProcedure& proc,
                                       const GtReg& addrReg, const GtReg& baseReg, int32_t offset) const
{
    // Before delegating this function to IGtGenCoder::ComputeRelAddress, check for opportunity to optimize
    // 64-bit address computation
    if ((RecordSize() <= _alignment) && IsPower2(RecordSize()) && 
        coder.ProfileBufferAddrModel().IsA64() && !coder.GenModel().IsArithmetic64Supported())
    {
        // Aligned records of a power-of-two size do not cross 4Gb boundary.
        // It means that relative addresses within the record can be calculated using 32-bit arithmetic
        IGtInsFactory& insF = coder.InstructionFactory();
        if (addrReg != baseReg)
        {
            proc += insF.MakeRegMov(addrReg, baseReg);
        }
        if (offset != 0)
        {
            GtReg addrRegL(addrReg, sizeof(uint32_t), 0);
            proc += insF.MakeAdd(addrRegL, addrRegL, offset);
        }
    }
    else
    {
        coder.ComputeRelAddress(proc, addrReg, baseReg, offset);
    }
}

#pragma pack(pop)
}
#endif
