SCReAM: Scale l4s_alpha update with steps_per_rtt and set g_down to 1/16

Previously, ScreamV2::UpdateL4SAlpha updated l4s_alpha on every feedback
packet using fixed EWMA gain factors (l4s_avg_g_up = 1/8,
l4s_avg_g_down = 1/128) regardless of the RTT. Because feedback arrives
approximately every reaction_interval (25 ms) while ECN-CE marking and
window adjustments occur over an RTT, updating l4s_alpha with unscaled
gains caused l4s_alpha to react multiple times per RTT on longer-RTT
paths, and the slow decay factor (1/128) kept l4s_alpha elevated well
above the actual CE marking fraction between marking events.

This change:
1. Updates the default value of l4s_avg_g_down from 1/128 to 1/16 so
   l4s_alpha decays faster when CE marking subsides.
2. Scales both l4s_avg_g_up and l4s_avg_g_down by
   steps_per_rtt = max(1.0, rtt / reaction_interval) in UpdateL4SAlpha
   (gated behind default-enabled field trial parameter
   ScaleL4sAlphaWithRtt).

Scaling the EWMA gain factors by steps_per_rtt ensures that l4s_alpha
converges over max(reaction_interval, rtt) rather than per feedback
packet, better matching TCP Prague which updates alpha once per RTT
(with a minimum interval of 25 ms) using g = 1/16.

Bug: webrtc:447037083
Change-Id: Ie9c81e26a4932117c3d5a673d1f9c962e9533bb5
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/507841
Commit-Queue: Per Kjellander <perkj@webrtc.org>
Reviewed-by: Rasmus Brandt <brandtr@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#48824}
diff --git a/modules/congestion_controller/scream/scream_v2.cc b/modules/congestion_controller/scream/scream_v2.cc
index ec23cbb..138efdd 100644
--- a/modules/congestion_controller/scream/scream_v2.cc
+++ b/modules/congestion_controller/scream/scream_v2.cc
@@ -112,21 +112,33 @@
   }
 }
 
