/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 * vim: set ts=8 sts=2 et sw=2 tw=80:
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "gc/ParallelMarking.h"

#include "gc/GCInternals.h"
#include "gc/GCLock.h"
#include "gc/ParallelWork.h"
#include "vm/GeckoProfiler.h"
#include "vm/HelperThreadState.h"
#include "vm/Runtime.h"

using namespace js;
using namespace js::gc;

using mozilla::TimeDuration;
using mozilla::TimeStamp;

using JS::SliceBudget;

class AutoAddTimeDuration {
  TimeStamp start_;
  TimeDuration& result_;

 public:
  explicit AutoAddTimeDuration(TimeDuration& result)
      : start_(TimeStamp::Now()), result_(result) {}
  TimeStamp start() const { return start_; }
  ~AutoAddTimeDuration() { result_ += TimeSince(start_); }
};

ParallelMarker::ParallelMarker(GCRuntime* gc) : gc(gc) {}

size_t ParallelMarker::workerCount() const { return gc->markers.length(); }

bool ParallelMarker::mark(const SliceBudget& sliceBudget) {
  MOZ_ASSERT(workerCount() <= gc->getMaxParallelThreads());

  if (markOneColor(MarkColor::Black, sliceBudget) == NotFinished) {
    return false;
  }
  MOZ_ASSERT(!hasWork(MarkColor::Black));

  if (markOneColor(MarkColor::Gray, sliceBudget) == NotFinished) {
    return false;
  }
  MOZ_ASSERT(!hasWork(MarkColor::Gray));

  // Handle any delayed marking, which is not performed in parallel.
  if (gc->hasDelayedMarking()) {
    gc->markAllDelayedChildren(ReportMarkTime);
  }

  return true;
}

bool ParallelMarker::markOneColor(MarkColor color,
                                  const SliceBudget& sliceBudget) {
  // Run a marking slice and return whether the stack is now empty.

  if (!hasWork(color)) {
    return true;
  }

  gcstats::AutoPhase ap(gc->stats(), gcstats::PhaseKind::PARALLEL_MARK);

  MOZ_ASSERT(workerCount() <= MaxParallelWorkers);
  mozilla::Maybe<ParallelMarkTask> tasks[MaxParallelWorkers];

  for (size_t i = 0; i < workerCount(); i++) {
    GCMarker* marker = gc->markers[i].get();
    tasks[i].emplace(this, marker, color, sliceBudget);

    // Attempt to populate empty mark stacks.
    //
    // TODO: When tuning for more than two markers we may need to adopt a more
    // sophisticated approach.
    if (!marker->hasEntriesForCurrentColor() && gc->marker().canDonateWork()) {
      GCMarker::moveWork(marker, &gc->marker(), false);
    }
  }

  AutoLockHelperThreadState lock;

  MOZ_ASSERT(activeTasks == 0);
  for (size_t i = 0; i < workerCount(); i++) {
    ParallelMarkTask& task = *tasks[i];
    if (task.hasWork()) {
      incActiveTasks(&task, lock);
    }
  }

  // There should always be enough parallel tasks to run our marking work.
  MOZ_RELEASE_ASSERT(gc->maxParallelThreads >= workerCount());

  // Run the parallel tasks, using the main thread for the first one.
  for (size_t i = 1; i < workerCount(); i++) {
    ParallelMarkTask& task = *tasks[i];
    gc->startTask(task, lock);
  }
  tasks[0]->runFromMainThread(lock);
  tasks[0]->recordDuration();  // Record stats as if it used a helper thread.
  for (size_t i = 1; i < workerCount(); i++) {
    gc->joinTask(*tasks[i], lock);
  }

#ifdef DEBUG
  MOZ_ASSERT(waitingTasks.ref().isEmpty());
  MOZ_ASSERT(waitingTaskCount == 0);
  MOZ_ASSERT(activeTasks == 0);
#endif

  return !hasWork(color);
}

bool ParallelMarker::hasWork(MarkColor color) const {
  for (const auto& marker : gc->markers) {
    if (marker->hasEntries(color)) {
      return true;
    }
  }

  return false;
}

