// Copyright 2019 The Dawn & Tint Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
//    list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
//    this list of conditions and the following disclaimer in the documentation
//    and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
//    contributors may be used to endorse or promote products derived from
//    this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#ifndef SRC_DAWN_TESTS_PERF_TESTS_DAWNPERFTEST_H_
#define SRC_DAWN_TESTS_PERF_TESTS_DAWNPERFTEST_H_

#include <memory>
#include <optional>
#include <string>
#include <vector>

#include "partition_alloc/pointers/raw_ptr.h"
#include "src/dawn/tests/DawnTest.h"
#include "src/utils/compiler.h"

void InitDawnPerfTestEnvironment(int argc, char** argv);

namespace dawn {
namespace utils {
class Timer;
}

class DawnPerfTestPlatform;

class DawnPerfTestEnvironment : public DawnTestEnvironment {
  public:
    DawnPerfTestEnvironment(int argc, char** argv);
    ~DawnPerfTestEnvironment() override;

    void SetUp() override;
    void TearDown() override;

    bool IsCalibrating() const;
    unsigned int OverrideStepsToRun() const;

    // Returns the path to the trace file, or nullptr if traces should
    // not be written to a json file.
    const char* GetTraceFile() const;

    DawnPerfTestPlatform* GetPlatform() const;

  private:
    // Only run calibration which allows the perf test runner to save time.
    bool mIsCalibrating = false;

    // If non-zero, overrides the number of steps.
    unsigned int mOverrideStepsToRun = 0;

    const char* mTraceFile = nullptr;

    std::unique_ptr<DawnPerfTestPlatform> mPlatform;
};

class DawnPerfTestBase {
    static constexpr double kCalibrationRunTimeSeconds = 1.0;
    static constexpr double kMaximumRunTimeSeconds = 10.0;
    static constexpr unsigned int kNumTrials = 3;

  public:
    // Perf test results are reported as the amortized time of |mStepsToRun| * |mIterationsPerStep|.
    // A test deriving from |DawnPerfTestBase| must call the base contructor with
    // |iterationsPerStep| appropriately to reflect the amount of work performed.
    // |maxStepsInFlight| may be used to mimic having multiple frames or workloads in flight which
    // is common with double or triple buffered applications.
    DawnPerfTestBase(DawnTestBase* test,
                     unsigned int iterationsPerStep,
                     unsigned int maxStepsInFlight);
    virtual ~DawnPerfTestBase();

  protected:
    // Call if the test step was aborted and the test should stop running.
    void AbortTest();

    void RunTest();
    void PrintPerIterationResultFromSeconds(const std::string& trace,
                                            double valueInSeconds,
                                            bool important) const;
    void PrintResult(const std::string& trace,
                     double value,
                     const std::string& units,
                     bool important) const;
    void PrintResult(const std::string& trace,
                     unsigned int value,
                     const std::string& units,
                     bool important) const;
    void AddGPUTime(double GPUTime);

  private:
    void DoRunLoop(double maxRunTime);
    void OutputResults();

    void PrintResultImpl(const std::string& trace,
                         const std::string& value,
                         const std::string& units,
                         bool important) const;

    virtual void Step() = 0;

    raw_ptr<DawnTestBase> mTest;
    bool mRunning = false;
    const unsigned int mIterationsPerStep;
    const unsigned int mMaxStepsInFlight;
    unsigned int mStepsToRun = 0;
    unsigned int mNumStepsPerformed = 0;
    double mCpuTime;
    std::unique_ptr<utils::Timer> mTimer;
    std::optional<double> mGPUTime;
};

template <typename Params = AdapterTestParam>
class DawnPerfTestWithParams : public DawnTestWithParams<Params>, public DawnPerfTestBase {
  protected:
    DawnPerfTestWithParams(unsigned int iterationsPerStep, unsigned int maxStepsInFlight)
        : DawnTestWithParams<Params>(),
          DawnPerfTestBase(this, iterationsPerStep, maxStepsInFlight) {}
    ~DawnPerfTestWithParams() override = default;

    // Since this function might fail but has no way to signal that it failed
    // so it should not be called directly or overridden. Unforutnately there is
    // no way to enforce that without a huge refactor. Marking it as `final`
    // at least means derived classes can't incorrectly override it.
    void SetUp() final {
        DawnTestWithParams<Params>::SetUp();

        DAWN_TEST_UNSUPPORTED_IF(this->IsCPU());

        if (mSupportsTimestampQuery) {
            InitializeGPUTimer();
        }

        SetUpPerfTest();
    }

