Danil Chapovalov | e546ff9 | 2023-07-21 12:06:20 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (c) 2023 The WebRTC project authors. All Rights Reserved. |
| 3 | * |
| 4 | * Use of this source code is governed by a BSD-style license |
| 5 | * that can be found in the LICENSE file in the root of the source |
| 6 | * tree. An additional intellectual property rights grant can be found |
| 7 | * in the file PATENTS. All contributing project authors may |
| 8 | * be found in the AUTHORS file in the root of the source tree. |
| 9 | */ |
| 10 | |
| 11 | #include "rtc_base/frequency_tracker.h" |
| 12 | |
| 13 | #include <cstdlib> |
| 14 | #include <limits> |
Florent Castelli | 8037fc6 | 2024-08-29 13:00:40 | [diff] [blame] | 15 | #include <optional> |
Danil Chapovalov | e546ff9 | 2023-07-21 12:06:20 | [diff] [blame] | 16 | |
Danil Chapovalov | e546ff9 | 2023-07-21 12:06:20 | [diff] [blame] | 17 | #include "api/units/frequency.h" |
| 18 | #include "api/units/time_delta.h" |
| 19 | #include "api/units/timestamp.h" |
| 20 | #include "test/gmock.h" |
| 21 | #include "test/gtest.h" |
| 22 | |
| 23 | namespace webrtc { |
| 24 | namespace { |
| 25 | |
| 26 | using ::testing::AllOf; |
| 27 | using ::testing::Gt; |
| 28 | using ::testing::Lt; |
| 29 | |
| 30 | constexpr TimeDelta kWindow = TimeDelta::Millis(500); |
| 31 | constexpr TimeDelta kEpsilon = TimeDelta::Millis(1); |
| 32 | |
| 33 | TEST(FrequencyTrackerTest, ReturnsNulloptInitially) { |
| 34 | Timestamp now = Timestamp::Seconds(12'345); |
| 35 | FrequencyTracker stats(kWindow); |
| 36 | |
Florent Castelli | 8037fc6 | 2024-08-29 13:00:40 | [diff] [blame] | 37 | EXPECT_EQ(stats.Rate(now), std::nullopt); |
Danil Chapovalov | e546ff9 | 2023-07-21 12:06:20 | [diff] [blame] | 38 | } |
| 39 | |
| 40 | TEST(FrequencyTrackerTest, ReturnsNulloptAfterSingleDataPoint) { |
| 41 | Timestamp now = Timestamp::Seconds(12'345); |
| 42 | FrequencyTracker stats(kWindow); |
| 43 | |
| 44 | stats.Update(now); |
| 45 | now += TimeDelta::Millis(10); |
| 46 | |
Florent Castelli | 8037fc6 | 2024-08-29 13:00:40 | [diff] [blame] | 47 | EXPECT_EQ(stats.Rate(now), std::nullopt); |
Danil Chapovalov | e546ff9 | 2023-07-21 12:06:20 | [diff] [blame] | 48 | } |
| 49 | |
| 50 | TEST(FrequencyTrackerTest, ReturnsRateAfterTwoMeasurements) { |
| 51 | Timestamp now = Timestamp::Seconds(12'345); |
| 52 | FrequencyTracker stats(kWindow); |
| 53 | |
| 54 | stats.Update(now); |
| 55 | now += TimeDelta::Millis(1); |
| 56 | stats.Update(now); |
| 57 | |
| 58 | // 1 event per 1 ms ~= 1'000 events per second. |
| 59 | EXPECT_EQ(stats.Rate(now), Frequency::Hertz(1'000)); |
| 60 | } |
| 61 | |
| 62 | TEST(FrequencyTrackerTest, MeasuresConstantRate) { |
| 63 | const Timestamp start = Timestamp::Seconds(12'345); |
| 64 | const TimeDelta kInterval = TimeDelta::Millis(10); |
| 65 | const Frequency kConstantRate = 1 / kInterval; |
| 66 | |
| 67 | Timestamp now = start; |
| 68 | FrequencyTracker stats(kWindow); |
| 69 | |
| 70 | stats.Update(now); |
| 71 | Frequency last_error = Frequency::PlusInfinity(); |
| 72 | for (TimeDelta i = TimeDelta::Zero(); i < kWindow; i += kInterval) { |
Tommi | 55c3600 | 2024-07-03 11:01:35 | [diff] [blame] | 73 | SCOPED_TRACE(ToString(i)); |
Danil Chapovalov | e546ff9 | 2023-07-21 12:06:20 | [diff] [blame] | 74 | now += kInterval; |
| 75 | stats.Update(now); |
| 76 | |
| 77 | // Until window is full, rate is measured over a smaller window and might |
| 78 | // look larger than the constant rate. |
Florent Castelli | 8037fc6 | 2024-08-29 13:00:40 | [diff] [blame] | 79 | std::optional<Frequency> rate = stats.Rate(now); |
Danil Chapovalov | e546ff9 | 2023-07-21 12:06:20 | [diff] [blame] | 80 | ASSERT_GE(rate, kConstantRate); |
| 81 | |
| 82 | // Expect the estimation error to decrease as the window is extended. |
| 83 | Frequency error = *rate - kConstantRate; |
| 84 | EXPECT_LE(error, last_error); |
| 85 | last_error = error; |
| 86 | } |
| 87 | |
| 88 | // Once window is full, rate measurment should be stable. |
| 89 | for (TimeDelta i = TimeDelta::Zero(); i < kInterval; |
| 90 | i += TimeDelta::Millis(1)) { |
Tommi | 55c3600 | 2024-07-03 11:01:35 | [diff] [blame] | 91 | SCOPED_TRACE(ToString(i)); |
Danil Chapovalov | e546ff9 | 2023-07-21 12:06:20 | [diff] [blame] | 92 | EXPECT_EQ(stats.Rate(now + i), kConstantRate); |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | TEST(FrequencyTrackerTest, CanMeasureFractionalRate) { |
| 97 | const TimeDelta kInterval = TimeDelta::Millis(134); |
| 98 | Timestamp now = Timestamp::Seconds(12'345); |
| 99 | // FrequencyTracker counts number of events in the window, thus when window is |
| 100 | // fraction of 1 second, number of events per second would always be integer. |
| 101 | const TimeDelta window = TimeDelta::Seconds(2); |
| 102 | |
| 103 | FrequencyTracker framerate(window); |
| 104 | framerate.Update(now); |
| 105 | for (TimeDelta i = TimeDelta::Zero(); i < window; i += kInterval) { |
| 106 | now += kInterval; |
| 107 | framerate.Update(now); |
| 108 | } |
| 109 | |
| 110 | // Should be aproximitly 7.5 fps |
| 111 | EXPECT_THAT(framerate.Rate(now), |
| 112 | AllOf(Gt(Frequency::Hertz(7)), Lt(Frequency::Hertz(8)))); |
| 113 | } |
| 114 | |
| 115 | TEST(FrequencyTrackerTest, IncreasingThenDecreasingRate) { |
| 116 | const int64_t kLargeSize = 1'500; |
| 117 | const int64_t kSmallSize = 300; |
| 118 | const TimeDelta kLargeInterval = TimeDelta::Millis(10); |
| 119 | const TimeDelta kSmallInterval = TimeDelta::Millis(2); |
| 120 | |
| 121 | Timestamp now = Timestamp::Seconds(12'345); |
| 122 | FrequencyTracker stats(kWindow); |
| 123 | |
| 124 | stats.Update(kLargeSize, now); |
| 125 | for (TimeDelta i = TimeDelta::Zero(); i < kWindow; i += kLargeInterval) { |
Tommi | 55c3600 | 2024-07-03 11:01:35 | [diff] [blame] | 126 | SCOPED_TRACE(ToString(i)); |
Danil Chapovalov | e546ff9 | 2023-07-21 12:06:20 | [diff] [blame] | 127 | now += kLargeInterval; |
| 128 | stats.Update(kLargeSize, now); |
| 129 | } |
Florent Castelli | 8037fc6 | 2024-08-29 13:00:40 | [diff] [blame] | 130 | std::optional<Frequency> last_rate = stats.Rate(now); |
Danil Chapovalov | e546ff9 | 2023-07-21 12:06:20 | [diff] [blame] | 131 | EXPECT_EQ(last_rate, kLargeSize / kLargeInterval); |
| 132 | |
| 133 | // Decrease rate with smaller measurments. |
| 134 | for (TimeDelta i = TimeDelta::Zero(); i < kWindow; i += kLargeInterval) { |
Tommi | 55c3600 | 2024-07-03 11:01:35 | [diff] [blame] | 135 | SCOPED_TRACE(ToString(i)); |
Danil Chapovalov | e546ff9 | 2023-07-21 12:06:20 | [diff] [blame] | 136 | now += kLargeInterval; |
| 137 | stats.Update(kSmallSize, now); |
| 138 | |
Florent Castelli | 8037fc6 | 2024-08-29 13:00:40 | [diff] [blame] | 139 | std::optional<Frequency> rate = stats.Rate(now); |
Danil Chapovalov | e546ff9 | 2023-07-21 12:06:20 | [diff] [blame] | 140 | EXPECT_LT(rate, last_rate); |
| 141 | |
| 142 | last_rate = rate; |
| 143 | } |
| 144 | EXPECT_EQ(last_rate, kSmallSize / kLargeInterval); |
| 145 | |
| 146 | // Increase rate with more frequent measurments. |
| 147 | for (TimeDelta i = TimeDelta::Zero(); i < kWindow; i += kSmallInterval) { |
Tommi | 55c3600 | 2024-07-03 11:01:35 | [diff] [blame] | 148 | SCOPED_TRACE(ToString(i)); |
Danil Chapovalov | e546ff9 | 2023-07-21 12:06:20 | [diff] [blame] | 149 | now += kSmallInterval; |
| 150 | stats.Update(kSmallSize, now); |
| 151 | |
Florent Castelli | 8037fc6 | 2024-08-29 13:00:40 | [diff] [blame] | 152 | std::optional<Frequency> rate = stats.Rate(now); |
Danil Chapovalov | e546ff9 | 2023-07-21 12:06:20 | [diff] [blame] | 153 | EXPECT_GE(rate, last_rate); |
| 154 | |
| 155 | last_rate = rate; |
| 156 | } |
| 157 | EXPECT_EQ(last_rate, kSmallSize / kSmallInterval); |
| 158 | } |
| 159 | |
| 160 | TEST(FrequencyTrackerTest, ResetAfterSilence) { |
| 161 | const TimeDelta kInterval = TimeDelta::Millis(10); |
| 162 | const int64_t kPixels = 640 * 360; |
| 163 | |
| 164 | Timestamp now = Timestamp::Seconds(12'345); |
| 165 | FrequencyTracker pixel_rate(kWindow); |
| 166 | |
| 167 | // Feed data until window has been filled. |
| 168 | pixel_rate.Update(kPixels, now); |
| 169 | for (TimeDelta i = TimeDelta::Zero(); i < kWindow; i += kInterval) { |
| 170 | now += kInterval; |
| 171 | pixel_rate.Update(kPixels, now); |
| 172 | } |
| 173 | ASSERT_GT(pixel_rate.Rate(now), Frequency::Zero()); |
| 174 | |
| 175 | now += kWindow + kEpsilon; |
| 176 | // Silence over window size should trigger auto reset for coming sample. |
Florent Castelli | 8037fc6 | 2024-08-29 13:00:40 | [diff] [blame] | 177 | EXPECT_EQ(pixel_rate.Rate(now), std::nullopt); |
Danil Chapovalov | e546ff9 | 2023-07-21 12:06:20 | [diff] [blame] | 178 | pixel_rate.Update(kPixels, now); |
| 179 | // Single measurment after reset is not enough to estimate the rate. |
Florent Castelli | 8037fc6 | 2024-08-29 13:00:40 | [diff] [blame] | 180 | EXPECT_EQ(pixel_rate.Rate(now), std::nullopt); |
Danil Chapovalov | e546ff9 | 2023-07-21 12:06:20 | [diff] [blame] | 181 | |
| 182 | // Manual reset, add the same check again. |
| 183 | pixel_rate.Reset(); |
Florent Castelli | 8037fc6 | 2024-08-29 13:00:40 | [diff] [blame] | 184 | EXPECT_EQ(pixel_rate.Rate(now), std::nullopt); |
Danil Chapovalov | e546ff9 | 2023-07-21 12:06:20 | [diff] [blame] | 185 | now += kInterval; |
| 186 | pixel_rate.Update(kPixels, now); |
Florent Castelli | 8037fc6 | 2024-08-29 13:00:40 | [diff] [blame] | 187 | EXPECT_EQ(pixel_rate.Rate(now), std::nullopt); |
Danil Chapovalov | e546ff9 | 2023-07-21 12:06:20 | [diff] [blame] | 188 | } |
| 189 | |
| 190 | TEST(FrequencyTrackerTest, ReturnsNulloptWhenOverflows) { |
| 191 | Timestamp now = Timestamp::Seconds(12'345); |
| 192 | FrequencyTracker stats(kWindow); |
| 193 | |
| 194 | int64_t very_large_number = std::numeric_limits<int64_t>::max(); |
| 195 | stats.Update(very_large_number, now); |
| 196 | now += kEpsilon; |
| 197 | stats.Update(very_large_number, now); |
| 198 | |
Florent Castelli | 8037fc6 | 2024-08-29 13:00:40 | [diff] [blame] | 199 | EXPECT_EQ(stats.Rate(now), std::nullopt); |
Danil Chapovalov | e546ff9 | 2023-07-21 12:06:20 | [diff] [blame] | 200 | } |
| 201 | |
| 202 | } // namespace |
| 203 | } // namespace webrtc |