Implement L4S version of Cubic congestion control for QUIC.

See go/prague-cubic for details.

PiperOrigin-RevId: 675316348
diff --git a/build/source_list.bzl b/build/source_list.bzl
index 4f1c45f..e32fc09 100644
--- a/build/source_list.bzl
+++ b/build/source_list.bzl
@@ -154,6 +154,7 @@
     "quic/core/congestion_control/hybrid_slow_start.h",
     "quic/core/congestion_control/loss_detection_interface.h",
     "quic/core/congestion_control/pacing_sender.h",
+    "quic/core/congestion_control/prague_sender.h",
     "quic/core/congestion_control/prr_sender.h",
     "quic/core/congestion_control/rtt_stats.h",
     "quic/core/congestion_control/send_algorithm_interface.h",
@@ -510,6 +511,7 @@
     "quic/core/congestion_control/general_loss_algorithm.cc",
     "quic/core/congestion_control/hybrid_slow_start.cc",
     "quic/core/congestion_control/pacing_sender.cc",
+    "quic/core/congestion_control/prague_sender.cc",
     "quic/core/congestion_control/prr_sender.cc",
     "quic/core/congestion_control/rtt_stats.cc",
     "quic/core/congestion_control/send_algorithm_interface.cc",
@@ -1186,6 +1188,7 @@
     "quic/core/congestion_control/general_loss_algorithm_test.cc",
     "quic/core/congestion_control/hybrid_slow_start_test.cc",
     "quic/core/congestion_control/pacing_sender_test.cc",
+    "quic/core/congestion_control/prague_sender_test.cc",
     "quic/core/congestion_control/prr_sender_test.cc",
     "quic/core/congestion_control/rtt_stats_test.cc",
     "quic/core/congestion_control/send_algorithm_test.cc",
diff --git a/build/source_list.gni b/build/source_list.gni
index e5285c3..98d1fc3 100644
--- a/build/source_list.gni
+++ b/build/source_list.gni
@@ -154,6 +154,7 @@
     "src/quiche/quic/core/congestion_control/hybrid_slow_start.h",
     "src/quiche/quic/core/congestion_control/loss_detection_interface.h",
     "src/quiche/quic/core/congestion_control/pacing_sender.h",
+    "src/quiche/quic/core/congestion_control/prague_sender.h",
     "src/quiche/quic/core/congestion_control/prr_sender.h",
     "src/quiche/quic/core/congestion_control/rtt_stats.h",
     "src/quiche/quic/core/congestion_control/send_algorithm_interface.h",
@@ -510,6 +511,7 @@
     "src/quiche/quic/core/congestion_control/general_loss_algorithm.cc",
     "src/quiche/quic/core/congestion_control/hybrid_slow_start.cc",
     "src/quiche/quic/core/congestion_control/pacing_sender.cc",
+    "src/quiche/quic/core/congestion_control/prague_sender.cc",
     "src/quiche/quic/core/congestion_control/prr_sender.cc",
     "src/quiche/quic/core/congestion_control/rtt_stats.cc",
     "src/quiche/quic/core/congestion_control/send_algorithm_interface.cc",
@@ -1187,6 +1189,7 @@
     "src/quiche/quic/core/congestion_control/general_loss_algorithm_test.cc",
     "src/quiche/quic/core/congestion_control/hybrid_slow_start_test.cc",
     "src/quiche/quic/core/congestion_control/pacing_sender_test.cc",
+    "src/quiche/quic/core/congestion_control/prague_sender_test.cc",
     "src/quiche/quic/core/congestion_control/prr_sender_test.cc",
     "src/quiche/quic/core/congestion_control/rtt_stats_test.cc",
     "src/quiche/quic/core/congestion_control/send_algorithm_test.cc",
diff --git a/build/source_list.json b/build/source_list.json
index b1daa01..e1ed864 100644
--- a/build/source_list.json
+++ b/build/source_list.json
@@ -153,6 +153,7 @@
     "quiche/quic/core/congestion_control/hybrid_slow_start.h",
     "quiche/quic/core/congestion_control/loss_detection_interface.h",
     "quiche/quic/core/congestion_control/pacing_sender.h",