    virtual void SetUpPerfTest() {}

    std::vector<wgpu::FeatureName> GetRequiredFeatures() override {
        std::vector<wgpu::FeatureName> requiredFeatures = {wgpu::FeatureName::TimestampQuery};
        mSupportsTimestampQuery = DawnTestWithParams<Params>::SupportsFeatures(requiredFeatures);
        if (mSupportsTimestampQuery) {
            return requiredFeatures;
        }
        return {};
    }

    bool SupportsTimestampQuery() const { return mSupportsTimestampQuery; }

    void RecordBeginTimestamp(wgpu::CommandEncoder encoder) {
        encoder.WriteTimestamp(mTimestampQuerySet, 0);
    }

    void RecordEndTimestampAndResolveQuerySet(wgpu::CommandEncoder encoder) {
        encoder.WriteTimestamp(mTimestampQuerySet, 1);
        ResolveTimestamps(encoder);
    }

    wgpu::PassTimestampWrites GetPassTimestampWrites() const {
        wgpu::PassTimestampWrites timestampWrites;
        timestampWrites.querySet = mTimestampQuerySet;
        timestampWrites.beginningOfPassWriteIndex = 0;
        timestampWrites.endOfPassWriteIndex = 1;
        return timestampWrites;
    }

    void ResolveTimestamps(wgpu::CommandEncoder encoder) {
        encoder.ResolveQuerySet(mTimestampQuerySet, 0, kTimestampQueryCount, mResolveBuffer, 0);
        encoder.CopyBufferToBuffer(mResolveBuffer, 0, mReadbackBuffer, 0,
                                   sizeof(uint64_t) * kTimestampQueryCount);
    }

    void ComputeGPUElapsedTime() {
        bool done = false;
        mReadbackBuffer.MapAsync(wgpu::MapMode::Read, 0, sizeof(uint64_t) * kTimestampQueryCount,
                                 wgpu::CallbackMode::AllowProcessEvents,
                                 [&done](wgpu::MapAsyncStatus, wgpu::StringView) { done = true; });
        while (!done) {
            DawnTestWithParams<Params>::WaitABit();
        }
        const uint64_t* readbackValues =
            static_cast<const uint64_t*>(mReadbackBuffer.GetConstMappedRange());
        ASSERT_EQ(2u, kTimestampQueryCount);
        double gpuTimeElapsed = (DAWN_UNSAFE_TODO(readbackValues[1]) - readbackValues[0]) / 1e9;
        AddGPUTime(gpuTimeElapsed);
        mReadbackBuffer.Unmap();
    }

  private:
    void InitializeGPUTimer() {
        DAWN_ASSERT(mSupportsTimestampQuery);

        wgpu::Device device = this->device;

        wgpu::QuerySetDescriptor querySetDescriptor;
        querySetDescriptor.count = kTimestampQueryCount;
        querySetDescriptor.type = wgpu::QueryType::Timestamp;
        mTimestampQuerySet = device.CreateQuerySet(&querySetDescriptor);

        wgpu::BufferDescriptor resolveBufferDescriptor;
        resolveBufferDescriptor.size = kTimestampQueryCount * sizeof(uint64_t);
        resolveBufferDescriptor.usage = wgpu::BufferUsage::QueryResolve |
                                        wgpu::BufferUsage::CopySrc | wgpu::BufferUsage::CopyDst;
        mResolveBuffer = device.CreateBuffer(&resolveBufferDescriptor);

        wgpu::BufferDescriptor readbackBufferDescriptor;
        readbackBufferDescriptor.size = kTimestampQueryCount * sizeof(uint64_t);
        readbackBufferDescriptor.usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::MapRead;
        mReadbackBuffer = device.CreateBuffer(&readbackBufferDescriptor);
    }

    static constexpr uint32_t kTimestampQueryCount = 2;

    bool mSupportsTimestampQuery = false;

    wgpu::QuerySet mTimestampQuerySet;
    wgpu::Buffer mResolveBuffer;
    wgpu::Buffer mReadbackBuffer;
};

using DawnPerfTest = DawnPerfTestWithParams<>;

}  // namespace dawn

#endif  // SRC_DAWN_TESTS_PERF_TESTS_DAWNPERFTEST_H_
