/* * 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 { 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& info) { return info.param ? "FixEnabled" : "FixDisabled"; }); } // namespace } // namespace webrtc