text: // Copyright 2015 The Chromium 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 "third_party/blink/renderer/core/dom/scripted_idle_task_controller.h"
  
  #include "base/location.h"
  #include "base/metrics/histogram_macros.h"
  #include "third_party/blink/public/platform/platform.h"
  #include "third_party/blink/renderer/bindings/core/v8/v8_idle_request_options.h"
  #include "third_party/blink/renderer/core/execution_context/execution_context.h"
  #include "third_party/blink/renderer/core/inspector/inspector_trace_events.h"
  #include "third_party/blink/renderer/core/probe/core_probes.h"
  #include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h"
  #include "third_party/blink/renderer/platform/scheduler/public/thread_scheduler.h"
  #include "third_party/blink/renderer/platform/wtf/functional.h"
  #include "third_party/blink/renderer/platform/wtf/ref_counted.h"
  
  namespace blink {
  
  namespace internal {
  
  class IdleRequestCallbackWrapper
      : public RefCounted<IdleRequestCallbackWrapper> {
   public:
    static scoped_refptr<IdleRequestCallbackWrapper> Create(
        ScriptedIdleTaskController::CallbackId id,
        ScriptedIdleTaskController* controller) {
      return base::AdoptRef(new IdleRequestCallbackWrapper(id, controller));
    }
    virtual ~IdleRequestCallbackWrapper() = default;
  
    static void IdleTaskFired(
        scoped_refptr<IdleRequestCallbackWrapper> callback_wrapper,
        base::TimeTicks deadline) {
      if (ScriptedIdleTaskController* controller =
              callback_wrapper->Controller()) {
        // If we are going to yield immediately, reschedule the callback for
        // later.
        if (ThreadScheduler::Current()->ShouldYieldForHighPriorityWork()) {
          controller->ScheduleCallback(std::move(callback_wrapper),
                                       /* timeout_millis */ 0);
          return;
        }
        controller->CallbackFired(callback_wrapper->Id(), deadline,
                                  IdleDeadline::CallbackType::kCalledWhenIdle);
      }
      callback_wrapper->Cancel();
    }
  
    static void TimeoutFired(
        scoped_refptr<IdleRequestCallbackWrapper> callback_wrapper) {
      if (ScriptedIdleTaskController* controller =
              callback_wrapper->Controller()) {
        controller->CallbackFired(callback_wrapper->Id(), base::TimeTicks::Now(),
                                  IdleDeadline::CallbackType::kCalledByTimeout);
      }
      callback_wrapper->Cancel();
    }
  
    void Cancel() { controller_ = nullptr; }
  
    ScriptedIdleTaskController::CallbackId Id() const { return id_; }
    ScriptedIdleTaskController* Controller() const { return controller_; }
  
   private:
    IdleRequestCallbackWrapper(ScriptedIdleTaskController::CallbackId id,
                               ScriptedIdleTaskController* controller)
        : id_(id), controller_(controller) {}
  
    ScriptedIdleTaskController::CallbackId id_;
    WeakPersistent<ScriptedIdleTaskController> controller_;
  };
  
  }  // namespace internal
  
  V8IdleTask::V8IdleTask(V8IdleRequestCallback* callback) : callback_(callback) {}
  
  void V8IdleTask::Trace(Visitor* visitor) const {
    visitor->Trace(callback_);
    IdleTask::Trace(visitor);
  }
  
  void V8IdleTask::invoke(IdleDeadline* deadline) {
    callback_->InvokeAndReportException(nullptr, deadline);
  }
  
  ScriptedIdleTaskController::ScriptedIdleTaskController(
      ExecutionContext* context)
      : ExecutionContextLifecycleStateObserver(context),
        scheduler_(ThreadScheduler::Current()),
        next_callback_id_(0),
        paused_(false) {}
  
  ScriptedIdleTaskController::~ScriptedIdleTaskController() = default;
  
  void ScriptedIdleTaskController::Trace(Visitor* visitor) const {
    visitor->Trace(idle_tasks_);
    ExecutionContextLifecycleStateObserver::Trace(visitor);
  }
  
  int ScriptedIdleTaskController::NextCallbackId() {
    while (true) {
      ++next_callback_id_;
  
      if (!IsValidCallbackId(next_callback_id_))
        next_callback_id_ = 1;
  
      if (!idle_tasks_.Contains(next_callback_id_))
        return next_callback_id_;
    }
  }
  
  ScriptedIdleTaskController::CallbackId
  ScriptedIdleTaskController::RegisterCallback(
      IdleTask* idle_task,
      const IdleRequestOptions* options) {
    DCHECK(idle_task);
  
    CallbackId id = NextCallbackId();
    idle_tasks_.Set(id, idle_task);
    uint32_t timeout_millis = options->timeout();
  
    probe::AsyncTaskScheduled(GetExecutionContext(), "requestIdleCallback",
                              idle_task->async_task_id());
  
    scoped_refptr<internal::IdleRequestCallbackWrapper> callback_wrapper =
        internal::IdleRequestCallbackWrapper::Create(id, this);
    ScheduleCallback(std::move(callback_wrapper), timeout_millis);
    TRACE_EVENT_INSTANT1("devtools.timeline", "RequestIdleCallback",
                         TRACE_EVENT_SCOPE_THREAD, "data",
                         inspector_idle_callback_request_event::Data(
                             GetExecutionContext(), id, timeout_millis));
    return id;
  }
  
  void ScriptedIdleTaskController::ScheduleCallback(
      scoped_refptr<internal::IdleRequestCallbackWrapper> callback_wrapper,
      uint32_t timeout_millis) {
    scheduler_->PostIdleTask(
        FROM_HERE, WTF::Bind(&internal::IdleRequestCallbackWrapper::IdleTaskFired,
                             callback_wrapper));
    if (timeout_millis > 0) {
      GetExecutionContext()
          ->GetTaskRunner(TaskType::kIdleTask)
          ->PostDelayedTask(
              FROM_HERE,
              WTF::Bind(&internal::IdleRequestCallbackWrapper::TimeoutFired,
                        callback_wrapper),
              base::TimeDelta::FromMilliseconds(timeout_millis));
    }
  }
  
  void ScriptedIdleTaskController::CancelCallback(CallbackId id) {
    TRACE_EVENT_INSTANT1(
        "devtools.timeline", "CancelIdleCallback", TRACE_EVENT_SCOPE_THREAD,
        "data",
        inspector_idle_callback_cancel_event::Data(GetExecutionContext(), id));
    if (!IsValidCallbackId(id))
      return;
  
    idle_tasks_.erase(id);
  }
  
  void ScriptedIdleTaskController::CallbackFired(
      CallbackId id,
      base::TimeTicks deadline,
      IdleDeadline::CallbackType callback_type) {
    if (!idle_tasks_.Contains(id))
      return;
  
    if (paused_) {
      if (callback_type == IdleDeadline::CallbackType::kCalledByTimeout) {
        // Queue for execution when we are resumed.
        pending_timeouts_.push_back(id);
      }
      // Just drop callbacks called while suspended, these will be reposted on the
      // idle task queue when we are resumed.
      return;
    }
  
    RunCallback(id, deadline, callback_type);
  }
  
  void ScriptedIdleTaskController::RunCallback(
      CallbackId id,
      base::TimeTicks deadline,
      IdleDeadline::CallbackType callback_type) {
    DCHECK(!paused_);
  
    // Keep the idle task in |idle_tasks_| so that it's still wrapper-traced.
    // TODO(https://crbug.com/796145): Remove this hack once on-stack objects
    // get supported by either of wrapper-tracing or unified GC.
    auto idle_task_iter = idle_tasks_.find(id);
    if (idle_task_iter == idle_tasks_.end())
      return;
    IdleTask* idle_task = idle_task_iter->value;
    DCHECK(idle_task);
  
    base::TimeDelta allotted_time =
        std::max(deadline - base::TimeTicks::Now(), base::TimeDelta());
  
    probe::AsyncTask async_task(GetExecutionContext(),
                                idle_task->async_task_id());
    probe::UserCallback probe(GetExecutionContext(), "requestIdleCallback",
                              AtomicString(), true);
  
    TRACE_EVENT1(
        "devtools.timeline", "FireIdleCallback", "data",
        inspector_idle_callback_fire_event::Data(
            GetExecutionContext(), id, allotted_time.InMillisecondsF(),
            callback_type == IdleDeadline::CallbackType::kCalledByTimeout));
    idle_task->invoke(
        MakeGarbageCollected<IdleDeadline>(deadline, callback_type));
  
    // Finally there is no need to keep the idle task alive.
    //
    // Do not use the iterator because the idle task might update |idle_tasks_|.
    idle_tasks_.erase(id);
  }
  
  void ScriptedIdleTaskController::ContextDestroyed() {
    idle_tasks_.clear();
  }
  
  void ScriptedIdleTaskController::ContextLifecycleStateChanged(
      mojom::FrameLifecycleState state) {
    if (state != mojom::FrameLifecycleState::kRunning)
      ContextPaused();
    else
      ContextUnpaused();
  }
  
  void ScriptedIdleTaskController::ContextPaused() {
    paused_ = true;
  }
  
  void ScriptedIdleTaskController::ContextUnpaused() {
    DCHECK(paused_);
    paused_ = false;
  
    // Run any pending timeouts as separate tasks, since it's not allowed to
    // execute script from lifecycle callbacks.
    for (auto& id : pending_timeouts_) {
      scoped_refptr<internal::IdleRequestCallbackWrapper> callback_wrapper =
          internal::IdleRequestCallbackWrapper::Create(id, this);
      GetExecutionContext()
          ->GetTaskRunner(TaskType::kIdleTask)
          ->PostTask(
              FROM_HERE,
              WTF::Bind(&internal::IdleRequestCallbackWrapper::TimeoutFired,
                        callback_wrapper));
    }
    pending_timeouts_.clear();
  
    // Repost idle tasks for any remaining callbacks.
    for (auto& idle_task : idle_tasks_) {
      scoped_refptr<internal::IdleRequestCallbackWrapper> callback_wrapper =
          internal::IdleRequestCallbackWrapper::Create(idle_task.key, this);
      scheduler_->PostIdleTask(
          FROM_HERE,
          WTF::Bind(&internal::IdleRequestCallbackWrapper::IdleTaskFired,
                    callback_wrapper));
    }
  }
  
  }  // namespace blink
  