niklase@google.com | 470e71d | 2011-07-07 08:21:25 +0000 | [diff] [blame] | 1 | /* |
stefan@webrtc.org | 9354cc9 | 2012-06-07 08:10:14 +0000 | [diff] [blame] | 2 | * Copyright (c) 2012 The WebRTC project authors. All Rights Reserved. |
niklase@google.com | 470e71d | 2011-07-07 08:21:25 +0000 | [diff] [blame] | 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 | |
Jonas Olsson | a4d8737 | 2019-07-05 19:08:33 +0200 | [diff] [blame] | 11 | #include "rtc_base/rate_statistics.h" |
| 12 | |
Yves Gerey | 3e70781 | 2018-11-28 16:47:49 +0100 | [diff] [blame] | 13 | #include <cstdlib> |
Stefan Holmer | fb8fc53 | 2016-04-22 15:48:23 +0200 | [diff] [blame] | 14 | |
Mirko Bonadei | 92ea95e | 2017-09-15 06:47:31 +0200 | [diff] [blame] | 15 | #include "test/gtest.h" |
niklase@google.com | 470e71d | 2011-07-07 08:21:25 +0000 | [diff] [blame] | 16 | |
| 17 | namespace { |
| 18 | |
sprang@webrtc.org | 37968a9 | 2013-12-03 10:31:59 +0000 | [diff] [blame] | 19 | using webrtc::RateStatistics; |
niklase@google.com | 470e71d | 2011-07-07 08:21:25 +0000 | [diff] [blame] | 20 | |
Stefan Holmer | fb8fc53 | 2016-04-22 15:48:23 +0200 | [diff] [blame] | 21 | const int64_t kWindowMs = 500; |
| 22 | |
sprang@webrtc.org | 37968a9 | 2013-12-03 10:31:59 +0000 | [diff] [blame] | 23 | class RateStatisticsTest : public ::testing::Test { |
solenberg@webrtc.org | d26457f | 2013-04-18 12:25:32 +0000 | [diff] [blame] | 24 | protected: |
Stefan Holmer | fb8fc53 | 2016-04-22 15:48:23 +0200 | [diff] [blame] | 25 | RateStatisticsTest() : stats_(kWindowMs, 8000) {} |
sprang@webrtc.org | 37968a9 | 2013-12-03 10:31:59 +0000 | [diff] [blame] | 26 | RateStatistics stats_; |
niklase@google.com | 470e71d | 2011-07-07 08:21:25 +0000 | [diff] [blame] | 27 | }; |
| 28 | |
sprang@webrtc.org | 37968a9 | 2013-12-03 10:31:59 +0000 | [diff] [blame] | 29 | TEST_F(RateStatisticsTest, TestStrictMode) { |
solenberg@webrtc.org | d26457f | 2013-04-18 12:25:32 +0000 | [diff] [blame] | 30 | int64_t now_ms = 0; |
Erik Språng | 51e6030 | 2016-06-10 22:13:21 +0200 | [diff] [blame] | 31 | EXPECT_FALSE(static_cast<bool>(stats_.Rate(now_ms))); |
| 32 | |
| 33 | const uint32_t kPacketSize = 1500u; |
| 34 | const uint32_t kExpectedRateBps = kPacketSize * 1000 * 8; |
| 35 | |
| 36 | // Single data point is not enough for valid estimate. |
| 37 | stats_.Update(kPacketSize, now_ms++); |
| 38 | EXPECT_FALSE(static_cast<bool>(stats_.Rate(now_ms))); |
| 39 | |
Stefan Holmer | fb8fc53 | 2016-04-22 15:48:23 +0200 | [diff] [blame] | 40 | // Expecting 1200 kbps since the window is initially kept small and grows as |
| 41 | // we have more data. |
Erik Språng | 51e6030 | 2016-06-10 22:13:21 +0200 | [diff] [blame] | 42 | stats_.Update(kPacketSize, now_ms); |
| 43 | EXPECT_EQ(kExpectedRateBps, *stats_.Rate(now_ms)); |
| 44 | |
sprang@webrtc.org | 37968a9 | 2013-12-03 10:31:59 +0000 | [diff] [blame] | 45 | stats_.Reset(); |
solenberg@webrtc.org | d26457f | 2013-04-18 12:25:32 +0000 | [diff] [blame] | 46 | // Expecting 0 after init. |
Erik Språng | 51e6030 | 2016-06-10 22:13:21 +0200 | [diff] [blame] | 47 | EXPECT_FALSE(static_cast<bool>(stats_.Rate(now_ms))); |
| 48 | |
| 49 | const int kInterval = 10; |
solenberg@webrtc.org | d26457f | 2013-04-18 12:25:32 +0000 | [diff] [blame] | 50 | for (int i = 0; i < 100000; ++i) { |
Erik Språng | 51e6030 | 2016-06-10 22:13:21 +0200 | [diff] [blame] | 51 | if (i % kInterval == 0) |
| 52 | stats_.Update(kPacketSize, now_ms); |
| 53 | |
solenberg@webrtc.org | d26457f | 2013-04-18 12:25:32 +0000 | [diff] [blame] | 54 | // Approximately 1200 kbps expected. Not exact since when packets |
| 55 | // are removed we will jump 10 ms to the next packet. |
Erik Språng | 51e6030 | 2016-06-10 22:13:21 +0200 | [diff] [blame] | 56 | if (i > kInterval) { |
Danil Chapovalov | 0a1d189 | 2018-06-21 11:48:25 +0200 | [diff] [blame] | 57 | absl::optional<uint32_t> rate = stats_.Rate(now_ms); |
Erik Språng | 51e6030 | 2016-06-10 22:13:21 +0200 | [diff] [blame] | 58 | EXPECT_TRUE(static_cast<bool>(rate)); |
| 59 | uint32_t samples = i / kInterval + 1; |
| 60 | uint64_t total_bits = samples * kPacketSize * 8; |
| 61 | uint32_t rate_bps = static_cast<uint32_t>((1000 * total_bits) / (i + 1)); |
| 62 | EXPECT_NEAR(rate_bps, *rate, 22000u); |
solenberg@webrtc.org | d26457f | 2013-04-18 12:25:32 +0000 | [diff] [blame] | 63 | } |
| 64 | now_ms += 1; |
| 65 | } |
Stefan Holmer | fb8fc53 | 2016-04-22 15:48:23 +0200 | [diff] [blame] | 66 | now_ms += kWindowMs; |
solenberg@webrtc.org | d26457f | 2013-04-18 12:25:32 +0000 | [diff] [blame] | 67 | // The window is 2 seconds. If nothing has been received for that time |
| 68 | // the estimate should be 0. |
Erik Språng | 51e6030 | 2016-06-10 22:13:21 +0200 | [diff] [blame] | 69 | EXPECT_FALSE(static_cast<bool>(stats_.Rate(now_ms))); |
niklase@google.com | 470e71d | 2011-07-07 08:21:25 +0000 | [diff] [blame] | 70 | } |
mikhal@webrtc.org | d89b52a | 2013-11-25 17:49:28 +0000 | [diff] [blame] | 71 | |
sprang@webrtc.org | 37968a9 | 2013-12-03 10:31:59 +0000 | [diff] [blame] | 72 | TEST_F(RateStatisticsTest, IncreasingThenDecreasingBitrate) { |
mikhal@webrtc.org | d89b52a | 2013-11-25 17:49:28 +0000 | [diff] [blame] | 73 | int64_t now_ms = 0; |
sprang@webrtc.org | 37968a9 | 2013-12-03 10:31:59 +0000 | [diff] [blame] | 74 | stats_.Reset(); |
mikhal@webrtc.org | d89b52a | 2013-11-25 17:49:28 +0000 | [diff] [blame] | 75 | // Expecting 0 after init. |
Erik Språng | 51e6030 | 2016-06-10 22:13:21 +0200 | [diff] [blame] | 76 | EXPECT_FALSE(static_cast<bool>(stats_.Rate(now_ms))); |
| 77 | |
| 78 | stats_.Update(1000, ++now_ms); |
Stefan Holmer | fb8fc53 | 2016-04-22 15:48:23 +0200 | [diff] [blame] | 79 | const uint32_t kExpectedBitrate = 8000000; |
mikhal@webrtc.org | d89b52a | 2013-11-25 17:49:28 +0000 | [diff] [blame] | 80 | // 1000 bytes per millisecond until plateau is reached. |
Stefan Holmer | fb8fc53 | 2016-04-22 15:48:23 +0200 | [diff] [blame] | 81 | int prev_error = kExpectedBitrate; |
Danil Chapovalov | 0a1d189 | 2018-06-21 11:48:25 +0200 | [diff] [blame] | 82 | absl::optional<uint32_t> bitrate; |
mikhal@webrtc.org | d89b52a | 2013-11-25 17:49:28 +0000 | [diff] [blame] | 83 | while (++now_ms < 10000) { |
| 84 | stats_.Update(1000, now_ms); |
Stefan Holmer | fb8fc53 | 2016-04-22 15:48:23 +0200 | [diff] [blame] | 85 | bitrate = stats_.Rate(now_ms); |
Erik Språng | 51e6030 | 2016-06-10 22:13:21 +0200 | [diff] [blame] | 86 | EXPECT_TRUE(static_cast<bool>(bitrate)); |
| 87 | int error = kExpectedBitrate - *bitrate; |
Stefan Holmer | fb8fc53 | 2016-04-22 15:48:23 +0200 | [diff] [blame] | 88 | error = std::abs(error); |
| 89 | // Expect the estimation error to decrease as the window is extended. |
| 90 | EXPECT_LE(error, prev_error + 1); |
| 91 | prev_error = error; |
mikhal@webrtc.org | d89b52a | 2013-11-25 17:49:28 +0000 | [diff] [blame] | 92 | } |
Stefan Holmer | fb8fc53 | 2016-04-22 15:48:23 +0200 | [diff] [blame] | 93 | // Window filled, expect to be close to 8000000. |
Erik Språng | 51e6030 | 2016-06-10 22:13:21 +0200 | [diff] [blame] | 94 | EXPECT_EQ(kExpectedBitrate, *bitrate); |
Stefan Holmer | fb8fc53 | 2016-04-22 15:48:23 +0200 | [diff] [blame] | 95 | |
mikhal@webrtc.org | d89b52a | 2013-11-25 17:49:28 +0000 | [diff] [blame] | 96 | // 1000 bytes per millisecond until 10-second mark, 8000 kbps expected. |
| 97 | while (++now_ms < 10000) { |
| 98 | stats_.Update(1000, now_ms); |
sprang@webrtc.org | 37968a9 | 2013-12-03 10:31:59 +0000 | [diff] [blame] | 99 | bitrate = stats_.Rate(now_ms); |
Erik Språng | 51e6030 | 2016-06-10 22:13:21 +0200 | [diff] [blame] | 100 | EXPECT_EQ(kExpectedBitrate, *bitrate); |
mikhal@webrtc.org | d89b52a | 2013-11-25 17:49:28 +0000 | [diff] [blame] | 101 | } |
Erik Språng | 51e6030 | 2016-06-10 22:13:21 +0200 | [diff] [blame] | 102 | |
mikhal@webrtc.org | d89b52a | 2013-11-25 17:49:28 +0000 | [diff] [blame] | 103 | // Zero bytes per millisecond until 0 is reached. |
| 104 | while (++now_ms < 20000) { |
| 105 | stats_.Update(0, now_ms); |
Danil Chapovalov | 0a1d189 | 2018-06-21 11:48:25 +0200 | [diff] [blame] | 106 | absl::optional<uint32_t> new_bitrate = stats_.Rate(now_ms); |
Erik Språng | 51e6030 | 2016-06-10 22:13:21 +0200 | [diff] [blame] | 107 | if (static_cast<bool>(new_bitrate) && *new_bitrate != *bitrate) { |
mikhal@webrtc.org | d89b52a | 2013-11-25 17:49:28 +0000 | [diff] [blame] | 108 | // New bitrate must be lower than previous one. |
Erik Språng | 51e6030 | 2016-06-10 22:13:21 +0200 | [diff] [blame] | 109 | EXPECT_LT(*new_bitrate, *bitrate); |
mikhal@webrtc.org | d89b52a | 2013-11-25 17:49:28 +0000 | [diff] [blame] | 110 | } else { |
| 111 | // 0 kbps expected. |
Erik Språng | 51e6030 | 2016-06-10 22:13:21 +0200 | [diff] [blame] | 112 | EXPECT_EQ(0u, *new_bitrate); |
mikhal@webrtc.org | d89b52a | 2013-11-25 17:49:28 +0000 | [diff] [blame] | 113 | break; |
| 114 | } |
| 115 | bitrate = new_bitrate; |
| 116 | } |
Erik Språng | 51e6030 | 2016-06-10 22:13:21 +0200 | [diff] [blame] | 117 | |
mikhal@webrtc.org | d89b52a | 2013-11-25 17:49:28 +0000 | [diff] [blame] | 118 | // Zero bytes per millisecond until 20-second mark, 0 kbps expected. |
| 119 | while (++now_ms < 20000) { |
| 120 | stats_.Update(0, now_ms); |
Erik Språng | 51e6030 | 2016-06-10 22:13:21 +0200 | [diff] [blame] | 121 | EXPECT_EQ(0u, *stats_.Rate(now_ms)); |
mikhal@webrtc.org | d89b52a | 2013-11-25 17:49:28 +0000 | [diff] [blame] | 122 | } |
| 123 | } |
Stefan Holmer | fb8fc53 | 2016-04-22 15:48:23 +0200 | [diff] [blame] | 124 | |
| 125 | TEST_F(RateStatisticsTest, ResetAfterSilence) { |
| 126 | int64_t now_ms = 0; |
| 127 | stats_.Reset(); |
| 128 | // Expecting 0 after init. |
Erik Språng | 51e6030 | 2016-06-10 22:13:21 +0200 | [diff] [blame] | 129 | EXPECT_FALSE(static_cast<bool>(stats_.Rate(now_ms))); |
| 130 | |
Stefan Holmer | fb8fc53 | 2016-04-22 15:48:23 +0200 | [diff] [blame] | 131 | const uint32_t kExpectedBitrate = 8000000; |
| 132 | // 1000 bytes per millisecond until the window has been filled. |
| 133 | int prev_error = kExpectedBitrate; |
Danil Chapovalov | 0a1d189 | 2018-06-21 11:48:25 +0200 | [diff] [blame] | 134 | absl::optional<uint32_t> bitrate; |
Stefan Holmer | fb8fc53 | 2016-04-22 15:48:23 +0200 | [diff] [blame] | 135 | while (++now_ms < 10000) { |
| 136 | stats_.Update(1000, now_ms); |
| 137 | bitrate = stats_.Rate(now_ms); |
Erik Språng | 51e6030 | 2016-06-10 22:13:21 +0200 | [diff] [blame] | 138 | if (bitrate) { |
| 139 | int error = kExpectedBitrate - *bitrate; |
| 140 | error = std::abs(error); |
| 141 | // Expect the estimation error to decrease as the window is extended. |
| 142 | EXPECT_LE(error, prev_error + 1); |
| 143 | prev_error = error; |
| 144 | } |
Stefan Holmer | fb8fc53 | 2016-04-22 15:48:23 +0200 | [diff] [blame] | 145 | } |
| 146 | // Window filled, expect to be close to 8000000. |
Erik Språng | 51e6030 | 2016-06-10 22:13:21 +0200 | [diff] [blame] | 147 | EXPECT_EQ(kExpectedBitrate, *bitrate); |
Stefan Holmer | fb8fc53 | 2016-04-22 15:48:23 +0200 | [diff] [blame] | 148 | |
| 149 | now_ms += kWindowMs + 1; |
Erik Språng | 51e6030 | 2016-06-10 22:13:21 +0200 | [diff] [blame] | 150 | EXPECT_FALSE(static_cast<bool>(stats_.Rate(now_ms))); |
Stefan Holmer | fb8fc53 | 2016-04-22 15:48:23 +0200 | [diff] [blame] | 151 | stats_.Update(1000, now_ms); |
Erik Språng | 51e6030 | 2016-06-10 22:13:21 +0200 | [diff] [blame] | 152 | ++now_ms; |
| 153 | stats_.Update(1000, now_ms); |
| 154 | // We expect two samples of 1000 bytes, and that the bitrate is measured over |
| 155 | // 500 ms, i.e. 2 * 8 * 1000 / 0.500 = 32000. |
| 156 | EXPECT_EQ(32000u, *stats_.Rate(now_ms)); |
| 157 | |
| 158 | // Reset, add the same samples again. |
| 159 | stats_.Reset(); |
| 160 | EXPECT_FALSE(static_cast<bool>(stats_.Rate(now_ms))); |
| 161 | stats_.Update(1000, now_ms); |
| 162 | ++now_ms; |
| 163 | stats_.Update(1000, now_ms); |
| 164 | // We expect two samples of 1000 bytes, and that the bitrate is measured over |
| 165 | // 2 ms (window size has been reset) i.e. 2 * 8 * 1000 / 0.002 = 8000000. |
| 166 | EXPECT_EQ(kExpectedBitrate, *stats_.Rate(now_ms)); |
| 167 | } |
| 168 | |
| 169 | TEST_F(RateStatisticsTest, HandlesChangingWindowSize) { |
| 170 | int64_t now_ms = 0; |
| 171 | stats_.Reset(); |
| 172 | |
| 173 | // Sanity test window size. |
| 174 | EXPECT_TRUE(stats_.SetWindowSize(kWindowMs, now_ms)); |
| 175 | EXPECT_FALSE(stats_.SetWindowSize(kWindowMs + 1, now_ms)); |
| 176 | EXPECT_FALSE(stats_.SetWindowSize(0, now_ms)); |
| 177 | EXPECT_TRUE(stats_.SetWindowSize(1, now_ms)); |
| 178 | EXPECT_TRUE(stats_.SetWindowSize(kWindowMs, now_ms)); |
| 179 | |
| 180 | // Fill the buffer at a rate of 1 byte / millisecond (8 kbps). |
| 181 | const int kBatchSize = 10; |
| 182 | for (int i = 0; i <= kWindowMs; i += kBatchSize) |
| 183 | stats_.Update(kBatchSize, now_ms += kBatchSize); |
| 184 | EXPECT_EQ(static_cast<uint32_t>(8000), *stats_.Rate(now_ms)); |
| 185 | |
| 186 | // Halve the window size, rate should stay the same. |
| 187 | EXPECT_TRUE(stats_.SetWindowSize(kWindowMs / 2, now_ms)); |
| 188 | EXPECT_EQ(static_cast<uint32_t>(8000), *stats_.Rate(now_ms)); |
| 189 | |
| 190 | // Double the window size again, rate should stay the same. (As the window |
| 191 | // won't actually expand until new bit and bobs fall into it. |
| 192 | EXPECT_TRUE(stats_.SetWindowSize(kWindowMs, now_ms)); |
| 193 | EXPECT_EQ(static_cast<uint32_t>(8000), *stats_.Rate(now_ms)); |
| 194 | |
| 195 | // Fill the now empty half with bits it twice the rate. |
| 196 | for (int i = 0; i < kWindowMs / 2; i += kBatchSize) |
| 197 | stats_.Update(kBatchSize * 2, now_ms += kBatchSize); |
| 198 | |
| 199 | // Rate should have increase be 50%. |
| 200 | EXPECT_EQ(static_cast<uint32_t>((8000 * 3) / 2), *stats_.Rate(now_ms)); |
| 201 | } |
| 202 | |
| 203 | TEST_F(RateStatisticsTest, RespectsWindowSizeEdges) { |
| 204 | int64_t now_ms = 0; |
| 205 | stats_.Reset(); |
| 206 | // Expecting 0 after init. |
| 207 | EXPECT_FALSE(static_cast<bool>(stats_.Rate(now_ms))); |
| 208 | |
| 209 | // One byte per ms, using one big sample. |
| 210 | stats_.Update(kWindowMs, now_ms); |
| 211 | now_ms += kWindowMs - 2; |
| 212 | // Shouldn't work! (Only one sample, not full window size.) |
| 213 | EXPECT_FALSE(static_cast<bool>(stats_.Rate(now_ms))); |
| 214 | |
| 215 | // Window size should be full, and the single data point should be accepted. |
| 216 | ++now_ms; |
Danil Chapovalov | 0a1d189 | 2018-06-21 11:48:25 +0200 | [diff] [blame] | 217 | absl::optional<uint32_t> bitrate = stats_.Rate(now_ms); |
Erik Språng | 51e6030 | 2016-06-10 22:13:21 +0200 | [diff] [blame] | 218 | EXPECT_TRUE(static_cast<bool>(bitrate)); |
| 219 | EXPECT_EQ(1000 * 8u, *bitrate); |
| 220 | |
| 221 | // Add another, now we have twice the bitrate. |
| 222 | stats_.Update(kWindowMs, now_ms); |
| 223 | bitrate = stats_.Rate(now_ms); |
| 224 | EXPECT_TRUE(static_cast<bool>(bitrate)); |
| 225 | EXPECT_EQ(2 * 1000 * 8u, *bitrate); |
| 226 | |
| 227 | // Now that first sample should drop out... |
| 228 | now_ms += 1; |
| 229 | bitrate = stats_.Rate(now_ms); |
| 230 | EXPECT_TRUE(static_cast<bool>(bitrate)); |
| 231 | EXPECT_EQ(1000 * 8u, *bitrate); |
| 232 | } |
| 233 | |
| 234 | TEST_F(RateStatisticsTest, HandlesZeroCounts) { |
| 235 | int64_t now_ms = 0; |
| 236 | stats_.Reset(); |
| 237 | // Expecting 0 after init. |
| 238 | EXPECT_FALSE(static_cast<bool>(stats_.Rate(now_ms))); |
| 239 | |
| 240 | stats_.Update(kWindowMs, now_ms); |
| 241 | now_ms += kWindowMs - 1; |
| 242 | stats_.Update(0, now_ms); |
Danil Chapovalov | 0a1d189 | 2018-06-21 11:48:25 +0200 | [diff] [blame] | 243 | absl::optional<uint32_t> bitrate = stats_.Rate(now_ms); |
Erik Språng | 51e6030 | 2016-06-10 22:13:21 +0200 | [diff] [blame] | 244 | EXPECT_TRUE(static_cast<bool>(bitrate)); |
| 245 | EXPECT_EQ(1000 * 8u, *bitrate); |
| 246 | |
| 247 | // Move window along so first data point falls out. |
| 248 | ++now_ms; |
| 249 | bitrate = stats_.Rate(now_ms); |
| 250 | EXPECT_TRUE(static_cast<bool>(bitrate)); |
| 251 | EXPECT_EQ(0u, *bitrate); |
| 252 | |
| 253 | // Move window so last data point falls out. |
| 254 | now_ms += kWindowMs; |
| 255 | EXPECT_FALSE(static_cast<bool>(stats_.Rate(now_ms))); |
| 256 | } |
| 257 | |
| 258 | TEST_F(RateStatisticsTest, HandlesQuietPeriods) { |
| 259 | int64_t now_ms = 0; |
| 260 | stats_.Reset(); |
| 261 | // Expecting 0 after init. |
| 262 | EXPECT_FALSE(static_cast<bool>(stats_.Rate(now_ms))); |
| 263 | |
| 264 | stats_.Update(0, now_ms); |
| 265 | now_ms += kWindowMs - 1; |
Danil Chapovalov | 0a1d189 | 2018-06-21 11:48:25 +0200 | [diff] [blame] | 266 | absl::optional<uint32_t> bitrate = stats_.Rate(now_ms); |
Erik Språng | 51e6030 | 2016-06-10 22:13:21 +0200 | [diff] [blame] | 267 | EXPECT_TRUE(static_cast<bool>(bitrate)); |
| 268 | EXPECT_EQ(0u, *bitrate); |
| 269 | |
| 270 | // Move window along so first data point falls out. |
| 271 | ++now_ms; |
| 272 | EXPECT_FALSE(static_cast<bool>(stats_.Rate(now_ms))); |
| 273 | |
| 274 | // Move window a long way out. |
| 275 | now_ms += 2 * kWindowMs; |
| 276 | stats_.Update(0, now_ms); |
| 277 | bitrate = stats_.Rate(now_ms); |
| 278 | EXPECT_TRUE(static_cast<bool>(bitrate)); |
| 279 | EXPECT_EQ(0u, *bitrate); |
Stefan Holmer | fb8fc53 | 2016-04-22 15:48:23 +0200 | [diff] [blame] | 280 | } |
solenberg@webrtc.org | d26457f | 2013-04-18 12:25:32 +0000 | [diff] [blame] | 281 | } // namespace |