// Copyright 2016 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "src/compiler-dispatcher/lazy-compile-dispatcher.h"

#include <atomic>

#include "include/v8-platform.h"
#include "src/base/platform/mutex.h"
#include "src/base/platform/time.h"
#include "src/codegen/compiler.h"
#include "src/common/globals.h"
#include "src/execution/isolate.h"
#include "src/flags/flags.h"
#include "src/heap/parked-scope.h"
#include "src/logging/counters.h"
#include "src/logging/runtime-call-stats-scope.h"
#include "src/objects/instance-type.h"
#include "src/objects/objects-inl.h"
#include "src/parsing/parse-info.h"
#include "src/parsing/scanner.h"
#include "src/tasks/cancelable-task.h"
#include "src/tasks/task-utils.h"
#include "src/zone/zone-list-inl.h"  // crbug.com/v8/8816

namespace v8 {
namespace internal {

// The maximum amount of time we should allow a single function's FinishNow to
// spend opportunistically finalizing other finalizable jobs.
static constexpr int kMaxOpportunisticFinalizeTimeMs = 1;

class LazyCompileDispatcher::JobTask : public v8::JobTask {
 public:
  explicit JobTask(LazyCompileDispatcher* lazy_compile_dispatcher)
      : lazy_compile_dispatcher_(lazy_compile_dispatcher) {}

  void Run(JobDelegate* delegate) final {
    lazy_compile_dispatcher_->DoBackgroundWork(delegate);
  }

  size_t GetMaxConcurrency(size_t worker_count) const final {
    size_t n = lazy_compile_dispatcher_->num_jobs_for_background_.load(
        std::memory_order_relaxed);
    if (v8_flags.lazy_compile_dispatcher_max_threads == 0) return n;
    return std::min(
        n, static_cast<size_t>(v8_flags.lazy_compile_dispatcher_max_threads));
  }

 private:
  LazyCompileDispatcher* lazy_compile_dispatcher_;
};

LazyCompileDispatcher::Job::Job(std::unique_ptr<BackgroundCompileTask> task)
    : task(std::move(task)), state(Job::State::kPending) {}

LazyCompileDispatcher::Job::~Job() = default;

LazyCompileDispatcher::LazyCompileDispatcher(Isolate* isolate,
                                             Platform* platform,
                                             size_t max_stack_size)
    : isolate_(isolate),
      worker_thread_runtime_call_stats_(
          isolate->counters()->worker_thread_runtime_call_stats()),
      background_compile_timer_(
          isolate->counters()->compile_function_on_background()),
      taskrunner_(platform->GetForegroundTaskRunner(
          reinterpret_cast<v8::Isolate*>(isolate))),
      platform_(platform),
      max_stack_size_(max_stack_size),
      trace_compiler_dispatcher_(v8_flags.trace_compiler_dispatcher),
      idle_task_manager_(new CancelableTaskManager()),
      idle_task_scheduled_(false),
      num_jobs_for_background_(0),
      main_thread_blocking_on_job_(nullptr),
      block_for_testing_(false),
      semaphore_for_testing_(0) {
  job_handle_ = platform_->PostJob(TaskPriority::kUserVisible,
                                   std::make_unique<JobTask>(this));
}

LazyCompileDispatcher::~LazyCompileDispatcher() {
  // AbortAll must be called before LazyCompileDispatcher is destroyed.
  CHECK(!job_handle_->IsValid());
}

namespace {

// If the SharedFunctionInfo's UncompiledData has a job slot, then write into
// it. Otherwise, allocate a new UncompiledData with a job slot, and then write
// into that. Since we have two optional slots (preparse data and job), this
// gets a little messy.
void SetUncompiledDataJobPointer(LocalIsolate* isolate,
                                 Handle<SharedFunctionInfo> shared_info,
                                 Address job_address) {
  UncompiledData uncompiled_data = shared_info->uncompiled_data();
  switch (uncompiled_data.map(isolate).instance_type()) {
    // The easy cases -- we already have a job slot, so can write into it and
    // return.
    case UNCOMPILED_DATA_WITH_PREPARSE_DATA_AND_JOB_TYPE:
      UncompiledDataWithPreparseDataAndJob::cast(uncompiled_data)
          .set_job(job_address);
      break;
    case UNCOMPILED_DATA_WITHOUT_PREPARSE_DATA_WITH_JOB_TYPE:
      UncompiledDataWithoutPreparseDataWithJob::cast(uncompiled_data)
          .set_job(job_address);
      break;

    // Otherwise, we'll have to allocate a new UncompiledData (with or without
    // preparse data as appropriate), set the job pointer on that, and update
    // the SharedFunctionInfo to use the new UncompiledData
    case UNCOMPILED_DATA_WITH_PREPARSE_DATA_TYPE: {
      Handle<String> inferred_name(uncompiled_data.inferred_name(), isolate);
      Handle<PreparseData> preparse_data(
          UncompiledDataWithPreparseData::cast(uncompiled_data).preparse_data(),
          isolate);
      Handle<UncompiledDataWithPreparseDataAndJob> new_uncompiled_data =
          isolate->factory()->NewUncompiledDataWithPreparseDataAndJob(
              inferred_name, uncompiled_data.start_position(),
              uncompiled_data.end_position(), preparse_data);

      new_uncompiled_data->set_job(job_address);
      shared_info->set_uncompiled_data(*new_uncompiled_data);
      break;
    }
    case UNCOMPILED_DATA_WITHOUT_PREPARSE_DATA_TYPE: {
      DCHECK(uncompiled_data.IsUncompiledDataWithoutPreparseData());
      Handle<String> inferred_name(uncompiled_data.inferred_name(), isolate);
      Handle<UncompiledDataWithoutPreparseDataWithJob> new_uncompiled_data =
          isolate->factory()->NewUncompiledDataWithoutPreparseDataWithJob(
              inferred_name, uncompiled_data.start_position(),
              uncompiled_data.end_position());

      new_uncompiled_data->set_job(job_address);
      shared_info->set_uncompiled_data(*new_uncompiled_data);
      break;
    }

    default:
      UNREACHABLE();
  }
}

}  // namespace

void LazyCompileDispatcher::Enqueue(
    LocalIsolate* isolate, Handle<SharedFunctionInfo> shared_info,
    std::unique_ptr<Utf16CharacterStream> character_stream) {
  TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("v8.compile"),
               "V8.LazyCompilerDispatcherEnqueue");
  RCS_SCOPE(isolate, RuntimeCallCounterId::kCompileEnqueueOnDispatcher);

