// Copyright 2021 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/baseline/baseline-batch-compiler.h"

#include <algorithm>

#include "src/baseline/baseline-compiler.h"
#include "src/codegen/compiler.h"
#include "src/execution/isolate.h"
#include "src/handles/global-handles-inl.h"
#include "src/heap/factory-inl.h"
#include "src/heap/heap-inl.h"
#include "src/heap/local-heap-inl.h"
#include "src/heap/parked-scope.h"
#include "src/objects/fixed-array-inl.h"
#include "src/objects/js-function-inl.h"
#include "src/utils/locked-queue-inl.h"

namespace v8 {
namespace internal {
namespace baseline {

static bool CanCompileWithConcurrentBaseline(Tagged<SharedFunctionInfo> shared,
                                             Isolate* isolate) {
  return !shared->HasBaselineCode() && CanCompileWithBaseline(isolate, shared);
}

class BaselineCompilerTask {
 public:
  BaselineCompilerTask(Isolate* isolate, PersistentHandles* handles,
                       Tagged<SharedFunctionInfo> sfi)
      : shared_function_info_(handles->NewHandle(sfi)),
        bytecode_(handles->NewHandle(sfi->GetBytecodeArray(isolate))) {
    DCHECK(sfi->is_compiled());
    shared_function_info_->set_is_sparkplug_compiling(true);
  }

  BaselineCompilerTask(const BaselineCompilerTask&) V8_NOEXCEPT = delete;
  BaselineCompilerTask(BaselineCompilerTask&&) V8_NOEXCEPT = default;

  // Executed in the background thread.
  void Compile(LocalIsolate* local_isolate) {
    base::ScopedTimer timer(v8_flags.log_function_events ? &time_taken_
                                                         : nullptr);
    BaselineCompiler compiler(local_isolate, shared_function_info_, bytecode_);
    compiler.GenerateCode();
    maybe_code_ = local_isolate->heap()->NewPersistentMaybeHandle(
        compiler.Build(local_isolate));
  }

  // Executed in the main thread.
  void Install(Isolate* isolate) {
    shared_function_info_->set_is_sparkplug_compiling(false);
    Handle<Code> code;
    if (!maybe_code_.ToHandle(&code)) return;
    if (v8_flags.print_code) {
      Print(*code);
    }
    // Don't install the code if the bytecode has been flushed or has
    // already some baseline code installed.
    if (!CanCompileWithConcurrentBaseline(*shared_function_info_, isolate)) {
      return;
    }

    shared_function_info_->set_baseline_code(*code, kReleaseStore);
    shared_function_info_->set_age(0);
    if (v8_flags.trace_baseline) {
      CodeTracer::Scope scope(isolate->GetCodeTracer());
      std::stringstream ss;
      ss << "[Concurrent Sparkplug Off Thread] Function ";
      ShortPrint(*shared_function_info_, ss);
      ss << " installed\n";
      OFStream os(scope.file());
      os << ss.str();
    }
    if (IsScript(shared_function_info_->script())) {
      Compiler::LogFunctionCompilation(
          isolate, LogEventListener::CodeTag::kFunction,
          handle(Script::cast(shared_function_info_->script()), isolate),
          shared_function_info_, Handle<FeedbackVector>(),
          Handle<AbstractCode>::cast(code), CodeKind::BASELINE,
          time_taken_.InMillisecondsF());
    }
  }

