299 lines
11 KiB
C++
299 lines
11 KiB
C++
// 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 <array>
|
|
#include <vector>
|
|
|
|
#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<TimingWheelHandle>()) {}
|
|
|
|
// 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<TimingWheelHandle> handle_;
|
|
};
|
|
|
|
// The default TimingWheelHandleAccessor, which simply forwards calls to the
|
|
// underlying type.
|
|
template <typename T>
|
|
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 <typename T>
|
|
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<Element, kWheelSize, TimingWheelHandleAccessor<Element>,
|
|
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<Element, kWheelSize, TimingWheelHandleAccessor<Element>,
|
|
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<Element, kWheelSize, TimingWheelHandleAccessor<Element>,
|
|
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<Element> 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<Element, kWheelSize, TimingWheelHandleAccessor<Element>,
|
|
GetDelayedRunTime>
|
|
timing_wheel(kBucketTimeDelta);
|
|
std::vector<Element> 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<Element, kWheelSize, TimingWheelHandleAccessor<Element>,
|
|
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<Element> 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<Element, kWheelSize, TimingWheelHandleAccessor<Element>,
|
|
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<Element> 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<Element, kWheelSize, TimingWheelHandleAccessor<Element>,
|
|
GetDelayedRunTime>
|
|
timing_wheel(kBucketTimeDelta);
|
|
const TimeTicks baseline = TimeTicks::Now();
|
|
const TimeDelta kTimePassed = 2 * kBucketTimeDelta;
|
|
const TimeDelta kTestDelay = kLastBucketIndex * kBucketTimeDelta;
|
|
std::vector<Element> 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<Element, kWheelSize, TimingWheelHandleAccessor<Element>,
|
|
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<Element> 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<Element, kWheelSize, TimingWheelHandleAccessor<Element>,
|
|
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<Element, kWheelSize, TimingWheelHandleAccessor<Element>,
|
|
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
|