blob: 5e0a0a50b21fc82fb1fa94f58ba64f3782e4e5d2 [file]
/*
* 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