| /* | |
| * 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 |