blob: 2e75d66a7be2463c21a763007cece1ae718a78dd [file] [log] [blame]
/*
* 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