 private:
  Handle<SharedFunctionInfo> shared_function_info_;
  Handle<BytecodeArray> bytecode_;
  MaybeHandle<Code> maybe_code_;
  base::TimeDelta time_taken_;
};

class BaselineBatchCompilerJob {
 public:
  BaselineBatchCompilerJob(Isolate* isolate, Handle<WeakFixedArray> task_queue,
                           int batch_size) {
    handles_ = isolate->NewPersistentHandles();
    tasks_.reserve(batch_size);
    for (int i = 0; i < batch_size; i++) {
      Tagged<MaybeObject> maybe_sfi = task_queue->get(i);
      // TODO(victorgomes): Do I need to clear the value?
      task_queue->set(i, ClearedValue(isolate));
      Tagged<HeapObject> obj;
      // Skip functions where weak reference is no longer valid.
      if (!maybe_sfi.GetHeapObjectIfWeak(&obj)) continue;
      // Skip functions where the bytecode has been flushed.
      Tagged<SharedFunctionInfo> shared = SharedFunctionInfo::cast(obj);
      if (!CanCompileWithConcurrentBaseline(shared, isolate)) continue;
      // Skip functions that are already being compiled.
      if (shared->is_sparkplug_compiling()) continue;
      tasks_.emplace_back(isolate, handles_.get(), shared);
    }
    if (v8_flags.trace_baseline) {
      CodeTracer::Scope scope(isolate->GetCodeTracer());
      PrintF(scope.file(), "[Concurrent Sparkplug] compiling %zu functions\n",
             tasks_.size());
    }
  }

  // Executed in the background thread.
  void Compile(LocalIsolate* local_isolate) {
    local_isolate->heap()->AttachPersistentHandles(std::move(handles_));
    for (auto& task : tasks_) {
      task.Compile(local_isolate);
    }
    // Get the handle back since we'd need them to install the code later.
    handles_ = local_isolate->heap()->DetachPersistentHandles();
  }

  // Executed in the main thread.
  void Install(Isolate* isolate) {
    HandleScope local_scope(isolate);
    for (auto& task : tasks_) {
      task.Install(isolate);
    }
  }

 private:
  std::vector<BaselineCompilerTask> tasks_;
  std::unique_ptr<PersistentHandles> handles_;
};

class ConcurrentBaselineCompiler {
 public:
  class JobDispatcher : public v8::JobTask {
   public:
    JobDispatcher(
        Isolate* isolate,
        LockedQueue<std::unique_ptr<BaselineBatchCompilerJob>>* incoming_queue,
        LockedQueue<std::unique_ptr<BaselineBatchCompilerJob>>* outcoming_queue)
        : isolate_(isolate),
          incoming_queue_(incoming_queue),
          outgoing_queue_(outcoming_queue) {}

    void Run(JobDelegate* delegate) override {
      LocalIsolate local_isolate(isolate_, ThreadKind::kBackground);
      UnparkedScope unparked_scope(&local_isolate);
      LocalHandleScope handle_scope(&local_isolate);

      while (!incoming_queue_->IsEmpty() && !delegate->ShouldYield()) {
        std::unique_ptr<BaselineBatchCompilerJob> job;
        if (!incoming_queue_->Dequeue(&job)) break;
        DCHECK_NOT_NULL(job);
        job->Compile(&local_isolate);
        outgoing_queue_->Enqueue(std::move(job));
      }
      isolate_->stack_guard()->RequestInstallBaselineCode();
    }

    size_t GetMaxConcurrency(size_t worker_count) const override {
      size_t max_threads = v8_flags.concurrent_sparkplug_max_threads;
      size_t num_tasks = incoming_queue_->size() + worker_count;
      if (max_threads > 0) {
        return std::min(max_threads, num_tasks);
      }
      return num_tasks;
    }

   private:
    Isolate* isolate_;
    LockedQueue<std::unique_ptr<BaselineBatchCompilerJob>>* incoming_queue_;
    LockedQueue<std::unique_ptr<BaselineBatchCompilerJob>>* outgoing_queue_;
  };

  explicit ConcurrentBaselineCompiler(Isolate* isolate) : isolate_(isolate) {
    if (v8_flags.concurrent_sparkplug) {
      TaskPriority priority =
          v8_flags.concurrent_sparkplug_high_priority_threads
              ? TaskPriority::kUserBlocking
              : TaskPriority::kUserVisible;
      job_handle_ = V8::GetCurrentPlatform()->PostJob(
          priority, std::make_unique<JobDispatcher>(isolate_, &incoming_queue_,
                                                    &outgoing_queue_));
    }
  }

