/* vim:set ts=2 sw=2 sts=2 et: */
/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/
 */

#include "gtest/gtest.h"
#include "gmock/gmock.h"

#include "mozilla/gfx/JobScheduler.h"

#ifndef WIN32
#include <pthread.h>
#include <sched.h>
#endif

#include <stdlib.h>
#include <time.h>

namespace test_scheduler {

using namespace mozilla::gfx;
using namespace mozilla;
using mozilla::gfx::SyncObject;

// Artificially cause threads to yield randomly in an attempt to make racy
// things more apparent (if any).
void MaybeYieldThread()
{
#ifndef WIN32
  if (rand() % 5 == 0) {
    sched_yield();
  }
#endif
}

/// Used by the TestCommand to check that tasks are processed in the right order.
struct SanityChecker {
  std::vector<uint64_t> mAdvancements;
  mozilla::gfx::CriticalSection mSection;

  explicit SanityChecker(uint64_t aNumCmdBuffers)
  {
    for (uint32_t i = 0; i < aNumCmdBuffers; ++i) {
      mAdvancements.push_back(0);
    }
  }

  virtual void Check(uint64_t aJobId, uint64_t aCmdId)
  {
    MaybeYieldThread();
    CriticalSectionAutoEnter lock(&mSection);
    MOZ_RELEASE_ASSERT(mAdvancements[aJobId] == aCmdId-1);
    mAdvancements[aJobId] = aCmdId;
  }
};

/// Run checks that are specific to TestSchulerJoin.
struct JoinTestSanityCheck : public SanityChecker {
  bool mSpecialJobHasRun;

  explicit JoinTestSanityCheck(uint64_t aNumCmdBuffers)
  : SanityChecker(aNumCmdBuffers)
  , mSpecialJobHasRun(false)
  {}

  virtual void Check(uint64_t aJobId, uint64_t aCmdId) override
  {
    // Job 0 is the special task executed when everything is joined after task 1
    if (aCmdId == 0) {
      MOZ_RELEASE_ASSERT(!mSpecialJobHasRun, "GFX: A special task has been executed.");
      mSpecialJobHasRun = true;
      for (auto advancement : mAdvancements) {
        // Because of the synchronization point (beforeFilter), all
        // task buffers should have run task 1 when task 0 is run.
        MOZ_RELEASE_ASSERT(advancement == 1, "GFX: task buffer has not run task 1.");
      }
    } else {
      // This check does not apply to task 0.
      SanityChecker::Check(aJobId, aCmdId);
    }

    if (aCmdId == 2) {
      MOZ_RELEASE_ASSERT(mSpecialJobHasRun, "GFX: Special job has not run.");
    }
  }
};

class TestJob : public Job
{
public:
  TestJob(uint64_t aCmdId, uint64_t aJobId, SanityChecker* aChecker,
           SyncObject* aStart, SyncObject* aCompletion)
  : Job(aStart, aCompletion, nullptr)
  , mCmdId(aCmdId)
  , mCmdBufferId(aJobId)
  , mSanityChecker(aChecker)
  {}

  JobStatus Run()
  {
    MaybeYieldThread();
    mSanityChecker->Check(mCmdBufferId, mCmdId);
    MaybeYieldThread();
    return JobStatus::Complete;
  }