+    "quiche/quic/core/congestion_control/prague_sender.h",
     "quiche/quic/core/congestion_control/prr_sender.h",
     "quiche/quic/core/congestion_control/rtt_stats.h",
     "quiche/quic/core/congestion_control/send_algorithm_interface.h",
@@ -509,6 +510,7 @@
     "quiche/quic/core/congestion_control/general_loss_algorithm.cc",
     "quiche/quic/core/congestion_control/hybrid_slow_start.cc",
     "quiche/quic/core/congestion_control/pacing_sender.cc",
+    "quiche/quic/core/congestion_control/prague_sender.cc",
     "quiche/quic/core/congestion_control/prr_sender.cc",
     "quiche/quic/core/congestion_control/rtt_stats.cc",
     "quiche/quic/core/congestion_control/send_algorithm_interface.cc",
@@ -1186,6 +1188,7 @@
     "quiche/quic/core/congestion_control/general_loss_algorithm_test.cc",
     "quiche/quic/core/congestion_control/hybrid_slow_start_test.cc",
     "quiche/quic/core/congestion_control/pacing_sender_test.cc",
+    "quiche/quic/core/congestion_control/prague_sender_test.cc",
     "quiche/quic/core/congestion_control/prr_sender_test.cc",
     "quiche/quic/core/congestion_control/rtt_stats_test.cc",
     "quiche/quic/core/congestion_control/send_algorithm_test.cc",