  ~ConcurrentBaselineCompiler() {
    if (job_handle_ && job_handle_->IsValid()) {
      // Wait for the job handle to complete, so that we know the queue
      // pointers are safe.
      job_handle_->Cancel();
    }
  }

  void CompileBatch(Handle<WeakFixedArray> task_queue, int batch_size) {
    DCHECK(v8_flags.concurrent_sparkplug);
    RCS_SCOPE(isolate_, RuntimeCallCounterId::kCompileBaseline);
    incoming_queue_.Enqueue(std::make_unique<BaselineBatchCompilerJob>(
        isolate_, task_queue, batch_size));
    job_handle_->NotifyConcurrencyIncrease();
  }

  void InstallBatch() {
    while (!outgoing_queue_.IsEmpty()) {
      std::unique_ptr<BaselineBatchCompilerJob> job;
      outgoing_queue_.Dequeue(&job);
      job->Install(isolate_);
    }
  }

 private:
  Isolate* isolate_;
  std::unique_ptr<JobHandle> job_handle_ = nullptr;
  LockedQueue<std::unique_ptr<BaselineBatchCompilerJob>> incoming_queue_;
  LockedQueue<std::unique_ptr<BaselineBatchCompilerJob>> outgoing_queue_;
};

BaselineBatchCompiler::BaselineBatchCompiler(Isolate* isolate)
    : isolate_(isolate),
      compilation_queue_(Handle<WeakFixedArray>::null()),
      last_index_(0),
      estimated_instruction_size_(0),
      enabled_(true) {
  if (v8_flags.concurrent_sparkplug) {
    concurrent_compiler_ =
        std::make_unique<ConcurrentBaselineCompiler>(isolate_);
  }
}

BaselineBatchCompiler::~BaselineBatchCompiler() {
  if (!compilation_queue_.is_null()) {
    GlobalHandles::Destroy(compilation_queue_.location());
    compilation_queue_ = Handle<WeakFixedArray>::null();
  }
}

bool BaselineBatchCompiler::concurrent() const {
  return v8_flags.concurrent_sparkplug &&
         !isolate_->UseEfficiencyModeForTiering();
}

void BaselineBatchCompiler::EnqueueFunction(Handle<JSFunction> function) {
  Handle<SharedFunctionInfo> shared(function->shared(), isolate_);
  // Immediately compile the function if batch compilation is disabled.
  if (!is_enabled()) {
    IsCompiledScope is_compiled_scope(
        function->shared()->is_compiled_scope(isolate_));
    Compiler::CompileBaseline(isolate_, function, Compiler::CLEAR_EXCEPTION,
                              &is_compiled_scope);
    return;
  }
  if (ShouldCompileBatch(*shared)) {
    if (concurrent()) {
      CompileBatchConcurrent(*shared);
    } else {
      CompileBatch(function);
    }
  } else {
    Enqueue(shared);
  }
}

void BaselineBatchCompiler::EnqueueSFI(Tagged<SharedFunctionInfo> shared) {
  if (!v8_flags.concurrent_sparkplug || !is_enabled()) return;
  if (ShouldCompileBatch(shared)) {
    CompileBatchConcurrent(shared);
  } else {
    Enqueue(Handle<SharedFunctionInfo>(shared, isolate_));
  }
}

void BaselineBatchCompiler::Enqueue(Handle<SharedFunctionInfo> shared) {
  EnsureQueueCapacity();
  compilation_queue_->set(last_index_++, MakeWeak(*shared));
}

void BaselineBatchCompiler::InstallBatch() {
  DCHECK(v8_flags.concurrent_sparkplug);
  concurrent_compiler_->InstallBatch();
}

void BaselineBatchCompiler::EnsureQueueCapacity() {
  if (compilation_queue_.is_null()) {
    compilation_queue_ = isolate_->global_handles()->Create(
        *isolate_->factory()->NewWeakFixedArray(kInitialQueueSize,
                                                AllocationType::kOld));
    return;
  }
  if (last_index_ >= compilation_queue_->length()) {
    Handle<WeakFixedArray> new_queue =
        isolate_->factory()->CopyWeakFixedArrayAndGrow(compilation_queue_,
                                                       last_index_);
    GlobalHandles::Destroy(compilation_queue_.location());
    compilation_queue_ = isolate_->global_handles()->Create(*new_queue);
  }
}

void BaselineBatchCompiler::CompileBatch(Handle<JSFunction> function) {
  {
    IsCompiledScope is_compiled_scope(
        function->shared()->is_compiled_scope(isolate_));
    Compiler::CompileBaseline(isolate_, function, Compiler::CLEAR_EXCEPTION,
                              &is_compiled_scope);
  }
  for (int i = 0; i < last_index_; i++) {
    Tagged<MaybeObject> maybe_sfi = compilation_queue_->get(i);
    MaybeCompileFunction(maybe_sfi);
    compilation_queue_->set(i, ClearedValue(isolate_));
  }
  ClearBatch();
}

void BaselineBatchCompiler::CompileBatchConcurrent(
    Tagged<SharedFunctionInfo> shared) {
  Enqueue(Handle<SharedFunctionInfo>(shared, isolate_));
  concurrent_compiler_->CompileBatch(compilation_queue_, last_index_);
  ClearBatch();
}

bool BaselineBatchCompiler::ShouldCompileBatch(
    Tagged<SharedFunctionInfo> shared) {
  // Early return if the function is compiled with baseline already or it is not
  // suitable for baseline compilation.
  if (shared->HasBaselineCode()) return false;
  // If we're already compiling this function, return.
  if (shared->is_sparkplug_compiling()) return false;
  if (!CanCompileWithBaseline(isolate_, shared)) return false;

  int estimated_size;
  {
    DisallowHeapAllocation no_gc;
    estimated_size = BaselineCompiler::EstimateInstructionSize(
        shared->GetBytecodeArray(isolate_));
  }
  estimated_instruction_size_ += estimated_size;
  if (v8_flags.trace_baseline_batch_compilation) {
    CodeTracer::Scope trace_scope(isolate_->GetCodeTracer());
    PrintF(trace_scope.file(), "[Baseline batch compilation] Enqueued SFI %s",
           shared->DebugNameCStr().get());
    PrintF(trace_scope.file(),
           " with estimated size %d (current budget: %d/%d)\n", estimated_size,
           estimated_instruction_size_,
           v8_flags.baseline_batch_compilation_threshold.value());
  }
  if (estimated_instruction_size_ >=
      v8_flags.baseline_batch_compilation_threshold) {
    if (v8_flags.trace_baseline_batch_compilation) {
      CodeTracer::Scope trace_scope(isolate_->GetCodeTracer());
      PrintF(trace_scope.file(),
             "[Baseline batch compilation] Compiling current batch of %d "
             "functions\n",
             (last_index_ + 1));
    }
    return true;
  }
  return false;
}

bool BaselineBatchCompiler::MaybeCompileFunction(
    Tagged<MaybeObject> maybe_sfi) {
  Tagged<HeapObject> heapobj;
  // Skip functions where the weak reference is no longer valid.
  if (!maybe_sfi.GetHeapObjectIfWeak(&heapobj)) return false;
  Handle<SharedFunctionInfo> shared =
      handle(SharedFunctionInfo::cast(heapobj), isolate_);
  // Skip functions where the bytecode has been flushed.
  if (!shared->is_compiled()) return false;

  IsCompiledScope is_compiled_scope(shared->is_compiled_scope(isolate_));
  return Compiler::CompileSharedWithBaseline(
      isolate_, shared, Compiler::CLEAR_EXCEPTION, &is_compiled_scope);
}

void BaselineBatchCompiler::ClearBatch() {
  estimated_instruction_size_ = 0;
  last_index_ = 0;
}

}  // namespace baseline
}  // namespace internal
}  // namespace v8
