| /* | |
| * Copyright (c) 2026 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 "rtc_base/clock_aligner.h" | |
| #include "api/environment/environment.h" | |
| #include "api/units/time_delta.h" | |
| #include "api/units/timestamp.h" | |
| #include "system_wrappers/include/clock.h" | |
| #include "test/create_test_environment.h" | |
| #include "test/gmock.h" | |
| #include "test/gtest.h" | |
| #include "test/near_matcher.h" | |
| namespace webrtc { | |
| namespace { | |
| using ::testing::Bool; | |
| using ::testing::TestParamInfo; | |
| using ::testing::TestWithParam; | |
| class ClockAlignerTest : public TestWithParam<bool> { | |
| protected: | |
| bool IsFixEnabled() const { return GetParam(); } | |
| Environment CreateEnvironment(Clock* clock) const { | |
| return CreateTestEnvironment( | |
| {.field_trials = IsFixEnabled() ? "WebRTC-ClockAligner/Enabled/" | |
| : "WebRTC-ClockAligner/Disabled/", | |
| .time = clock}); | |
| } | |
| }; | |
| // Occurs when the clock runs faster than WebRTC's monotonic clock, | |
| // or when the system wall clock steps forward (e.g. NTP forward sync). | |
| TEST_P(ClockAlignerTest, RatchetsDownWhenAheadOfClock) { | |
| SimulatedClock clock(Timestamp::Seconds(100)); | |
| Environment env = CreateEnvironment(&clock); | |
| ClockAligner aligner(env); | |
| const Timestamp kEpoch = Timestamp::Seconds(10); | |
| // First packet establishes offset. | |
| EXPECT_EQ(aligner.Align(kEpoch), clock.CurrentTime()); | |
| // Advance time by 10 ms. Socket indicates packet received 25 ms later. | |
| // Arrival time would be in the future, so offset ratchets down in both modes. | |
| clock.AdvanceTime(TimeDelta::Millis(10)); | |
| EXPECT_EQ(aligner.Align(kEpoch + TimeDelta::Millis(25)), clock.CurrentTime()); | |
| } | |
| // Occurs when NTP (or cellular NITZ / manual clock adjustments) steps the | |
| // system wall clock backwards while packets are actively flowing. | |
| TEST_P(ClockAlignerTest, RecoversFromBackwardClockStep) { | |
| SimulatedClock clock(Timestamp::Seconds(100)); | |
| Environment env = CreateEnvironment(&clock); | |
| ClockAligner aligner(env); | |
| const Timestamp kEpoch = Timestamp::Seconds(10); | |
| // First packet establishes offset. | |
| EXPECT_EQ(aligner.Align(kEpoch), clock.CurrentTime()); | |
| // Advance time by 20 ms. Timestamp steps backward by 450 ms (e.g. NTP | |
| // adjustment). | |
| clock.AdvanceTime(TimeDelta::Millis(20)); | |
| Timestamp time = kEpoch + TimeDelta::Millis(20) - TimeDelta::Millis(450); | |
| if (IsFixEnabled()) { | |
| // With the fix enabled, a backward clock step triggers immediate | |
| // re-anchoring to current time. | |
| EXPECT_EQ(aligner.Align(time), clock.CurrentTime()); | |
| } else { | |
| // In legacy behavior, the offset is not updated and the arrival time | |
| // latches 450 ms in the past. | |
| Timestamp arrival_time = aligner.Align(time); | |
| EXPECT_EQ(clock.CurrentTime() - arrival_time, TimeDelta::Millis(450)); | |
| } | |
| } | |
| // Occurs when a burst of packets arrives at the network interface and is | |
| // queued in the kernel receive buffer (SO_RCVBUF) while the WebRTC thread | |
| // is busy or delayed. | |
| TEST_P(ClockAlignerTest, PreservesBurstSpacing) { | |
| SimulatedClock clock(Timestamp::Seconds(100)); | |
| Environment env = CreateEnvironment(&clock); | |
| ClockAligner aligner(env); | |
| const Timestamp kEpoch = Timestamp::Seconds(10); | |
| const Timestamp kStartTime = clock.CurrentTime(); | |
| // First packet at t = 0 ms. | |
| EXPECT_EQ(aligner.Align(kEpoch), kStartTime); | |
| // Userspace thread was busy and wakes up at t = 30 ms to drain packets. | |
| clock.AdvanceTime(TimeDelta::Millis(30)); | |
| // Packet 2 arrived at socket 5 ms after packet 1. | |
| Timestamp arrival_2 = aligner.Align(kEpoch + TimeDelta::Millis(5)); | |
| EXPECT_THAT(arrival_2 - kStartTime, Near(TimeDelta::Millis(5))); | |
| // Packet 3 arrived at socket 15 ms after packet 1 (10 ms after packet 2), | |
| // read in same userspace turn without advancing clock. | |
| Timestamp arrival_3 = aligner.Align(kEpoch + TimeDelta::Millis(15)); | |
| EXPECT_EQ(arrival_3 - arrival_2, TimeDelta::Millis(10)); | |
| } | |
| // Occurs after periods with no traffic (e.g. audio mute, DTX/silence periods, | |
| // network reconnects, or ICE candidate pair switching) during which the | |
| // socket clock may have drifted or been adjusted. | |
| TEST_P(ClockAlignerTest, ReanchorsAfterIdleGap) { | |
| SimulatedClock clock(Timestamp::Seconds(100)); | |
| Environment env = CreateEnvironment(&clock); | |
| ClockAligner aligner(env); | |
| const Timestamp kEpoch = Timestamp::Seconds(10); | |
| // First packet establishes offset. | |
| EXPECT_EQ(aligner.Align(kEpoch), clock.CurrentTime()); | |
| // Connection idle gap of 2 seconds. | |
| clock.AdvanceTime(TimeDelta::Seconds(2)); | |
| // Socket timestamp advanced by only 1600 ms while monotonic time advanced by | |
| // 2000 ms. | |
| Timestamp time = kEpoch + TimeDelta::Millis(1600); | |
| if (IsFixEnabled()) { | |
| // Elapsed time exceeded the idle threshold; offset is re-anchored. | |
| EXPECT_EQ(aligner.Align(time), clock.CurrentTime()); | |
| } else { | |
| // In legacy behavior, the offset is not re-anchored and arrival time lags | |
| // by 400 ms. | |
| Timestamp arrival_time = aligner.Align(time); | |
| EXPECT_EQ(clock.CurrentTime() - arrival_time, TimeDelta::Millis(400)); | |
| } | |
| } | |
| // Occurs when the system clock steps backward during an inter-packet gap but | |
| // the socket timestamp still advances, or when packets sit in OS receive | |
| // buffers during long thread starvation or application suspension. | |
| TEST_P(ClockAlignerTest, ReanchorsOnMaxStaleness) { | |
| SimulatedClock clock(Timestamp::Seconds(100)); | |
| Environment env = CreateEnvironment(&clock); | |
| ClockAligner aligner(env); | |
| const Timestamp kEpoch = Timestamp::Seconds(10); | |
| // First packet establishes offset. | |
| EXPECT_EQ(aligner.Align(kEpoch), clock.CurrentTime()); | |
| // Advance time by 700 ms. | |
| clock.AdvanceTime(TimeDelta::Millis(700)); | |
| // Socket timestamp advanced by only 100 ms (arrival time lag is 600 ms). | |
| Timestamp time = kEpoch + TimeDelta::Millis(100); | |
| if (IsFixEnabled()) { | |
| // Lag exceeds staleness threshold; offset is re-anchored. | |
| EXPECT_EQ(aligner.Align(time), clock.CurrentTime()); | |
| } else { | |
| // In legacy behavior, arrival time lags by 600 ms in the past. | |
| Timestamp arrival_time = aligner.Align(time); | |
| EXPECT_EQ(clock.CurrentTime() - arrival_time, TimeDelta::Millis(600)); | |
| } | |
| } | |
| // Occurs when the socket clock oscillator runs slightly slower than the host | |
| // monotonic clock (hardware clock frequency drift, typically tens or hundreds | |
| // of ppm). | |
| TEST_P(ClockAlignerTest, TracksPositiveClockDrift) { | |
| SimulatedClock clock(Timestamp::Seconds(100)); | |
| Environment env = CreateEnvironment(&clock); | |
| ClockAligner aligner(env); | |
| const Timestamp kEpoch = Timestamp::Seconds(10); | |
| // First packet establishes offset (offset = 100s - 10s = 90s). | |
| EXPECT_EQ(aligner.Align(kEpoch), clock.CurrentTime()); | |
| // Socket clock runs slightly slower: over 1000 ms real time, socket advanced | |
| // only 999 ms. Drift is 1 ms over 1000 ms = 1000 ppm. | |
| clock.AdvanceTime(TimeDelta::Millis(1000)); | |
| Timestamp time = kEpoch + TimeDelta::Millis(999); | |
| if (IsFixEnabled()) { | |
| // Offset expands by 1 ms to track the clock drift. | |
| EXPECT_EQ(aligner.Align(time), clock.CurrentTime()); | |
| } else { | |
| // Legacy behavior never increases offset; arrival time lags by 1 ms. | |
| Timestamp arrival_time = aligner.Align(time); | |
| EXPECT_EQ(clock.CurrentTime() - arrival_time, TimeDelta::Millis(1)); | |
| } | |
| } | |
| INSTANTIATE_TEST_SUITE_P(All, | |
| ClockAlignerTest, | |
| Bool(), | |
| [](const TestParamInfo<bool>& info) { | |
| return info.param ? "FixEnabled" : "FixDisabled"; | |
| }); | |
| } // namespace | |
| } // namespace webrtc |