// 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. #include #include #include "base/task/sequence_manager/timing_wheel.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" namespace base::sequence_manager::internal { namespace { class Element { public: Element(TimeTicks delayed_run_time) : delayed_run_time(delayed_run_time), handle_(std::make_unique()) {} // Implementation of TimingWheel contract. void SetTimingWheelHandle(TimingWheelHandle handle) { DCHECK(handle.IsValid()); if (handle_) *handle_ = handle; } void ClearTimingWheelHandle() { if (handle_) handle_->Reset(); } TimingWheelHandle* handle() const { return handle_.get(); } TimeTicks delayed_run_time; private: std::unique_ptr handle_; }; // The default TimingWheelHandleAccessor, which simply forwards calls to the // underlying type. template struct TimingWheelHandleAccessor { void SetTimingWheelHandle(T* element, TimingWheelHandle handle) const { element->SetTimingWheelHandle(handle); } void ClearTimingWheelHandle(T* element) const { element->ClearTimingWheelHandle(); } }; // Gets the delayed run time of the |element|. struct GetDelayedRunTime { template TimeTicks operator()(const T& element) { return element.delayed_run_time; } }; // The total number of buckets that we use for the TimingWheel in all the unit // tests. const size_t kWheelSize = 100; // The time period used for each bucket in the TimingWheel in all the unit // tests. const TimeDelta kBucketTimeDelta = Microseconds(500); // The TimingWheel has an index which points to the last updated bucket. On // initialization, the index points at bucket 0. The immediate // insertable bucket is always the bucket after the index. Hence, index 1 is // considered the first bucket. Suppose, the timing wheel is updated and the // index now points at Index X. This would mean that while the index is // pointing at Index X, no element can be inserted at Index X. const size_t kFirstBucketIndex = 1; // The index of the last bucket in the TimingWheel const size_t kLastBucketIndex = kWheelSize - 1; } // namespace // Tests the construction of the object. TEST(TimingWheelTest, Initialize) { TimingWheel, GetDelayedRunTime> timing_wheel(kBucketTimeDelta); EXPECT_EQ(timing_wheel.time_delta_per_bucket(), kBucketTimeDelta); EXPECT_EQ(timing_wheel.total_elements(), 0u); } // Tests whether an element can be added to the first bucket. TEST(TimingWheelTest, InsertElement) { TimingWheel, GetDelayedRunTime> timing_wheel(kBucketTimeDelta); const TimeTicks baseline = TimeTicks::Now(); // Pick a time delta that ensures the element is added to the first bucket. const TimeDelta kTestDelay = kFirstBucketIndex * kBucketTimeDelta; const auto it = timing_wheel.Insert({baseline + kTestDelay}, kTestDelay); const TimingWheelHandle* handle = it->handle(); EXPECT_EQ(handle->element_index(), 0u); EXPECT_EQ(handle->bucket_index(), kFirstBucketIndex); EXPECT_EQ(timing_wheel.total_elements(), 1u); } // Tests advancing time by a delta equal to the wheel's complete time delta. TEST(TimingWheelTest, AdvancingFullWheel) { TimingWheel, GetDelayedRunTime> timing_wheel(kBucketTimeDelta); const TimeTicks baseline = TimeTicks::Now(); const TimeDelta kTimePassed = kBucketTimeDelta * kLastBucketIndex; // Pick time delta that ensures the elements are added to the last bucket. const TimeDelta kLastBucketMinDelay = kLastBucketIndex * kBucketTimeDelta; timing_wheel.Insert({baseline + kLastBucketMinDelay}, kLastBucketMinDelay); std::vector expired_elements; timing_wheel.AdvanceTimeAndRemoveExpiredElements(kTimePassed, expired_elements); EXPECT_EQ(expired_elements.size(), 1u); EXPECT_EQ(timing_wheel.total_elements(), 0u); } // Tests advancing time by a delta greater than the wheel's complete time delta. TEST(TimingWheelTest, AdvancingMoreThanFullWheel) { TimingWheel, GetDelayedRunTime> timing_wheel(kBucketTimeDelta); std::vector expired_elements; const TimeDelta kTimePassed = kBucketTimeDelta * kWheelSize + kBucketTimeDelta; timing_wheel.AdvanceTimeAndRemoveExpiredElements(kTimePassed, expired_elements); EXPECT_EQ(timing_wheel.total_elements(), 0u); EXPECT_EQ(expired_elements.size(), 0u); } // Tests whether multiple elements can be added to different buckets. TEST(TimingWheelTest, InsertMultipleElementsInDifferentBuckets) { TimingWheel, GetDelayedRunTime> timing_wheel(kBucketTimeDelta); const TimeTicks baseline = TimeTicks::Now(); const size_t kBucketIndex1 = 50; const size_t kBucketIndex2 = 75; const TimeDelta kTestDelay1 = kBucketIndex1 * kBucketTimeDelta; const TimeDelta kTestDelay2 = kBucketIndex2 * kBucketTimeDelta; // Insert five elements that are due before |kTestDelay1|. timing_wheel.Insert({baseline + kTestDelay1}, kTestDelay1); timing_wheel.Insert({baseline + kTestDelay1 - Milliseconds(2)}, kTestDelay1); timing_wheel.Insert({baseline + kTestDelay1 - Milliseconds(5)}, kTestDelay1); timing_wheel.Insert({baseline + kTestDelay1 - Milliseconds(10)}, kTestDelay1); timing_wheel.Insert({baseline + kTestDelay1 - Milliseconds(10)}, kTestDelay1); // Insert two elements that are due after |kTestDelay1|. timing_wheel.Insert({baseline + kTestDelay2 - Milliseconds(4)}, kTestDelay2); timing_wheel.Insert({baseline + kTestDelay2}, kTestDelay2); std::vector expired_elements; // Advances time by |kTestDelay1| so that all the elements except the last // two can be picked up. timing_wheel.AdvanceTimeAndRemoveExpiredElements(kTestDelay1, expired_elements); EXPECT_EQ(expired_elements.size(), 5u); EXPECT_EQ(timing_wheel.total_elements(), 2u); // Need to advance time by at least (kTestDelay2 - kTestDelay1) to pick the // last elements. timing_wheel.AdvanceTimeAndRemoveExpiredElements(kTestDelay2 - kTestDelay1, expired_elements); EXPECT_EQ(expired_elements.size(), 7u); EXPECT_EQ(timing_wheel.total_elements(), 0u); } // Tests whether multiple elements that expire at the same time can be added // to the same buckets. TEST(TimingWheelTest, InsertMultipleSimilarElementsInSameBucket) { TimingWheel, GetDelayedRunTime> timing_wheel(kBucketTimeDelta); const TimeTicks baseline = TimeTicks::Now(); const size_t kBucketIndex = 15; const TimeDelta kTestDelay = kBucketIndex * kBucketTimeDelta; timing_wheel.Insert({baseline + kTestDelay}, kTestDelay); timing_wheel.Insert({baseline + kTestDelay}, kTestDelay); std::vector expired_elements; timing_wheel.AdvanceTimeAndRemoveExpiredElements(kTestDelay, expired_elements); EXPECT_EQ(expired_elements.size(), 2u); EXPECT_EQ(timing_wheel.total_elements(), 0u); } // Tests the circular nature of the timing wheel, and ensure that when the // current index advances, the buckets before it can now be used for elements // whose deadline is greater than kBucketTimeDelta * kWheelSize. TEST(TimingWheelTest, InsertToLastBucketButNotLastIndex) { TimingWheel, GetDelayedRunTime> timing_wheel(kBucketTimeDelta); const TimeTicks baseline = TimeTicks::Now(); const TimeDelta kTimePassed = 2 * kBucketTimeDelta; const TimeDelta kTestDelay = kLastBucketIndex * kBucketTimeDelta; std::vector expired_elements; // First advance time to change the current index of the wheel. timing_wheel.AdvanceTimeAndRemoveExpiredElements(kTimePassed, expired_elements); timing_wheel.Insert({baseline + kTestDelay + kTimePassed}, kTestDelay); // Advance time to pick up the element. timing_wheel.AdvanceTimeAndRemoveExpiredElements(kTestDelay, expired_elements); EXPECT_EQ(expired_elements.size(), 1u); } // Tests removal of an element using its handle. TEST(TimingWheelTest, RemoveElement) { TimingWheel, GetDelayedRunTime> timing_wheel(kBucketTimeDelta); const TimeTicks baseline = TimeTicks::Now(); const TimeDelta kLastBucketMinDelay = kLastBucketIndex * kBucketTimeDelta; const TimeDelta kLastBucketMaxDelay = (kLastBucketIndex + 1) * kBucketTimeDelta - Microseconds(1u); const TimeDelta kTimePassed = kBucketTimeDelta * kLastBucketIndex; timing_wheel.Insert({baseline + kLastBucketMinDelay}, kLastBucketMinDelay); const TimingWheelHandle* handle = timing_wheel .Insert({baseline + kLastBucketMaxDelay}, kLastBucketMaxDelay) ->handle(); timing_wheel.Remove(*handle); EXPECT_EQ(timing_wheel.total_elements(), 1u); std::vector expired_elements; timing_wheel.AdvanceTimeAndRemoveExpiredElements(kTimePassed, expired_elements); EXPECT_EQ(expired_elements.size(), 1u); EXPECT_EQ(timing_wheel.total_elements(), 0u); } // Tests whether we get the earliest due element from two elements in different // buckets. TEST(TimingWheelTest, TopElementfromDifferentElements) { TimingWheel, GetDelayedRunTime> timing_wheel(kBucketTimeDelta); const TimeTicks baseline = TimeTicks::Now(); const TimeDelta kTestDelay1 = kBucketTimeDelta; const TimeDelta kTestDelay2 = kBucketTimeDelta * 2; Element element1(baseline + kTestDelay1); Element element2(baseline + kTestDelay2); const TimingWheelHandle* handle = element1.handle(); timing_wheel.Insert(std::move(element1), kTestDelay1); timing_wheel.Insert(std::move(element2), kTestDelay2); EXPECT_EQ(handle, timing_wheel.Top().handle()); EXPECT_EQ(timing_wheel.total_elements(), 2u); } // Tests whether we get the earliest due element when two elements are in the // same bucket. TEST(TimingWheelTest, TopElementFromSimilarElements) { TimingWheel, GetDelayedRunTime> timing_wheel(kBucketTimeDelta); const TimeTicks baseline = TimeTicks::Now(); const TimeDelta kTestDelay1 = kBucketTimeDelta; const TimeDelta kTestDelay2 = kBucketTimeDelta + Milliseconds(1); Element element1(baseline + kTestDelay1); Element element2(baseline + kTestDelay2); const TimingWheelHandle* handle = element1.handle(); timing_wheel.Insert(std::move(element1), kTestDelay1); timing_wheel.Insert(std::move(element2), kTestDelay2); EXPECT_EQ(handle, timing_wheel.Top().handle()); EXPECT_EQ(timing_wheel.total_elements(), 2u); } } // namespace base::sequence_manager::internal