+TimeDelta ScreamV2::ReactionInterval() const {
+  return params_.use_feedback_interval_for_virtual_rtt.Get()
+             ? std::max(params_.virtual_rtt.Get(), feedback_interval_)
+             : params_.virtual_rtt.Get();
+}
+
 void ScreamV2::UpdateL4SAlpha(const ScreamFeedback& parsed) {
   // 4.2.1.3.
   if (parsed.num_received_packets == 0) {
     return;
   }
 
+  double steps_per_rtt =
+      params_.scale_l4s_alpha_with_rtt.Get()
+          ? std::max(1.0,
+                     delay_based_congestion_control_.rtt() / ReactionInterval())
+          : 1.0;
   double fraction_marked = static_cast<double>(parsed.num_ce_marked_packets) /
                            parsed.num_received_packets;
   // Fast attack slow decay EWMA filter.
   if (fraction_marked > l4s_alpha_) {
-    l4s_alpha_ = std::min(params_.l4s_avg_g_up.Get() * fraction_marked +
-                              (1.0 - params_.l4s_avg_g_up.Get()) * l4s_alpha_,
-                          1.0);
+    double g_up = params_.l4s_avg_g_up.Get() / steps_per_rtt;
+    l4s_alpha_ =
+        std::min(g_up * fraction_marked + (1.0 - g_up) * l4s_alpha_, 1.0);
   } else {
-    l4s_alpha_ = (1.0 - params_.l4s_avg_g_down.Get()) * l4s_alpha_;
+    double g_down = params_.l4s_avg_g_down.Get() / steps_per_rtt;
+    l4s_alpha_ = (1.0 - g_down) * l4s_alpha_;
   }
 }
 void ScreamV2::UpdateRefWindow(const ScreamFeedback& parsed) {
@@ -144,10 +156,7 @@
 
   TimeDelta time_since_last_reaction =
       parsed.feedback_time - last_reaction_to_congestion_time_;
-  TimeDelta reaction_interval =
-      params_.use_feedback_interval_for_virtual_rtt.Get()
-          ? std::max(params_.virtual_rtt.Get(), feedback_interval_)
-          : params_.virtual_rtt.Get();
+  TimeDelta reaction_interval = ReactionInterval();
 
   if ((is_virtual_ce || is_ce || is_loss) &&
       time_since_last_reaction >=
diff --git a/modules/congestion_controller/scream/scream_v2.h b/modules/congestion_controller/scream/scream_v2.h
index 87c2f37..353ddeb 100644
--- a/modules/congestion_controller/scream/scream_v2.h
+++ b/modules/congestion_controller/scream/scream_v2.h
@@ -135,6 +135,7 @@
   void UpdateFeedbackInterval(Timestamp feedback_time);
   void UpdateTargetRate(const ScreamFeedback& parsed);
   void UpdateReceiveRate(const ScreamFeedback& parsed);
+  TimeDelta ReactionInterval() const;
 
   const Environment env_;
   const ScreamV2Parameters params_;
diff --git a/modules/congestion_controller/scream/scream_v2_parameters.cc b/modules/congestion_controller/scream/scream_v2_parameters.cc
index 8e1e4df..5887978 100644
--- a/modules/congestion_controller/scream/scream_v2_parameters.cc
+++ b/modules/congestion_controller/scream/scream_v2_parameters.cc
@@ -25,7 +25,8 @@
 ScreamV2Parameters::ScreamV2Parameters(const FieldTrialsView* trials)
     : min_ref_window("MinRefWindow", DataSize::Bytes(1000)),
       l4s_avg_g_up("L4sAvgGUp", 1.0 / 8.0),
-      l4s_avg_g_down("L4sAvgGDown", 1.0 / 128.0),
+      l4s_avg_g_down("L4sAvgGDown", 1.0 / 16.0),
+      scale_l4s_alpha_with_rtt("ScaleL4sAlphaWithRtt", true),
       rtts_with_loss_before_backoff("RttsWithLossBeforeBackoff", 3),
       lossless_rtts_before_clear("LosslessRttsBeforeClear", 2),
       smoothed_rtt_avg_g("SmoothedRttAvgG", 1.0 / 8.0),
@@ -90,6 +91,7 @@
       {&min_ref_window,
        &l4s_avg_g_up,
        &l4s_avg_g_down,
+       &scale_l4s_alpha_with_rtt,
        &rtts_with_loss_before_backoff,
        &lossless_rtts_before_clear,
 
diff --git a/modules/congestion_controller/scream/scream_v2_parameters.h b/modules/congestion_controller/scream/scream_v2_parameters.h
index a919c67..3b6c590 100644
--- a/modules/congestion_controller/scream/scream_v2_parameters.h
+++ b/modules/congestion_controller/scream/scream_v2_parameters.h
@@ -29,6 +29,9 @@
   // Exponentially Weighted Moving Average (EWMA) factor for l4s_alpha.
   FieldTrialParameter<double> l4s_avg_g_up;
   FieldTrialParameter<double> l4s_avg_g_down;
+  // If true, scales `l4s_avg_g_up` and `l4s_avg_g_down` by `steps_per_rtt` so
+  // `l4s_alpha` is updated relative to `max(reaction_interval, rtt)`.
+  FieldTrialParameter<bool> scale_l4s_alpha_with_rtt;
 
   // The number of consecutive RTTs with loss required to trigger a backoff.
   // Used as the step-up value (1.0 / rtts_with_loss_before_backoff) in the
diff --git a/modules/congestion_controller/scream/scream_v2_unittest.cc b/modules/congestion_controller/scream/scream_v2_unittest.cc
index 184cf18..573334b 100644
--- a/modules/congestion_controller/scream/scream_v2_unittest.cc
+++ b/modules/congestion_controller/scream/scream_v2_unittest.cc
@@ -10,6 +10,7 @@
 #include "modules/congestion_controller/scream/scream_v2.h"
 
 #include <algorithm>
+#include <cmath>
 
 #include "api/environment/environment.h"
 #include "api/transport/ecn_marking.h"
@@ -235,18 +236,16 @@
   Timestamp start_time = clock.CurrentTime();
   TimeDelta feedback_interval = TimeDelta::Millis(25);
 
-  TransportPacketsFeedback feedback =
-      CreateFeedback(clock.CurrentTime(), /*rtt=*/TimeDelta::Millis(10),
-                     /*number_of_ect1_packets=*/20,
-                     /*number_of_packets_in_flight=*/20);
-  // CE mark 20% of packets.
-  for (int i = 0; i < 4; ++i) {
-    feedback.packet_feedbacks[i].ecn = EcnMarking::kCe;
-  }
-
   double l4s_alpha = scream.l4s_alpha();
   while (clock.CurrentTime() < start_time + TimeDelta::Seconds(2)) {
-    feedback.feedback_time = clock.CurrentTime();
+    TransportPacketsFeedback feedback =
+        CreateFeedback(clock.CurrentTime(), /*rtt=*/TimeDelta::Millis(10),
+                       /*number_of_ect1_packets=*/20,
+                       /*number_of_packets_in_flight=*/20);
+    // CE mark 20% of packets.
+    for (int i = 0; i < 4; ++i) {
+      feedback.packet_feedbacks[i].ecn = EcnMarking::kCe;
+    }
     scream.OnTransportPacketsFeedback(feedback);
     EXPECT_GT(scream.l4s_alpha(), l4s_alpha);
     clock.AdvanceTime(feedback_interval);
@@ -255,6 +254,42 @@
   EXPECT_NEAR(scream.l4s_alpha(), 0.2, 0.01);
 }
 
+TEST(ScreamV2Test, L4sAlphaScalesUpAndDownWithRtt) {
+  SimulatedClock clock(Timestamp::Seconds(1'234));
+  Environment env = CreateTestEnvironment({.time = &clock});
+  ScreamV2 scream(env);
+
+  TimeDelta rtt = TimeDelta::Millis(100);
+  TimeDelta feedback_interval = TimeDelta::Millis(25);
+
+  // First feedback with 100% CE marks at RTT = 100ms (steps_per_rtt = 4).
+  // With L4sAvgGUp = 1/8 and steps_per_rtt = 4, g_up = (1/8) / 4 = 1/32.
+  TransportPacketsFeedback ce_feedback =
+      CreateFeedback(clock.CurrentTime(), rtt,
+                     /*number_of_ect1_packets=*/20,
+                     /*number_of_packets_in_flight=*/20);
+  for (auto& packet : ce_feedback.packet_feedbacks) {
+    packet.ecn = EcnMarking::kCe;
+  }
+  scream.OnTransportPacketsFeedback(ce_feedback);
+  EXPECT_DOUBLE_EQ(scream.l4s_alpha(), (1.0 / 8.0) / 4.0);
+
+  // Next 4 feedbacks (1 RTT) with 0% CE marks.
+  // With L4sAvgGDown = 1/16 and steps_per_rtt = 4, g_down = (1/16) / 4 = 1/64.
+  double alpha_before_decay = scream.l4s_alpha();
+  for (int i = 0; i < 4; ++i) {
+    clock.AdvanceTime(feedback_interval);
+    TransportPacketsFeedback clear_feedback =
+        CreateFeedback(clock.CurrentTime(), rtt,
+                       /*number_of_ect1_packets=*/20,
+                       /*number_of_packets_in_flight=*/20);
+    scream.OnTransportPacketsFeedback(clear_feedback);
+  }
+  double expected_decay_factor = std::pow(1.0 - (1.0 / 16.0) / 4.0, 4);
+  EXPECT_NEAR(scream.l4s_alpha(), alpha_before_decay * expected_decay_factor,
+              1e-6);
+}
+
 struct AdaptsToLinkCapacityParams {
   SimulatedNetwork::Config network_config;
   bool send_as_ect1 = true;