/* * Copyright (c) 2024 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. An additional intellectual property rights grant can be found * in the file PATENTS. All contributing project authors may * be found in the AUTHORS file in the root of the source tree. */ #include "video/rate_utilization_tracker.h" #include "api/units/data_rate.h" #include "api/units/data_size.h" #include "api/units/time_delta.h" #include "api/units/timestamp.h" #include "test/gmock.h" #include "test/gtest.h" namespace webrtc { namespace { using ::testing::Not; constexpr int kDefaultMaxDataPoints = 10; constexpr TimeDelta kDefaultTimeWindow = TimeDelta::Seconds(1); constexpr Timestamp kStartTime = Timestamp::Millis(9876654); constexpr double kAllowedError = 0.002; // 0.2% error allowed. MATCHER_P(PrettyCloseTo, expected, "") { return arg && std::abs(*arg - expected) < kAllowedError; } TEST(RateUtilizationTrackerTest, NoDataInNoDataOut) { RateUtilizationTracker tracker(kDefaultMaxDataPoints, kDefaultTimeWindow); EXPECT_FALSE(tracker.GetRateUtilizationFactor(kStartTime).has_value()); } TEST(RateUtilizationTrackerTest, NoUtilizationWithoutDataPoints) { RateUtilizationTracker tracker(kDefaultMaxDataPoints, kDefaultTimeWindow); tracker.OnDataRateChanged(DataRate::KilobitsPerSec(100), kStartTime); EXPECT_FALSE(tracker.GetRateUtilizationFactor(kStartTime).has_value()); } TEST(RateUtilizationTrackerTest, NoUtilizationWithoutRateUpdates) { RateUtilizationTracker tracker(kDefaultMaxDataPoints, kDefaultTimeWindow); tracker.OnDataProduced(DataSize::Bytes(100), kStartTime); EXPECT_FALSE(tracker.GetRateUtilizationFactor(kStartTime).has_value()); } TEST(RateUtilizationTrackerTest, SingleDataPoint) { RateUtilizationTracker tracker(kDefaultMaxDataPoints, kDefaultTimeWindow); constexpr TimeDelta kFrameInterval = TimeDelta::Seconds(1) / 33; constexpr DataRate kTargetRate = DataRate::KilobitsPerSec(100); constexpr DataSize kIdealFrameSize = kTargetRate * kFrameInterval; tracker.OnDataRateChanged(kTargetRate, kStartTime); tracker.OnDataProduced(kIdealFrameSize, kStartTime); // From the start, the window is extended to cover the expected duration for // the last frame - resulting in 100% utilization. EXPECT_THAT(tracker.GetRateUtilizationFactor(kStartTime), PrettyCloseTo(1.0)); // At the expected frame interval the utilization is still 100%. EXPECT_THAT(tracker.GetRateUtilizationFactor(kStartTime + kFrameInterval), PrettyCloseTo(1.0)); // After two frame intervals the utilization is half the expected. EXPECT_THAT(tracker.GetRateUtilizationFactor(kStartTime + 2 * kFrameInterval), PrettyCloseTo(0.5)); } TEST(RateUtilizationTrackerTest, TwoDataPoints) { RateUtilizationTracker tracker(kDefaultMaxDataPoints, kDefaultTimeWindow); constexpr TimeDelta kFrameInterval = TimeDelta::Seconds(1) / 33; constexpr DataRate kTargetRate = DataRate::KilobitsPerSec(100); constexpr DataSize kIdealFrameSize = kTargetRate * kFrameInterval; tracker.OnDataRateChanged(kTargetRate, kStartTime); tracker.OnDataProduced(kIdealFrameSize, kStartTime); tracker.OnDataProduced(kIdealFrameSize, kStartTime + kFrameInterval); EXPECT_THAT(tracker.GetRateUtilizationFactor(kStartTime + 2 * kFrameInterval), PrettyCloseTo(1.0)); // After two three frame interval we have two utilizated intervals and one // unitilzed => 2/3 utilization. EXPECT_THAT(tracker.GetRateUtilizationFactor(kStartTime + 3 * kFrameInterval), PrettyCloseTo(2.0 / 3.0)); } TEST(RateUtilizationTrackerTest, TwoDataPointsConsistentOveruse) { RateUtilizationTracker tracker(kDefaultMaxDataPoints, kDefaultTimeWindow); constexpr TimeDelta kFrameInterval = TimeDelta::Seconds(1) / 33; constexpr DataRate kTargetRate = DataRate::KilobitsPerSec(100); constexpr DataSize kIdealFrameSize = kTargetRate * kFrameInterval; tracker.OnDataRateChanged(kTargetRate, kStartTime); tracker.OnDataProduced(kIdealFrameSize * 2, kStartTime); tracker.OnDataProduced(kIdealFrameSize * 2, kStartTime + kFrameInterval); // Note that the last data point is presumed to be sent at the designated rate // and no new data points produced until the buffers empty. Thus the // overshoot is just 4/3 unstead of 4/2. EXPECT_THAT(tracker.GetRateUtilizationFactor(kStartTime + 2 * kFrameInterval), PrettyCloseTo(4.0 / 3.0)); } TEST(RateUtilizationTrackerTest, OveruseWithFrameDrop) { RateUtilizationTracker tracker(kDefaultMaxDataPoints, kDefaultTimeWindow); constexpr TimeDelta kFrameInterval = TimeDelta::Seconds(1) / 33; constexpr DataRate kTargetRate = DataRate::KilobitsPerSec(100); constexpr DataSize kIdealFrameSize = kTargetRate * kFrameInterval; // First frame is 2x larger than it should be. tracker.OnDataRateChanged(kTargetRate, kStartTime); tracker.OnDataProduced(kIdealFrameSize * 2, kStartTime); // Compensate by dropping a frame before the next nominal-size one. tracker.OnDataProduced(kIdealFrameSize, kStartTime + 2 * kFrameInterval); EXPECT_THAT(tracker.GetRateUtilizationFactor(kStartTime + 3 * kFrameInterval), PrettyCloseTo(1.0)); } TEST(RateUtilizationTrackerTest, VaryingRate) { RateUtilizationTracker tracker(kDefaultMaxDataPoints, kDefaultTimeWindow); constexpr TimeDelta kFrameInterval = TimeDelta::Seconds(1) / 33; constexpr DataRate kTargetRate = DataRate::KilobitsPerSec(100); constexpr DataSize kIdealFrameSize = kTargetRate * kFrameInterval; // Rate goes up, rate comes down... tracker.OnDataRateChanged(kTargetRate, kStartTime); tracker.OnDataProduced(kIdealFrameSize, kStartTime); tracker.OnDataRateChanged(kTargetRate * 2, kStartTime + kFrameInterval); tracker.OnDataProduced(kIdealFrameSize * 2, kStartTime + kFrameInterval); tracker.OnDataRateChanged(kTargetRate, kStartTime + 2 * kFrameInterval); tracker.OnDataProduced(kIdealFrameSize, kStartTime + 2 * kFrameInterval); EXPECT_THAT(tracker.GetRateUtilizationFactor(kStartTime + 3 * kFrameInterval), PrettyCloseTo(1.0)); } TEST(RateUtilizationTrackerTest, VaryingRateMidFrameInterval) { RateUtilizationTracker tracker(kDefaultMaxDataPoints, kDefaultTimeWindow); constexpr TimeDelta kFrameInterval = TimeDelta::Seconds(1) / 33; constexpr DataRate kTargetRate = DataRate::KilobitsPerSec(100); constexpr DataSize kIdealFrameSize = kTargetRate * kFrameInterval; // First frame 1/3 too large tracker.OnDataRateChanged(kTargetRate, kStartTime); tracker.OnDataProduced(kIdealFrameSize * (3.0 / 2.0), kStartTime); // Mid frame interval double the target rate. Should lead to no overshoot. tracker.OnDataRateChanged(kTargetRate * 2, kStartTime + kFrameInterval / 2); EXPECT_THAT(tracker.GetRateUtilizationFactor(kStartTime + kFrameInterval), PrettyCloseTo(1.0)); } TEST(RateUtilizationTrackerTest, VaryingRateAfterLastDataPoint) { RateUtilizationTracker tracker(kDefaultMaxDataPoints, kDefaultTimeWindow); constexpr TimeDelta kFrameInterval = TimeDelta::Seconds(1) / 33; constexpr DataRate kTargetRate = DataRate::KilobitsPerSec(100); constexpr DataSize kIdealFrameSize = kTargetRate * kFrameInterval; tracker.OnDataRateChanged(kTargetRate, kStartTime); // Data point is just after the rate update. tracker.OnDataProduced(kIdealFrameSize, kStartTime + TimeDelta::Micros(1)); // Half an interval past the last frame double the target rate. tracker.OnDataRateChanged(kTargetRate * 2, kStartTime + kFrameInterval / 2); // The last data point should now extend only to 2/3 the way to the next frame // interval. EXPECT_THAT(tracker.GetRateUtilizationFactor(kStartTime + kFrameInterval * (2.0 / 3.0)), PrettyCloseTo(1.0)); EXPECT_THAT(tracker.GetRateUtilizationFactor(kStartTime + kFrameInterval * (2.3 / 3.0)), Not(PrettyCloseTo(1.0))); } TEST(RateUtilizationTrackerTest, DataPointLimit) { // Set max data points to two. RateUtilizationTracker tracker(/*max_data_points=*/2, kDefaultTimeWindow); constexpr TimeDelta kFrameInterval = TimeDelta::Seconds(1) / 33; constexpr DataRate kTargetRate = DataRate::KilobitsPerSec(100); constexpr DataSize kIdealFrameSize = kTargetRate * kFrameInterval; // Insert two frames that are too large. tracker.OnDataRateChanged(kTargetRate, kStartTime); tracker.OnDataProduced(kIdealFrameSize * 2, kStartTime); tracker.OnDataProduced(kIdealFrameSize * 2, kStartTime + 1 * kFrameInterval); EXPECT_THAT(tracker.GetRateUtilizationFactor(kStartTime + 1 * kFrameInterval), Not(PrettyCloseTo(1.0))); // Insert two frames of the correct size. Past grievances have been forgotten. tracker.OnDataProduced(kIdealFrameSize, kStartTime + 2 * kFrameInterval); tracker.OnDataProduced(kIdealFrameSize, kStartTime + 3 * kFrameInterval); EXPECT_THAT(tracker.GetRateUtilizationFactor(kStartTime + 3 * kFrameInterval), PrettyCloseTo(1.0)); } TEST(RateUtilizationTrackerTest, WindowSizeLimit) { constexpr TimeDelta kFrameInterval = TimeDelta::Seconds(1) / 33; constexpr DataRate kTargetRate = DataRate::KilobitsPerSec(100); constexpr DataSize kIdealFrameSize = kTargetRate * kFrameInterval; // Number of data points enough, but time window too small. RateUtilizationTracker tracker(/*max_data_points=*/4, /*time_window=*/ 2 * kFrameInterval - TimeDelta::Millis(1)); // Insert two frames that are too large. tracker.OnDataRateChanged(kTargetRate, kStartTime); tracker.OnDataProduced(kIdealFrameSize * 2, kStartTime); tracker.OnDataProduced(kIdealFrameSize * 2, kStartTime + 1 * kFrameInterval); EXPECT_THAT(tracker.GetRateUtilizationFactor(kStartTime + 1 * kFrameInterval), Not(PrettyCloseTo(1.0))); // Insert two frames of the correct size. Past grievances have been forgotten. tracker.OnDataProduced(kIdealFrameSize, kStartTime + 2 * kFrameInterval); tracker.OnDataProduced(kIdealFrameSize, kStartTime + 3 * kFrameInterval); EXPECT_THAT(tracker.GetRateUtilizationFactor(kStartTime + 3 * kFrameInterval), PrettyCloseTo(1.0)); } TEST(RateUtilizationTrackerTest, EqualTimestampsTreatedAtSameDataPoint) { // Set max data points to two. RateUtilizationTracker tracker(/*max_data_points=*/2, kDefaultTimeWindow); constexpr TimeDelta kFrameInterval = TimeDelta::Seconds(1) / 33; constexpr DataRate kTargetRate = DataRate::KilobitsPerSec(100); constexpr DataSize kIdealFrameSize = kTargetRate * kFrameInterval; tracker.OnDataRateChanged(kTargetRate, kStartTime); tracker.OnDataProduced(kIdealFrameSize, kStartTime); EXPECT_THAT(tracker.GetRateUtilizationFactor(kStartTime), PrettyCloseTo(1.0)); // This is viewed as an undershoot. tracker.OnDataProduced(kIdealFrameSize, kStartTime + (kFrameInterval * 2)); EXPECT_THAT( tracker.GetRateUtilizationFactor(kStartTime + (kFrameInterval * 2)), PrettyCloseTo(2.0 / 3.0)); // Add the same data point again. Treated as layered frame so will accumulate // in the same data point. This is expected to have a send time twice as long // now, reducing the undershoot. tracker.OnDataProduced(kIdealFrameSize, kStartTime + (kFrameInterval * 2)); EXPECT_THAT( tracker.GetRateUtilizationFactor(kStartTime + (kFrameInterval * 2)), PrettyCloseTo(3.0 / 4.0)); } TEST(RateUtilizationTrackerTest, FullRateAfterLastDataPoint) { RateUtilizationTracker tracker(kDefaultMaxDataPoints, kDefaultTimeWindow); constexpr TimeDelta kFrameInterval = TimeDelta::Seconds(1) / 33; constexpr DataRate kTargetRate = DataRate::KilobitsPerSec(100); constexpr DataSize kIdealFrameSize = kTargetRate * kFrameInterval; tracker.OnDataRateChanged(kTargetRate, kStartTime); tracker.OnDataProduced(kIdealFrameSize, kStartTime); // New rate update, but accumulated rate for last data point fully saturated // by next to last rate update. tracker.OnDataRateChanged(kTargetRate, kStartTime + kFrameInterval * 2); EXPECT_THAT(tracker.GetRateUtilizationFactor(kStartTime + kFrameInterval * 3), PrettyCloseTo(1.0 / 3.0)); } } // namespace } // namespace webrtc