// Copyright 2019 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.

#ifndef BASE_TASK_SEQUENCE_MANAGER_WORK_DEDUPLICATOR_H_
#define BASE_TASK_SEQUENCE_MANAGER_WORK_DEDUPLICATOR_H_

#include <atomic>

#include "base/base_export.h"
#include "base/task/sequence_manager/associated_thread_id.h"

namespace base {
namespace sequence_manager {
namespace internal {

// This class's job is to prevent redundant DoWorks being posted, which are
// expensive. The idea is a DoWork will (maybe) run a task before computing the
// delay till the next task. If the task run posts another task, we don't want
// it to schedule work because the DoWork will post a continuation as needed
// with the latest state taken into consideration (fences, enable / disable
// queue, task cancellation, etc...) Other threads can also post DoWork at any
// time, including while we're computing the delay till the next task. To
// account for that, we have split a DoWork up into two sections:
// [OnWorkStarted .. WillCheckForMoreWork] and
// [WillCheckForMoreWork .. DidCheckForMoreWork] where DidCheckForMoreWork
// detects if another thread called OnWorkRequested.
//
// Nesting is assumed to be dealt with by the ThreadController.
//
// Most methods are thread-affine except for On(Delayed)WorkRequested which are
// is thread-safe.
class BASE_EXPORT WorkDeduplicator {
 public:
  // Creates an unbound WorkDeduplicator. BindToCurrentThread must be called
  // before work can be scheduled.
  explicit WorkDeduplicator(
      scoped_refptr<AssociatedThreadId> associated_thread);

  ~WorkDeduplicator();

  enum ShouldScheduleWork {
    kScheduleImmediate,
    kNotNeeded,
  };

  // Returns ShouldScheduleWork::kSchedule if OnWorkRequested was called while
  // unbound. Must be called on the associated thread.
  ShouldScheduleWork BindToCurrentThread();

  // Returns true if it's OK to schedule a DoWork without risk of task
  // duplication. Returns false if:
  // * We are unbound
  // * We are in a DoWork
  // * There is a pending DoWork
  //
  // Otherwise sets the pending DoWork flag and returns true.
  // Can be called on any thread.
  //
  //    DoWork
  //    ---------------------------------------------------------------------
  //    | <- OnWorkStarted                       |                          |
  //    |                WillCheckForMoreWork -> |                          |
  //    |                                        |   DidCheckForMoreWork -> |
  //    ---------------------------------------------------------------------
  // ^                            ^                           ^               ^
  // |                            |                           |               |
  // A                            B                           C               D
  //
  // Consider a DoWork and calls to OnWorkRequested at various times:
  // A: return ShouldScheduleWork::kNotNeeded because there's a pending DoWork.
  // B: return ShouldScheduleWork::kNotNeeded because we're in a DoWork.
  // C: return ShouldScheduleWork::kNotNeeded because we're in a DoWork, however
  //    DidCheckForMoreWork should subsequently return
  //    ShouldScheduleWork::kSchedule.
  // D: If DidCheckForMoreWork(kIsImmediate::kIsImmediate) was called then it
  //    should ShouldScheduleWork::kNotNeeded because there's a pending DoWork.
  //    Otherwise it should return ShouldScheduleWork::kSchedule, but a
  //    subsequent call to OnWorkRequested should return
  //    ShouldScheduleWork::kNotNeeded because there's now a pending DoWork.
  ShouldScheduleWork OnWorkRequested();

  // Returns ShouldScheduleWork::kSchedule if it's OK to schedule a
  // DoDelayedWork without risk of redundancy. Deduplication of delayed work is
  // assumed to have been done by the caller, the purpose of this method it to
  // check if there's a pending Do(Some)Work which would schedule a delayed
  // continuation as needed.
  //
  // Returns ShouldScheduleWork::kNotNeeded if:
  // * We are unbound
  // * We are in a DoWork
  // * There is a pending DoWork
  //
  // Must be called on the associated thread.
  ShouldScheduleWork OnDelayedWorkRequested() const;

  // Marks us as having entered a DoWork, clearing the pending DoWork flag.
  // Must be called on the associated thread.
  void OnWorkStarted();

  // Marks us as being about to check if we have more work. This notification
  // helps prevent DoWork duplication in two scenarios:
  // * A cross-thread immediate task is posted while we are running a task. If
  //   the TaskQueue is disabled we can avoid a potentially spurious DoWork.
  // * A task is run which posts an immediate task but the ThreadControllerImpl
  //   work batch size is 2, and there's no further work. The immediate task ran
  //   in the work batch so we don't need another DoWork.
  void WillCheckForMoreWork();

  enum NextTask {
    kIsImmediate,
    kIsDelayed,
  };

  // Marks us as exiting DoWork. Returns ShouldScheduleWork::kSchedule if an
  // immediate DoWork continuation should be posted. This method takes into
  // account any OnWorkRequested's called between BeforeComputeDelayTillNextTask
  // and here. Must be called on the associated thread.
  ShouldScheduleWork DidCheckForMoreWork(NextTask next_task);

  // For ThreadControllerWithMessagePumpImpl. The MessagePump calls DoWork and
  // DoDelayed work sequentially. If DoWork returns
  // ShouldScheduleWork::kSchedule, the pump will call ScheduleWork. We remember
  // if DoWork will be scheduled so we don't accidentally call it twice from
  // DoDelayedWork. Must be called on the associated thread.
  // TODO(alexclarke): Remove these when the DoWork/DoDelayed work merger
  // happens.
  void OnDelayedWorkStarted();
  ShouldScheduleWork OnDelayedWorkEnded(NextTask next_task);

 private:
  enum Flags {
    kInDoWorkFlag = 1 << 0,
    kPendingDoWorkFlag = 1 << 1,
    kBoundFlag = 1 << 2,
  };

  enum State {
    kUnbound = 0,
    kIdle = Flags::kBoundFlag,
    kDoWorkPending = Flags::kPendingDoWorkFlag | Flags::kBoundFlag,
    kInDoWork = Flags::kInDoWorkFlag | Flags::kBoundFlag,
  };

  std::atomic<int> state_{State::kUnbound};

  scoped_refptr<AssociatedThreadId> associated_thread_;

  // TODO(alexclarke): Remove when the DoWork/DoDelayed work merger happens.
  ShouldScheduleWork last_work_check_result_ = ShouldScheduleWork::kNotNeeded;
};

}  // namespace internal
}  // namespace sequence_manager
}  // namespace base

#endif  // BASE_TASK_SEQUENCE_MANAGER_WORK_DEDUPLICATOR_H_