ParallelMarkTask::ParallelMarkTask(ParallelMarker* pm, GCMarker* marker,
                                   MarkColor color, const SliceBudget& budget)
    : GCParallelTask(pm->gc, gcstats::PhaseKind::PARALLEL_MARK, GCUse::Marking),
      pm(pm),
      marker(marker),
      color(*marker, color),
      budget(budget) {
  marker->enterParallelMarkingMode(pm);
}

ParallelMarkTask::~ParallelMarkTask() {
  MOZ_ASSERT(!isWaiting.refNoCheck());
  marker->leaveParallelMarkingMode();
}

bool ParallelMarkTask::hasWork() const {
  return marker->hasEntriesForCurrentColor();
}

void ParallelMarkTask::recordDuration() {
  // Record times separately to avoid double counting when these are summed.
  gc->stats().recordParallelPhase(gcstats::PhaseKind::PARALLEL_MARK_MARK,
                                  markTime.ref());
  gc->stats().recordParallelPhase(gcstats::PhaseKind::PARALLEL_MARK_WAIT,
                                  waitTime.ref());
  TimeDuration other = duration() - markTime.ref() - waitTime.ref();
  if (other < TimeDuration::Zero()) {
    other = TimeDuration::Zero();
  }
  gc->stats().recordParallelPhase(gcstats::PhaseKind::PARALLEL_MARK_OTHER,
                                  other);
}

void ParallelMarkTask::run(AutoLockHelperThreadState& lock) {
  AutoUpdateMarkStackRanges updateRanges(*marker);

  for (;;) {
    if (hasWork()) {
      if (!tryMarking(lock)) {
        return;
      }
    } else {
      if (!requestWork(lock)) {
        return;
      }
    }
  }

  MOZ_ASSERT(!isWaiting);
}

bool ParallelMarkTask::tryMarking(AutoLockHelperThreadState& lock) {
  MOZ_ASSERT(hasWork());
  MOZ_ASSERT(marker->isParallelMarking());

  // Mark until budget exceeded or we run out of work.
  bool finished;
  {
    AutoUnlockHelperThreadState unlock(lock);

    AutoAddTimeDuration time(markTime.ref());
    finished = marker->markCurrentColorInParallel(budget);

    GeckoProfilerRuntime& profiler = gc->rt->geckoProfiler();
    if (profiler.enabled()) {
      profiler.markInterval("Parallel marking ran", time.start(), nullptr,
                            JS::ProfilingCategoryPair::GCCC);
    }
  }

  MOZ_ASSERT_IF(finished, !hasWork());
  pm->decActiveTasks(this, lock);

  return finished;
}

bool ParallelMarkTask::requestWork(AutoLockHelperThreadState& lock) {
  MOZ_ASSERT(!hasWork());

  if (!pm->hasActiveTasks(lock)) {
    return false;  // All other tasks are empty. We're finished.
  }

  budget.forceCheck();
  if (budget.isOverBudget()) {
    return false;  // Over budget or interrupted.
  }

  // Add ourselves to the waiting list and wait for another task to give us
  // work. The task with work calls ParallelMarker::donateWorkFrom.
  waitUntilResumed(lock);

  return true;
}

void ParallelMarkTask::waitUntilResumed(AutoLockHelperThreadState& lock) {
  AutoAddTimeDuration time(waitTime.ref());

  pm->addTaskToWaitingList(this, lock);

  // Set isWaiting flag and wait for another thread to clear it and resume us.
  MOZ_ASSERT(!isWaiting);
  isWaiting = true;

  do {
    MOZ_ASSERT(pm->hasActiveTasks(lock));
    resumed.wait(lock);
  } while (isWaiting);

  MOZ_ASSERT(!pm->isTaskInWaitingList(this, lock));

  GeckoProfilerRuntime& profiler = gc->rt->geckoProfiler();
  if (profiler.enabled()) {
    char details[32];
    SprintfLiteral(details, "markers=%zu", pm->workerCount());
    profiler.markInterval("Parallel marking wait", time.start(), details,
                          JS::ProfilingCategoryPair::GCCC);
  }
}