  Job* job = new Job(std::make_unique<BackgroundCompileTask>(
      isolate_, shared_info, std::move(character_stream),
      worker_thread_runtime_call_stats_, background_compile_timer_,
      static_cast<int>(max_stack_size_)));

  SetUncompiledDataJobPointer(isolate, shared_info,
                              reinterpret_cast<Address>(job));

  // Post a a background worker task to perform the compilation on the worker
  // thread.
  {
    base::MutexGuard lock(&mutex_);
    if (trace_compiler_dispatcher_) {
      PrintF("LazyCompileDispatcher: enqueued job for ");
      shared_info->ShortPrint();
      PrintF("\n");
    }

#ifdef DEBUG
    all_jobs_.insert(job);
#endif
    pending_background_jobs_.push_back(job);
    NotifyAddedBackgroundJob(lock);
  }
  // This is not in NotifyAddedBackgroundJob to avoid being inside the mutex.
  job_handle_->NotifyConcurrencyIncrease();
}

bool LazyCompileDispatcher::IsEnqueued(
    Handle<SharedFunctionInfo> function) const {
  Job* job = nullptr;
  Object function_data = function->function_data(kAcquireLoad);
  if (function_data.IsUncompiledDataWithPreparseDataAndJob()) {
    job = reinterpret_cast<Job*>(
        UncompiledDataWithPreparseDataAndJob::cast(function_data).job());
  } else if (function_data.IsUncompiledDataWithoutPreparseDataWithJob()) {
    job = reinterpret_cast<Job*>(
        UncompiledDataWithoutPreparseDataWithJob::cast(function_data).job());
  }
  return job != nullptr;
}

void LazyCompileDispatcher::WaitForJobIfRunningOnBackground(
    Job* job, const base::MutexGuard& lock) {
  TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("v8.compile"),
               "V8.LazyCompilerDispatcherWaitForBackgroundJob");
  RCS_SCOPE(isolate_, RuntimeCallCounterId::kCompileWaitForDispatcher);

