| /* | |
| * 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 |