void ParallelMarkTask::resume() {
  {
    AutoLockHelperThreadState lock;
    MOZ_ASSERT(isWaiting);

    isWaiting = false;

    // Increment the active task count before donateWorkFrom() returns so this
    // can't reach zero before the waiting task runs again.
    if (hasWork()) {
      pm->incActiveTasks(this, lock);
    }
  }

  resumed.notify_all();
}

void ParallelMarkTask::resumeOnFinish(const AutoLockHelperThreadState& lock) {
  MOZ_ASSERT(isWaiting);
  MOZ_ASSERT(!hasWork());

  isWaiting = false;
  resumed.notify_all();
}

void ParallelMarker::addTaskToWaitingList(
    ParallelMarkTask* task, const AutoLockHelperThreadState& lock) {
  MOZ_ASSERT(!task->hasWork());
  MOZ_ASSERT(hasActiveTasks(lock));
  MOZ_ASSERT(!isTaskInWaitingList(task, lock));
  MOZ_ASSERT(waitingTaskCount < workerCount() - 1);

  waitingTasks.ref().pushBack(task);
  waitingTaskCount++;
}

#ifdef DEBUG
bool ParallelMarker::isTaskInWaitingList(
    const ParallelMarkTask* task, const AutoLockHelperThreadState& lock) const {
  // The const cast is because ElementProbablyInList is not const.
  return const_cast<ParallelMarkTaskList&>(waitingTasks.ref())
      .ElementProbablyInList(const_cast<ParallelMarkTask*>(task));
}
#endif

void ParallelMarker::incActiveTasks(ParallelMarkTask* task,
                                    const AutoLockHelperThreadState& lock) {
  MOZ_ASSERT(task->hasWork());
  MOZ_ASSERT(activeTasks < workerCount());

  activeTasks++;
}

void ParallelMarker::decActiveTasks(ParallelMarkTask* task,
                                    const AutoLockHelperThreadState& lock) {
  MOZ_ASSERT(activeTasks != 0);

  activeTasks--;

  if (activeTasks == 0) {
    while (!waitingTasks.ref().isEmpty()) {
      ParallelMarkTask* task = waitingTasks.ref().popFront();
      MOZ_ASSERT(waitingTaskCount != 0);
      waitingTaskCount--;
      task->resumeOnFinish(lock);
    }
  }
}

void ParallelMarker::donateWorkFrom(GCMarker* src) {
  GeckoProfilerRuntime& profiler = gc->rt->geckoProfiler();

  if (!gHelperThreadLock.tryLock()) {
    if (profiler.enabled()) {
      profiler.markEvent("Parallel marking donate failed", "lock already held");
    }
    return;
  }

  // Check there are tasks waiting for work while holding the lock.
  if (waitingTaskCount == 0) {
    gHelperThreadLock.unlock();
    if (profiler.enabled()) {
      profiler.markEvent("Parallel marking donate failed", "no tasks waiting");
    }
    return;
  }

  // Take the first waiting task off the list.
  ParallelMarkTask* waitingTask = waitingTasks.ref().popFront();
  waitingTaskCount--;

  // |task| is not running so it's safe to move work to it.
  MOZ_ASSERT(waitingTask->isWaiting);

  gHelperThreadLock.unlock();

  // Move some work from this thread's mark stack to the waiting task.
  MOZ_ASSERT(!waitingTask->hasWork());
  size_t wordsMoved = GCMarker::moveWork(waitingTask->marker, src, true);

  gc->stats().count(gcstats::COUNT_PARALLEL_MARK_INTERRUPTIONS);

  if (profiler.enabled()) {
    char details[32];
    SprintfLiteral(details, "words=%zu", wordsMoved);
    profiler.markEvent("Parallel marking donated work", details,
                       JS::ProfilingCategoryPair::GCCC);
  }

  // Resume waiting task.
  waitingTask->resume();
}