  if (!job->is_running_on_background()) {
    if (job->state == Job::State::kPending) {
      DCHECK_EQ(std::count(pending_background_jobs_.begin(),
                           pending_background_jobs_.end(), job),
                1);

      // TODO(leszeks): Remove from pending jobs without walking the whole
      // vector.
      pending_background_jobs_.erase(
          std::remove(pending_background_jobs_.begin(),
                      pending_background_jobs_.end(), job));
      job->state = Job::State::kPendingToRunOnForeground;
      NotifyRemovedBackgroundJob(lock);
    } else {
      DCHECK_EQ(job->state, Job::State::kReadyToFinalize);
      DCHECK_EQ(
          std::count(finalizable_jobs_.begin(), finalizable_jobs_.end(), job),
          1);

      // TODO(leszeks): Remove from finalizable jobs without walking the whole
      // vector.
      finalizable_jobs_.erase(
          std::remove(finalizable_jobs_.begin(), finalizable_jobs_.end(), job));
      job->state = Job::State::kFinalizingNow;
    }
    return;
  }
  DCHECK_NULL(main_thread_blocking_on_job_);
  main_thread_blocking_on_job_ = job;
  while (main_thread_blocking_on_job_ != nullptr) {
    main_thread_blocking_signal_.Wait(&mutex_);
  }

  DCHECK_EQ(job->state, Job::State::kReadyToFinalize);
  DCHECK_EQ(std::count(finalizable_jobs_.begin(), finalizable_jobs_.end(), job),
            1);

  // TODO(leszeks): Remove from finalizable jobs without walking the whole
  // vector.
  finalizable_jobs_.erase(
      std::remove(finalizable_jobs_.begin(), finalizable_jobs_.end(), job));
  job->state = Job::State::kFinalizingNow;
}

bool LazyCompileDispatcher::FinishNow(Handle<SharedFunctionInfo> function) {
  TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("v8.compile"),
               "V8.LazyCompilerDispatcherFinishNow");
  RCS_SCOPE(isolate_, RuntimeCallCounterId::kCompileFinishNowOnDispatcher);
  if (trace_compiler_dispatcher_) {
    PrintF("LazyCompileDispatcher: finishing ");
    function->ShortPrint();
    PrintF(" now\n");
  }

  Job* job;

  {
    base::MutexGuard lock(&mutex_);
    job = GetJobFor(function, lock);
    WaitForJobIfRunningOnBackground(job, lock);
  }

  if (job->state == Job::State::kPendingToRunOnForeground) {
    job->task->RunOnMainThread(isolate_);
    job->state = Job::State::kFinalizingNow;
  }

  if (DEBUG_BOOL) {
    base::MutexGuard lock(&mutex_);
    DCHECK_EQ(std::count(pending_background_jobs_.begin(),
                         pending_background_jobs_.end(), job),
              0);
    DCHECK_EQ(
        std::count(finalizable_jobs_.begin(), finalizable_jobs_.end(), job), 0);
    DCHECK_EQ(job->state, Job::State::kFinalizingNow);
  }

  bool success = Compiler::FinalizeBackgroundCompileTask(
      job->task.get(), isolate_, Compiler::KEEP_EXCEPTION);
  job->state = Job::State::kFinalized;

  DCHECK_NE(success, isolate_->has_pending_exception());
  DeleteJob(job);

  // Opportunistically finalize all other jobs for a maximum time of
  // kMaxOpportunisticFinalizeTimeMs.
  double deadline_in_seconds = platform_->MonotonicallyIncreasingTime() +
                               kMaxOpportunisticFinalizeTimeMs / 1000.0;
  while (deadline_in_seconds > platform_->MonotonicallyIncreasingTime()) {
    if (!FinalizeSingleJob()) break;
  }

  return success;
}

void LazyCompileDispatcher::AbortJob(Handle<SharedFunctionInfo> shared_info) {
  if (trace_compiler_dispatcher_) {
    PrintF("LazyCompileDispatcher: aborting job for ");
    shared_info->ShortPrint();
    PrintF("\n");
  }
  base::LockGuard<base::Mutex> lock(&mutex_);

  Job* job = GetJobFor(shared_info, lock);
  if (job->is_running_on_background()) {
    // Job is currently running on the background thread, wait until it's done
    // and remove job then.
    job->state = Job::State::kAbortRequested;
  } else {
    if (job->state == Job::State::kPending) {
      DCHECK_EQ(std::count(pending_background_jobs_.begin(),
                           pending_background_jobs_.end(), job),
                1);

      pending_background_jobs_.erase(
          std::remove(pending_background_jobs_.begin(),
                      pending_background_jobs_.end(), job));
      job->state = Job::State::kAbortingNow;
      NotifyRemovedBackgroundJob(lock);
    } else if (job->state == Job::State::kReadyToFinalize) {
      DCHECK_EQ(
          std::count(finalizable_jobs_.begin(), finalizable_jobs_.end(), job),
          1);

      finalizable_jobs_.erase(
          std::remove(finalizable_jobs_.begin(), finalizable_jobs_.end(), job));
      job->state = Job::State::kAbortingNow;
    } else {
      UNREACHABLE();
    }
    job->task->AbortFunction();
    job->state = Job::State::kFinalized;
    DeleteJob(job, lock);
  }
}

