blob: f783c8a17331daa1bc8c40c413808457b99fa606 [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 "modules/congestion_controller/ect1_policy.h"
#include <algorithm>
#include <optional>
#include "api/units/time_delta.h"
#include "api/units/timestamp.h"
#include "rtc_base/logging.h"
namespace webrtc {
namespace {
// How long ECT(1) packets must be sent without any feedback arriving before
// assuming the path drops them. The timeout is short until an ECT(1) packet
// has been acknowledged, so that a path that drops them from the start is
// detected quickly. Once the path is known to deliver them, the timeout
// matches the ICE unwritable timeout, so that an outage affecting all packets
// has made ICE look for another route before the marking is blamed for it.
constexpr TimeDelta kEct1FeedbackTimeoutBeforeAck = TimeDelta::Millis(500);
constexpr TimeDelta kEct1FeedbackTimeoutAfterAck = TimeDelta::Seconds(5);
// The most a single gap between two sent packets can contribute to the time
// spent sending, so that idle time does not count toward the timeout. A sender
// that sends rarely, such as a screencast of static content without audio,
// therefore needs longer to reach the timeout. That delays detection but can
// not cause a false one.
constexpr TimeDelta kMaxCountedSendGap = TimeDelta::Millis(200);
// Resuming after a pause can then never by itself trigger detection. A sender
// that gets no feedback still reaches the timeout, because the pacer keeps
// sending a packet every kCongestedPacketInterval.
static_assert(kMaxCountedSendGap < kEct1FeedbackTimeoutBeforeAck);
// Sending ECT(1) is retried once per route this long after the path was judged
// to drop the marking. A round trip time longer than the timeout used before
// the first acknowledgement makes that judgement wrong, and the round trip
// time can also improve during a call.
constexpr TimeDelta kEct1RetryDelay = TimeDelta::Seconds(10);
} // namespace
void Ect1Policy::SetFeedbackSupportsEcn(bool supports_ecn) {
feedback_supports_ecn_ = supports_ecn;
LogIfDecisionChanged();
}
void Ect1Policy::SetCongestionControllerSupportsEcn(bool supports_ecn) {
congestion_controller_supports_ecn_ = supports_ecn;
LogIfDecisionChanged();
}
void Ect1Policy::OnNetworkRouteChanged() {
// The new path may handle the ECT(1) marking even if the old one did not.
route_bleaches_ect1_ = false;
ect1_drop_detected_time_ = std::nullopt;
ect1_drop_retried_ = false;
ect1_preserved_on_route_ = false;
time_sending_ect1_without_feedback_ = TimeDelta::Zero();
LogIfDecisionChanged();
}
void Ect1Policy::OnPacketsFeedback(bool has_bleached_ect1,
bool has_preserved_ect1) {
// Feedback of any kind proves that packets reach the receiver, so the path
// is not dropping them and the time spent sending starts over.
time_sending_ect1_without_feedback_ = TimeDelta::Zero();
if (has_preserved_ect1) {
ect1_preserved_on_route_ = true;
}
if (has_bleached_ect1 && !route_bleaches_ect1_) {
route_bleaches_ect1_ = true;
RTC_LOG(LS_INFO) << "Transport does not preserve the ECT(1) marking. Stop "
"sending ECT(1) on this route.";
}
LogIfDecisionChanged();
}
void Ect1Policy::OnPacketSent(Timestamp send_time, bool sent_as_ect1) {
if (ect1_drop_detected_time_.has_value() && !ect1_drop_retried_ &&
send_time - *ect1_drop_detected_time_ > kEct1RetryDelay) {
ect1_drop_retried_ = true;
// Clearing the drop detection time makes ShouldSendEct1() true again.
ect1_drop_detected_time_ = std::nullopt;
time_sending_ect1_without_feedback_ = TimeDelta::Zero();
RTC_LOG(LS_INFO) << "Retrying ECT(1) on this route.";
}
if (sent_as_ect1) {
if (last_send_time_.has_value()) {
time_sending_ect1_without_feedback_ +=
std::min(send_time - *last_send_time_, kMaxCountedSendGap);
}
TimeDelta timeout = Ect1FeedbackTimeout();
if (!ect1_drop_detected_time_.has_value() &&
time_sending_ect1_without_feedback_ > timeout) {
ect1_drop_detected_time_ = send_time;
RTC_LOG(LS_INFO) << "No feedback while sending ECT(1) packets during "
<< ToString(timeout)
<< ". Assuming the transport drops them. Stop sending "
"ECT(1) on this route.";
}
}
last_send_time_ = send_time;
LogIfDecisionChanged();
}
TimeDelta Ect1Policy::Ect1FeedbackTimeout() const {
return ect1_preserved_on_route_ ? kEct1FeedbackTimeoutAfterAck
: kEct1FeedbackTimeoutBeforeAck;
}
bool Ect1Policy::ShouldSendEct1() const {
return feedback_supports_ecn_ && congestion_controller_supports_ecn_ &&
!route_bleaches_ect1_ && !ect1_drop_detected_time_.has_value();
}
void Ect1Policy::LogIfDecisionChanged() {
bool send_ect1 = ShouldSendEct1();
if (send_ect1 == logged_send_ect1_) {
return;
}
logged_send_ect1_ = send_ect1;
RTC_LOG(LS_INFO) << "Sending packets as "
<< (send_ect1 ? "ECT(1)." : "Not-ECT.");
}
} // namespace webrtc