  uint64_t mCmdId;
  uint64_t mCmdBufferId;
  SanityChecker* mSanityChecker;
};

/// This test creates aNumCmdBuffers task buffers with sync objects set up
/// so that all tasks will join after command 5 before a task buffer runs
/// a special task (task 0) after which all task buffers fork again.
/// This simulates the kind of scenario where all tiles must join at
/// a certain point to execute, say, a filter, and fork again after the filter
/// has been processed.
/// The main thread is only blocked when waiting for the completion of the entire
/// task stream (it doesn't have to wait at the filter's sync points to orchestrate it).
void TestSchedulerJoin(uint32_t aNumThreads, uint32_t aNumCmdBuffers)
{
  JoinTestSanityCheck check(aNumCmdBuffers);

  RefPtr<SyncObject> beforeFilter = new SyncObject(aNumCmdBuffers);
  RefPtr<SyncObject> afterFilter = new SyncObject();
  RefPtr<SyncObject> completion = new SyncObject(aNumCmdBuffers);


  for (uint32_t i = 0; i < aNumCmdBuffers; ++i) {
    Job* t1 = new TestJob(1, i, &check, nullptr, beforeFilter);
    JobScheduler::SubmitJob(t1);
    MaybeYieldThread();
  }
  beforeFilter->FreezePrerequisites();

  // This task buffer is executed when all other tasks have joined after task 1
  JobScheduler::SubmitJob(
    new TestJob(0, 0, &check, beforeFilter, afterFilter)
  );
  afterFilter->FreezePrerequisites();

  for (uint32_t i = 0; i < aNumCmdBuffers; ++i) {
    Job* t2 = new TestJob(2, i, &check, afterFilter, completion);
    JobScheduler::SubmitJob(t2);
    MaybeYieldThread();
  }
  completion->FreezePrerequisites();

  JobScheduler::Join(completion);

  MaybeYieldThread();

  for (auto advancement : check.mAdvancements) {
    EXPECT_TRUE(advancement == 2);
  }
}

/// This test creates several chains of 10 task, tasks of a given chain are executed
/// sequentially, and chains are exectuted in parallel.
/// This simulates the typical scenario where we want to process sequences of drawing
/// commands for several tiles in parallel.
void TestSchedulerChain(uint32_t aNumThreads, uint32_t aNumCmdBuffers)
{
  SanityChecker check(aNumCmdBuffers);

  RefPtr<SyncObject> completion = new SyncObject(aNumCmdBuffers);

  uint32_t numJobs = 10;

  for (uint32_t i = 0; i < aNumCmdBuffers; ++i) {

    std::vector<RefPtr<SyncObject>> syncs;
    std::vector<Job*> tasks;
    syncs.reserve(numJobs);
    tasks.reserve(numJobs);

    for (uint32_t t = 0; t < numJobs-1; ++t) {
      syncs.push_back(new SyncObject());
      tasks.push_back(new TestJob(t+1, i, &check, t == 0 ? nullptr
                                                          : syncs[t-1].get(),
                                   syncs[t]));
      syncs.back()->FreezePrerequisites();
    }

    tasks.push_back(new TestJob(numJobs, i, &check, syncs.back(), completion));

    if (i % 2 == 0) {
      // submit half of the tasks in order
      for (Job* task : tasks) {
        JobScheduler::SubmitJob(task);
        MaybeYieldThread();
      }
    } else {
      // ... and submit the other half in reverse order
      for (int32_t reverse = numJobs-1; reverse >= 0; --reverse) {
        JobScheduler::SubmitJob(tasks[reverse]);
        MaybeYieldThread();
      }
    }
  }
  completion->FreezePrerequisites();

  JobScheduler::Join(completion);

  for (auto advancement : check.mAdvancements) {
    EXPECT_TRUE(advancement == numJobs);
  }
}

} // namespace test_scheduler

TEST(Moz2D, JobScheduler_Shutdown) {
  srand(time(nullptr));
  for (uint32_t threads = 1; threads < 16; ++threads) {
    for (uint32_t i = 1; i < 1000; ++i) {
      mozilla::gfx::JobScheduler::Init(threads, threads);
      mozilla::gfx::JobScheduler::ShutDown();
    }
  }
}

TEST(Moz2D, JobScheduler_Join) {
  srand(time(nullptr));
  for (uint32_t threads = 1; threads < 8; ++threads) {
    for (uint32_t queues = 1; queues < threads; ++queues) {
      for (uint32_t buffers = 1; buffers < 100; buffers += 3) {
        mozilla::gfx::JobScheduler::Init(threads, queues);
        test_scheduler::TestSchedulerJoin(threads, buffers);
        mozilla::gfx::JobScheduler::ShutDown();
      }
    }
  }
}

TEST(Moz2D, JobScheduler_Chain) {
  srand(time(nullptr));
  for (uint32_t threads = 1; threads < 8; ++threads) {
    for (uint32_t queues = 1; queues < threads; ++queues) {
      for (uint32_t buffers = 1; buffers < 100; buffers += 3) {
        mozilla::gfx::JobScheduler::Init(threads, queues);
        test_scheduler::TestSchedulerChain(threads, buffers);
        mozilla::gfx::JobScheduler::ShutDown();
      }
    }
  }
}