void LazyCompileDispatcher::AbortAll() {
  idle_task_manager_->TryAbortAll();
  job_handle_->Cancel();

  {
    base::MutexGuard lock(&mutex_);
    for (Job* job : pending_background_jobs_) {
      job->task->AbortFunction();
      job->state = Job::State::kFinalized;
      DeleteJob(job, lock);
    }
    pending_background_jobs_.clear();
    for (Job* job : finalizable_jobs_) {
      job->task->AbortFunction();
      job->state = Job::State::kFinalized;
      DeleteJob(job, lock);
    }
    finalizable_jobs_.clear();
    for (Job* job : jobs_to_dispose_) {
      delete job;
    }
    jobs_to_dispose_.clear();

    DCHECK_EQ(all_jobs_.size(), 0);
    num_jobs_for_background_ = 0;
    VerifyBackgroundTaskCount(lock);
  }

  idle_task_manager_->CancelAndWait();
}

LazyCompileDispatcher::Job* LazyCompileDispatcher::GetJobFor(
    Handle<SharedFunctionInfo> shared, const base::MutexGuard&) const {
  Object function_data = shared->function_data(kAcquireLoad);
  if (function_data.IsUncompiledDataWithPreparseDataAndJob()) {
    return reinterpret_cast<Job*>(
        UncompiledDataWithPreparseDataAndJob::cast(function_data).job());
  } else if (function_data.IsUncompiledDataWithoutPreparseDataWithJob()) {
    return reinterpret_cast<Job*>(
        UncompiledDataWithoutPreparseDataWithJob::cast(function_data).job());
  }
  return nullptr;
}

void LazyCompileDispatcher::ScheduleIdleTaskFromAnyThread(
    const base::MutexGuard&) {
  if (!taskrunner_->IdleTasksEnabled()) return;
  if (idle_task_scheduled_) return;

  idle_task_scheduled_ = true;
  // TODO(leszeks): Using a full task manager for a single cancellable task is
  // overkill, we could probably do the cancelling ourselves.
  taskrunner_->PostIdleTask(MakeCancelableIdleTask(
      idle_task_manager_.get(),
      [this](double deadline_in_seconds) { DoIdleWork(deadline_in_seconds); }));
}

void LazyCompileDispatcher::DoBackgroundWork(JobDelegate* delegate) {
  TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("v8.compile"),
               "V8.LazyCompileDispatcherDoBackgroundWork");

  LocalIsolate isolate(isolate_, ThreadKind::kBackground);
  UnparkedScope unparked_scope(&isolate);
  LocalHandleScope handle_scope(&isolate);

  ReusableUnoptimizedCompileState reusable_state(&isolate);

  while (true) {
    // Return immediately on yield, avoiding the second loop.
    if (delegate->ShouldYield()) return;

    Job* job = nullptr;
    {
      base::MutexGuard lock(&mutex_);

      if (pending_background_jobs_.empty()) break;
      job = pending_background_jobs_.back();
      pending_background_jobs_.pop_back();
      DCHECK_EQ(job->state, Job::State::kPending);

      job->state = Job::State::kRunning;
    }

    if (V8_UNLIKELY(block_for_testing_.Value())) {
      block_for_testing_.SetValue(false);
      semaphore_for_testing_.Wait();
    }

    if (trace_compiler_dispatcher_) {
      PrintF("LazyCompileDispatcher: doing background work\n");
    }

    job->task->Run(&isolate, &reusable_state);

    {
      base::MutexGuard lock(&mutex_);
      if (job->state == Job::State::kRunning) {
        job->state = Job::State::kReadyToFinalize;
        // Schedule an idle task to finalize the compilation on the main thread
        // if the job has a shared function info registered.
      } else {
        DCHECK_EQ(job->state, Job::State::kAbortRequested);
        job->state = Job::State::kAborted;
      }
      finalizable_jobs_.push_back(job);
      NotifyRemovedBackgroundJob(lock);

      if (main_thread_blocking_on_job_ == job) {
        main_thread_blocking_on_job_ = nullptr;
        main_thread_blocking_signal_.NotifyOne();
      } else {
        ScheduleIdleTaskFromAnyThread(lock);
      }
    }
  }

  while (!delegate->ShouldYield()) {
    Job* job = nullptr;
    {
      base::MutexGuard lock(&mutex_);
      if (jobs_to_dispose_.empty()) break;
      job = jobs_to_dispose_.back();
      jobs_to_dispose_.pop_back();
      if (jobs_to_dispose_.empty()) {
        num_jobs_for_background_--;
      }
    }
    delete job;
  }

  // Don't touch |this| anymore after this point, as it might have been
  // deleted.
}

