| /* |
| * 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)); |
| } |
| |
| } // namespace |
| } // namespace webrtc |