diff --git a/quiche/quic/core/congestion_control/prague_sender.cc b/quiche/quic/core/congestion_control/prague_sender.cc
new file mode 100644
index 0000000..b707efa
--- /dev/null
+++ b/quiche/quic/core/congestion_control/prague_sender.cc
@@ -0,0 +1,158 @@
+// Copyright (c) 2024 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "quiche/quic/core/congestion_control/prague_sender.h"
+
+#include <algorithm>
+
+#include "quiche/quic/core/congestion_control/rtt_stats.h"
+#include "quiche/quic/core/congestion_control/tcp_cubic_sender_bytes.h"
+#include "quiche/quic/core/quic_clock.h"
+#include "quiche/quic/core/quic_connection_stats.h"
+#include "quiche/quic/core/quic_time.h"
+#include "quiche/quic/core/quic_types.h"
+
+namespace quic {
+
+PragueSender::PragueSender(const QuicClock* clock, const RttStats* rtt_stats,
+                           QuicPacketCount initial_tcp_congestion_window,
+                           QuicPacketCount max_congestion_window,
+                           QuicConnectionStats* stats)
+    : TcpCubicSenderBytes(clock, rtt_stats, false,
+                          initial_tcp_congestion_window, max_congestion_window,
+                          stats),
+      connection_start_time_(clock->Now()),
+      last_alpha_update_(connection_start_time_) {}
+
+void PragueSender::OnCongestionEvent(bool rtt_updated,
+                                     QuicByteCount prior_in_flight,
+                                     QuicTime event_time,
+                                     const AckedPacketVector& acked_packets,
+                                     const LostPacketVector& lost_packets,
+                                     QuicPacketCount num_ect,
+                                     QuicPacketCount num_ce) {
+  if (!ect1_enabled_) {
+    TcpCubicSenderBytes::OnCongestionEvent(rtt_updated, prior_in_flight,
+                                           event_time, acked_packets,
+                                           lost_packets, num_ect, num_ce);
+    return;
+  }
+  // Update Prague-specific variables.
+  if (rtt_updated) {
+    rtt_virt_ = std::max(rtt_stats()->smoothed_rtt(), kPragueRttVirtMin);
+  }
+  if (prague_alpha_.has_value()) {
+    ect_count_ += num_ect;
+    ce_count_ += num_ce;
+    if (event_time - last_alpha_update_ > rtt_virt_) {
+      // Update alpha once per virtual RTT.
+      float frac = static_cast<float>(ce_count_) /
+                   static_cast<float>(ect_count_ + ce_count_);
+      prague_alpha_ =
+          (1 - kPragueEwmaGain) * *prague_alpha_ + kPragueEwmaGain * frac;
+      last_alpha_update_ = event_time;
+      ect_count_ = 0;
+      ce_count_ = 0;
+    }
+  } else if (num_ce > 0) {
+    last_alpha_update_ = event_time;
+    prague_alpha_ = 1.0;
+    ect_count_ = num_ect;
+    ce_count_ = num_ce;
+  }
+  if (!lost_packets.empty() && last_congestion_response_time_.has_value() &&
+      (event_time - *last_congestion_response_time_ < rtt_virt_)) {
+    // Give credit for recent ECN cwnd reductions if there is a packet loss.
+    QuicByteCount previous_reduction = last_congestion_response_size_;
+    last_congestion_response_time_.reset();
+    set_congestion_window(GetCongestionWindow() + previous_reduction);
+  }
+  // Due to shorter RTTs with L4S, and the longer virtual RTT, after 500 RTTs
+  // congestion avoidance should grow slower than in Cubic.
+  if (!reduce_rtt_dependence_) {
+    reduce_rtt_dependence_ =
+        !InSlowStart() && lost_packets.empty() &&
+        (event_time - connection_start_time_) >
+            kRoundsBeforeReducedRttDependence * rtt_stats()->smoothed_rtt();
+  }
+  float congestion_avoidance_deflator;
+  if (reduce_rtt_dependence_) {
+    congestion_avoidance_deflator =
+        static_cast<float>(rtt_stats()->smoothed_rtt().ToMicroseconds()) /
+        static_cast<float>(rtt_virt_.ToMicroseconds());
+    congestion_avoidance_deflator *= congestion_avoidance_deflator;
+  } else {
+    congestion_avoidance_deflator = 1.0f;
+  }
+  QuicByteCount original_cwnd = GetCongestionWindow();
+  if (num_ce == 0 || !lost_packets.empty()) {
+    // Fast path. No ECN specific logic except updating stats, adjusting for
+    // previous CE responses, and reduced RTT dependence.
+    TcpCubicSenderBytes::OnCongestionEvent(rtt_updated, prior_in_flight,
+                                           event_time, acked_packets,
+                                           lost_packets, num_ect, num_ce);
+    if (lost_packets.empty() && reduce_rtt_dependence_ &&
+        original_cwnd < GetCongestionWindow()) {
+      QuicByteCount cwnd_increase = GetCongestionWindow() - original_cwnd;
+      set_congestion_window(original_cwnd +
+                            cwnd_increase * congestion_avoidance_deflator);
+    }
+    return;
+  }
+  // num_ce > 0 and lost_packets is empty.
+  if (InSlowStart()) {
+    ExitSlowstart();
+  }
+  // Estimate bytes that were CE marked
+  QuicByteCount bytes_acked = 0;
+  for (auto packet : acked_packets) {
+    bytes_acked += packet.bytes_acked;
+  }
+  float ce_fraction =
+      static_cast<float>(num_ce) / static_cast<float>(num_ect + num_ce);
+  QuicByteCount bytes_ce = bytes_acked * ce_fraction;
+  QuicPacketCount ce_packets_remaining = num_ce;
+  bytes_acked -= bytes_ce;
+  if (!last_congestion_response_time_.has_value() ||
+      event_time - *last_congestion_response_time_ > rtt_virt_) {
+    last_congestion_response_time_ = event_time;
+    // Create a synthetic loss to trigger a loss response. The packet number
+    // needs to be large enough to not be before the last loss response, which
+    // should be easy since acked packet numbers should be higher than lost
+    // packet numbers, due to the delay in detecting loss.
+    while (ce_packets_remaining > 0) {
+      OnPacketLost(acked_packets.back().packet_number, bytes_ce,
+                   prior_in_flight);
+      bytes_ce = 0;
+      ce_packets_remaining--;
+    }
+    QuicByteCount cwnd_reduction = original_cwnd - GetCongestionWindow();
+    last_congestion_response_size_ = cwnd_reduction * *prague_alpha_;
+    set_congestion_window(original_cwnd - last_congestion_response_size_);
+    set_slowstart_threshold(GetCongestionWindow());
+    ExitRecovery();
+  }
+  if (num_ect == 0) {
+    return;
+  }
+  for (const AckedPacket& acked : acked_packets) {
+    // Timing matters so report all of the packets faithfully, but reduce the
+    // size to reflect that some bytes were marked CE.
+    OnPacketAcked(
+        acked.packet_number,
+        acked.bytes_acked * (1 - ce_fraction) * congestion_avoidance_deflator,
+        prior_in_flight, event_time);
+  }
+}
+
+CongestionControlType PragueSender::GetCongestionControlType() const {
+  return kPragueCubic;
+}
+
+bool PragueSender::EnableECT1() {
+  ect1_enabled_ = true;
+  return true;
+}
+
+}  // namespace quic
diff --git a/quiche/quic/core/congestion_control/prague_sender.h b/quiche/quic/core/congestion_control/prague_sender.h
new file mode 100644
index 0000000..a13ebae
--- /dev/null
+++ b/quiche/quic/core/congestion_control/prague_sender.h
@@ -0,0 +1,82 @@
+// Copyright (c) 2024 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// A modification of Cubic to match Prague congestion control, as described in
+// draft-briscoe-iccrg-prague-congestion-control-04.
+
+#ifndef QUICHE_QUIC_CORE_CONGESTION_CONTROL_PRAGUE_SENDER_H_
+#define QUICHE_QUIC_CORE_CONGESTION_CONTROL_PRAGUE_SENDER_H_
+
+#include <optional>
+
+#include "quiche/quic/core/congestion_control/rtt_stats.h"
+#include "quiche/quic/core/congestion_control/send_algorithm_interface.h"
+#include "quiche/quic/core/congestion_control/tcp_cubic_sender_bytes.h"
+#include "quiche/quic/core/quic_connection_stats.h"
+#include "quiche/quic/core/quic_time.h"
+#include "quiche/quic/core/quic_types.h"
+#include "quiche/common/platform/api/quiche_export.h"
+
+namespace quic {
+
+class RttStats;
+
+constexpr float kPragueEwmaGain = 1 / 16.0;
+constexpr QuicTime::Delta kPragueRttVirtMin =
+    QuicTime::Delta::FromMilliseconds(25);
+constexpr int kRoundsBeforeReducedRttDependence = 500;
+
+namespace test {
+class PragueSenderPeer;
+}  // namespace test
+
+class QUICHE_EXPORT PragueSender : public TcpCubicSenderBytes {
+ public:
+  PragueSender(const QuicClock* clock, const RttStats* rtt_stats,
+               QuicPacketCount initial_tcp_congestion_window,
+               QuicPacketCount max_congestion_window,
+               QuicConnectionStats* stats);
+  PragueSender(const PragueSender&) = delete;
+  PragueSender& operator=(const PragueSender&) = delete;
+  ~PragueSender() override {}
+
+  // Start implementation of SendAlgorithmInterface overrides.
+  void OnCongestionEvent(bool rtt_updated, QuicByteCount prior_in_flight,
+                         QuicTime event_time,
+                         const AckedPacketVector& acked_packets,
+                         const LostPacketVector& lost_packets,
+                         QuicPacketCount num_ect,
+                         QuicPacketCount num_ce) override;
+  CongestionControlType GetCongestionControlType() const override;
+  bool EnableECT1() override;
+  // End implementation of SendAlgorithmInterface overrides.
+
+ private:
+  friend class test::PragueSenderPeer;
+
+  bool ect1_enabled_ = false;
+
+  // Tracks the life of the connection to begin reducing RTT dependence of
+  // congestion avoidance after 500 RTTs.
+  QuicTime connection_start_time_;
+  bool reduce_rtt_dependence_ = false;
+
+  // Alpha-related variables
+  std::optional<float> prague_alpha_;
+  QuicPacketCount ect_count_ = 0;
+  QuicPacketCount ce_count_ = 0;
+
+  // Virtual RTT related variables
+  QuicTime::Delta rtt_virt_ = kPragueRttVirtMin;
+  QuicTime last_alpha_update_;
+
+  // Accounting for recent CE-based cwnd reductions that are "credit" for future
+  // loss responses.
+  std::optional<QuicTime> last_congestion_response_time_;
+  QuicByteCount last_congestion_response_size_;
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_CORE_CONGESTION_CONTROL_PRAGUE_SENDER_H_
diff --git a/quiche/quic/core/congestion_control/prague_sender_test.cc b/quiche/quic/core/congestion_control/prague_sender_test.cc
new file mode 100644
index 0000000..3b38eec
--- /dev/null
+++ b/quiche/quic/core/congestion_control/prague_sender_test.cc
@@ -0,0 +1,273 @@
+// Copyright (c) 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "quiche/quic/core/congestion_control/prague_sender.h"
+
+#include <cstdint>
+#include <optional>
+
+#include "quiche/quic/core/congestion_control/cubic_bytes.h"
+#include "quiche/quic/core/congestion_control/rtt_stats.h"
+#include "quiche/quic/core/congestion_control/send_algorithm_interface.h"
+#include "quiche/quic/core/quic_clock.h"
+#include "quiche/quic/core/quic_connection_stats.h"
+#include "quiche/quic/core/quic_constants.h"
+#include "quiche/quic/core/quic_packet_number.h"
+#include "quiche/quic/core/quic_time.h"
+#include "quiche/quic/core/quic_types.h"
+#include "quiche/quic/platform/api/quic_test.h"
+#include "quiche/quic/test_tools/mock_clock.h"
+
+namespace quic::test {
+
+// TODO(ianswett): A number of theses tests were written with the assumption of
+// an initial CWND of 10. They have carefully calculated values which should be
+// updated to be based on kInitialCongestionWindow.
+const uint32_t kInitialCongestionWindowPackets = 10;
+const uint32_t kMaxCongestionWindowPackets = 200;
+const QuicTime::Delta kRtt = QuicTime::Delta::FromMilliseconds(10);
+
+class PragueSenderPeer : public PragueSender {
+ public:
+  explicit PragueSenderPeer(const QuicClock* clock)
+      : PragueSender(clock, &rtt_stats_, kInitialCongestionWindowPackets,
+                     kMaxCongestionWindowPackets, &stats_) {}
+
+  QuicTimeDelta rtt_virt() const { return rtt_virt_; }
+  bool InReducedRttDependenceMode() const { return reduce_rtt_dependence_; }
+  float alpha() const { return *prague_alpha_; }
+
+  RttStats rtt_stats_;
+  QuicConnectionStats stats_;
+};
+
+class PragueSenderTest : public QuicTest {
+ protected:
+  PragueSenderTest()
+      : one_ms_(QuicTime::Delta::FromMilliseconds(1)),
+        sender_(&clock_),
+        packet_number_(1),
+        acked_packet_number_(0),
+        bytes_in_flight_(0),
+        cubic_(&clock_) {
+    EXPECT_TRUE(sender_.EnableECT1());
+  }
+
+  int SendAvailableSendWindow() {
+    return SendAvailableSendWindow(kDefaultTCPMSS);
+  }
+
+  int SendAvailableSendWindow(QuicPacketLength /*packet_length*/) {
+    // Send as long as TimeUntilSend returns Zero.
+    int packets_sent = 0;
+    bool can_send = sender_.CanSend(bytes_in_flight_);
+    while (can_send) {
+      sender_.OnPacketSent(clock_.Now(), bytes_in_flight_,
+                           QuicPacketNumber(packet_number_++), kDefaultTCPMSS,
+                           HAS_RETRANSMITTABLE_DATA);
+      ++packets_sent;
+      bytes_in_flight_ += kDefaultTCPMSS;
+      can_send = sender_.CanSend(bytes_in_flight_);
+    }
+    return packets_sent;
+  }
+
+  // Normal is that TCP acks every other segment.
+  void AckNPackets(int n, int ce) {
+    EXPECT_LE(ce, n);
+    sender_.rtt_stats_.UpdateRtt(kRtt, QuicTime::Delta::Zero(), clock_.Now());
+    AckedPacketVector acked_packets;
+    LostPacketVector lost_packets;
+    for (int i = 0; i < n; ++i) {
+      ++acked_packet_number_;
+      acked_packets.push_back(
+          AckedPacket(QuicPacketNumber(acked_packet_number_), kDefaultTCPMSS,
+                      QuicTime::Zero()));
+    }
+    sender_.OnCongestionEvent(true, bytes_in_flight_, clock_.Now(),
+                              acked_packets, lost_packets, n - ce, ce);
+    bytes_in_flight_ -= n * kDefaultTCPMSS;
+    clock_.AdvanceTime(one_ms_);
+  }
+
+  void LoseNPackets(int n) { LoseNPackets(n, kDefaultTCPMSS); }
+
+  void LoseNPackets(int n, QuicPacketLength packet_length) {
+    AckedPacketVector acked_packets;
+    LostPacketVector lost_packets;
+    for (int i = 0; i < n; ++i) {
+      ++acked_packet_number_;
+      lost_packets.push_back(
+          LostPacket(QuicPacketNumber(acked_packet_number_), packet_length));
+    }
+    sender_.OnCongestionEvent(false, bytes_in_flight_, clock_.Now(),
+                              acked_packets, lost_packets, 0, 0);
+    bytes_in_flight_ -= n * packet_length;
+  }
+
+  // Does not increment acked_packet_number_.
+  void LosePacket(uint64_t packet_number) {
+    AckedPacketVector acked_packets;
+    LostPacketVector lost_packets;
+    lost_packets.push_back(
+        LostPacket(QuicPacketNumber(packet_number), kDefaultTCPMSS));
+    sender_.OnCongestionEvent(false, bytes_in_flight_, clock_.Now(),
+                              acked_packets, lost_packets, 0, 0);
+    bytes_in_flight_ -= kDefaultTCPMSS;
+  }
+
+  void MaybeUpdateAlpha(float& alpha, QuicTime& last_update, uint64_t& ect,
+                        uint64_t& ce) {
+    if (clock_.Now() - last_update > kPragueRttVirtMin) {
+      float frac = static_cast<float>(ce) / static_cast<float>(ect + ce);
+      alpha = (1 - kPragueEwmaGain) * alpha + kPragueEwmaGain * frac;
+      last_update = clock_.Now();
+      ect = 0;
+      ce = 0;
+    }
+  }
+
+  const QuicTime::Delta one_ms_;
+  MockClock clock_;
+  PragueSenderPeer sender_;
+  uint64_t packet_number_;
+  uint64_t acked_packet_number_;
+  QuicByteCount bytes_in_flight_;
+  // Since CubicBytes is not mockable, this copy will verify that PragueSender
+  // is getting results equivalent to the expected calls to CubicBytes.
+  CubicBytes cubic_;
+};
+
+TEST_F(PragueSenderTest, EcnResponseInCongestionAvoidance) {
+  int num_sent = SendAvailableSendWindow();
+
+  // Make sure we fall out of slow start.
+  QuicByteCount expected_cwnd = sender_.GetCongestionWindow();
+  LoseNPackets(1);
+  expected_cwnd = cubic_.CongestionWindowAfterPacketLoss(expected_cwnd);
+  EXPECT_EQ(expected_cwnd, sender_.GetCongestionWindow());
+
+  // Ack the rest of the outstanding packets to get out of recovery.
+  for (int i = 1; i < num_sent; ++i) {
+    AckNPackets(1, 0);
+  }
+  // Exiting recovery; cwnd should not have increased.
+  EXPECT_EQ(expected_cwnd, sender_.GetCongestionWindow());
+  EXPECT_EQ(0u, bytes_in_flight_);
+  // Send a new window of data and ack all; cubic growth should occur.
+  num_sent = SendAvailableSendWindow();
+
+  // Ack packets until the CWND increases.
+  QuicByteCount original_cwnd = sender_.GetCongestionWindow();
+  while (sender_.GetCongestionWindow() == original_cwnd) {
+    AckNPackets(1, 0);
+    expected_cwnd = cubic_.CongestionWindowAfterAck(
+        kDefaultTCPMSS, expected_cwnd, kRtt, clock_.Now());
+    EXPECT_EQ(expected_cwnd, sender_.GetCongestionWindow());
+    SendAvailableSendWindow();
+  }
+  // Bytes in flight may be larger than the CWND if the CWND isn't an exact
+  // multiple of the packet sizes being sent.
+  EXPECT_GE(bytes_in_flight_, sender_.GetCongestionWindow());
+
+  // Advance time 2 seconds waiting for an ack.
+  clock_.AdvanceTime(kRtt);
+
+  // First CE mark. Should be treated as a loss. Alpha = 1 so it is the full
+  // Cubic loss response.
+  original_cwnd = sender_.GetCongestionWindow();
+  AckNPackets(2, 1);
+  // Process the "loss", then the ack.
+  expected_cwnd = cubic_.CongestionWindowAfterPacketLoss(expected_cwnd);
+  QuicByteCount expected_ssthresh = expected_cwnd;
+  QuicByteCount loss_reduction = original_cwnd - expected_cwnd;
+  expected_cwnd = cubic_.CongestionWindowAfterAck(
+      kDefaultTCPMSS / 2, expected_cwnd, kRtt, clock_.Now());
+  expected_cwnd = cubic_.CongestionWindowAfterAck(
+      kDefaultTCPMSS / 2, expected_cwnd, kRtt, clock_.Now());
+  EXPECT_EQ(expected_cwnd, sender_.GetCongestionWindow());
+  EXPECT_EQ(expected_ssthresh, sender_.GetSlowStartThreshold());
+
+  // Second CE mark is ignored.
+  AckNPackets(1, 1);
+  EXPECT_EQ(expected_cwnd, sender_.GetCongestionWindow());
+
+  // Since there was a full loss response, a subsequent loss should incorporate
+  // that.
+  LoseNPackets(1);
+  expected_cwnd =
+      cubic_.CongestionWindowAfterPacketLoss(expected_cwnd + loss_reduction);
+  EXPECT_EQ(expected_cwnd, sender_.GetCongestionWindow());
+  EXPECT_EQ(expected_cwnd, sender_.GetSlowStartThreshold());
+
+  // With 10ms inputs, rtt_virt_ should be at the minimum value.
+  EXPECT_EQ(sender_.rtt_virt().ToMilliseconds(), 25);
+}
+
+TEST_F(PragueSenderTest, EcnResponseInSlowStart) {
+  SendAvailableSendWindow();
+  AckNPackets(1, 1);
+  EXPECT_FALSE(sender_.InSlowStart());
+}
+
+TEST_F(PragueSenderTest, ReducedRttDependence) {
+  float expected_alpha;
+  uint64_t num_ect = 0;
+  uint64_t num_ce = 0;
+  std::optional<QuicTime> last_alpha_update;
+  std::optional<QuicTime> last_decrease;
+  // While trying to get to 50 RTTs, check that alpha is being updated properly,
+  // and is applied to CE response.
+  while (!sender_.InReducedRttDependenceMode()) {
+    int num_sent = SendAvailableSendWindow();
+    clock_.AdvanceTime(kRtt);
+    for (int i = 0; (i < num_sent - 1); ++i) {
+      if (last_alpha_update.has_value()) {
+        ++num_ect;
+        MaybeUpdateAlpha(expected_alpha, last_alpha_update.value(), num_ect,
+                         num_ce);
+      }
+      AckNPackets(1, 0);
+    }
+    QuicByteCount cwnd = sender_.GetCongestionWindow();
+    num_ce++;
+    if (last_alpha_update.has_value()) {
+      MaybeUpdateAlpha(expected_alpha, last_alpha_update.value(), num_ect,
+                       num_ce);
+    } else {
+      // First CE mark starts the update
+      expected_alpha = 1.0;
+      last_alpha_update = clock_.Now();
+    }
+    AckNPackets(1, 1);
+    bool simulated_loss = false;
+    if (!last_decrease.has_value() ||
+        (clock_.Now() - last_decrease.value() > sender_.rtt_virt())) {
+      QuicByteCount new_cwnd = cubic_.CongestionWindowAfterPacketLoss(cwnd);
+      // Add one byte to fix a rounding error.
+      QuicByteCount reduction = (cwnd - new_cwnd) * expected_alpha;
+      cwnd -= reduction;
+      last_decrease = clock_.Now();
+      simulated_loss = true;
+    }
+    EXPECT_EQ(expected_alpha, sender_.alpha());
+    EXPECT_EQ(cwnd, sender_.GetCongestionWindow());
+    // This is the one spot where PragueSender has to manually update ssthresh.
+    if (simulated_loss) {
+      EXPECT_EQ(cwnd, sender_.GetSlowStartThreshold());
+    }
+  }
+  SendAvailableSendWindow();
+  // Next ack should be scaled by 1/M^2 = 1/2.5^2
+  QuicByteCount expected_cwnd = sender_.GetCongestionWindow();
+  QuicByteCount expected_increase =
+      cubic_.CongestionWindowAfterAck(kDefaultTCPMSS, expected_cwnd, kRtt,
+                                      clock_.Now()) -
+      expected_cwnd;
+  expected_increase = static_cast<float>(expected_increase) / (2.5 * 2.5);
+  AckNPackets(1, 0);
+  EXPECT_EQ(expected_cwnd + expected_increase, sender_.GetCongestionWindow());
+}
+
+}  // namespace quic::test
diff --git a/quiche/quic/core/congestion_control/send_algorithm_interface.cc b/quiche/quic/core/congestion_control/send_algorithm_interface.cc
index 89b1ce9..64b4c0d 100644
--- a/quiche/quic/core/congestion_control/send_algorithm_interface.cc
+++ b/quiche/quic/core/congestion_control/send_algorithm_interface.cc
@@ -7,6 +7,7 @@
 #include "absl/base/attributes.h"
 #include "quiche/quic/core/congestion_control/bbr2_sender.h"
 #include "quiche/quic/core/congestion_control/bbr_sender.h"
+#include "quiche/quic/core/congestion_control/prague_sender.h"
 #include "quiche/quic/core/congestion_control/tcp_cubic_sender_bytes.h"
 #include "quiche/quic/core/quic_packets.h"
 #include "quiche/quic/platform/api/quic_bug_tracker.h"
@@ -51,6 +52,9 @@
       return new TcpCubicSenderBytes(clock, rtt_stats, true /* use Reno */,
                                      initial_congestion_window,
                                      max_congestion_window, stats);
+    case kPragueCubic:
+      return new PragueSender(clock, rtt_stats, initial_congestion_window,
+                              max_congestion_window, stats);
   }
   return nullptr;
 }