LazyCompileDispatcher::Job* LazyCompileDispatcher::PopSingleFinalizeJob() {
  base::MutexGuard lock(&mutex_);

  if (finalizable_jobs_.empty()) return nullptr;

  Job* job = finalizable_jobs_.back();
  finalizable_jobs_.pop_back();
  DCHECK(job->state == Job::State::kReadyToFinalize ||
         job->state == Job::State::kAborted);
  if (job->state == Job::State::kReadyToFinalize) {
    job->state = Job::State::kFinalizingNow;
  } else {
    DCHECK_EQ(job->state, Job::State::kAborted);
    job->state = Job::State::kAbortingNow;
  }
  return job;
}

bool LazyCompileDispatcher::FinalizeSingleJob() {
  Job* job = PopSingleFinalizeJob();
  if (job == nullptr) return false;

  if (trace_compiler_dispatcher_) {
    PrintF("LazyCompileDispatcher: idle finalizing job\n");
  }

  if (job->state == Job::State::kFinalizingNow) {
    HandleScope scope(isolate_);
    Compiler::FinalizeBackgroundCompileTask(job->task.get(), isolate_,
                                            Compiler::CLEAR_EXCEPTION);
  } else {
    DCHECK_EQ(job->state, Job::State::kAbortingNow);
    job->task->AbortFunction();
  }
  job->state = Job::State::kFinalized;
  DeleteJob(job);
  return true;
}

void LazyCompileDispatcher::DoIdleWork(double deadline_in_seconds) {
  TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("v8.compile"),
               "V8.LazyCompilerDispatcherDoIdleWork");
  {
    base::MutexGuard lock(&mutex_);
    idle_task_scheduled_ = false;
  }

  if (trace_compiler_dispatcher_) {
    PrintF("LazyCompileDispatcher: received %0.1lfms of idle time\n",
           (deadline_in_seconds - platform_->MonotonicallyIncreasingTime()) *
               static_cast<double>(base::Time::kMillisecondsPerSecond));
  }
  while (deadline_in_seconds > platform_->MonotonicallyIncreasingTime()) {
    // Find a job which is pending finalization and has a shared function info
    auto there_was_a_job = FinalizeSingleJob();
    if (!there_was_a_job) return;
  }

  // We didn't return above so there still might be jobs to finalize.
  {
    base::MutexGuard lock(&mutex_);
    ScheduleIdleTaskFromAnyThread(lock);
  }
}

void LazyCompileDispatcher::DeleteJob(Job* job) {
  DCHECK(job->state == Job::State::kFinalized);
  base::MutexGuard lock(&mutex_);
  DeleteJob(job, lock);
}

void LazyCompileDispatcher::DeleteJob(Job* job, const base::MutexGuard&) {
  DCHECK(job->state == Job::State::kFinalized);
#ifdef DEBUG
  all_jobs_.erase(job);
#endif
  jobs_to_dispose_.push_back(job);
  if (jobs_to_dispose_.size() == 1) {
    num_jobs_for_background_++;
  }
}

#ifdef DEBUG
void LazyCompileDispatcher::VerifyBackgroundTaskCount(const base::MutexGuard&) {
  size_t pending_jobs = 0;
  size_t running_jobs = 0;
  size_t finalizable_jobs = 0;

  for (Job* job : all_jobs_) {
    switch (job->state) {
      case Job::State::kPending:
        pending_jobs++;
        break;
      case Job::State::kRunning:
      case Job::State::kAbortRequested:
        running_jobs++;
        break;
      case Job::State::kReadyToFinalize:
      case Job::State::kAborted:
        finalizable_jobs++;
        break;
      case Job::State::kPendingToRunOnForeground:
      case Job::State::kFinalizingNow:
      case Job::State::kAbortingNow:
      case Job::State::kFinalized:
        // Ignore.
        break;
    }
  }

  CHECK_EQ(pending_background_jobs_.size(), pending_jobs);
  CHECK_EQ(finalizable_jobs_.size(), finalizable_jobs);
  CHECK_EQ(num_jobs_for_background_.load(),
           pending_jobs + running_jobs + (jobs_to_dispose_.empty() ? 0 : 1));
}
#endif

}  // namespace internal
}  // namespace v8
