// Copyright 2022 The Chromium Authors // 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_TIMING_WHEEL_H_ #define BASE_TASK_SEQUENCE_MANAGER_TIMING_WHEEL_H_ #include #include #include #include #include #include "base/notreached.h" #include "base/time/time.h" namespace base::sequence_manager::internal { // Intended as a wrapper around a |bucket_index_| and |element_element_index_| // in the vector storage backing a TimingWheel. A TimingWheelHandle is // associated with each element in a TimingWheel, and is maintained by // the timing wheel as the object moves around within it. It can be used to // subsequently remove the element, or update it in place. class BASE_EXPORT TimingWheelHandle { public: enum : size_t { kInvalidIndex = std::numeric_limits::max() }; constexpr TimingWheelHandle() = default; constexpr TimingWheelHandle(const TimingWheelHandle& other) = default; TimingWheelHandle(TimingWheelHandle&& other) noexcept; TimingWheelHandle& operator=(const TimingWheelHandle& other) = default; TimingWheelHandle& operator=(TimingWheelHandle&& other) noexcept; ~TimingWheelHandle() = default; // Returns an invalid TimingWheelHandle. static TimingWheelHandle Invalid(); // Resets this handle back to an invalid state. void Reset(); bool IsValid() const; // Accessors. size_t bucket_index() const; size_t element_index() const; private: template friend class TimingWheel; // Only TimingWheels can create valid TimingWheelHandles. explicit TimingWheelHandle(size_t bucket_index, size_t element_index); // The index of the bucket in the timing wheel where the element is in. size_t bucket_index_ = kInvalidIndex; // The index of the element in the bucket where the element is in. size_t element_index_ = kInvalidIndex; }; // This class implements a container that acts as timer queue where element are // associated with a delay. It provides efficient retrieval of earliest // elements. It also provides constant time element removal. To facilitate this, // each element has associated with it a TimingWheelHandle (an opaque wrapper // around the index at which the element is stored), which is maintained by the // wheel as elements move within it. Only elements whose delay is between // |time_delta_per_bucket_| and WheelSize*|time_delta_per_bucket_| can be // inserted in a TimingWheel. |T| is a typename for element. |WheelSize| is the // number of buckets this TimingWheel has. |TimingWheelHandleAccessor| is the // type of the object which under the hood manages the TimingWheelHandle. // |GetDelayedRunTime| is a functor which returns the time when the element is // due at. template class TimingWheel { public: // Constructs a TimingWheel instance where each bucket corresponds to a // TimeDelta of |time_delta_per_bucket_|. explicit TimingWheel( TimeDelta time_delta_per_bucket, const GetDelayedRunTime& get_delayed_run_time = GetDelayedRunTime()) : time_delta_per_bucket_(time_delta_per_bucket), last_updated_bucket_index_(0), time_passed_(Microseconds(0)), get_delayed_run_time_(get_delayed_run_time) {} TimingWheel(TimingWheel&&) = delete; TimingWheel& operator=(TimingWheel&&) = delete; TimingWheel(const TimingWheel&) = delete; TimingWheel& operator=(const TimingWheel&) = delete; ~TimingWheel() = default; // Inserts the |element| into the bucket based on its delay. This is the delay // relative to a baseline implied by the last call to // |AdvanceTimeAndRemoveExpiredElements| method. typename std::vector::const_iterator Insert(T element, const TimeDelta delay) { DCHECK_GE(delay, time_delta_per_bucket_); DCHECK_LT(delay, time_delta_per_bucket_ * WheelSize); const size_t bucket_index = CalculateBucketIndex(delay); auto& bucket = buckets_[bucket_index]; bucket.push_back(std::move(element)); const size_t element_index = bucket.size() - 1; // Sets the handle for the element. timing_wheel_handle_accessor.SetTimingWheelHandle( &buckets_[bucket_index][element_index], TimingWheelHandle(bucket_index, element_index)); total_elements_ += 1; return bucket.cend() - 1; } // Removes the element which holds this |handle|. void Remove(TimingWheelHandle handle) { DCHECK(handle.IsValid()); const size_t bucket_index = handle.bucket_index(); const size_t element_index = handle.element_index(); DCHECK(IsBounded(bucket_index, element_index)); auto& bucket = buckets_[bucket_index]; const size_t last_index_of_bucket = bucket.size() - 1; // Swaps the element's position with the last element for removal. The // swapped element is assigned with an updated handle. if (element_index != last_index_of_bucket) { timing_wheel_handle_accessor.SetTimingWheelHandle( &bucket[last_index_of_bucket], TimingWheelHandle(bucket_index, element_index)); std::swap(bucket[element_index], bucket[last_index_of_bucket]); } // The handle of the last element doesn't need to be cleared since the // element is destroyed right after. bucket.pop_back(); total_elements_ -= 1; } // Updates the internal state to reflect the latest wakeup and returns the // expired elements through an out-parameter so that the caller can keep using // the same vector when advancing multiple TimingWheels. void AdvanceTimeAndRemoveExpiredElements(const TimeDelta time_delta, std::vector& expired_elements) { const size_t nb_buckets_passed = (time_passed_ + time_delta) / time_delta_per_bucket_ + 1; const size_t new_bucket_index = (last_updated_bucket_index_ + nb_buckets_passed) % WheelSize; const TimeDelta new_time_passed = (time_passed_ + time_delta) % time_delta_per_bucket_; // Ensures each bucket is iterated over at most once if |nb_buckets_passed| // is bigger than the total number of buckets. const size_t nb_buckets_to_traverse = std::min(nb_buckets_passed, WheelSize); for (size_t i = 0; i < nb_buckets_to_traverse; i++) { last_updated_bucket_index_ = (last_updated_bucket_index_ + 1) % WheelSize; ExtractElementsFromBucket(last_updated_bucket_index_, expired_elements); } last_updated_bucket_index_ = new_bucket_index; time_passed_ = new_time_passed; } // Returns the earliest due element. typename std::vector::const_reference Top() { DCHECK(total_elements_ != 0); for (size_t i = 0; i < WheelSize; i++) { const size_t bucket_index = (i + last_updated_bucket_index_) % WheelSize; auto& bucket = buckets_[bucket_index]; if (bucket.size() == 0) { continue; } auto it = std::min_element( bucket.begin(), bucket.end(), [this](const T& a, const T& b) { return get_delayed_run_time_(a) > get_delayed_run_time_(b); }); return *it; } NOTREACHED(); return buckets_[0].back(); } TimeDelta time_delta_per_bucket() { return time_delta_per_bucket_; } size_t total_elements() { return total_elements_; } private: // Checks if the |bucket_index| and |element_index| is bounded. bool IsBounded(const size_t bucket_index, const size_t element_index) const { return bucket_index < WheelSize && element_index < buckets_[bucket_index].size(); } // Calculates the index at which a task with |delay| should be inserted in. size_t CalculateBucketIndex(const TimeDelta delay) const { const size_t nb_buckets_passed = (delay + time_passed_) / time_delta_per_bucket_; const size_t bucket_index = last_updated_bucket_index_ + nb_buckets_passed; // |bucket_index| should not be more than |WheelSize|. return bucket_index % WheelSize; } // Removes the elements from the index in |buckets_| and returns the // expired elements through an out-parameter. void ExtractElementsFromBucket(const size_t bucket_index, std::vector& expired_elements) { std::vector& bucket = buckets_[bucket_index]; expired_elements.reserve(expired_elements.size() + bucket.size()); for (auto& element : bucket) { timing_wheel_handle_accessor.ClearTimingWheelHandle(&element); expired_elements.push_back(std::move(element)); } total_elements_ -= bucket.size(); bucket.clear(); } TimingWheelHandleAccessor timing_wheel_handle_accessor; // The time period each bucket contains. const TimeDelta time_delta_per_bucket_; // The buckets where the elements are added according to their delay. std::array, WheelSize> buckets_; // The index of the bucket that was last updated. This helps in Inserting and // expired elements. size_t last_updated_bucket_index_; // The time passed unaccounted for after updating // |last_updated_bucket_index_|. This will be aggregated with the // |time_passed_| at the next wakeup. TimeDelta time_passed_; // The number of elements in |buckets_|. size_t total_elements_ = 0; // The functor to get the delayed run time of elements. GetDelayedRunTime get_delayed_run_time_; }; } // namespace base::sequence_manager::internal #endif