diff --git a/quiche/quic/core/congestion_control/tcp_cubic_sender_bytes.h b/quiche/quic/core/congestion_control/tcp_cubic_sender_bytes.h
index 9162f62..de003f7 100644
--- a/quiche/quic/core/congestion_control/tcp_cubic_sender_bytes.h
+++ b/quiche/quic/core/congestion_control/tcp_cubic_sender_bytes.h
@@ -86,7 +86,6 @@
 
   bool IsCwndLimited(QuicByteCount bytes_in_flight) const;
 
-  // TODO(ianswett): Remove these and migrate to OnCongestionEvent.
   void OnPacketAcked(QuicPacketNumber acked_packet_number,
                      QuicByteCount acked_bytes, QuicByteCount prior_in_flight,
                      QuicTime event_time);
@@ -100,6 +99,13 @@
                          QuicByteCount acked_bytes,
                          QuicByteCount prior_in_flight, QuicTime event_time);
   void HandleRetransmissionTimeout();
+  const RttStats* rtt_stats() const { return rtt_stats_; }
+
+  void set_congestion_window(QuicByteCount cwnd) { congestion_window_ = cwnd; }
+  void set_slowstart_threshold(QuicByteCount ssthresh) {
+    slowstart_threshold_ = ssthresh;
+  }
+  void ExitRecovery() { largest_sent_at_last_cutback_.Clear(); }
 
  private:
   friend class test::TcpCubicSenderBytesPeer;
diff --git a/quiche/quic/core/quic_types.cc b/quiche/quic/core/quic_types.cc
index 31314cb..fff80a9 100644
--- a/quiche/quic/core/quic_types.cc
+++ b/quiche/quic/core/quic_types.cc
@@ -325,6 +325,8 @@
       return "PCC";
     case kGoogCC:
       return "GoogCC";
+    case kPragueCubic:
+      return "PRAGUE_CUBIC";
   }
   return absl::StrCat("Unknown(", static_cast<int>(cc_type), ")");
 }
diff --git a/quiche/quic/core/quic_types.h b/quiche/quic/core/quic_types.h
index af3380b..2ffd824 100644
--- a/quiche/quic/core/quic_types.h
+++ b/quiche/quic/core/quic_types.h
@@ -453,6 +453,7 @@
   kGoogCC,
   kBBRv2,  // TODO(rch): This is effectively BBRv3. We should finish the
            // implementation and rename this enum.
+  kPragueCubic,
 };
 
 QUICHE_EXPORT std::string CongestionControlTypeToString(