Branch Bbr2Sender to create Bbr3Sender and no long have modes extend from Bbr2ModeBase.

This code is not reachable in production yet. I will remove more code and possibly move some structs to bbr2_misc.h before it reaches production.

PiperOrigin-RevId: 901611719
diff --git a/build/source_list.bzl b/build/source_list.bzl
index f87c9b1..9a665df 100644
--- a/build/source_list.bzl
+++ b/build/source_list.bzl
@@ -155,6 +155,7 @@
     "quic/core/congestion_control/bbr2_probe_rtt.h",
     "quic/core/congestion_control/bbr2_sender.h",
     "quic/core/congestion_control/bbr2_startup.h",
+    "quic/core/congestion_control/bbr3_sender.h",
     "quic/core/congestion_control/bbr_sender.h",
     "quic/core/congestion_control/cubic_bytes.h",
     "quic/core/congestion_control/general_loss_algorithm.h",
@@ -521,6 +522,7 @@
     "quic/core/congestion_control/bbr2_probe_rtt.cc",
     "quic/core/congestion_control/bbr2_sender.cc",
     "quic/core/congestion_control/bbr2_startup.cc",
+    "quic/core/congestion_control/bbr3_sender.cc",
     "quic/core/congestion_control/bbr_sender.cc",
     "quic/core/congestion_control/cubic_bytes.cc",
     "quic/core/congestion_control/general_loss_algorithm.cc",
@@ -1239,6 +1241,7 @@
     "oblivious_http/oblivious_http_integration_test.cc",
     "quic/core/congestion_control/bandwidth_sampler_test.cc",
     "quic/core/congestion_control/bbr2_simulator_test.cc",
+    "quic/core/congestion_control/bbr3_simulator_test.cc",
     "quic/core/congestion_control/bbr_sender_test.cc",
     "quic/core/congestion_control/cubic_bytes_test.cc",
     "quic/core/congestion_control/general_loss_algorithm_test.cc",
diff --git a/build/source_list.gni b/build/source_list.gni
index d1be9d0..392014b 100644
--- a/build/source_list.gni
+++ b/build/source_list.gni
@@ -155,6 +155,7 @@
     "src/quiche/quic/core/congestion_control/bbr2_probe_rtt.h",
     "src/quiche/quic/core/congestion_control/bbr2_sender.h",
     "src/quiche/quic/core/congestion_control/bbr2_startup.h",
+    "src/quiche/quic/core/congestion_control/bbr3_sender.h",
     "src/quiche/quic/core/congestion_control/bbr_sender.h",
     "src/quiche/quic/core/congestion_control/cubic_bytes.h",
     "src/quiche/quic/core/congestion_control/general_loss_algorithm.h",
@@ -521,6 +522,7 @@
     "src/quiche/quic/core/congestion_control/bbr2_probe_rtt.cc",
     "src/quiche/quic/core/congestion_control/bbr2_sender.cc",
     "src/quiche/quic/core/congestion_control/bbr2_startup.cc",
+    "src/quiche/quic/core/congestion_control/bbr3_sender.cc",
     "src/quiche/quic/core/congestion_control/bbr_sender.cc",
     "src/quiche/quic/core/congestion_control/cubic_bytes.cc",
     "src/quiche/quic/core/congestion_control/general_loss_algorithm.cc",
@@ -1240,6 +1242,7 @@
     "src/quiche/oblivious_http/oblivious_http_integration_test.cc",
     "src/quiche/quic/core/congestion_control/bandwidth_sampler_test.cc",
     "src/quiche/quic/core/congestion_control/bbr2_simulator_test.cc",
+    "src/quiche/quic/core/congestion_control/bbr3_simulator_test.cc",
     "src/quiche/quic/core/congestion_control/bbr_sender_test.cc",
     "src/quiche/quic/core/congestion_control/cubic_bytes_test.cc",
     "src/quiche/quic/core/congestion_control/general_loss_algorithm_test.cc",
diff --git a/build/source_list.json b/build/source_list.json
index a953857..d692ed9 100644
--- a/build/source_list.json
+++ b/build/source_list.json
@@ -154,6 +154,7 @@
     "quiche/quic/core/congestion_control/bbr2_probe_rtt.h",
     "quiche/quic/core/congestion_control/bbr2_sender.h",
     "quiche/quic/core/congestion_control/bbr2_startup.h",
+    "quiche/quic/core/congestion_control/bbr3_sender.h",
     "quiche/quic/core/congestion_control/bbr_sender.h",
     "quiche/quic/core/congestion_control/cubic_bytes.h",
     "quiche/quic/core/congestion_control/general_loss_algorithm.h",
@@ -520,6 +521,7 @@
     "quiche/quic/core/congestion_control/bbr2_probe_rtt.cc",
     "quiche/quic/core/congestion_control/bbr2_sender.cc",
     "quiche/quic/core/congestion_control/bbr2_startup.cc",
+    "quiche/quic/core/congestion_control/bbr3_sender.cc",
     "quiche/quic/core/congestion_control/bbr_sender.cc",
     "quiche/quic/core/congestion_control/cubic_bytes.cc",
     "quiche/quic/core/congestion_control/general_loss_algorithm.cc",
@@ -1239,6 +1241,7 @@
     "quiche/oblivious_http/oblivious_http_integration_test.cc",
     "quiche/quic/core/congestion_control/bandwidth_sampler_test.cc",
     "quiche/quic/core/congestion_control/bbr2_simulator_test.cc",
+    "quiche/quic/core/congestion_control/bbr3_simulator_test.cc",
     "quiche/quic/core/congestion_control/bbr_sender_test.cc",
     "quiche/quic/core/congestion_control/cubic_bytes_test.cc",
     "quiche/quic/core/congestion_control/general_loss_algorithm_test.cc",
diff --git a/quiche/quic/core/congestion_control/bbr3_sender.cc b/quiche/quic/core/congestion_control/bbr3_sender.cc
new file mode 100644
index 0000000..71f4352
--- /dev/null
+++ b/quiche/quic/core/congestion_control/bbr3_sender.cc
@@ -0,0 +1,1316 @@
+// Copyright 2026 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/bbr3_sender.h"
+
+#include <algorithm>
+#include <cstddef>
+#include <ostream>
+#include <sstream>
+#include <string>
+
+#include "quiche/quic/core/congestion_control/bandwidth_sampler.h"
+#include "quiche/quic/core/congestion_control/bbr2_misc.h"
+#include "quiche/quic/core/crypto/crypto_protocol.h"
+#include "quiche/quic/core/quic_bandwidth.h"
+#include "quiche/quic/core/quic_tag.h"
+#include "quiche/quic/core/quic_types.h"
+#include "quiche/quic/platform/api/quic_flag_utils.h"
+#include "quiche/quic/platform/api/quic_flags.h"
+#include "quiche/quic/platform/api/quic_logging.h"
+#include "quiche/common/platform/api/quiche_logging.h"
+#include "quiche/common/print_elements.h"
+
+namespace quic {
+
+namespace {
+// Constants based on TCP defaults.
+// The minimum CWND to ensure delayed acks don't reduce bandwidth measurements.
+// Does not inflate the pacing rate.
+const QuicByteCount kDefaultMinimumCongestionWindow = 4 * kMaxSegmentSize;
+
+const float kInitialPacingGain = 2.885f;
+
+const int kMaxModeChangesPerCongestionEvent = 4;
+}  // namespace
+
+Bbr3Sender::Bbr3Sender(QuicTime now, const RttStats* rtt_stats,
+                       const QuicUnackedPacketMap* unacked_packets,
+                       QuicPacketCount initial_cwnd_in_packets,
+                       QuicPacketCount max_cwnd_in_packets, QuicRandom* random,
+                       QuicConnectionStats* stats, BbrSender* old_sender)
+    : mode_(Bbr2Mode::STARTUP),
+      rtt_stats_(rtt_stats),
+      unacked_packets_(unacked_packets),
+      random_(random),
+      connection_stats_(stats),
+      params_(kDefaultMinimumCongestionWindow,
+              max_cwnd_in_packets * kDefaultTCPMSS),
+      model_(&params_, rtt_stats->SmoothedOrInitialRtt(),
+             rtt_stats->last_update_time(),
+             /*cwnd_gain=*/1.0,
+             /*pacing_gain=*/kInitialPacingGain,
+             old_sender ? &old_sender->sampler_ : nullptr),
+      initial_cwnd_(params_.cwnd_limits.ApplyLimits(
+          (old_sender) ? old_sender->GetCongestionWindow()
+                       : (initial_cwnd_in_packets * kDefaultTCPMSS))),
+      cwnd_(initial_cwnd_),
+      pacing_rate_(kInitialPacingGain *
+                   QuicBandwidth::FromBytesAndTimeDelta(
+                       cwnd_, rtt_stats->SmoothedOrInitialRtt())),
+      last_sample_is_app_limited_(false) {
+  // Increment, instead of reset startup stats, so we don't lose data recorded
+  // before QuicConnection switched send algorithm to BBRv2.
+  ++connection_stats_->slowstart_count;
+  if (!connection_stats_->slowstart_duration.IsRunning()) {
+    connection_stats_->slowstart_duration.Start(now);
+  }
+  // Enter() is never called for Startup, so the gains needs to be set here.
+  model_.set_pacing_gain(params_.startup_pacing_gain);
+  model_.set_cwnd_gain(params_.startup_cwnd_gain);
+  QUIC_DVLOG(2) << this << " Initializing Bbr3Sender. mode:" << mode_
+                << ", PacingRate:" << pacing_rate_ << ", Cwnd:" << cwnd_
+                << ", CwndLimits:" << params_.cwnd_limits << "  @ " << now;
+  QUICHE_DCHECK_EQ(mode_, Bbr2Mode::STARTUP);
+}
+
+void Bbr3Sender::SetFromConfig(const QuicConfig& config,
+                               Perspective perspective) {
+  if (config.HasClientRequestedIndependentOption(kB2NA, perspective)) {
+    params_.add_ack_height_to_queueing_threshold = false;
+  }
+  if (config.HasClientRequestedIndependentOption(kB2RP, perspective)) {
+    params_.avoid_unnecessary_probe_rtt = false;
+  }
+  if (config.HasClientRequestedIndependentOption(k1RTT, perspective)) {
+    params_.startup_full_bw_rounds = 1;
+  }
+  if (config.HasClientRequestedIndependentOption(k2RTT, perspective)) {
+    params_.startup_full_bw_rounds = 2;
+  }
+  if (config.HasClientRequestedIndependentOption(kB2HR, perspective)) {
+    params_.inflight_hi_headroom = 0.15;
+  }
+  if (config.HasClientRequestedIndependentOption(kICW1, perspective)) {
+    max_cwnd_when_network_parameters_adjusted_ = 100 * kDefaultTCPMSS;
+  }
+
+  ApplyConnectionOptions(config.ClientRequestedIndependentOptions(perspective));
+}
+
+void Bbr3Sender::ApplyConnectionOptions(
+    const QuicTagVector& connection_options) {
+  if (GetQuicReloadableFlag(quic_bbr2_extra_acked_window) &&
+      ContainsQuicTag(connection_options, kBBR4)) {
+    QUIC_RELOADABLE_FLAG_COUNT_N(quic_bbr2_extra_acked_window, 1, 2);
+    model_.SetMaxAckHeightTrackerWindowLength(20);
+  }
+  if (GetQuicReloadableFlag(quic_bbr2_extra_acked_window) &&
+      ContainsQuicTag(connection_options, kBBR5)) {
+    QUIC_RELOADABLE_FLAG_COUNT_N(quic_bbr2_extra_acked_window, 2, 2);
+    model_.SetMaxAckHeightTrackerWindowLength(40);
+  }
+  if (ContainsQuicTag(connection_options, kBBQ1)) {
+    params_.startup_pacing_gain = 2.773;
+    params_.drain_pacing_gain = 1.0 / params_.drain_cwnd_gain;
+  }
+  if (ContainsQuicTag(connection_options, kBBQ2)) {
+    params_.startup_cwnd_gain = 2.885;
+    params_.drain_cwnd_gain = 2.885;
+    model_.set_cwnd_gain(params_.startup_cwnd_gain);
+  }
+  if (ContainsQuicTag(connection_options, kB2LO)) {
+    params_.ignore_inflight_lo = true;
+  }
+  if (ContainsQuicTag(connection_options, kB2NE)) {
+    params_.always_exit_startup_on_excess_loss = true;
+  }
+  if (ContainsQuicTag(connection_options, kB2SL)) {
+    params_.startup_loss_exit_use_max_delivered_for_inflight_hi = false;
+  }
+  if (ContainsQuicTag(connection_options, kB2H2)) {
+    params_.limit_inflight_hi_by_max_delivered = true;
+  }
+  if (ContainsQuicTag(connection_options, kB2DL)) {
+    params_.use_bytes_delivered_for_inflight_hi = true;
+  }
+  if (ContainsQuicTag(connection_options, kB2RC)) {
+    params_.enable_reno_coexistence = false;
+  }
+  if (ContainsQuicTag(connection_options, kBSAO)) {
+    model_.EnableOverestimateAvoidance();
+  }
+  if (ContainsQuicTag(connection_options, kBBQ6)) {
+    params_.decrease_startup_pacing_at_end_of_round = true;
+  }
+  if (ContainsQuicTag(connection_options, kBBQ7)) {
+    params_.bw_lo_mode_ = Bbr2Params::QuicBandwidthLoMode::MIN_RTT_REDUCTION;
+  }
+  if (ContainsQuicTag(connection_options, kBBQ8)) {
+    params_.bw_lo_mode_ = Bbr2Params::QuicBandwidthLoMode::INFLIGHT_REDUCTION;
+  }
+  if (ContainsQuicTag(connection_options, kBBQ9)) {
+    params_.bw_lo_mode_ = Bbr2Params::QuicBandwidthLoMode::CWND_REDUCTION;
+  }
+  if (ContainsQuicTag(connection_options, kB202)) {
+    params_.max_probe_up_queue_rounds = 1;
+  }
+  if (ContainsQuicTag(connection_options, kB203)) {
+    params_.probe_up_ignore_inflight_hi = false;
+  }
+  if (ContainsQuicTag(connection_options, kB204)) {
+    model_.SetReduceExtraAckedOnBandwidthIncrease(true);
+  }
+  if (ContainsQuicTag(connection_options, kB205)) {
+    params_.startup_include_extra_acked = true;
+  }
+  if (ContainsQuicTag(connection_options, kB207)) {
+    params_.max_startup_queue_rounds = 1;
+  }
+  if (ContainsQuicTag(connection_options, kBBRA)) {
+    model_.SetStartNewAggregationEpochAfterFullRound(true);
+  }
+  if (ContainsQuicTag(connection_options, kBBRB)) {
+    model_.SetLimitMaxAckHeightTrackerBySendRate(true);
+  }
+  if (ContainsQuicTag(connection_options, kADP0)) {
+    model_.SetEnableAppDrivenPacing(true);
+  }
+  if (ContainsQuicTag(connection_options, kB206)) {
+    params_.startup_full_loss_count = params_.probe_bw_full_loss_count;
+  }
+  if (ContainsQuicTag(connection_options, kBBHI)) {
+    params_.probe_up_simplify_inflight_hi = true;
+    // Simplify inflight_hi is intended as an alternative to ignoring it,
+    // so ensure we're not ignoring it.
+    params_.probe_up_ignore_inflight_hi = false;
+  }
+  if (ContainsQuicTag(connection_options, kBB2U)) {
+    params_.max_probe_up_queue_rounds = 2;
+  }
+  if (ContainsQuicTag(connection_options, kBB2S)) {
+    params_.max_startup_queue_rounds = 2;
+  }
+}
+
+Limits<QuicByteCount> Bbr3Sender::GetCwndLimitsByMode() const {
+  switch (mode_) {
+    case Bbr2Mode::STARTUP:
+      // Inflight_lo is never set in STARTUP.
+      QUICHE_DCHECK_EQ(Bbr2NetworkModel::inflight_lo_default(),
+                       model_.inflight_lo());
+      return NoGreaterThan(model_.inflight_lo());
+    case Bbr2Mode::PROBE_BW: {
+      if (probe_bw_.phase == ProbePhase::PROBE_CRUISE) {
+        return NoGreaterThan(
+            std::min(model_.inflight_lo(), model_.inflight_hi_with_headroom()));
+      }
+      if (params_.probe_up_ignore_inflight_hi &&
+          probe_bw_.phase == ProbePhase::PROBE_UP) {
+        // Similar to STARTUP.
+        return NoGreaterThan(model_.inflight_lo());
+      }
+      return NoGreaterThan(
+          std::min(model_.inflight_lo(), model_.inflight_hi()));
+    }
+    case Bbr2Mode::DRAIN:
+      return NoGreaterThan(model_.inflight_lo());
+    case Bbr2Mode::PROBE_RTT: {
+      QuicByteCount inflight_upper_bound =
+          std::min(model_.inflight_lo(), model_.inflight_hi_with_headroom());
+      return NoGreaterThan(std::min(inflight_upper_bound, InflightTarget()));
+    }
+    default:
+      QUICHE_NOTREACHED();
+      return Unlimited<QuicByteCount>();
+  }
+}
+
+void Bbr3Sender::AdjustNetworkParameters(const NetworkParams& params) {
+  model_.UpdateNetworkParameters(params.rtt);
+
+  if (mode_ == Bbr2Mode::STARTUP) {
+    const QuicByteCount prior_cwnd = cwnd_;
+
+    QuicBandwidth effective_bandwidth =
+        std::max(params.bandwidth, model_.BandwidthEstimate());
+    connection_stats_->cwnd_bootstrapping_rtt_us =
+        model_.MinRtt().ToMicroseconds();
+
+    if (params.max_initial_congestion_window > 0) {
+      max_cwnd_when_network_parameters_adjusted_ =
+          params.max_initial_congestion_window * kDefaultTCPMSS;
+    }
+    cwnd_ = params_.cwnd_limits.ApplyLimits(
+        std::min(max_cwnd_when_network_parameters_adjusted_,
+                 model_.BDP(effective_bandwidth)));
+
+    if (!params.allow_cwnd_to_decrease) {
+      cwnd_ = std::max(cwnd_, prior_cwnd);
+    }
+
+    pacing_rate_ = std::max(pacing_rate_, QuicBandwidth::FromBytesAndTimeDelta(
+                                              cwnd_, model_.MinRtt()));
+  }
+}
+
+void Bbr3Sender::SetInitialCongestionWindowInPackets(
+    QuicPacketCount congestion_window) {
+  if (mode_ == Bbr2Mode::STARTUP) {
+    // The cwnd limits is unchanged and still applies to the new cwnd.
+    cwnd_ = params_.cwnd_limits.ApplyLimits(congestion_window * kDefaultTCPMSS);
+  }
+}
+
+void Bbr3Sender::SetApplicationDrivenPacingRate(
+    QuicBandwidth application_bandwidth_target) {
+  QUIC_CODE_COUNT(quic_bbr2_set_app_driven_pacing_rate);
+  model_.SetApplicationBandwidthTarget(application_bandwidth_target);
+}
+
+void Bbr3Sender::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*/) {
+  QUIC_DVLOG(3) << this
+                << " OnCongestionEvent. prior_in_flight:" << prior_in_flight
+                << " prior_cwnd:" << cwnd_ << "  @ " << event_time;
+  Bbr2CongestionEvent congestion_event;
+  congestion_event.prior_cwnd = cwnd_;
+  congestion_event.prior_bytes_in_flight = prior_in_flight;
+  bool is_probing_for_bandwidth = false;
+  if (mode_ == Bbr2Mode::STARTUP) {
+    is_probing_for_bandwidth = true;
+  } else if (mode_ == Bbr2Mode::PROBE_BW) {
+    is_probing_for_bandwidth = probe_bw_.phase == ProbePhase::PROBE_REFILL ||
+                               probe_bw_.phase == ProbePhase::PROBE_UP;
+  }
+  congestion_event.is_probing_for_bandwidth = is_probing_for_bandwidth;
+
+  model_.OnCongestionEventStart(event_time, acked_packets, lost_packets,
+                                &congestion_event);
+
+  if (InSlowStart()) {
+    if (!lost_packets.empty()) {
+      connection_stats_->slowstart_packets_lost += lost_packets.size();
+      connection_stats_->slowstart_bytes_lost += congestion_event.bytes_lost;
+    }
+    if (congestion_event.end_of_round_trip) {
+      ++connection_stats_->slowstart_num_rtts;
+    }
+  }
+
+  // Number of mode changes allowed for this congestion event.
+  int mode_changes_allowed = kMaxModeChangesPerCongestionEvent;
+  while (true) {
+    Bbr2Mode prev_mode = mode_;
+    switch (mode_) {
+      case Bbr2Mode::STARTUP:
+        mode_ = OnCongestionEventStartup(congestion_event);
+        break;
+      case Bbr2Mode::DRAIN:
+        mode_ = OnCongestionEventDrain(congestion_event);
+        break;
+      case Bbr2Mode::PROBE_BW:
+        mode_ = OnCongestionEventProbeBw(prior_in_flight, event_time,
+                                         congestion_event);
+        break;
+      case Bbr2Mode::PROBE_RTT:
+        mode_ = OnCongestionEventProbeRtt(congestion_event);
+        break;
+    }
+
+    if (mode_ == prev_mode) {
+      break;
+    }
+
+    QUIC_DVLOG(2) << this << " Mode change:  " << prev_mode << " ==> " << mode_
+                  << "  @ " << event_time;
+
+    if (prev_mode == Bbr2Mode::STARTUP) {
+      LeaveStartup(event_time);
+    }
+
+    if (mode_ == Bbr2Mode::PROBE_BW) {
+      if (probe_bw_.phase == ProbePhase::PROBE_NOT_STARTED) {
+        EnterProbeDown(/*probed_too_high=*/false, /*stopped_risky_probe=*/false,
+                       event_time);
+      } else {
+        QUICHE_DCHECK(probe_bw_.phase == ProbePhase::PROBE_CRUISE ||
+                      probe_bw_.phase == ProbePhase::PROBE_REFILL);
+        probe_bw_.cycle_start_time = event_time;
+        if (probe_bw_.phase == ProbePhase::PROBE_CRUISE) {
+          EnterProbeCruise(event_time);
+        } else if (probe_bw_.phase == ProbePhase::PROBE_REFILL) {
+          EnterProbeRefill(probe_bw_.probe_up_rounds, event_time);
+        }
+      }
+    } else if (mode_ == Bbr2Mode::PROBE_RTT) {
+      EnterProbeRtt();
+    }
+
+    --mode_changes_allowed;
+    if (mode_changes_allowed < 0) {
+      QUIC_BUG(quic_bug_10443_1)
+          << "Exceeded max number of mode changes per congestion event.";
+      break;
+    }
+  }
+
+  UpdatePacingRate(congestion_event.bytes_acked);
+  QUIC_BUG_IF(quic_bug_10443_2, pacing_rate_.IsZero())
+      << "Pacing rate must not be zero!";
+
+  UpdateCongestionWindow(congestion_event.bytes_acked);
+  QUIC_BUG_IF(quic_bug_10443_3, cwnd_ == 0u)
+      << "Congestion window must not be zero!";
+
+  model_.OnCongestionEventFinish(unacked_packets_->GetLeastUnacked(),
+                                 congestion_event);
+  last_sample_is_app_limited_ =
+      congestion_event.last_packet_send_state.is_app_limited;
+  if (!last_sample_is_app_limited_) {
+    has_non_app_limited_sample_ = true;
+  }
+  if (congestion_event.bytes_in_flight == 0 &&
+      params_.avoid_unnecessary_probe_rtt) {
+    OnEnterQuiescence(event_time);
+  }
+
+  QUIC_DVLOG(3)
+      << this
+      << " END CongestionEvent(acked:" << quiche::PrintElements(acked_packets)
+      << ", lost:" << lost_packets.size() << ") "
+      << ", Mode:" << mode_ << ", RttCount:" << model_.RoundTripCount()
+      << ", BytesInFlight:" << congestion_event.bytes_in_flight
+      << ", PacingRate:" << PacingRate(0) << ", CWND:" << GetCongestionWindow()
+      << ", PacingGain:" << model_.pacing_gain()
+      << ", CwndGain:" << model_.cwnd_gain()
+      << ", BandwidthEstimate(kbps):" << BandwidthEstimate().ToKBitsPerSecond()
+      << ", MinRTT(us):" << model_.MinRtt().ToMicroseconds()
+      << ", BDP:" << model_.BDP(BandwidthEstimate())
+      << ", BandwidthLatest(kbps):"
+      << model_.bandwidth_latest().ToKBitsPerSecond()
+      << ", BandwidthLow(kbps):" << model_.bandwidth_lo().ToKBitsPerSecond()
+      << ", BandwidthHigh(kbps):" << model_.MaxBandwidth().ToKBitsPerSecond()
+      << ", InflightLatest:" << model_.inflight_latest()
+      << ", InflightLow:" << model_.inflight_lo()
+      << ", InflightHigh:" << model_.inflight_hi()
+      << ", TotalAcked:" << model_.total_bytes_acked()
+      << ", TotalLost:" << model_.total_bytes_lost()
+      << ", TotalSent:" << model_.total_bytes_sent() << "  @ " << event_time;
+}
+
+void Bbr3Sender::UpdatePacingRate(QuicByteCount bytes_acked) {
+  if (BandwidthEstimate().IsZero()) {
+    return;
+  }
+
+  if (model_.total_bytes_acked() == bytes_acked) {
+    // After the first ACK, cwnd_ is still the initial congestion window.
+    pacing_rate_ = QuicBandwidth::FromBytesAndTimeDelta(cwnd_, model_.MinRtt());
+    return;
+  }
+
+  QuicBandwidth target_rate = model_.pacing_gain() * model_.BandwidthEstimate();
+  if (model_.full_bandwidth_reached()) {
+    pacing_rate_ = target_rate;
+    return;
+  }
+  if (params_.decrease_startup_pacing_at_end_of_round &&
+      model_.pacing_gain() < Params().startup_pacing_gain) {
+    pacing_rate_ = target_rate;
+    return;
+  }
+  if (params_.bw_lo_mode_ != Bbr2Params::DEFAULT &&
+      model_.loss_events_in_round() > 0) {
+    pacing_rate_ = target_rate;
+    return;
+  }
+
+  // By default, the pacing rate never decreases in STARTUP.
+  if (target_rate > pacing_rate_) {
+    pacing_rate_ = target_rate;
+  }
+}
+
+void Bbr3Sender::UpdateCongestionWindow(QuicByteCount bytes_acked) {
+  QuicByteCount target_cwnd = GetTargetCongestionWindow(model_.cwnd_gain());
+
+  const QuicByteCount prior_cwnd = cwnd_;
+  if (model_.full_bandwidth_reached() || Params().startup_include_extra_acked) {
+    target_cwnd += model_.MaxAckHeight();
+    cwnd_ = std::min(prior_cwnd + bytes_acked, target_cwnd);
+  } else if (prior_cwnd < target_cwnd || prior_cwnd < 2 * initial_cwnd_) {
+    cwnd_ = prior_cwnd + bytes_acked;
+  }
+  const QuicByteCount desired_cwnd = cwnd_;
+
+  cwnd_ = GetCwndLimitsByMode().ApplyLimits(cwnd_);
+  const QuicByteCount model_limited_cwnd = cwnd_;
+
+  cwnd_ = params_.cwnd_limits.ApplyLimits(cwnd_);
+
+  QUIC_DVLOG(3) << this << " Updating CWND. target_cwnd:" << target_cwnd
+                << ", max_ack_height:" << model_.MaxAckHeight()
+                << ", full_bw:" << model_.full_bandwidth_reached()
+                << ", bytes_acked:" << bytes_acked
+                << ", inflight_lo:" << model_.inflight_lo()
+                << ", inflight_hi:" << model_.inflight_hi() << ". (prior_cwnd) "
+                << prior_cwnd << " => (desired_cwnd) " << desired_cwnd
+                << " => (model_limited_cwnd) " << model_limited_cwnd
+                << " => (final_cwnd) " << cwnd_;
+}
+
+QuicByteCount Bbr3Sender::GetTargetCongestionWindow(float gain) const {
+  return std::max(model_.BDP(model_.BandwidthEstimate(), gain),
+                  params_.cwnd_limits.Min());
+}
+
+void Bbr3Sender::OnPacketSent(QuicTime sent_time, QuicByteCount bytes_in_flight,
+                              QuicPacketNumber packet_number,
+                              QuicByteCount bytes,
+                              HasRetransmittableData is_retransmittable) {
+  QUIC_DVLOG(3) << this << " OnPacketSent: pkn:" << packet_number
+                << ", bytes:" << bytes << ", cwnd:" << cwnd_
+                << ", inflight:" << bytes_in_flight + bytes
+                << ", total_sent:" << model_.total_bytes_sent() + bytes
+                << ", total_acked:" << model_.total_bytes_acked()
+                << ", total_lost:" << model_.total_bytes_lost() << "  @ "
+                << sent_time;
+  if (InSlowStart()) {
+    ++connection_stats_->slowstart_packets_sent;
+    connection_stats_->slowstart_bytes_sent += bytes;
+  }
+  if (bytes_in_flight == 0 && params_.avoid_unnecessary_probe_rtt) {
+    OnExitQuiescence(sent_time);
+  }
+  model_.OnPacketSent(sent_time, bytes_in_flight, packet_number, bytes,
+                      is_retransmittable);
+}
+
+void Bbr3Sender::OnPacketNeutered(QuicPacketNumber packet_number) {
+  model_.OnPacketNeutered(packet_number);
+}
+
+bool Bbr3Sender::CanSend(QuicByteCount bytes_in_flight) {
+  const bool result = bytes_in_flight < GetCongestionWindow();
+  return result;
+}
+
+QuicByteCount Bbr3Sender::GetCongestionWindow() const {
+  // TODO(wub): Implement Recovery?
+  return cwnd_;
+}
+
+QuicBandwidth Bbr3Sender::PacingRate(QuicByteCount /*bytes_in_flight*/) const {
+  return pacing_rate_;
+}
+
+void Bbr3Sender::OnApplicationLimited(QuicByteCount bytes_in_flight) {
+  if (bytes_in_flight >= GetCongestionWindow()) {
+    return;
+  }
+
+  model_.OnApplicationLimited();
+  QUIC_DVLOG(2) << this << " Becoming application limited. Last sent packet: "
+                << model_.last_sent_packet()
+                << ", CWND: " << GetCongestionWindow();
+}
+
+QuicByteCount Bbr3Sender::GetTargetBytesInflight() const {
+  QuicByteCount bdp = model_.BDP(model_.BandwidthEstimate());
+  return std::min(bdp, GetCongestionWindow());
+}
+
+void Bbr3Sender::PopulateConnectionStats(QuicConnectionStats* stats) const {
+  stats->num_ack_aggregation_epochs = model_.num_ack_aggregation_epochs();
+}
+
+void Bbr3Sender::OnEnterQuiescence(QuicTime now) {
+  last_quiescence_start_ = now;
+}
+
+void Bbr3Sender::LeaveStartup(QuicTime now) {
+  connection_stats_->slowstart_duration.Stop(now);
+  // Clear bandwidth_lo if it's set during STARTUP.
+  model_.clear_bandwidth_lo();
+}
+
+void Bbr3Sender::OnExitQuiescence(QuicTime now) {
+  if (last_quiescence_start_ == QuicTime::Zero()) {
+    return;
+  }
+
+  Bbr2Mode prev_mode = mode_;
+  switch (mode_) {
+    case Bbr2Mode::STARTUP:
+    case Bbr2Mode::DRAIN:
+      break;
+    case Bbr2Mode::PROBE_BW:
+      QUIC_DVLOG(3) << this << " Postponing min_rtt_timestamp("
+                    << model_.MinRttTimestamp() << ") by "
+                    << now - last_quiescence_start_;
+      model_.PostponeMinRttTimestamp(now - last_quiescence_start_);
+      break;
+    case Bbr2Mode::PROBE_RTT:
+      if (now > probe_rtt_.exit_time) {
+        mode_ = Bbr2Mode::PROBE_BW;
+      }
+      break;
+  }
+
+  if (mode_ != prev_mode) {
+    QUICHE_DCHECK_EQ(mode_, Bbr2Mode::PROBE_BW);
+    if (probe_bw_.phase == ProbePhase::PROBE_NOT_STARTED) {
+      EnterProbeDown(/*probed_too_high=*/false, /*stopped_risky_probe=*/false,
+                     now);
+    } else {
+      QUICHE_DCHECK(probe_bw_.phase == ProbePhase::PROBE_CRUISE ||
+                    probe_bw_.phase == ProbePhase::PROBE_REFILL);
+      probe_bw_.cycle_start_time = now;
+      if (probe_bw_.phase == ProbePhase::PROBE_CRUISE) {
+        EnterProbeCruise(now);
+      } else if (probe_bw_.phase == ProbePhase::PROBE_REFILL) {
+        EnterProbeRefill(probe_bw_.probe_up_rounds, now);
+      }
+    }
+  }
+  last_quiescence_start_ = QuicTime::Zero();
+}
+
+std::string Bbr3Sender::GetDebugState() const {
+  std::ostringstream stream;
+  stream << ExportDebugState();
+  return stream.str();
+}
+
+Bbr3Sender::DebugState Bbr3Sender::ExportDebugState() const {
+  DebugState s;
+  s.mode = mode_;
+  s.round_trip_count = model_.RoundTripCount();
+  s.bandwidth_hi = model_.MaxBandwidth();
+  s.bandwidth_lo = model_.bandwidth_lo();
+  s.bandwidth_est = BandwidthEstimate();
+  s.inflight_hi = model_.inflight_hi();
+  s.inflight_lo = model_.inflight_lo();
+  s.max_ack_height = model_.MaxAckHeight();
+  s.min_rtt = model_.MinRtt();
+  s.min_rtt_timestamp = model_.MinRttTimestamp();
+  s.congestion_window = cwnd_;
+  s.pacing_rate = pacing_rate_;
+  s.last_sample_is_app_limited = last_sample_is_app_limited_;
+  s.end_of_app_limited_phase = model_.end_of_app_limited_phase();
+
+  s.startup.full_bandwidth_reached = model_.full_bandwidth_reached();
+  s.startup.full_bandwidth_baseline = model_.full_bandwidth_baseline();
+  s.startup.round_trips_without_bandwidth_growth =
+      model_.rounds_without_bandwidth_growth();
+
+  s.drain.drain_target = DrainTarget();
+
+  s.probe_bw.phase = probe_bw_.phase;
+  s.probe_bw.cycle_start_time = probe_bw_.cycle_start_time;
+  s.probe_bw.phase_start_time = probe_bw_.phase_start_time;
+
+  s.probe_rtt.inflight_target = InflightTarget();
+  s.probe_rtt.exit_time = probe_rtt_.exit_time;
+
+  return s;
+}
+
+std::ostream& operator<<(std::ostream& os, const Bbr3Sender::DebugState& s) {
+  os << "mode: " << s.mode << "\n";
+  os << "round_trip_count: " << s.round_trip_count << "\n";
+  os << "bandwidth_hi ~ lo ~ est: " << s.bandwidth_hi << " ~ " << s.bandwidth_lo
+     << " ~ " << s.bandwidth_est << "\n";
+  os << "min_rtt: " << s.min_rtt << "\n";
+  os << "min_rtt_timestamp: " << s.min_rtt_timestamp << "\n";
+  os << "congestion_window: " << s.congestion_window << "\n";
+  os << "pacing_rate: " << s.pacing_rate << "\n";
+  os << "last_sample_is_app_limited: " << s.last_sample_is_app_limited << "\n";
+
+  os << "startup: {full_bw_reached: " << s.startup.full_bandwidth_reached
+     << ", full_bw_baseline: " << s.startup.full_bandwidth_baseline
+     << ", rounds_without_growth: "
+     << s.startup.round_trips_without_bandwidth_growth << "}\n";
+  os << "drain: {drain_target: " << s.drain.drain_target << "}\n";
+  os << "probe_bw: {phase: " << ProbePhaseToString(s.probe_bw.phase)
+     << ", cycle_start_time: " << s.probe_bw.cycle_start_time
+     << ", phase_start_time: " << s.probe_bw.phase_start_time << "}\n";
+  os << "probe_rtt: {inflight_target: " << s.probe_rtt.inflight_target
+     << ", exit_time: " << s.probe_rtt.exit_time << "}\n";
+
+  return os;
+}
+
+Bbr2Mode Bbr3Sender::OnCongestionEventStartup(
+    const Bbr2CongestionEvent& congestion_event) {
+  if (model_.full_bandwidth_reached()) {
+    QUIC_BUG(quic_bug_10463_2)
+        << "In STARTUP, but full_bandwidth_reached is true.";
+    return Bbr2Mode::DRAIN;
+  }
+  if (!congestion_event.end_of_round_trip) {
+    return Bbr2Mode::STARTUP;
+  }
+  bool has_bandwidth_growth = model_.HasBandwidthGrowth(congestion_event);
+  if (params_.max_startup_queue_rounds > 0 && !has_bandwidth_growth) {
+    // 1.75 is less than the 2x CWND gain, but substantially more than 1.25x,
+    // the minimum bandwidth increase expected during STARTUP.
+    model_.CheckPersistentQueue(congestion_event, 1.75);
+  }
+  // TCP BBR always exits upon excessive losses. QUIC BBRv1 does not exit
+  // upon excessive losses, if enough bandwidth growth is observed or if the
+  // sample was app limited.
+  if (params_.always_exit_startup_on_excess_loss ||
+      (!congestion_event.last_packet_send_state.is_app_limited &&
+       !has_bandwidth_growth)) {
+    CheckExcessiveLosses(congestion_event);
+  }
+
+  if (params_.decrease_startup_pacing_at_end_of_round) {
+    QUICHE_DCHECK_GT(model_.pacing_gain(), 0);
+    if (!congestion_event.last_packet_send_state.is_app_limited) {
+      // Multiply by startup_pacing_gain, so if the bandwidth doubles,
+      // the pacing gain will be the full startup_pacing_gain.
+      if (startup_.max_bw_at_round_beginning > QuicBandwidth::Zero()) {
+        const float bandwidth_ratio = std::max(
+            1., model_.MaxBandwidth().ToBitsPerSecond() /
+                    static_cast<double>(
+                        startup_.max_bw_at_round_beginning.ToBitsPerSecond()));
+        // Even when bandwidth isn't increasing, use a gain large enough to
+        // cause a full_bw_threshold increase.
+        const float new_gain =
+            ((bandwidth_ratio - 1) *
+             (params_.startup_pacing_gain - params_.full_bw_threshold)) +
+            params_.full_bw_threshold;
+        // Allow the pacing gain to decrease.
+        model_.set_pacing_gain(std::min(params_.startup_pacing_gain, new_gain));
+        // Clear bandwidth_lo if it's less than the pacing rate.
+        // This avoids a constantly app-limited flow from having it's pacing
+        // gain effectively decreased below 1.25.
+        if (model_.bandwidth_lo() <
+            model_.MaxBandwidth() * model_.pacing_gain()) {
+          model_.clear_bandwidth_lo();
+        }
+      }
+      startup_.max_bw_at_round_beginning = model_.MaxBandwidth();
+    }
+  }
+
+  // TODO(wub): Maybe implement STARTUP => PROBE_RTT.
+  return model_.full_bandwidth_reached() ? Bbr2Mode::DRAIN : Bbr2Mode::STARTUP;
+}
+
+void Bbr3Sender::CheckExcessiveLosses(
+    const Bbr2CongestionEvent& congestion_event) {
+  QUICHE_DCHECK(congestion_event.end_of_round_trip);
+
+  if (model_.full_bandwidth_reached()) {
+    return;
+  }
+
+  // At the end of a round trip. Check if loss is too high in this round.
+  if (model_.IsInflightTooHigh(congestion_event,
+                               params_.startup_full_loss_count)) {
+    QuicByteCount new_inflight_hi = model_.BDP();
+    if (params_.startup_loss_exit_use_max_delivered_for_inflight_hi) {
+      if (new_inflight_hi < model_.max_bytes_delivered_in_round()) {
+        new_inflight_hi = model_.max_bytes_delivered_in_round();
+      }
+    }
+    QUIC_DVLOG(3) << this << " Exiting STARTUP due to loss at round "
+                  << model_.RoundTripCount()
+                  << ". inflight_hi:" << new_inflight_hi;
+    // TODO(ianswett): Add a shared method to set inflight_hi in the model.
+    model_.set_inflight_hi(new_inflight_hi);
+    model_.set_full_bandwidth_reached();
+    connection_stats_->bbr_exit_startup_due_to_loss = true;
+  }
+}
+
+Bbr2Mode Bbr3Sender::OnCongestionEventDrain(
+    const Bbr2CongestionEvent& congestion_event) {
+  model_.set_pacing_gain(params_.drain_pacing_gain);
+
+  // Only STARTUP can transition to DRAIN, both of them use the same cwnd gain.
+  QUICHE_DCHECK_EQ(model_.cwnd_gain(), params_.drain_cwnd_gain);
+  model_.set_cwnd_gain(params_.drain_cwnd_gain);
+
+  QuicByteCount drain_target = DrainTarget();
+  if (congestion_event.bytes_in_flight <= drain_target) {
+    QUIC_DVLOG(3) << this << " Exiting DRAIN. bytes_in_flight:"
+                  << congestion_event.bytes_in_flight
+                  << ", bdp:" << model_.BDP()
+                  << ", drain_target:" << drain_target << "  @ "
+                  << congestion_event.event_time;
+    return Bbr2Mode::PROBE_BW;
+  }
+
+  QUIC_DVLOG(3) << this << " Staying in DRAIN. bytes_in_flight:"
+                << congestion_event.bytes_in_flight << ", bdp:" << model_.BDP()
+                << ", drain_target:" << drain_target << "  @ "
+                << congestion_event.event_time;
+  return Bbr2Mode::DRAIN;
+}
+
+QuicByteCount Bbr3Sender::DrainTarget() const {
+  QuicByteCount bdp = model_.BDP();
+  return std::max<QuicByteCount>(bdp, GetMinimumCongestionWindow());
+}
+
+Bbr2Mode Bbr3Sender::OnCongestionEventProbeBw(
+    QuicByteCount prior_in_flight, QuicTime event_time,
+    const Bbr2CongestionEvent& congestion_event) {
+  QUICHE_DCHECK_NE(probe_bw_.phase, ProbePhase::PROBE_NOT_STARTED);
+
+  if (congestion_event.end_of_round_trip) {
+    if (probe_bw_.cycle_start_time != event_time) {
+      ++probe_bw_.rounds_since_probe;
+    }
+    if (probe_bw_.phase_start_time != event_time) {
+      ++probe_bw_.rounds_in_phase;
+    }
+  }
+
+  bool switch_to_probe_rtt = false;
+
+  if (probe_bw_.phase == ProbePhase::PROBE_UP) {
+    UpdateProbeUp(congestion_event);
+  } else if (probe_bw_.phase == ProbePhase::PROBE_DOWN) {
+    UpdateProbeDown(prior_in_flight, congestion_event);
+    // Maybe transition to PROBE_RTT at the end of this cycle.
+    if (probe_bw_.phase != ProbePhase::PROBE_DOWN &&
+        model_.MaybeExpireMinRtt(congestion_event)) {
+      switch_to_probe_rtt = true;
+    }
+  } else if (probe_bw_.phase == ProbePhase::PROBE_CRUISE) {
+    UpdateProbeCruise(congestion_event);
+  } else if (probe_bw_.phase == ProbePhase::PROBE_REFILL) {
+    UpdateProbeRefill(congestion_event);
+  }
+
+  // Do not need to set the gains if switching to PROBE_RTT, they will be set
+  // when Bbr2ProbeRttMode::Enter is called.
+  if (!switch_to_probe_rtt) {
+    model_.set_pacing_gain(PacingGainForPhase(probe_bw_.phase));
+    model_.set_cwnd_gain(params_.probe_bw_cwnd_gain);
+  }
+
+  return switch_to_probe_rtt ? Bbr2Mode::PROBE_RTT : Bbr2Mode::PROBE_BW;
+}
+
+void Bbr3Sender::UpdateProbeDown(QuicByteCount prior_in_flight,
+                                 const Bbr2CongestionEvent& congestion_event) {
+  QUICHE_DCHECK_EQ(probe_bw_.phase, ProbePhase::PROBE_DOWN);
+
+  if (probe_bw_.rounds_in_phase == 1 && congestion_event.end_of_round_trip) {
+    probe_bw_.is_sample_from_probing = false;
+
+    if (!congestion_event.last_packet_send_state.is_app_limited) {
+      QUIC_DVLOG(2)
+          << this << " Advancing max bw filter after one round in PROBE_DOWN.";
+      model_.AdvanceMaxBandwidthFilter();
+      probe_bw_.has_advanced_max_bw = true;
+    }
+
+    if (probe_bw_.last_cycle_stopped_risky_probe &&
+        !probe_bw_.last_cycle_probed_too_high) {
+      EnterProbeRefill(/*probe_up_rounds=*/0, congestion_event.event_time);
+      return;
+    }
+  }
+
+  MaybeAdaptUpperBounds(congestion_event);
+
+  if (IsTimeToProbeBandwidth(congestion_event)) {
+    EnterProbeRefill(/*probe_up_rounds=*/0, congestion_event.event_time);
+    return;
+  }
+
+  if (HasStayedLongEnoughInProbeDown(congestion_event)) {
+    QUIC_DVLOG(3) << this << " Proportional time based PROBE_DOWN exit";
+    EnterProbeCruise(congestion_event.event_time);
+    return;
+  }
+
+  const QuicByteCount inflight_with_headroom =
+      model_.inflight_hi_with_headroom();
+  QUIC_DVLOG(3)
+      << this << " Checking if have enough inflight headroom. prior_in_flight:"
+      << prior_in_flight << " congestion_event.bytes_in_flight:"
+      << congestion_event.bytes_in_flight
+      << ", inflight_with_headroom:" << inflight_with_headroom;
+  QuicByteCount bytes_in_flight = congestion_event.bytes_in_flight;
+
+  if (bytes_in_flight > inflight_with_headroom) {
+    // Stay in PROBE_DOWN.
+    return;
+  }
+
+  // Transition to PROBE_CRUISE iff we've drained to target.
+  QuicByteCount bdp = model_.BDP();
+  QUIC_DVLOG(3) << this << " Checking if drained to target. bytes_in_flight:"
+                << bytes_in_flight << ", bdp:" << bdp;
+  if (bytes_in_flight < bdp) {
+    EnterProbeCruise(congestion_event.event_time);
+  }
+}
+
+Bbr3Sender::AdaptUpperBoundsResult Bbr3Sender::MaybeAdaptUpperBounds(
+    const Bbr2CongestionEvent& congestion_event) {
+  const SendTimeState& send_state = congestion_event.last_packet_send_state;
+  if (!send_state.is_valid) {
+    QUIC_DVLOG(3) << this << " " << ProbePhaseToString(probe_bw_.phase)
+                  << ": NOT_ADAPTED_INVALID_SAMPLE";
+    return NOT_ADAPTED_INVALID_SAMPLE;
+  }
+
+  QuicByteCount inflight_at_send = BytesInFlight(send_state);
+  if (params_.use_bytes_delivered_for_inflight_hi) {
+    if (congestion_event.last_packet_send_state.total_bytes_acked <=
+        model_.total_bytes_acked()) {
+      inflight_at_send =
+          model_.total_bytes_acked() -
+          congestion_event.last_packet_send_state.total_bytes_acked;
+    } else {
+      QUIC_BUG(quic_bug_10463_3)
+          << "Total_bytes_acked(" << model_.total_bytes_acked()
+          << ") < send_state.total_bytes_acked("
+          << congestion_event.last_packet_send_state.total_bytes_acked << ")";
+    }
+  }
+  if (model_.IsInflightTooHigh(congestion_event,
+                               params_.probe_bw_full_loss_count)) {
+    if (probe_bw_.is_sample_from_probing) {
+      probe_bw_.is_sample_from_probing = false;
+      if (!send_state.is_app_limited || params_.max_probe_up_queue_rounds > 0) {
+        const QuicByteCount inflight_target =
+            GetTargetBytesInflight() * (1.0 - params_.beta);
+        if (params_.limit_inflight_hi_by_max_delivered) {
+          QuicByteCount new_inflight_hi =
+              std::max(inflight_at_send, inflight_target);
+          if (new_inflight_hi < model_.max_bytes_delivered_in_round()) {
+            new_inflight_hi = model_.max_bytes_delivered_in_round();
+          }
+          QUIC_DVLOG(3) << this
+                        << " Setting inflight_hi due to loss. new_inflight_hi:"
+                        << new_inflight_hi
+                        << ", inflight_at_send:" << inflight_at_send
+                        << ", inflight_target:" << inflight_target
+                        << ", max_bytes_delivered_in_round:"
+                        << model_.max_bytes_delivered_in_round() << "  @ "
+                        << congestion_event.event_time;
+          model_.set_inflight_hi(new_inflight_hi);
+        } else {
+          model_.set_inflight_hi(std::max(inflight_at_send, inflight_target));
+        }
+      }
+
+      QUIC_DVLOG(3) << this << " " << ProbePhaseToString(probe_bw_.phase)
+                    << ": ADAPTED_PROBED_TOO_HIGH";
+      return ADAPTED_PROBED_TOO_HIGH;
+    }
+    return ADAPTED_OK;
+  }
+
+  if (model_.inflight_hi() == model_.inflight_hi_default()) {
+    QUIC_DVLOG(3) << this << " " << ProbePhaseToString(probe_bw_.phase)
+                  << ": NOT_ADAPTED_INFLIGHT_HIGH_NOT_SET";
+    return NOT_ADAPTED_INFLIGHT_HIGH_NOT_SET;
+  }
+
+  // Raise the upper bound for inflight.
+  if (inflight_at_send > model_.inflight_hi()) {
+    QUIC_DVLOG(3)
+        << this << " " << ProbePhaseToString(probe_bw_.phase)
+        << ": Adapting inflight_hi from inflight_at_send. inflight_at_send:"
+        << inflight_at_send << ", old inflight_hi:" << model_.inflight_hi();
+    model_.set_inflight_hi(inflight_at_send);
+  }
+
+  return ADAPTED_OK;
+}
+
+bool Bbr3Sender::IsTimeToProbeBandwidth(
+    const Bbr2CongestionEvent& congestion_event) const {
+  if (HasCycleLasted(probe_bw_.probe_wait_time, congestion_event)) {
+    return true;
+  }
+
+  if (IsTimeToProbeForRenoCoexistence(1.0, congestion_event)) {
+    ++connection_stats_->bbr_num_short_cycles_for_reno_coexistence;
+    return true;
+  }
+  return false;
+}
+
+bool Bbr3Sender::HasStayedLongEnoughInProbeDown(
+    const Bbr2CongestionEvent& congestion_event) const {
+  return HasPhaseLasted(model_.MinRtt(), congestion_event);
+}
+
+bool Bbr3Sender::HasCycleLasted(
+    QuicTime::Delta duration,
+    const Bbr2CongestionEvent& congestion_event) const {
+  bool result =
+      (congestion_event.event_time - probe_bw_.cycle_start_time) > duration;
+  QUIC_DVLOG(3) << this << " " << ProbePhaseToString(probe_bw_.phase)
+                << ": HasCycleLasted=" << result << ". elapsed:"
+                << (congestion_event.event_time - probe_bw_.cycle_start_time)
+                << ", duration:" << duration;
+  return result;
+}
+
+bool Bbr3Sender::HasPhaseLasted(
+    QuicTime::Delta duration,
+    const Bbr2CongestionEvent& congestion_event) const {
+  bool result =
+      (congestion_event.event_time - probe_bw_.phase_start_time) > duration;
+  QUIC_DVLOG(3) << this << " " << ProbePhaseToString(probe_bw_.phase)
+                << ": HasPhaseLasted=" << result << ". elapsed:"
+                << (congestion_event.event_time - probe_bw_.phase_start_time)
+                << ", duration:" << duration;
+  return result;
+}
+
+bool Bbr3Sender::IsTimeToProbeForRenoCoexistence(
+    double probe_wait_fraction,
+    const Bbr2CongestionEvent& /*congestion_event*/) const {
+  if (!params_.enable_reno_coexistence) {
+    return false;
+  }
+
+  uint64_t rounds = params_.probe_bw_probe_max_rounds;
+  if (params_.probe_bw_probe_reno_gain > 0.0) {
+    QuicByteCount target_bytes_inflight = GetTargetBytesInflight();
+    uint64_t reno_rounds = params_.probe_bw_probe_reno_gain *
+                           target_bytes_inflight / kDefaultTCPMSS;
+    rounds = std::min(rounds, reno_rounds);
+  }
+  bool result = probe_bw_.rounds_since_probe >= (rounds * probe_wait_fraction);
+  QUIC_DVLOG(3) << this << " " << ProbePhaseToString(probe_bw_.phase)
+                << ": IsTimeToProbeForRenoCoexistence=" << result
+                << ". rounds_since_probe:" << probe_bw_.rounds_since_probe
+                << ", rounds:" << rounds
+                << ", probe_wait_fraction:" << probe_wait_fraction;
+  return result;
+}
+
+void Bbr3Sender::RaiseInflightHighSlope() {
+  QUICHE_DCHECK_EQ(probe_bw_.phase, ProbePhase::PROBE_UP);
+  uint64_t growth_this_round = 1 << probe_bw_.probe_up_rounds;
+  // The number 30 below means |growth_this_round| is capped at 1G and the lower
+  // bound of |probe_up_bytes| is (practically) 1 mss, at this speed inflight_hi
+  // grows by approximately 1 packet per packet acked.
+  probe_bw_.probe_up_rounds =
+      std::min<uint64_t>(probe_bw_.probe_up_rounds + 1, 30);
+  uint64_t probe_up_bytes = GetCongestionWindow() / growth_this_round;
+  probe_bw_.probe_up_bytes =
+      std::max<QuicByteCount>(probe_up_bytes, kDefaultTCPMSS);
+  QUIC_DVLOG(3) << this << " Rasing inflight_hi slope. probe_up_rounds:"
+                << probe_bw_.probe_up_rounds
+                << ", probe_up_bytes:" << probe_bw_.probe_up_bytes;
+}
+
+void Bbr3Sender::ProbeInflightHighUpward(
+    const Bbr2CongestionEvent& congestion_event) {
+  QUICHE_DCHECK_EQ(probe_bw_.phase, ProbePhase::PROBE_UP);
+  if (params_.probe_up_ignore_inflight_hi) {
+    return;
+  }
+  if (params_.probe_up_simplify_inflight_hi) {
+    // Raise inflight_hi exponentially if it was utilized this round.
+    probe_bw_.probe_up_acked += congestion_event.bytes_acked;
+    if (!congestion_event.end_of_round_trip) {
+      return;
+    }
+    if (!model_.inflight_hi_limited_in_round() ||
+        model_.loss_events_in_round() > 0) {
+      probe_bw_.probe_up_acked = 0;
+      return;
+    }
+  } else {
+    if (congestion_event.prior_bytes_in_flight < congestion_event.prior_cwnd) {
+      QUIC_DVLOG(3) << this
+                    << " Raising inflight_hi early return: Not cwnd limited.";
+      // Not fully utilizing cwnd, so can't safely grow.
+      return;
+    }
+
+    if (congestion_event.prior_cwnd < model_.inflight_hi()) {
+      QUIC_DVLOG(3)
+          << this
+          << " Raising inflight_hi early return: inflight_hi not fully used.";
+      // Not fully using inflight_hi, so don't grow it.
+      return;
+    }
+
+    // Increase inflight_hi by the number of probe_up_bytes within
+    // probe_up_acked.
+    probe_bw_.probe_up_acked += congestion_event.bytes_acked;
+  }
+
+  if (probe_bw_.probe_up_acked >= probe_bw_.probe_up_bytes) {
+    uint64_t delta = probe_bw_.probe_up_acked / probe_bw_.probe_up_bytes;
+    probe_bw_.probe_up_acked -= delta * probe_bw_.probe_up_bytes;
+    QuicByteCount new_inflight_hi =
+        model_.inflight_hi() + delta * kDefaultTCPMSS;
+    if (new_inflight_hi > model_.inflight_hi()) {
+      QUIC_DVLOG(3) << this << " Raising inflight_hi from "
+                    << model_.inflight_hi() << " to " << new_inflight_hi
+                    << ". probe_up_bytes:" << probe_bw_.probe_up_bytes
+                    << ", delta:" << delta
+                    << ", (new)probe_up_acked:" << probe_bw_.probe_up_acked;
+
+      model_.set_inflight_hi(new_inflight_hi);
+    } else {
+      QUIC_BUG(quic_bug_10463_4)
+          << "Not growing inflight_hi due to wrap around. Old value:"
+          << model_.inflight_hi() << ", new value:" << new_inflight_hi;
+    }
+  }
+
+  if (congestion_event.end_of_round_trip) {
+    RaiseInflightHighSlope();
+  }
+}
+
+void Bbr3Sender::UpdateProbeCruise(
+    const Bbr2CongestionEvent& congestion_event) {
+  QUICHE_DCHECK_EQ(probe_bw_.phase, ProbePhase::PROBE_CRUISE);
+  MaybeAdaptUpperBounds(congestion_event);
+  QUICHE_DCHECK(!probe_bw_.is_sample_from_probing);
+
+  if (IsTimeToProbeBandwidth(congestion_event)) {
+    EnterProbeRefill(/*probe_up_rounds=*/0, congestion_event.event_time);
+    return;
+  }
+}
+
+void Bbr3Sender::UpdateProbeRefill(
+    const Bbr2CongestionEvent& congestion_event) {
+  QUICHE_DCHECK_EQ(probe_bw_.phase, ProbePhase::PROBE_REFILL);
+  MaybeAdaptUpperBounds(congestion_event);
+  QUICHE_DCHECK(!probe_bw_.is_sample_from_probing);
+
+  if (probe_bw_.rounds_in_phase > 0 && congestion_event.end_of_round_trip) {
+    EnterProbeUp(congestion_event.event_time);
+    return;
+  }
+}
+
+void Bbr3Sender::UpdateProbeUp(const Bbr2CongestionEvent& congestion_event) {
+  QUICHE_DCHECK_EQ(probe_bw_.phase, ProbePhase::PROBE_UP);
+  if (MaybeAdaptUpperBounds(congestion_event) == ADAPTED_PROBED_TOO_HIGH) {
+    EnterProbeDown(/*probed_too_high=*/true, /*stopped_risky_probe=*/false,
+                   congestion_event.event_time);
+    return;
+  }
+
+  ProbeInflightHighUpward(congestion_event);
+
+  bool is_risky = false;
+  bool is_queuing = false;
+  if (probe_bw_.last_cycle_probed_too_high &&
+      congestion_event.prior_bytes_in_flight >= model_.inflight_hi()) {
+    is_risky = true;
+    QUIC_DVLOG(3) << this << " Probe is too risky. last_cycle_probed_too_high:"
+                  << probe_bw_.last_cycle_probed_too_high
+                  << ", prior_in_flight:"
+                  << congestion_event.prior_bytes_in_flight
+                  << ", inflight_hi:" << model_.inflight_hi();
+  } else if (probe_bw_.rounds_in_phase > 0) {
+    if (params_.max_probe_up_queue_rounds > 0) {
+      if (congestion_event.end_of_round_trip) {
+        model_.CheckPersistentQueue(congestion_event,
+                                    params_.full_bw_threshold);
+        if (model_.rounds_with_queueing() >=
+            params_.max_probe_up_queue_rounds) {
+          is_queuing = true;
+        }
+      }
+    } else {
+      QuicByteCount queuing_threshold_extra_bytes =
+          model_.QueueingThresholdExtraBytes();
+      if (params_.add_ack_height_to_queueing_threshold) {
+        queuing_threshold_extra_bytes += model_.MaxAckHeight();
+      }
+      QuicByteCount queuing_threshold =
+          (params_.full_bw_threshold * model_.BDP()) +
+          queuing_threshold_extra_bytes;
+
+      is_queuing = congestion_event.bytes_in_flight >= queuing_threshold;
+
+      QUIC_DVLOG(3) << this
+                    << " Checking if building up a queue. prior_in_flight:"
+                    << congestion_event.prior_bytes_in_flight
+                    << ", post_in_flight:" << congestion_event.bytes_in_flight
+                    << ", threshold:" << queuing_threshold
+                    << ", is_queuing:" << is_queuing
+                    << ", max_bw:" << model_.MaxBandwidth()
+                    << ", min_rtt:" << model_.MinRtt();
+    }
+  }
+
+  if (is_risky || is_queuing) {
+    EnterProbeDown(/*probed_too_high=*/false, /*stopped_risky_probe=*/is_risky,
+                   congestion_event.event_time);
+  }
+}
+
+void Bbr3Sender::EnterProbeDown(bool probed_too_high, bool stopped_risky_probe,
+                                QuicTime now) {
+  QUIC_DVLOG(2) << this
+                << " Phase change: " << ProbePhaseToString(probe_bw_.phase)
+                << " ==> "
+                << "PROBE_DOWN" << " after " << now - probe_bw_.phase_start_time
+                << ", or " << probe_bw_.rounds_in_phase
+                << " rounds. probed_too_high:" << probed_too_high
+                << ", stopped_risky_probe:" << stopped_risky_probe << "  @ "
+                << now;
+  probe_bw_.last_cycle_probed_too_high = probed_too_high;
+  probe_bw_.last_cycle_stopped_risky_probe = stopped_risky_probe;
+
+  probe_bw_.cycle_start_time = now;
+  probe_bw_.phase = ProbePhase::PROBE_DOWN;
+  probe_bw_.rounds_in_phase = 0;
+  probe_bw_.phase_start_time = now;
+  ++connection_stats_->bbr_num_cycles;
+  if (params_.bw_lo_mode_ != Bbr2Params::QuicBandwidthLoMode::DEFAULT) {
+    model_.clear_bandwidth_lo();
+  }
+
+  // Pick probe wait time.
+  probe_bw_.rounds_since_probe =
+      RandomUint64(params_.probe_bw_max_probe_rand_rounds);
+  probe_bw_.probe_wait_time =
+      params_.probe_bw_probe_base_duration +
+      QuicTime::Delta::FromMicroseconds(RandomUint64(
+          params_.probe_bw_probe_max_rand_duration.ToMicroseconds()));
+
+  probe_bw_.probe_up_bytes = std::numeric_limits<QuicByteCount>::max();
+  probe_bw_.probe_up_app_limited_since_inflight_hi_limited_ = false;
+  probe_bw_.has_advanced_max_bw = false;
+  model_.RestartRoundEarly();
+}
+
+void Bbr3Sender::EnterProbeCruise(QuicTime now) {
+  if (probe_bw_.phase == ProbePhase::PROBE_DOWN) {
+    ExitProbeDown();
+  }
+  QUIC_DVLOG(2) << this
+                << " Phase change: " << ProbePhaseToString(probe_bw_.phase)
+                << " ==> "
+                << "PROBE_CRUISE" << " after "
+                << now - probe_bw_.phase_start_time << ", or "
+                << probe_bw_.rounds_in_phase << " rounds.  @ " << now;
+
+  model_.cap_inflight_lo(model_.inflight_hi());
+  probe_bw_.phase = ProbePhase::PROBE_CRUISE;
+  probe_bw_.rounds_in_phase = 0;
+  probe_bw_.phase_start_time = now;
+  probe_bw_.is_sample_from_probing = false;
+}
+
+void Bbr3Sender::EnterProbeRefill(uint64_t probe_up_rounds, QuicTime now) {
+  if (probe_bw_.phase == ProbePhase::PROBE_DOWN) {
+    ExitProbeDown();
+  }
+  QUIC_DVLOG(2) << this
+                << " Phase change: " << ProbePhaseToString(probe_bw_.phase)
+                << " ==> "
+                << "PROBE_REFILL" << " after "
+                << now - probe_bw_.phase_start_time << ", or "
+                << probe_bw_.rounds_in_phase
+                << " rounds. probe_up_rounds:" << probe_up_rounds << "  @ "
+                << now;
+  probe_bw_.phase = ProbePhase::PROBE_REFILL;
+  probe_bw_.rounds_in_phase = 0;
+  probe_bw_.phase_start_time = now;
+  probe_bw_.is_sample_from_probing = false;
+  probe_bw_.last_cycle_stopped_risky_probe = false;
+
+  model_.clear_bandwidth_lo();
+  model_.clear_inflight_lo();
+  probe_bw_.probe_up_rounds = probe_up_rounds;
+  probe_bw_.probe_up_acked = 0;
+  model_.RestartRoundEarly();
+}
+
+void Bbr3Sender::EnterProbeUp(QuicTime now) {
+  QUICHE_DCHECK_EQ(probe_bw_.phase, ProbePhase::PROBE_REFILL);
+  QUIC_DVLOG(2) << this
+                << " Phase change: " << ProbePhaseToString(probe_bw_.phase)
+                << " ==> "
+                << "PROBE_UP" << " after " << now - probe_bw_.phase_start_time
+                << ", or " << probe_bw_.rounds_in_phase << " rounds.  @ "
+                << now;
+  probe_bw_.phase = ProbePhase::PROBE_UP;
+  probe_bw_.rounds_in_phase = 0;
+  probe_bw_.phase_start_time = now;
+  probe_bw_.is_sample_from_probing = true;
+  RaiseInflightHighSlope();
+
+  model_.RestartRoundEarly();
+}
+
+void Bbr3Sender::ExitProbeDown() {
+  QUICHE_DCHECK_EQ(probe_bw_.phase, ProbePhase::PROBE_DOWN);
+  if (!probe_bw_.has_advanced_max_bw) {
+    QUIC_DVLOG(2) << this << " Advancing max bw filter at end of cycle.";
+    model_.AdvanceMaxBandwidthFilter();
+    probe_bw_.has_advanced_max_bw = true;
+  }
+}
+
+void Bbr3Sender::EnterProbeRtt() {
+  model_.set_pacing_gain(1.0);
+  model_.set_cwnd_gain(1.0);
+  probe_rtt_.exit_time = QuicTime::Zero();
+}
+
+Bbr2Mode Bbr3Sender::OnCongestionEventProbeRtt(
+    const Bbr2CongestionEvent& congestion_event) {
+  if (probe_rtt_.exit_time == QuicTime::Zero()) {
+    if (congestion_event.bytes_in_flight <= InflightTarget() ||
+        congestion_event.bytes_in_flight <= GetMinimumCongestionWindow()) {
+      probe_rtt_.exit_time =
+          congestion_event.event_time + params_.probe_rtt_duration;
+      QUIC_DVLOG(2) << this << " PROBE_RTT exit time set to "
+                    << probe_rtt_.exit_time
+                    << ". bytes_inflight:" << congestion_event.bytes_in_flight
+                    << ", inflight_target:" << InflightTarget()
+                    << ", min_congestion_window:"
+                    << GetMinimumCongestionWindow() << "  @ "
+                    << congestion_event.event_time;
+    }
+    return Bbr2Mode::PROBE_RTT;
+  }
+
+  return congestion_event.event_time > probe_rtt_.exit_time
+             ? Bbr2Mode::PROBE_BW
+             : Bbr2Mode::PROBE_RTT;
+}
+
+QuicByteCount Bbr3Sender::InflightTarget() const {
+  return model_.BDP(model_.MaxBandwidth(),
+                    params_.probe_rtt_inflight_target_bdp_fraction);
+}
+
+float Bbr3Sender::PacingGainForPhase(ProbePhase phase) const {
+  if (phase == ProbePhase::PROBE_UP) {
+    return params_.probe_bw_probe_up_pacing_gain;
+  }
+  if (phase == ProbePhase::PROBE_DOWN) {
+    return params_.probe_bw_probe_down_pacing_gain;
+  }
+  return params_.probe_bw_default_pacing_gain;
+}
+
+}  // namespace quic
diff --git a/quiche/quic/core/congestion_control/bbr3_sender.h b/quiche/quic/core/congestion_control/bbr3_sender.h
new file mode 100644
index 0000000..6f61233
--- /dev/null
+++ b/quiche/quic/core/congestion_control/bbr3_sender.h
@@ -0,0 +1,300 @@
+// Copyright 2026 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.
+
+#ifndef QUICHE_QUIC_CORE_CONGESTION_CONTROL_BBR3_SENDER_H_
+#define QUICHE_QUIC_CORE_CONGESTION_CONTROL_BBR3_SENDER_H_
+
+#include <cstdint>
+
+#include "quiche/quic/core/congestion_control/bandwidth_sampler.h"
+#include "quiche/quic/core/congestion_control/bbr2_misc.h"
+#include "quiche/quic/core/congestion_control/bbr_sender.h"
+#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/windowed_filter.h"
+#include "quiche/quic/core/quic_bandwidth.h"
+#include "quiche/quic/core/quic_types.h"
+#include "quiche/quic/platform/api/quic_export.h"
+#include "quiche/quic/platform/api/quic_flags.h"
+
+namespace quic {
+
+class QUICHE_EXPORT Bbr3Sender final : public SendAlgorithmInterface {
+ public:
+  Bbr3Sender(QuicTime now, const RttStats* rtt_stats,
+             const QuicUnackedPacketMap* unacked_packets,
+             QuicPacketCount initial_cwnd_in_packets,
+             QuicPacketCount max_cwnd_in_packets, QuicRandom* random,
+             QuicConnectionStats* stats, BbrSender* old_sender);
+
+  ~Bbr3Sender() override = default;
+
+  // Start implementation of SendAlgorithmInterface.
+  bool InSlowStart() const override { return mode_ == Bbr2Mode::STARTUP; }
+
+  bool InRecovery() const override {
+    // TODO(wub): Implement Recovery.
+    return false;
+  }
+
+  void SetFromConfig(const QuicConfig& config,
+                     Perspective perspective) override;
+
+  void ApplyConnectionOptions(const QuicTagVector& connection_options) override;
+
+  void AdjustNetworkParameters(const NetworkParams& params) override;
+
+  void SetInitialCongestionWindowInPackets(
+      QuicPacketCount congestion_window) override;
+
+  void SetApplicationDrivenPacingRate(
+      QuicBandwidth application_bandwidth_target) override;
+
+  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;
+
+  void OnPacketSent(QuicTime sent_time, QuicByteCount bytes_in_flight,
+                    QuicPacketNumber packet_number, QuicByteCount bytes,
+                    HasRetransmittableData is_retransmittable) override;
+
+  void OnPacketNeutered(QuicPacketNumber packet_number) override;
+
+  void OnRetransmissionTimeout(bool /*packets_retransmitted*/) override {}
+
+  void OnConnectionMigration() override {}
+
+  bool CanSend(QuicByteCount bytes_in_flight) override;
+
+  QuicBandwidth PacingRate(QuicByteCount bytes_in_flight) const override;
+
+  QuicBandwidth BandwidthEstimate() const override {
+    return model_.BandwidthEstimate();
+  }
+
+  bool HasGoodBandwidthEstimateForResumption() const override {
+    return has_non_app_limited_sample_;
+  }
+
+  QuicByteCount GetCongestionWindow() const override;
+
+  QuicByteCount GetSlowStartThreshold() const override { return 0; }
+
+  CongestionControlType GetCongestionControlType() const override {
+    return kBBRv2;
+  }
+
+  std::string GetDebugState() const override;
+
+  void OnApplicationLimited(QuicByteCount bytes_in_flight) override;
+
+  void PopulateConnectionStats(QuicConnectionStats* stats) const override;
+
+  bool EnableECT0() override { return false; }
+  bool EnableECT1() override { return false; }
+  void ReduceMemoryUsage() override { model_.ReduceMemoryUsage(); }
+  // End implementation of SendAlgorithmInterface.
+
+  const Bbr2Params& Params() const { return params_; }
+
+  QuicByteCount GetMinimumCongestionWindow() const {
+    return params_.cwnd_limits.Min();
+  }
+
+  // Returns the min of BDP and congestion window.
+  QuicByteCount GetTargetBytesInflight() const;
+
+  bool IsBandwidthOverestimateAvoidanceEnabled() const {
+    return model_.IsBandwidthOverestimateAvoidanceEnabled();
+  }
+
+  struct QUICHE_EXPORT DebugState {
+    Bbr2Mode mode;
+
+    // Shared states.
+    QuicRoundTripCount round_trip_count;
+    QuicBandwidth bandwidth_hi = QuicBandwidth::Zero();
+    QuicBandwidth bandwidth_lo = QuicBandwidth::Zero();
+    QuicBandwidth bandwidth_est = QuicBandwidth::Zero();
+    QuicByteCount inflight_hi;
+    QuicByteCount inflight_lo;
+    QuicByteCount max_ack_height;
+    QuicTime::Delta min_rtt = QuicTime::Delta::Zero();
+    QuicTime min_rtt_timestamp = QuicTime::Zero();
+    QuicByteCount congestion_window;
+    QuicBandwidth pacing_rate = QuicBandwidth::Zero();
+    bool last_sample_is_app_limited;
+    QuicPacketNumber end_of_app_limited_phase;
+
+    // Mode-specific states.
+    struct QUICHE_EXPORT Startup {
+      bool full_bandwidth_reached = false;
+      QuicBandwidth full_bandwidth_baseline = QuicBandwidth::Zero();
+      QuicRoundTripCount round_trips_without_bandwidth_growth = 0;
+    } startup;
+
+    struct QUICHE_EXPORT Drain {
+      QuicByteCount drain_target = 0;
+    } drain;
+
+    struct QUICHE_EXPORT ProbeBw {
+      ProbePhase phase = ProbePhase::PROBE_NOT_STARTED;
+      QuicTime cycle_start_time = QuicTime::Zero();
+      QuicTime phase_start_time = QuicTime::Zero();
+    } probe_bw;
+
+    struct QUICHE_EXPORT ProbeRtt {
+      QuicByteCount inflight_target = 0;
+      QuicTime exit_time = QuicTime::Zero();
+    } probe_rtt;
+  };
+
+  DebugState ExportDebugState() const;
+
+  const Bbr2NetworkModel& GetNetworkModel() const { return model_; }
+
+ private:
+  void UpdatePacingRate(QuicByteCount bytes_acked);
+  void UpdateCongestionWindow(QuicByteCount bytes_acked);
+  QuicByteCount GetTargetCongestionWindow(float gain) const;
+  // Helper function for Bbr2Mode transitions.
+  void LeaveStartup(QuicTime now);
+  Bbr2Mode OnCongestionEventStartup(
+      const Bbr2CongestionEvent& congestion_event);
+  void CheckExcessiveLosses(const Bbr2CongestionEvent& congestion_event);
+
+  Bbr2Mode OnCongestionEventDrain(const Bbr2CongestionEvent& congestion_event);
+  QuicByteCount DrainTarget() const;
+
+  void OnEnterQuiescence(QuicTime now);
+  void OnExitQuiescence(QuicTime now);
+
+  Bbr2Mode OnCongestionEventProbeBw(
+      QuicByteCount prior_in_flight, QuicTime event_time,
+      const Bbr2CongestionEvent& congestion_event);
+
+  void UpdateProbeUp(const Bbr2CongestionEvent& congestion_event);
+  void UpdateProbeDown(QuicByteCount prior_in_flight,
+                       const Bbr2CongestionEvent& congestion_event);
+  void UpdateProbeCruise(const Bbr2CongestionEvent& congestion_event);
+  void UpdateProbeRefill(const Bbr2CongestionEvent& congestion_event);
+
+  enum AdaptUpperBoundsResult : uint8_t {
+    ADAPTED_OK,
+    ADAPTED_PROBED_TOO_HIGH,
+    NOT_ADAPTED_INFLIGHT_HIGH_NOT_SET,
+    NOT_ADAPTED_INVALID_SAMPLE,
+  };
+
+  AdaptUpperBoundsResult MaybeAdaptUpperBounds(
+      const Bbr2CongestionEvent& congestion_event);
+
+  void EnterProbeDown(bool probed_too_high, bool stopped_risky_probe,
+                      QuicTime now);
+  void EnterProbeCruise(QuicTime now);
+  void EnterProbeRefill(uint64_t probe_up_rounds, QuicTime now);
+  void EnterProbeUp(QuicTime now);
+  void ExitProbeDown();
+
+  bool IsTimeToProbeBandwidth(
+      const Bbr2CongestionEvent& congestion_event) const;
+  bool HasStayedLongEnoughInProbeDown(
+      const Bbr2CongestionEvent& congestion_event) const;
+  bool HasCycleLasted(QuicTime::Delta duration,
+                      const Bbr2CongestionEvent& congestion_event) const;
+  bool HasPhaseLasted(QuicTime::Delta duration,
+                      const Bbr2CongestionEvent& congestion_event) const;
+  bool IsTimeToProbeForRenoCoexistence(
+      double probe_wait_fraction,
+      const Bbr2CongestionEvent& congestion_event) const;
+
+  void RaiseInflightHighSlope();
+  void ProbeInflightHighUpward(const Bbr2CongestionEvent& congestion_event);
+  float PacingGainForPhase(ProbePhase phase) const;
+
+  void EnterProbeRtt();
+  Bbr2Mode OnCongestionEventProbeRtt(
+      const Bbr2CongestionEvent& congestion_event);
+  QuicByteCount InflightTarget() const;
+
+  uint64_t RandomUint64(uint64_t max) const {
+    return random_->RandUint64() % max;
+  }
+
+  // Cwnd limits imposed by the current Bbr2 mode.
+  Limits<QuicByteCount> GetCwndLimitsByMode() const;
+
+  // Cwnd limits imposed by caller.
+
+  Bbr2Mode mode_;
+
+  const RttStats* const rtt_stats_;
+  const QuicUnackedPacketMap* const unacked_packets_;
+  QuicRandom* random_;
+  QuicConnectionStats* connection_stats_;
+
+  // Don't use it directly outside of SetFromConfig and ApplyConnectionOptions.
+  // Instead, use params() to get read-only access.
+  Bbr2Params params_;
+
+  Bbr2NetworkModel model_;
+
+  const QuicByteCount initial_cwnd_;
+
+  // Current cwnd and pacing rate.
+  QuicByteCount cwnd_;
+  QuicBandwidth pacing_rate_;
+
+  QuicTime last_quiescence_start_ = QuicTime::Zero();
+
+  // Max congestion window when adjusting network parameters.
+  QuicByteCount max_cwnd_when_network_parameters_adjusted_ =
+      kMaxInitialCongestionWindow * kDefaultTCPMSS;
+
+  // Startup state.
+  struct StartupState {
+    QuicBandwidth max_bw_at_round_beginning = QuicBandwidth::Zero();
+  } startup_;
+
+  // Probe BW state.
+  struct ProbeBWState {
+    QuicTime cycle_start_time = QuicTime::Zero();
+    ProbePhase phase = ProbePhase::PROBE_NOT_STARTED;
+    uint64_t rounds_in_phase = 0;
+    QuicTime phase_start_time = QuicTime::Zero();
+    QuicRoundTripCount rounds_since_probe = 0;
+    QuicTime::Delta probe_wait_time = QuicTime::Delta::Zero();
+    uint64_t probe_up_rounds = 0;
+    QuicByteCount probe_up_bytes = std::numeric_limits<QuicByteCount>::max();
+    QuicByteCount probe_up_acked = 0;
+    bool probe_up_app_limited_since_inflight_hi_limited_ = false;
+    // Whether max bandwidth filter window has advanced in this cycle. It is
+    // advanced once per cycle.
+    bool has_advanced_max_bw = false;
+    bool is_sample_from_probing = false;
+
+    bool last_cycle_probed_too_high = false;
+    bool last_cycle_stopped_risky_probe = false;
+  } probe_bw_;
+
+  // Probe RTT state.
+  struct ProbeRTTState {
+    QuicTime exit_time = QuicTime::Zero();
+  } probe_rtt_;
+
+  bool has_non_app_limited_sample_ = false;
+
+  // Debug only.
+  bool last_sample_is_app_limited_;
+};
+
+QUICHE_EXPORT std::ostream& operator<<(std::ostream& os,
+                                       const Bbr3Sender::DebugState& state);
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_CORE_CONGESTION_CONTROL_BBR3_SENDER_H_
diff --git a/quiche/quic/core/congestion_control/bbr3_simulator_test.cc b/quiche/quic/core/congestion_control/bbr3_simulator_test.cc
new file mode 100644
index 0000000..0adb9d1
--- /dev/null
+++ b/quiche/quic/core/congestion_control/bbr3_simulator_test.cc
@@ -0,0 +1,2611 @@
+// Copyright 2026 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 <algorithm>
+#include <array>
+#include <memory>
+#include <optional>
+#include <sstream>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "absl/strings/str_cat.h"
+#include "quiche/quic/core/congestion_control/bbr2_misc.h"
+#include "quiche/quic/core/congestion_control/bbr3_sender.h"
+#include "quiche/quic/core/congestion_control/bbr_sender.h"
+#include "quiche/quic/core/congestion_control/tcp_cubic_sender_bytes.h"
+#include "quiche/quic/core/quic_bandwidth.h"
+#include "quiche/quic/core/quic_packet_number.h"
+#include "quiche/quic/core/quic_types.h"
+#include "quiche/quic/platform/api/quic_expect_bug.h"
+#include "quiche/quic/platform/api/quic_flags.h"
+#include "quiche/quic/platform/api/quic_logging.h"
+#include "quiche/quic/platform/api/quic_test.h"
+#include "quiche/quic/test_tools/quic_config_peer.h"
+#include "quiche/quic/test_tools/quic_connection_peer.h"
+#include "quiche/quic/test_tools/quic_sent_packet_manager_peer.h"
+#include "quiche/quic/test_tools/quic_test_utils.h"
+#include "quiche/quic/test_tools/send_algorithm_test_result.pb.h"
+#include "quiche/quic/test_tools/send_algorithm_test_utils.h"
+#include "quiche/quic/test_tools/simulator/link.h"
+#include "quiche/quic/test_tools/simulator/quic_endpoint.h"
+#include "quiche/quic/test_tools/simulator/simulator.h"
+#include "quiche/quic/test_tools/simulator/switch.h"
+#include "quiche/quic/test_tools/simulator/traffic_policer.h"
+#include "quiche/common/platform/api/quiche_command_line_flags.h"
+
+using testing::AllOf;
+using testing::Ge;
+using testing::Le;
+
+DEFINE_QUICHE_COMMAND_LINE_FLAG(
+    std::string, quic_bbr3_test_regression_mode, "",
+    "One of a) 'record' to record test result (one file per test), or "
+    "b) 'regress' to regress against recorded results, or "
+    "c) <anything else> for non-regression mode.");
+
+namespace quic {
+
+namespace test {
+
+// Use the initial CWND of 10, as 32 is too much for the test network.
+const uint32_t kDefaultInitialCwndPackets = 10;
+const uint32_t kDefaultInitialCwndBytes =
+    kDefaultInitialCwndPackets * kDefaultTCPMSS;
+
+struct LinkParams {
+  LinkParams(int64_t kilo_bits_per_sec, int64_t delay_us)
+      : bandwidth(QuicBandwidth::FromKBitsPerSecond(kilo_bits_per_sec)),
+        delay(QuicTime::Delta::FromMicroseconds(delay_us)) {}
+  QuicBandwidth bandwidth;
+  QuicTime::Delta delay;
+};
+
+struct TrafficPolicerParams {
+  std::string name = "policer";
+  QuicByteCount initial_burst_size;
+  QuicByteCount max_bucket_size;
+  QuicBandwidth target_bandwidth = QuicBandwidth::Zero();
+};
+
+// All Bbr3DefaultTopologyTests uses the default network topology:
+//
+//            Sender
+//               |
+//               |  <-- local_link
+//               |
+//        Network switch
+//               *  <-- the bottleneck queue in the direction
+//               |          of the receiver
+//               |
+//               |  <-- test_link
+//               |
+//               |
+//           Receiver
+class DefaultTopologyParams {
+ public:
+  LinkParams local_link = {10000, 2000};
+  LinkParams test_link = {4000, 30000};
+
+  const simulator::SwitchPortNumber switch_port_count = 2;
+  // Network switch queue capacity, in number of BDPs.
+  float switch_queue_capacity_in_bdp = 2;
+
+  std::optional<TrafficPolicerParams> sender_policer_params;
+
+  QuicBandwidth BottleneckBandwidth() const {
+    return std::min(local_link.bandwidth, test_link.bandwidth);
+  }
+
+  // Round trip time of a single full size packet.
+  QuicTime::Delta RTT() const {
+    return 2 * (local_link.delay + test_link.delay +
+                local_link.bandwidth.TransferTime(kMaxOutgoingPacketSize) +
+                test_link.bandwidth.TransferTime(kMaxOutgoingPacketSize));
+  }
+
+  QuicByteCount BDP() const { return BottleneckBandwidth() * RTT(); }
+
+  QuicByteCount SwitchQueueCapacity() const {
+    return switch_queue_capacity_in_bdp * BDP();
+  }
+
+  std::string ToString() const {
+    std::ostringstream os;
+    os << "{ BottleneckBandwidth: " << BottleneckBandwidth()
+       << " RTT: " << RTT() << " BDP: " << BDP()
+       << " BottleneckQueueSize: " << SwitchQueueCapacity() << "}";
+    return os.str();
+  }
+};
+
+class Bbr3SimulatorTest : public QuicTest {
+ protected:
+  Bbr3SimulatorTest() : simulator_(&random_) {
+    // Prevent the server(receiver), which only sends acks, from closing
+    // connection due to too many outstanding packets.
+    SetQuicFlag(quic_max_tracked_packet_count, 1000000);
+  }
+
+  void SetUp() override {
+    if (quiche::GetQuicheCommandLineFlag(
+            FLAGS_quic_bbr3_test_regression_mode) == "regress") {
+      SendAlgorithmTestResult expected;
+      ASSERT_TRUE(LoadSendAlgorithmTestResult(&expected));
+      random_seed_ = expected.random_seed();
+    } else {
+      random_seed_ = QuicRandom::GetInstance()->RandUint64();
+    }
+    random_.set_seed(random_seed_);
+    QUIC_LOG(INFO) << "Using random seed: " << random_seed_;
+  }
+
+  ~Bbr3SimulatorTest() override {
+    const std::string regression_mode =
+        quiche::GetQuicheCommandLineFlag(FLAGS_quic_bbr3_test_regression_mode);
+    const QuicTime::Delta simulated_duration =
+        SimulatedNow() - QuicTime::Zero();
+    if (regression_mode == "record") {
+      RecordSendAlgorithmTestResult(random_seed_,
+                                    simulated_duration.ToMicroseconds());
+    } else if (regression_mode == "regress") {
+      CompareSendAlgorithmTestResult(simulated_duration.ToMicroseconds());
+    }
+  }
+
+  QuicTime SimulatedNow() const { return simulator_.GetClock()->Now(); }
+
+  uint64_t random_seed_;
+  SimpleRandom random_;
+  simulator::Simulator simulator_;
+};
+
+class Bbr3DefaultTopologyTest : public Bbr3SimulatorTest {
+ protected:
+  Bbr3DefaultTopologyTest()
+      : sender_endpoint_(&simulator_, "Sender", "Receiver",
+                         Perspective::IS_CLIENT, TestConnectionId(42)),
+        receiver_endpoint_(&simulator_, "Receiver", "Sender",
+                           Perspective::IS_SERVER, TestConnectionId(42)) {
+    sender_ = SetupBbr3Sender(&sender_endpoint_, /*old_sender=*/nullptr);
+  }
+
+  ~Bbr3DefaultTopologyTest() {
+    const auto* test_info =
+        ::testing::UnitTest::GetInstance()->current_test_info();
+    const Bbr3Sender::DebugState& debug_state = sender_->ExportDebugState();
+    QUIC_LOG(INFO) << "Bbr3DefaultTopologyTest." << test_info->name()
+                   << " completed at simulated time: "
+                   << SimulatedNow().ToDebuggingValue() / 1e6
+                   << " sec. packet loss:"
+                   << sender_loss_rate_in_packets() * 100
+                   << "%, bw_hi:" << debug_state.bandwidth_hi;
+  }
+
+  QuicUnackedPacketMap* GetUnackedMap(QuicConnection* connection) {
+    return QuicSentPacketManagerPeer::GetUnackedPacketMap(
+        QuicConnectionPeer::GetSentPacketManager(connection));
+  }
+
+  Bbr3Sender* SetupBbr3Sender(simulator::QuicEndpoint* endpoint,
+                              BbrSender* old_sender) {
+    // Ownership of the sender will be overtaken by the endpoint.
+    Bbr3Sender* sender = new Bbr3Sender(
+        endpoint->connection()->clock()->Now(),
+        endpoint->connection()->sent_packet_manager().GetRttStats(),
+        GetUnackedMap(endpoint->connection()), kDefaultInitialCwndPackets,
+        GetQuicFlag(quic_max_congestion_window), &random_,
+        QuicConnectionPeer::GetStats(endpoint->connection()), old_sender);
+    QuicConnectionPeer::SetSendAlgorithm(endpoint->connection(), sender);
+    const int kTestMaxPacketSize = 1350;
+    endpoint->connection()->SetMaxPacketLength(kTestMaxPacketSize);
+    endpoint->RecordTrace();
+    return sender;
+  }
+
+  void CreateNetwork(const DefaultTopologyParams& params) {
+    QUIC_LOG(INFO) << "CreateNetwork with parameters: " << params.ToString();
+    switch_ = std::make_unique<simulator::Switch>(&simulator_, "Switch",
+                                                  params.switch_port_count,
+                                                  params.SwitchQueueCapacity());
+
+    // WARNING: The order to add links to network_links_ matters, because some
+    // tests adjusts the link bandwidth on the fly.
+
+    // Local link connects sender and port 1.
+    network_links_.push_back(std::make_unique<simulator::SymmetricLink>(
+        &sender_endpoint_, switch_->port(1), params.local_link.bandwidth,
+        params.local_link.delay));
+
+    // Test link connects receiver and port 2.
+    if (params.sender_policer_params.has_value()) {
+      const TrafficPolicerParams& policer_params =
+          params.sender_policer_params.value();
+      sender_policer_ = std::make_unique<simulator::TrafficPolicer>(
+          &simulator_, policer_params.name, policer_params.initial_burst_size,
+          policer_params.max_bucket_size, policer_params.target_bandwidth,
+          switch_->port(2));
+      network_links_.push_back(std::make_unique<simulator::SymmetricLink>(
+          &receiver_endpoint_, sender_policer_.get(),
+          params.test_link.bandwidth, params.test_link.delay));
+    } else {
+      network_links_.push_back(std::make_unique<simulator::SymmetricLink>(
+          &receiver_endpoint_, switch_->port(2), params.test_link.bandwidth,
+          params.test_link.delay));
+    }
+  }
+
+  simulator::SymmetricLink* TestLink() { return network_links_[1].get(); }
+
+  void DoSimpleTransfer(QuicByteCount transfer_size, QuicTime::Delta timeout) {
+    sender_endpoint_.AddBytesToTransfer(transfer_size);
+    // TODO(wub): consider rewriting this to run until the receiver actually
+    // receives the intended amount of bytes.
+    bool simulator_result = simulator_.RunUntilOrTimeout(
+        [this]() { return sender_endpoint_.bytes_to_transfer() == 0; },
+        timeout);
+    EXPECT_TRUE(simulator_result)
+        << "Simple transfer failed.  Bytes remaining: "
+        << sender_endpoint_.bytes_to_transfer();
+    QUIC_LOG(INFO) << "Simple transfer state: " << sender_->ExportDebugState();
+  }
+
+  // Drive the simulator by sending enough data to enter PROBE_BW.
+  void DriveOutOfStartup(const DefaultTopologyParams& params) {
+    ASSERT_FALSE(sender_->ExportDebugState().startup.full_bandwidth_reached);
+    DoSimpleTransfer(1024 * 1024, QuicTime::Delta::FromSeconds(15));
+    EXPECT_EQ(Bbr2Mode::PROBE_BW, sender_->ExportDebugState().mode);
+    EXPECT_APPROX_EQ(params.BottleneckBandwidth(),
+                     sender_->ExportDebugState().bandwidth_hi, 0.02f);
+  }
+
+  // Send |bytes|-sized bursts of data |number_of_bursts| times, waiting for
+  // |wait_time| between each burst.
+  void SendBursts(const DefaultTopologyParams& params, size_t number_of_bursts,
+                  QuicByteCount bytes, QuicTime::Delta wait_time) {
+    ASSERT_EQ(0u, sender_endpoint_.bytes_to_transfer());
+    for (size_t i = 0; i < number_of_bursts; i++) {
+      sender_endpoint_.AddBytesToTransfer(bytes);
+
+      // Transfer data and wait for three seconds between each transfer.
+      simulator_.RunFor(wait_time);
+
+      // Ensure the connection did not time out.
+      ASSERT_TRUE(sender_endpoint_.connection()->connected());
+      ASSERT_TRUE(receiver_endpoint_.connection()->connected());
+    }
+
+    simulator_.RunFor(wait_time + params.RTT());
+    ASSERT_EQ(0u, sender_endpoint_.bytes_to_transfer());
+  }
+
+  template <class TerminationPredicate>
+  bool SendUntilOrTimeout(TerminationPredicate termination_predicate,
+                          QuicTime::Delta timeout) {
+    EXPECT_EQ(0u, sender_endpoint_.bytes_to_transfer());
+    const QuicTime deadline = SimulatedNow() + timeout;
+    do {
+      sender_endpoint_.AddBytesToTransfer(4 * kDefaultTCPMSS);
+      if (simulator_.RunUntilOrTimeout(
+              [this]() { return sender_endpoint_.bytes_to_transfer() == 0; },
+              deadline - SimulatedNow()) &&
+          termination_predicate()) {
+        return true;
+      }
+    } while (SimulatedNow() < deadline);
+    return false;
+  }
+
+  void EnableAggregation(QuicByteCount aggregation_bytes,
+                         QuicTime::Delta aggregation_timeout) {
+    switch_->port_queue(1)->EnableAggregation(aggregation_bytes,
+                                              aggregation_timeout);
+  }
+
+  void SetConnectionOption(QuicTag option) {
+    SetConnectionOption(std::move(option), sender_);
+  }
+
+  void SetConnectionOption(QuicTag option, Bbr3Sender* sender) {
+    QuicConfig config;
+    QuicTagVector options;
+    options.push_back(option);
+    QuicConfigPeer::SetReceivedConnectionOptions(&config, options);
+    sender->SetFromConfig(config, Perspective::IS_SERVER);
+  }
+
+  bool Bbr3ModeIsOneOf(const std::vector<Bbr2Mode>& expected_modes) const {
+    const Bbr2Mode mode = sender_->ExportDebugState().mode;
+    for (Bbr2Mode expected_mode : expected_modes) {
+      if (mode == expected_mode) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  const RttStats* rtt_stats() {
+    return sender_endpoint_.connection()->sent_packet_manager().GetRttStats();
+  }
+
+  QuicConnection* sender_connection() { return sender_endpoint_.connection(); }
+
+  Bbr3Sender::DebugState sender_debug_state() const {
+    return sender_->ExportDebugState();
+  }
+
+  const QuicConnectionStats& sender_connection_stats() {
+    return sender_connection()->GetStats();
+  }
+
+  QuicUnackedPacketMap* sender_unacked_map() {
+    return GetUnackedMap(sender_connection());
+  }
+
+  float sender_loss_rate_in_packets() {
+    return static_cast<float>(sender_connection_stats().packets_lost) /
+           sender_connection_stats().packets_sent;
+  }
+
+  simulator::QuicEndpoint sender_endpoint_;
+  simulator::QuicEndpoint receiver_endpoint_;
+  Bbr3Sender* sender_;
+
+  std::unique_ptr<simulator::Switch> switch_;
+  std::unique_ptr<simulator::TrafficPolicer> sender_policer_;
+  std::vector<std::unique_ptr<simulator::SymmetricLink>> network_links_;
+};
+
+TEST_F(Bbr3DefaultTopologyTest, NormalStartup) {
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  // Run until the full bandwidth is reached and check how many rounds it was.
+  sender_endpoint_.AddBytesToTransfer(12 * 1024 * 1024);
+  QuicRoundTripCount max_bw_round = 0;
+  QuicBandwidth max_bw(QuicBandwidth::Zero());
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this, &max_bw, &max_bw_round]() {
+        if (max_bw * 1.001 < sender_->ExportDebugState().bandwidth_hi) {
+          max_bw = sender_->ExportDebugState().bandwidth_hi;
+          max_bw_round = sender_->ExportDebugState().round_trip_count;
+        }
+        return sender_->ExportDebugState().startup.full_bandwidth_reached;
+      },
+      QuicTime::Delta::FromSeconds(5));
+  ASSERT_TRUE(simulator_result);
+  EXPECT_EQ(Bbr2Mode::DRAIN, sender_->ExportDebugState().mode);
+  EXPECT_EQ(3u, sender_->ExportDebugState().round_trip_count - max_bw_round);
+  EXPECT_EQ(
+      3u,
+      sender_->ExportDebugState().startup.round_trips_without_bandwidth_growth);
+  EXPECT_EQ(0u, sender_connection_stats().packets_lost);
+  EXPECT_APPROX_EQ(params.BottleneckBandwidth(),
+                   sender_->ExportDebugState().bandwidth_hi, 0.01f);
+  EXPECT_FALSE(sender_->ExportDebugState().last_sample_is_app_limited);
+}
+
+TEST_F(Bbr3DefaultTopologyTest, NormalStartupB207) {
+  SetConnectionOption(kB207);
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  // Run until the full bandwidth is reached and check how many rounds it was.
+  sender_endpoint_.AddBytesToTransfer(12 * 1024 * 1024);
+  QuicRoundTripCount max_bw_round = 0;
+  QuicBandwidth max_bw(QuicBandwidth::Zero());
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this, &max_bw, &max_bw_round]() {
+        if (max_bw < sender_->ExportDebugState().bandwidth_hi) {
+          max_bw = sender_->ExportDebugState().bandwidth_hi;
+          max_bw_round = sender_->ExportDebugState().round_trip_count;
+        }
+        return sender_->ExportDebugState().startup.full_bandwidth_reached;
+      },
+      QuicTime::Delta::FromSeconds(5));
+  ASSERT_TRUE(simulator_result);
+  EXPECT_EQ(Bbr2Mode::DRAIN, sender_->ExportDebugState().mode);
+  EXPECT_EQ(1u, sender_->ExportDebugState().round_trip_count - max_bw_round);
+  EXPECT_EQ(
+      1u,
+      sender_->ExportDebugState().startup.round_trips_without_bandwidth_growth);
+  EXPECT_APPROX_EQ(params.BottleneckBandwidth(),
+                   sender_->ExportDebugState().bandwidth_hi, 0.01f);
+  EXPECT_EQ(0u, sender_connection_stats().packets_lost);
+}
+
+// Add extra_acked to CWND in STARTUP and exit STARTUP on a persistent queue.
+TEST_F(Bbr3DefaultTopologyTest, NormalStartupB207andB205) {
+  SetConnectionOption(kB205);
+  SetConnectionOption(kB207);
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  // Run until the full bandwidth is reached and check how many rounds it was.
+  sender_endpoint_.AddBytesToTransfer(12 * 1024 * 1024);
+  QuicRoundTripCount max_bw_round = 0;
+  QuicBandwidth max_bw(QuicBandwidth::Zero());
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this, &max_bw, &max_bw_round]() {
+        if (max_bw < sender_->ExportDebugState().bandwidth_hi) {
+          max_bw = sender_->ExportDebugState().bandwidth_hi;
+          max_bw_round = sender_->ExportDebugState().round_trip_count;
+        }
+        return sender_->ExportDebugState().startup.full_bandwidth_reached;
+      },
+      QuicTime::Delta::FromSeconds(5));
+  ASSERT_TRUE(simulator_result);
+  EXPECT_EQ(Bbr2Mode::DRAIN, sender_->ExportDebugState().mode);
+  EXPECT_EQ(1u, sender_->ExportDebugState().round_trip_count - max_bw_round);
+  EXPECT_EQ(
+      2u,
+      sender_->ExportDebugState().startup.round_trips_without_bandwidth_growth);
+  EXPECT_APPROX_EQ(params.BottleneckBandwidth(),
+                   sender_->ExportDebugState().bandwidth_hi, 0.01f);
+  EXPECT_EQ(0u, sender_connection_stats().packets_lost);
+}
+
+// Add extra_acked to CWND in STARTUP and exit STARTUP on a persistent queue.
+TEST_F(Bbr3DefaultTopologyTest, NormalStartupBB2S) {
+  SetConnectionOption(kBB2S);
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  // Run until the full bandwidth is reached and check how many rounds it was.
+  sender_endpoint_.AddBytesToTransfer(12 * 1024 * 1024);
+  QuicRoundTripCount max_bw_round = 0;
+  QuicBandwidth max_bw(QuicBandwidth::Zero());
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this, &max_bw, &max_bw_round]() {
+        if (max_bw * 1.001 < sender_->ExportDebugState().bandwidth_hi) {
+          max_bw = sender_->ExportDebugState().bandwidth_hi;
+          max_bw_round = sender_->ExportDebugState().round_trip_count;
+        }
+        return sender_->ExportDebugState().startup.full_bandwidth_reached;
+      },
+      QuicTime::Delta::FromSeconds(5));
+  ASSERT_TRUE(simulator_result);
+  EXPECT_EQ(Bbr2Mode::DRAIN, sender_->ExportDebugState().mode);
+  // BB2S reduces 3 rounds without bandwidth growth to 2.
+  EXPECT_EQ(2u, sender_->ExportDebugState().round_trip_count - max_bw_round);
+  EXPECT_EQ(
+      2u,
+      sender_->ExportDebugState().startup.round_trips_without_bandwidth_growth);
+  EXPECT_APPROX_EQ(params.BottleneckBandwidth(),
+                   sender_->ExportDebugState().bandwidth_hi, 0.01f);
+  EXPECT_EQ(0u, sender_connection_stats().packets_lost);
+}
+
+// Test a simple long data transfer in the default setup.
+TEST_F(Bbr3DefaultTopologyTest, SimpleTransfer) {
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  // At startup make sure we are at the default.
+  EXPECT_EQ(kDefaultInitialCwndBytes, sender_->GetCongestionWindow());
+  // At startup make sure we can send.
+  EXPECT_TRUE(sender_->CanSend(0));
+  // And that window is un-affected.
+  EXPECT_EQ(kDefaultInitialCwndBytes, sender_->GetCongestionWindow());
+
+  // Verify that Sender is in slow start.
+  EXPECT_TRUE(sender_->InSlowStart());
+
+  // Verify that pacing rate is based on the initial RTT.
+  QuicBandwidth expected_pacing_rate = QuicBandwidth::FromBytesAndTimeDelta(
+      2.885 * kDefaultInitialCwndBytes, rtt_stats()->initial_rtt());
+  EXPECT_APPROX_EQ(expected_pacing_rate.ToBitsPerSecond(),
+                   sender_->PacingRate(0).ToBitsPerSecond(), 0.01f);
+
+  ASSERT_GE(params.BDP(), kDefaultInitialCwndBytes + kDefaultTCPMSS);
+
+  DoSimpleTransfer(12 * 1024 * 1024, QuicTime::Delta::FromSeconds(30));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+  EXPECT_EQ(0u, sender_connection_stats().packets_lost);
+  EXPECT_FALSE(sender_->ExportDebugState().last_sample_is_app_limited);
+
+  // The margin here is quite high, since there exists a possibility that the
+  // connection just exited high gain cycle.
+  EXPECT_APPROX_EQ(params.RTT(), rtt_stats()->smoothed_rtt(), 1.0f);
+}
+
+TEST_F(Bbr3DefaultTopologyTest, SimpleTransferB2RC) {
+  SetConnectionOption(kB2RC);
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  // Transfer 12MB.
+  DoSimpleTransfer(12 * 1024 * 1024, QuicTime::Delta::FromSeconds(35));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+
+  EXPECT_APPROX_EQ(params.BottleneckBandwidth(),
+                   sender_->ExportDebugState().bandwidth_hi, 0.01f);
+
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.05);
+  // The margin here is high, because the aggregation greatly increases
+  // smoothed rtt.
+  EXPECT_GE(params.RTT() * 4, rtt_stats()->smoothed_rtt());
+  EXPECT_APPROX_EQ(params.RTT(), rtt_stats()->min_rtt(), 0.2f);
+}
+
+TEST_F(Bbr3DefaultTopologyTest, SimpleTransferB201) {
+  SetConnectionOption(kB201);
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  // Transfer 12MB.
+  DoSimpleTransfer(12 * 1024 * 1024, QuicTime::Delta::FromSeconds(35));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+
+  EXPECT_APPROX_EQ(params.BottleneckBandwidth(),
+                   sender_->ExportDebugState().bandwidth_hi, 0.01f);
+
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.05);
+  // The margin here is high, because the aggregation greatly increases
+  // smoothed rtt.
+  EXPECT_GE(params.RTT() * 4, rtt_stats()->smoothed_rtt());
+  EXPECT_APPROX_EQ(params.RTT(), rtt_stats()->min_rtt(), 0.2f);
+}
+
+TEST_F(Bbr3DefaultTopologyTest, SimpleTransferB206) {
+  SetConnectionOption(kB206);
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  // Transfer 12MB.
+  DoSimpleTransfer(12 * 1024 * 1024, QuicTime::Delta::FromSeconds(35));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+
+  EXPECT_APPROX_EQ(params.BottleneckBandwidth(),
+                   sender_->ExportDebugState().bandwidth_hi, 0.01f);
+
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.05);
+  // The margin here is high, because the aggregation greatly increases
+  // smoothed rtt.
+  EXPECT_GE(params.RTT() * 4, rtt_stats()->smoothed_rtt());
+  EXPECT_APPROX_EQ(params.RTT(), rtt_stats()->min_rtt(), 0.2f);
+}
+
+TEST_F(Bbr3DefaultTopologyTest, SimpleTransferB207) {
+  SetConnectionOption(kB207);
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  // Transfer 12MB.
+  DoSimpleTransfer(12 * 1024 * 1024, QuicTime::Delta::FromSeconds(35));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+
+  EXPECT_APPROX_EQ(params.BottleneckBandwidth(),
+                   sender_->ExportDebugState().bandwidth_hi, 0.01f);
+
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.05);
+  // The margin here is high, because the aggregation greatly increases
+  // smoothed rtt.
+  EXPECT_GE(params.RTT() * 4, rtt_stats()->smoothed_rtt());
+  EXPECT_APPROX_EQ(params.RTT(), rtt_stats()->min_rtt(), 0.2f);
+}
+
+TEST_F(Bbr3DefaultTopologyTest, SimpleTransferBBRB) {
+  SetConnectionOption(kBBRB);
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  // Transfer 12MB.
+  DoSimpleTransfer(12 * 1024 * 1024, QuicTime::Delta::FromSeconds(35));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+
+  EXPECT_APPROX_EQ(params.BottleneckBandwidth(),
+                   sender_->ExportDebugState().bandwidth_hi, 0.01f);
+
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.05);
+  // The margin here is high, because the aggregation greatly increases
+  // smoothed rtt.
+  EXPECT_GE(params.RTT() * 4, rtt_stats()->smoothed_rtt());
+  EXPECT_APPROX_EQ(params.RTT(), rtt_stats()->min_rtt(), 0.2f);
+}
+
+TEST_F(Bbr3DefaultTopologyTest, SimpleTransferBBR4) {
+  SetQuicReloadableFlag(quic_bbr2_extra_acked_window, true);
+  SetConnectionOption(kBBR4);
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  // Transfer 12MB.
+  DoSimpleTransfer(12 * 1024 * 1024, QuicTime::Delta::FromSeconds(35));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+
+  EXPECT_APPROX_EQ(params.BottleneckBandwidth(),
+                   sender_->ExportDebugState().bandwidth_hi, 0.01f);
+
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.05);
+  // The margin here is high, because the aggregation greatly increases
+  // smoothed rtt.
+  EXPECT_GE(params.RTT() * 4, rtt_stats()->smoothed_rtt());
+  EXPECT_APPROX_EQ(params.RTT(), rtt_stats()->min_rtt(), 0.2f);
+}
+
+TEST_F(Bbr3DefaultTopologyTest, SimpleTransferBBR5) {
+  SetQuicReloadableFlag(quic_bbr2_extra_acked_window, true);
+  SetConnectionOption(kBBR5);
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  // Transfer 12MB.
+  DoSimpleTransfer(12 * 1024 * 1024, QuicTime::Delta::FromSeconds(35));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+
+  EXPECT_APPROX_EQ(params.BottleneckBandwidth(),
+                   sender_->ExportDebugState().bandwidth_hi, 0.01f);
+
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.05);
+  // The margin here is high, because the aggregation greatly increases
+  // smoothed rtt.
+  EXPECT_GE(params.RTT() * 4, rtt_stats()->smoothed_rtt());
+  EXPECT_APPROX_EQ(params.RTT(), rtt_stats()->min_rtt(), 0.2f);
+}
+
+TEST_F(Bbr3DefaultTopologyTest, SimpleTransferBBQ1) {
+  SetConnectionOption(kBBQ1);
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  // Transfer 12MB.
+  DoSimpleTransfer(12 * 1024 * 1024, QuicTime::Delta::FromSeconds(35));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+
+  EXPECT_APPROX_EQ(params.BottleneckBandwidth(),
+                   sender_->ExportDebugState().bandwidth_hi, 0.01f);
+
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.05);
+  // The margin here is high, because the aggregation greatly increases
+  // smoothed rtt.
+  EXPECT_GE(params.RTT() * 4, rtt_stats()->smoothed_rtt());
+  EXPECT_APPROX_EQ(params.RTT(), rtt_stats()->min_rtt(), 0.2f);
+}
+
+TEST_F(Bbr3DefaultTopologyTest, SimpleTransferSmallBuffer) {
+  DefaultTopologyParams params;
+  params.switch_queue_capacity_in_bdp = 0.5;
+  CreateNetwork(params);
+
+  DoSimpleTransfer(12 * 1024 * 1024, QuicTime::Delta::FromSeconds(30));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+  EXPECT_APPROX_EQ(params.BottleneckBandwidth(),
+                   sender_->ExportDebugState().bandwidth_hi, 0.02f);
+  EXPECT_GE(sender_connection_stats().packets_lost, 0u);
+  EXPECT_FALSE(sender_->ExportDebugState().last_sample_is_app_limited);
+}
+
+TEST_F(Bbr3DefaultTopologyTest, SimpleTransferSmallBufferB2H2) {
+  SetConnectionOption(kB2H2);
+  DefaultTopologyParams params;
+  params.switch_queue_capacity_in_bdp = 0.5;
+  CreateNetwork(params);
+
+  DoSimpleTransfer(12 * 1024 * 1024, QuicTime::Delta::FromSeconds(30));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+  EXPECT_APPROX_EQ(params.BottleneckBandwidth(),
+                   sender_->ExportDebugState().bandwidth_hi, 0.02f);
+  EXPECT_GE(sender_connection_stats().packets_lost, 0u);
+  EXPECT_FALSE(sender_->ExportDebugState().last_sample_is_app_limited);
+}
+
+TEST_F(Bbr3DefaultTopologyTest, SimpleTransfer2RTTAggregationBytes) {
+  SetConnectionOption(kBSAO);
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+  // 2 RTTs of aggregation, with a max of 10kb.
+  EnableAggregation(10 * 1024, 2 * params.RTT());
+
+  // Transfer 12MB.
+  DoSimpleTransfer(12 * 1024 * 1024, QuicTime::Delta::FromSeconds(35));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+
+  EXPECT_APPROX_EQ(params.BottleneckBandwidth(),
+                   sender_->ExportDebugState().bandwidth_hi, 0.01f);
+
+  EXPECT_EQ(sender_loss_rate_in_packets(), 0);
+  // The margin here is high, because both link level aggregation and ack
+  // decimation can greatly increase smoothed rtt.
+  EXPECT_GE(params.RTT() * 5, rtt_stats()->smoothed_rtt());
+  EXPECT_APPROX_EQ(params.RTT(), rtt_stats()->min_rtt(), 0.2f);
+}
+
+TEST_F(Bbr3DefaultTopologyTest, SimpleTransfer2RTTAggregationBytesB201) {
+  SetConnectionOption(kB201);
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+  // 2 RTTs of aggregation, with a max of 10kb.
+  EnableAggregation(10 * 1024, 2 * params.RTT());
+
+  // Transfer 12MB.
+  DoSimpleTransfer(12 * 1024 * 1024, QuicTime::Delta::FromSeconds(35));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+
+  // TODO(wub): Tighten the error bound once BSAO is default enabled.
+  EXPECT_APPROX_EQ(params.BottleneckBandwidth(),
+                   sender_->ExportDebugState().bandwidth_hi, 0.5f);
+
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.01);
+  // The margin here is high, because both link level aggregation and ack
+  // decimation can greatly increase smoothed rtt.
+  EXPECT_GE(params.RTT() * 5, rtt_stats()->smoothed_rtt());
+  EXPECT_APPROX_EQ(params.RTT(), rtt_stats()->min_rtt(), 0.2f);
+}
+
+TEST_F(Bbr3DefaultTopologyTest, SimpleTransferAckDecimation) {
+  SetConnectionOption(kBSAO);
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  // Transfer 12MB.
+  DoSimpleTransfer(12 * 1024 * 1024, QuicTime::Delta::FromSeconds(35));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+
+  EXPECT_APPROX_EQ(params.BottleneckBandwidth(),
+                   sender_->ExportDebugState().bandwidth_hi, 0.01f);
+
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.001);
+  EXPECT_FALSE(sender_->ExportDebugState().last_sample_is_app_limited);
+  // The margin here is high, because the aggregation greatly increases
+  // smoothed rtt.
+  EXPECT_GE(params.RTT() * 3, rtt_stats()->smoothed_rtt());
+  EXPECT_APPROX_EQ(params.RTT(), rtt_stats()->min_rtt(), 0.1f);
+}
+
+// Test Bbr2's reaction to a 100x bandwidth decrease during a transfer.
+TEST_F(Bbr3DefaultTopologyTest, QUIC_SLOW_TEST(BandwidthDecrease)) {
+  DefaultTopologyParams params;
+  params.local_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(15000);
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(10000);
+  CreateNetwork(params);
+
+  sender_endpoint_.AddBytesToTransfer(20 * 1024 * 1024);
+
+  // We can transfer ~12MB in the first 10 seconds. The rest ~8MB needs about
+  // 640 seconds.
+  simulator_.RunFor(QuicTime::Delta::FromSeconds(10));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+  QUIC_LOG(INFO) << "Bandwidth decreasing at time " << SimulatedNow();
+
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_est, 0.1f);
+  EXPECT_EQ(0u, sender_connection_stats().packets_lost);
+
+  // Now decrease the bottleneck bandwidth from 10Mbps to 100Kbps.
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(100);
+  TestLink()->set_bandwidth(params.test_link.bandwidth);
+
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() { return sender_endpoint_.bytes_to_transfer() == 0; },
+      QuicTime::Delta::FromSeconds(800));
+  EXPECT_TRUE(simulator_result);
+}
+
+// Test Bbr2's reaction to a 100x bandwidth increase during a transfer with B203
+TEST_F(Bbr3DefaultTopologyTest, QUIC_SLOW_TEST(BandwidthIncreaseB203)) {
+  SetConnectionOption(kB203);
+  DefaultTopologyParams params;
+  params.local_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(15000);
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(100);
+  CreateNetwork(params);
+
+  sender_endpoint_.AddBytesToTransfer(20 * 1024 * 1024);
+
+  simulator_.RunFor(QuicTime::Delta::FromSeconds(15));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+  QUIC_LOG(INFO) << "Bandwidth increasing at time " << SimulatedNow();
+
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_est, 0.1f);
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.30);
+
+  // Now increase the bottleneck bandwidth from 100Kbps to 10Mbps.
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(10000);
+  TestLink()->set_bandwidth(params.test_link.bandwidth);
+
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() { return sender_endpoint_.bytes_to_transfer() == 0; },
+      QuicTime::Delta::FromSeconds(50));
+  EXPECT_TRUE(simulator_result);
+  // Ensure the full bandwidth is discovered.
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_hi, 0.02f);
+}
+
+// Test Bbr2's reaction to a 100x bandwidth increase during a transfer with BBQ0
+TEST_F(Bbr3DefaultTopologyTest, QUIC_SLOW_TEST(BandwidthIncreaseBBQ0)) {
+  SetConnectionOption(kBBQ0);
+  DefaultTopologyParams params;
+  params.local_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(15000);
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(100);
+  CreateNetwork(params);
+
+  sender_endpoint_.AddBytesToTransfer(10 * 1024 * 1024);
+
+  simulator_.RunFor(QuicTime::Delta::FromSeconds(15));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+  QUIC_LOG(INFO) << "Bandwidth increasing at time " << SimulatedNow();
+
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_est, 0.1f);
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.30);
+
+  // Now increase the bottleneck bandwidth from 100Kbps to 10Mbps.
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(10000);
+  TestLink()->set_bandwidth(params.test_link.bandwidth);
+
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() { return sender_endpoint_.bytes_to_transfer() == 0; },
+      QuicTime::Delta::FromSeconds(50));
+  EXPECT_TRUE(simulator_result);
+  // Ensure the full bandwidth is discovered.
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_hi, 0.02f);
+}
+
+// Test Bbr2's reaction to a 100x bandwidth increase during a transfer with BBQ0
+// in the presence of ACK aggregation.
+TEST_F(Bbr3DefaultTopologyTest,
+       QUIC_SLOW_TEST(BandwidthIncreaseBBQ0Aggregation)) {
+  SetConnectionOption(kBBQ0);
+  DefaultTopologyParams params;
+  params.local_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(15000);
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(100);
+  CreateNetwork(params);
+
+  // 2 RTTs of aggregation, with a max of 10kb.
+  EnableAggregation(10 * 1024, 2 * params.RTT());
+
+  // Reduce the payload to 2MB because 10MB takes too long.
+  sender_endpoint_.AddBytesToTransfer(2 * 1024 * 1024);
+
+  simulator_.RunFor(QuicTime::Delta::FromSeconds(15));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+  QUIC_LOG(INFO) << "Bandwidth increasing at time " << SimulatedNow();
+
+  // This is much farther off when aggregation is present,
+  // Ideally BSAO or another option would fix this.
+  // TODO(ianswett) Make these bound tighter once overestimation is reduced.
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_est, 0.6f);
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.35);
+
+  // Now increase the bottleneck bandwidth from 100Kbps to 10Mbps.
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(10000);
+  TestLink()->set_bandwidth(params.test_link.bandwidth);
+
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() { return sender_endpoint_.bytes_to_transfer() == 0; },
+      QuicTime::Delta::FromSeconds(50));
+  EXPECT_TRUE(simulator_result);
+  // Ensure at least 10% of full bandwidth is discovered.
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_hi, 0.90f);
+}
+
+// Test Bbr2's reaction to a 100x bandwidth increase during a transfer with B202
+TEST_F(Bbr3DefaultTopologyTest, QUIC_SLOW_TEST(BandwidthIncreaseB202)) {
+  SetConnectionOption(kB202);
+  DefaultTopologyParams params;
+  params.local_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(15000);
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(100);
+  CreateNetwork(params);
+
+  sender_endpoint_.AddBytesToTransfer(10 * 1024 * 1024);
+
+  simulator_.RunFor(QuicTime::Delta::FromSeconds(15));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+  QUIC_LOG(INFO) << "Bandwidth increasing at time " << SimulatedNow();
+
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_est, 0.1f);
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.30);
+
+  // Now increase the bottleneck bandwidth from 100Kbps to 10Mbps.
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(10000);
+  TestLink()->set_bandwidth(params.test_link.bandwidth);
+
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() { return sender_endpoint_.bytes_to_transfer() == 0; },
+      QuicTime::Delta::FromSeconds(50));
+  EXPECT_TRUE(simulator_result);
+  // Ensure the full bandwidth is discovered.
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_hi, 0.1f);
+}
+
+// Test Bbr2's reaction to a 100x bandwidth increase during a transfer with B202
+// in the presence of ACK aggregation.
+TEST_F(Bbr3DefaultTopologyTest,
+       QUIC_SLOW_TEST(BandwidthIncreaseB202Aggregation)) {
+  SetConnectionOption(kB202);
+  DefaultTopologyParams params;
+  params.local_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(15000);
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(100);
+  CreateNetwork(params);
+
+  // 2 RTTs of aggregation, with a max of 10kb.
+  EnableAggregation(10 * 1024, 2 * params.RTT());
+
+  // Reduce the payload to 2MB because 10MB takes too long.
+  sender_endpoint_.AddBytesToTransfer(2 * 1024 * 1024);
+
+  simulator_.RunFor(QuicTime::Delta::FromSeconds(15));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+  QUIC_LOG(INFO) << "Bandwidth increasing at time " << SimulatedNow();
+
+  // This is much farther off when aggregation is present,
+  // Ideally BSAO or another option would fix this.
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_est, 0.6f);
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.35);
+
+  // Now increase the bottleneck bandwidth from 100Kbps to 10Mbps.
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(10000);
+  TestLink()->set_bandwidth(params.test_link.bandwidth);
+
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() { return sender_endpoint_.bytes_to_transfer() == 0; },
+      QuicTime::Delta::FromSeconds(50));
+  EXPECT_TRUE(simulator_result);
+  // Ensure at least 10% of full bandwidth is discovered.
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_hi, 0.92f);
+}
+
+// Test Bbr2's reaction to a 100x bandwidth increase during a transfer.
+TEST_F(Bbr3DefaultTopologyTest, QUIC_SLOW_TEST(BandwidthIncrease)) {
+  DefaultTopologyParams params;
+  params.local_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(15000);
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(100);
+  CreateNetwork(params);
+
+  sender_endpoint_.AddBytesToTransfer(10 * 1024 * 1024);
+
+  simulator_.RunFor(QuicTime::Delta::FromSeconds(15));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+  QUIC_LOG(INFO) << "Bandwidth increasing at time " << SimulatedNow();
+
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_est, 0.1f);
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.30);
+
+  // Now increase the bottleneck bandwidth from 100Kbps to 10Mbps.
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(10000);
+  TestLink()->set_bandwidth(params.test_link.bandwidth);
+
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() { return sender_endpoint_.bytes_to_transfer() == 0; },
+      QuicTime::Delta::FromSeconds(50));
+  EXPECT_TRUE(simulator_result);
+  // Ensure the full bandwidth is discovered.
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_hi, 0.02f);
+}
+
+// Test Bbr2's reaction to a 100x bandwidth increase during a transfer in the
+// presence of ACK aggregation.
+TEST_F(Bbr3DefaultTopologyTest, QUIC_SLOW_TEST(BandwidthIncreaseAggregation)) {
+  DefaultTopologyParams params;
+  params.local_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(15000);
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(100);
+  CreateNetwork(params);
+
+  // 2 RTTs of aggregation, with a max of 10kb.
+  EnableAggregation(10 * 1024, 2 * params.RTT());
+
+  // Reduce the payload to 2MB because 10MB takes too long.
+  sender_endpoint_.AddBytesToTransfer(2 * 1024 * 1024);
+
+  simulator_.RunFor(QuicTime::Delta::FromSeconds(15));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+  QUIC_LOG(INFO) << "Bandwidth increasing at time " << SimulatedNow();
+
+  // This is much farther off when aggregation is present,
+  // Ideally BSAO or another option would fix this.
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_est, 0.60f);
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.35);
+
+  // Now increase the bottleneck bandwidth from 100Kbps to 10Mbps.
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(10000);
+  TestLink()->set_bandwidth(params.test_link.bandwidth);
+
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() { return sender_endpoint_.bytes_to_transfer() == 0; },
+      QuicTime::Delta::FromSeconds(50));
+  EXPECT_TRUE(simulator_result);
+  // Ensure at least 10% of full bandwidth is discovered.
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_hi, 0.91f);
+}
+
+// Test Bbr2's reaction to a 100x bandwidth increase during a transfer with BBHI
+TEST_F(Bbr3DefaultTopologyTest, QUIC_SLOW_TEST(BandwidthIncreaseBBHI)) {
+  SetConnectionOption(kBBHI);
+  DefaultTopologyParams params;
+  params.local_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(15000);
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(100);
+  CreateNetwork(params);
+
+  sender_endpoint_.AddBytesToTransfer(10 * 1024 * 1024);
+
+  simulator_.RunFor(QuicTime::Delta::FromSeconds(15));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+  QUIC_LOG(INFO) << "Bandwidth increasing at time " << SimulatedNow();
+
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_est, 0.1f);
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.30);
+
+  // Now increase the bottleneck bandwidth from 100Kbps to 10Mbps.
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(10000);
+  TestLink()->set_bandwidth(params.test_link.bandwidth);
+
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() { return sender_endpoint_.bytes_to_transfer() == 0; },
+      QuicTime::Delta::FromSeconds(50));
+  EXPECT_TRUE(simulator_result);
+  // Ensure the full bandwidth is discovered.
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_hi, 0.02f);
+}
+
+// Test Bbr2's reaction to a 100x bandwidth increase during a transfer with BBHI
+// in the presence of ACK aggregation.
+TEST_F(Bbr3DefaultTopologyTest,
+       QUIC_SLOW_TEST(BandwidthIncreaseBBHIAggregation)) {
+  SetConnectionOption(kBBHI);
+  DefaultTopologyParams params;
+  params.local_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(15000);
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(100);
+  CreateNetwork(params);
+
+  // 2 RTTs of aggregation, with a max of 10kb.
+  EnableAggregation(10 * 1024, 2 * params.RTT());
+
+  // Reduce the payload to 2MB because 10MB takes too long.
+  sender_endpoint_.AddBytesToTransfer(2 * 1024 * 1024);
+
+  simulator_.RunFor(QuicTime::Delta::FromSeconds(15));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+  QUIC_LOG(INFO) << "Bandwidth increasing at time " << SimulatedNow();
+
+  // This is much farther off when aggregation is present,
+  // Ideally BSAO or another option would fix this.
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_est, 0.60f);
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.35);
+
+  // Now increase the bottleneck bandwidth from 100Kbps to 10Mbps.
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(10000);
+  TestLink()->set_bandwidth(params.test_link.bandwidth);
+
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() { return sender_endpoint_.bytes_to_transfer() == 0; },
+      QuicTime::Delta::FromSeconds(50));
+  EXPECT_TRUE(simulator_result);
+  // Ensure the full bandwidth is discovered.
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_hi, 0.90f);
+}
+
+// Test Bbr2's reaction to a 100x bandwidth increase during a transfer with BBHI
+// and B202, which changes the exit criteria to be based on
+// min_bytes_in_flight_in_round, in the presence of ACK aggregation.
+TEST_F(Bbr3DefaultTopologyTest,
+       QUIC_SLOW_TEST(BandwidthIncreaseBBHI_B202Aggregation)) {
+  SetConnectionOption(kBBHI);
+  SetConnectionOption(kB202);
+  DefaultTopologyParams params;
+  params.local_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(15000);
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(100);
+  CreateNetwork(params);
+
+  // 2 RTTs of aggregation, with a max of 10kb.
+  EnableAggregation(10 * 1024, 2 * params.RTT());
+
+  // Reduce the payload to 2MB because 10MB takes too long.
+  sender_endpoint_.AddBytesToTransfer(2 * 1024 * 1024);
+
+  simulator_.RunFor(QuicTime::Delta::FromSeconds(15));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+  QUIC_LOG(INFO) << "Bandwidth increasing at time " << SimulatedNow();
+
+  // This is much farther off when aggregation is present,
+  // Ideally BSAO or another option would fix this.
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_est, 0.60f);
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.35);
+
+  // Now increase the bottleneck bandwidth from 100Kbps to 10Mbps.
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(10000);
+  TestLink()->set_bandwidth(params.test_link.bandwidth);
+
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() { return sender_endpoint_.bytes_to_transfer() == 0; },
+      QuicTime::Delta::FromSeconds(50));
+  EXPECT_TRUE(simulator_result);
+  // Ensure at least 18% of the bandwidth is discovered.
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_hi, 0.85f);
+}
+
+// Test Bbr2's reaction to a 100x bandwidth increase during a transfer with B204
+TEST_F(Bbr3DefaultTopologyTest, QUIC_SLOW_TEST(BandwidthIncreaseB204)) {
+  SetConnectionOption(kB204);
+  DefaultTopologyParams params;
+  params.local_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(15000);
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(100);
+  CreateNetwork(params);
+
+  sender_endpoint_.AddBytesToTransfer(10 * 1024 * 1024);
+
+  simulator_.RunFor(QuicTime::Delta::FromSeconds(15));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+  QUIC_LOG(INFO) << "Bandwidth increasing at time " << SimulatedNow();
+
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_est, 0.1f);
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.25);
+  EXPECT_LE(sender_->ExportDebugState().max_ack_height, 2000u);
+
+  // Now increase the bottleneck bandwidth from 100Kbps to 10Mbps.
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(10000);
+  TestLink()->set_bandwidth(params.test_link.bandwidth);
+
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() { return sender_endpoint_.bytes_to_transfer() == 0; },
+      QuicTime::Delta::FromSeconds(50));
+  EXPECT_TRUE(simulator_result);
+  // Ensure the full bandwidth is discovered.
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_hi, 0.02f);
+}
+
+// Test Bbr2's reaction to a 100x bandwidth increase during a transfer with B204
+// in the presence of ACK aggregation.
+TEST_F(Bbr3DefaultTopologyTest,
+       QUIC_SLOW_TEST(BandwidthIncreaseB204Aggregation)) {
+  SetConnectionOption(kB204);
+  DefaultTopologyParams params;
+  params.local_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(15000);
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(100);
+  CreateNetwork(params);
+
+  // 2 RTTs of aggregation, with a max of 10kb.
+  EnableAggregation(10 * 1024, 2 * params.RTT());
+
+  // Reduce the payload to 2MB because 10MB takes too long.
+  sender_endpoint_.AddBytesToTransfer(2 * 1024 * 1024);
+
+  simulator_.RunFor(QuicTime::Delta::FromSeconds(15));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+  QUIC_LOG(INFO) << "Bandwidth increasing at time " << SimulatedNow();
+
+  // This is much farther off when aggregation is present, and B204 actually
+  // is increasing overestimation, which is surprising.
+  // Ideally BSAO or another option would fix this.
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_est, 0.60f);
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.35);
+  EXPECT_LE(sender_->ExportDebugState().max_ack_height, 10000u);
+
+  // Now increase the bottleneck bandwidth from 100Kbps to 10Mbps.
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(10000);
+  TestLink()->set_bandwidth(params.test_link.bandwidth);
+
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() { return sender_endpoint_.bytes_to_transfer() == 0; },
+      QuicTime::Delta::FromSeconds(50));
+  EXPECT_TRUE(simulator_result);
+  // Ensure at least 10% of full bandwidth is discovered.
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_hi, 0.95f);
+}
+
+// Test Bbr2's reaction to a 100x bandwidth increase during a transfer with B205
+TEST_F(Bbr3DefaultTopologyTest, QUIC_SLOW_TEST(BandwidthIncreaseB205)) {
+  SetConnectionOption(kB205);
+  DefaultTopologyParams params;
+  params.local_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(15000);
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(100);
+  CreateNetwork(params);
+
+  sender_endpoint_.AddBytesToTransfer(10 * 1024 * 1024);
+
+  simulator_.RunFor(QuicTime::Delta::FromSeconds(15));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+  QUIC_LOG(INFO) << "Bandwidth increasing at time " << SimulatedNow();
+
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_est, 0.1f);
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.10);
+
+  // Now increase the bottleneck bandwidth from 100Kbps to 10Mbps.
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(10000);
+  TestLink()->set_bandwidth(params.test_link.bandwidth);
+
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() { return sender_endpoint_.bytes_to_transfer() == 0; },
+      QuicTime::Delta::FromSeconds(50));
+  EXPECT_TRUE(simulator_result);
+  // Ensure the full bandwidth is discovered.
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_hi, 0.1f);
+}
+
+// Test Bbr2's reaction to a 100x bandwidth increase during a transfer with B205
+// in the presence of ACK aggregation.
+TEST_F(Bbr3DefaultTopologyTest,
+       QUIC_SLOW_TEST(BandwidthIncreaseB205Aggregation)) {
+  SetConnectionOption(kB205);
+  DefaultTopologyParams params;
+  params.local_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(15000);
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(100);
+  CreateNetwork(params);
+
+  // 2 RTTs of aggregation, with a max of 10kb.
+  EnableAggregation(10 * 1024, 2 * params.RTT());
+
+  // Reduce the payload to 2MB because 10MB takes too long.
+  sender_endpoint_.AddBytesToTransfer(2 * 1024 * 1024);
+
+  simulator_.RunFor(QuicTime::Delta::FromSeconds(15));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+  QUIC_LOG(INFO) << "Bandwidth increasing at time " << SimulatedNow();
+
+  // This is much farther off when aggregation is present,
+  // Ideally BSAO or another option would fix this.
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_est, 0.45f);
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.15);
+
+  // Now increase the bottleneck bandwidth from 100Kbps to 10Mbps.
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(10000);
+  TestLink()->set_bandwidth(params.test_link.bandwidth);
+
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() { return sender_endpoint_.bytes_to_transfer() == 0; },
+      QuicTime::Delta::FromSeconds(50));
+  EXPECT_TRUE(simulator_result);
+  // Ensure at least 5% of full bandwidth is discovered.
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_hi, 0.9f);
+}
+
+// Test Bbr2's reaction to a 100x bandwidth increase during a transfer with BB2U
+TEST_F(Bbr3DefaultTopologyTest, QUIC_SLOW_TEST(BandwidthIncreaseBB2U)) {
+  SetConnectionOption(kBB2U);
+  DefaultTopologyParams params;
+  params.local_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(15000);
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(100);
+  CreateNetwork(params);
+
+  sender_endpoint_.AddBytesToTransfer(10 * 1024 * 1024);
+
+  simulator_.RunFor(QuicTime::Delta::FromSeconds(15));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+  QUIC_LOG(INFO) << "Bandwidth increasing at time " << SimulatedNow();
+
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_est, 0.1f);
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.25);
+
+  // Now increase the bottleneck bandwidth from 100Kbps to 10Mbps.
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(10000);
+  TestLink()->set_bandwidth(params.test_link.bandwidth);
+
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() { return sender_endpoint_.bytes_to_transfer() == 0; },
+      QuicTime::Delta::FromSeconds(50));
+  EXPECT_TRUE(simulator_result);
+  // Ensure the full bandwidth is discovered.
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_hi, 0.1f);
+}
+
+// Test Bbr2's reaction to a 100x bandwidth increase during a transfer with BB2U
+// in the presence of ACK aggregation.
+TEST_F(Bbr3DefaultTopologyTest,
+       QUIC_SLOW_TEST(BandwidthIncreaseBB2UAggregation)) {
+  SetConnectionOption(kBB2U);
+  DefaultTopologyParams params;
+  params.local_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(15000);
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(100);
+  CreateNetwork(params);
+
+  // 2 RTTs of aggregation, with a max of 10kb.
+  EnableAggregation(10 * 1024, 2 * params.RTT());
+
+  // Reduce the payload to 5MB because 10MB takes too long.
+  sender_endpoint_.AddBytesToTransfer(5 * 1024 * 1024);
+
+  simulator_.RunFor(QuicTime::Delta::FromSeconds(15));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+  QUIC_LOG(INFO) << "Bandwidth increasing at time " << SimulatedNow();
+
+  // This is much farther off when aggregation is present,
+  // Ideally BSAO or another option would fix this.
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_est, 0.45f);
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.30);
+
+  // Now increase the bottleneck bandwidth from 100Kbps to 10Mbps.
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(10000);
+  TestLink()->set_bandwidth(params.test_link.bandwidth);
+
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() { return sender_endpoint_.bytes_to_transfer() == 0; },
+      QuicTime::Delta::FromSeconds(50));
+  EXPECT_TRUE(simulator_result);
+  // Ensure at least 15% of the full bandwidth is observed.
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_hi, 0.85f);
+}
+
+// Test Bbr2's reaction to a 100x bandwidth increase during a transfer with BB2U
+// and BBHI in the presence of ACK aggregation.
+TEST_F(Bbr3DefaultTopologyTest,
+       QUIC_SLOW_TEST(BandwidthIncreaseBB2UandBBHIAggregation)) {
+  SetConnectionOption(kBB2U);
+  SetConnectionOption(kBBHI);
+  DefaultTopologyParams params;
+  params.local_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(15000);
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(100);
+  CreateNetwork(params);
+
+  // 2 RTTs of aggregation, with a max of 10kb.
+  EnableAggregation(10 * 1024, 2 * params.RTT());
+
+  // Reduce the payload to 5MB because 10MB takes too long.
+  sender_endpoint_.AddBytesToTransfer(5 * 1024 * 1024);
+
+  simulator_.RunFor(QuicTime::Delta::FromSeconds(15));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+  QUIC_LOG(INFO) << "Bandwidth increasing at time " << SimulatedNow();
+
+  // This is much farther off when aggregation is present,
+  // Ideally BSAO or another option would fix this.
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_est, 0.45f);
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.30);
+
+  // Now increase the bottleneck bandwidth from 100Kbps to 10Mbps.
+  params.test_link.bandwidth = QuicBandwidth::FromKBitsPerSecond(10000);
+  TestLink()->set_bandwidth(params.test_link.bandwidth);
+
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() { return sender_endpoint_.bytes_to_transfer() == 0; },
+      QuicTime::Delta::FromSeconds(50));
+  EXPECT_TRUE(simulator_result);
+  // Ensure at least 15% of the full bandwidth is observed.
+  EXPECT_APPROX_EQ(params.test_link.bandwidth,
+                   sender_->ExportDebugState().bandwidth_hi, 0.85f);
+}
+
+// Test the number of losses incurred by the startup phase in a situation when
+// the buffer is less than BDP.
+TEST_F(Bbr3DefaultTopologyTest, PacketLossOnSmallBufferStartup) {
+  DefaultTopologyParams params;
+  params.switch_queue_capacity_in_bdp = 0.5;
+  CreateNetwork(params);
+
+  DriveOutOfStartup(params);
+  // Packet loss is smaller with a CWND gain of 2 than 2.889.
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.05);
+}
+
+// Test the number of losses decreases with packet-conservation pacing.
+TEST_F(Bbr3DefaultTopologyTest, PacketLossBBQ6SmallBufferStartup) {
+  SetConnectionOption(kBBQ2);  // Increase CWND gain.
+  SetConnectionOption(kBBQ6);
+  DefaultTopologyParams params;
+  params.switch_queue_capacity_in_bdp = 0.5;
+  CreateNetwork(params);
+
+  DriveOutOfStartup(params);
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.0575);
+  // bandwidth_lo is cleared exiting STARTUP.
+  EXPECT_EQ(sender_->ExportDebugState().bandwidth_lo,
+            QuicBandwidth::Infinite());
+}
+
+// Test the number of losses decreases with min_rtt packet-conservation pacing.
+TEST_F(Bbr3DefaultTopologyTest, PacketLossBBQ7SmallBufferStartup) {
+  SetConnectionOption(kBBQ2);  // Increase CWND gain.
+  SetConnectionOption(kBBQ7);
+  DefaultTopologyParams params;
+  params.switch_queue_capacity_in_bdp = 0.5;
+  CreateNetwork(params);
+
+  DriveOutOfStartup(params);
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.06);
+  // bandwidth_lo is cleared exiting STARTUP.
+  EXPECT_EQ(sender_->ExportDebugState().bandwidth_lo,
+            QuicBandwidth::Infinite());
+}
+
+// Test the number of losses decreases with Inflight packet-conservation pacing.
+TEST_F(Bbr3DefaultTopologyTest, PacketLossBBQ8SmallBufferStartup) {
+  SetConnectionOption(kBBQ2);  // Increase CWND gain.
+  SetConnectionOption(kBBQ8);
+  DefaultTopologyParams params;
+  params.switch_queue_capacity_in_bdp = 0.5;
+  CreateNetwork(params);
+
+  DriveOutOfStartup(params);
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.065);
+  // bandwidth_lo is cleared exiting STARTUP.
+  EXPECT_EQ(sender_->ExportDebugState().bandwidth_lo,
+            QuicBandwidth::Infinite());
+}
+
+// Test the number of losses decreases with CWND packet-conservation pacing.
+TEST_F(Bbr3DefaultTopologyTest, PacketLossBBQ9SmallBufferStartup) {
+  SetConnectionOption(kBBQ2);  // Increase CWND gain.
+  SetConnectionOption(kBBQ9);
+  DefaultTopologyParams params;
+  params.switch_queue_capacity_in_bdp = 0.5;
+  CreateNetwork(params);
+
+  DriveOutOfStartup(params);
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.065);
+  // bandwidth_lo is cleared exiting STARTUP.
+  EXPECT_EQ(sender_->ExportDebugState().bandwidth_lo,
+            QuicBandwidth::Infinite());
+}
+
+// Verify the behavior of the algorithm in the case when the connection sends
+// small bursts of data after sending continuously for a while.
+TEST_F(Bbr3DefaultTopologyTest, ApplicationLimitedBursts) {
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  EXPECT_FALSE(sender_->HasGoodBandwidthEstimateForResumption());
+  DriveOutOfStartup(params);
+  EXPECT_FALSE(sender_->ExportDebugState().last_sample_is_app_limited);
+  EXPECT_TRUE(sender_->HasGoodBandwidthEstimateForResumption());
+
+  SendBursts(params, 20, 512, QuicTime::Delta::FromSeconds(3));
+  EXPECT_TRUE(sender_->ExportDebugState().last_sample_is_app_limited);
+  EXPECT_TRUE(sender_->HasGoodBandwidthEstimateForResumption());
+  EXPECT_APPROX_EQ(params.BottleneckBandwidth(),
+                   sender_->ExportDebugState().bandwidth_hi, 0.01f);
+}
+
+// Verify the behavior of the algorithm in the case when the connection sends
+// small bursts of data and then starts sending continuously.
+TEST_F(Bbr3DefaultTopologyTest, ApplicationLimitedBurstsWithoutPrior) {
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  SendBursts(params, 40, 512, QuicTime::Delta::FromSeconds(3));
+  EXPECT_TRUE(sender_->ExportDebugState().last_sample_is_app_limited);
+
+  DriveOutOfStartup(params);
+  EXPECT_APPROX_EQ(params.BottleneckBandwidth(),
+                   sender_->ExportDebugState().bandwidth_hi, 0.01f);
+  EXPECT_FALSE(sender_->ExportDebugState().last_sample_is_app_limited);
+}
+
+// Verify that the DRAIN phase works correctly.
+TEST_F(Bbr3DefaultTopologyTest, Drain) {
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  const QuicTime::Delta timeout = QuicTime::Delta::FromSeconds(10);
+  // Get the queue at the bottleneck, which is the outgoing queue at the port to
+  // which the receiver is connected.
+  const simulator::Queue* queue = switch_->port_queue(2);
+  bool simulator_result;
+
+  // We have no intention of ever finishing this transfer.
+  sender_endpoint_.AddBytesToTransfer(100 * 1024 * 1024);
+
+  // Run the startup, and verify that it fills up the queue.
+  ASSERT_EQ(Bbr2Mode::STARTUP, sender_->ExportDebugState().mode);
+  simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() {
+        return sender_->ExportDebugState().mode != Bbr2Mode::STARTUP;
+      },
+      timeout);
+  ASSERT_TRUE(simulator_result);
+  ASSERT_EQ(Bbr2Mode::DRAIN, sender_->ExportDebugState().mode);
+  EXPECT_APPROX_EQ(sender_->BandwidthEstimate() * (1 / 2.885f),
+                   sender_->PacingRate(0), 0.01f);
+
+  // BBR uses CWND gain of 2 during STARTUP, hence it will fill the buffer with
+  // approximately 1 BDP.  Here, we use 0.95 to give some margin for error.
+  EXPECT_GE(queue->bytes_queued(), 0.95 * params.BDP());
+
+  // Observe increased RTT due to bufferbloat.
+  const QuicTime::Delta queueing_delay =
+      params.test_link.bandwidth.TransferTime(queue->bytes_queued());
+  EXPECT_APPROX_EQ(params.RTT() + queueing_delay, rtt_stats()->latest_rtt(),
+                   0.1f);
+
+  // Transition to the drain phase and verify that it makes the queue
+  // have at most a BDP worth of packets.
+  simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() { return sender_->ExportDebugState().mode != Bbr2Mode::DRAIN; },
+      timeout);
+  ASSERT_TRUE(simulator_result);
+  ASSERT_EQ(Bbr2Mode::PROBE_BW, sender_->ExportDebugState().mode);
+  EXPECT_LE(queue->bytes_queued(), params.BDP());
+
+  // Wait for a few round trips and ensure we're in appropriate phase of gain
+  // cycling before taking an RTT measurement.
+  const QuicRoundTripCount start_round_trip =
+      sender_->ExportDebugState().round_trip_count;
+  simulator_result = simulator_.RunUntilOrTimeout(
+      [this, start_round_trip]() {
+        const auto& debug_state = sender_->ExportDebugState();
+        QuicRoundTripCount rounds_passed =
+            debug_state.round_trip_count - start_round_trip;
+        return rounds_passed >= 4 && debug_state.mode == Bbr2Mode::PROBE_BW &&
+               debug_state.probe_bw.phase == ProbePhase::PROBE_REFILL;
+      },
+      timeout);
+  ASSERT_TRUE(simulator_result);
+
+  // Observe the bufferbloat go away.
+  EXPECT_APPROX_EQ(params.RTT(), rtt_stats()->smoothed_rtt(), 0.1f);
+}
+
+// Ensure that a connection that is app-limited and is at sufficiently low
+// bandwidth will not exit high gain phase, and similarly ensure that the
+// connection will exit low gain early if the number of bytes in flight is low.
+TEST_F(Bbr3DefaultTopologyTest, InFlightAwareGainCycling) {
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+  DriveOutOfStartup(params);
+
+  const QuicTime::Delta timeout = QuicTime::Delta::FromSeconds(5);
+  bool simulator_result;
+
+  // Start a few cycles prior to the high gain one.
+  simulator_result = SendUntilOrTimeout(
+      [this]() {
+        return sender_->ExportDebugState().probe_bw.phase ==
+               ProbePhase::PROBE_REFILL;
+      },
+      timeout);
+  ASSERT_TRUE(simulator_result);
+
+  // Send at 10% of available rate.  Run for 3 seconds, checking in the middle
+  // and at the end.  The pacing gain should be high throughout.
+  QuicBandwidth target_bandwidth = 0.1f * params.BottleneckBandwidth();
+  QuicTime::Delta burst_interval = QuicTime::Delta::FromMilliseconds(300);
+  for (int i = 0; i < 2; i++) {
+    SendBursts(params, 5, target_bandwidth * burst_interval, burst_interval);
+    EXPECT_EQ(Bbr2Mode::PROBE_BW, sender_->ExportDebugState().mode);
+    EXPECT_EQ(ProbePhase::PROBE_UP, sender_->ExportDebugState().probe_bw.phase);
+    EXPECT_APPROX_EQ(params.BottleneckBandwidth(),
+                     sender_->ExportDebugState().bandwidth_hi, 0.02f);
+  }
+
+  if (GetQuicReloadableFlag(quic_pacing_remove_non_initial_burst)) {
+    QuicSentPacketManagerPeer::GetPacingSender(
+        &sender_connection()->sent_packet_manager())
+        ->SetBurstTokens(10);
+  }
+
+  // Now that in-flight is almost zero and the pacing gain is still above 1,
+  // send approximately 1.4 BDPs worth of data. This should cause the PROBE_BW
+  // mode to enter low gain cycle(PROBE_DOWN), and exit it earlier than one
+  // min_rtt due to running out of data to send.
+  sender_endpoint_.AddBytesToTransfer(1.4 * params.BDP());
+  simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() {
+        return sender_->ExportDebugState().probe_bw.phase ==
+               ProbePhase::PROBE_DOWN;
+      },
+      timeout);
+  ASSERT_TRUE(simulator_result);
+  simulator_.RunFor(0.75 * sender_->ExportDebugState().min_rtt);
+  EXPECT_EQ(Bbr2Mode::PROBE_BW, sender_->ExportDebugState().mode);
+  EXPECT_EQ(ProbePhase::PROBE_CRUISE,
+            sender_->ExportDebugState().probe_bw.phase);
+}
+
+// Test exiting STARTUP earlier upon loss due to loss.
+TEST_F(Bbr3DefaultTopologyTest, ExitStartupDueToLoss) {
+  DefaultTopologyParams params;
+  params.switch_queue_capacity_in_bdp = 0.5;
+  CreateNetwork(params);
+
+  // Run until the full bandwidth is reached and check how many rounds it was.
+  sender_endpoint_.AddBytesToTransfer(12 * 1024 * 1024);
+  QuicRoundTripCount max_bw_round = 0;
+  QuicBandwidth max_bw(QuicBandwidth::Zero());
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this, &max_bw, &max_bw_round]() {
+        if (max_bw < sender_->ExportDebugState().bandwidth_hi) {
+          max_bw = sender_->ExportDebugState().bandwidth_hi;
+          max_bw_round = sender_->ExportDebugState().round_trip_count;
+        }
+        return sender_->ExportDebugState().startup.full_bandwidth_reached;
+      },
+      QuicTime::Delta::FromSeconds(5));
+  ASSERT_TRUE(simulator_result);
+  EXPECT_EQ(Bbr2Mode::DRAIN, sender_->ExportDebugState().mode);
+  EXPECT_GE(2u, sender_->ExportDebugState().round_trip_count - max_bw_round);
+  EXPECT_EQ(
+      1u,
+      sender_->ExportDebugState().startup.round_trips_without_bandwidth_growth);
+  EXPECT_NE(0u, sender_connection_stats().packets_lost);
+  EXPECT_FALSE(sender_->ExportDebugState().last_sample_is_app_limited);
+
+  EXPECT_GT(sender_->ExportDebugState().inflight_hi, 1.2f * params.BDP());
+}
+
+// Test exiting STARTUP earlier upon loss due to loss when connection option
+// B2SL is used.
+TEST_F(Bbr3DefaultTopologyTest, ExitStartupDueToLossB2SL) {
+  SetConnectionOption(kB2SL);
+  DefaultTopologyParams params;
+  params.switch_queue_capacity_in_bdp = 0.5;
+  CreateNetwork(params);
+
+  // Run until the full bandwidth is reached and check how many rounds it was.
+  sender_endpoint_.AddBytesToTransfer(12 * 1024 * 1024);
+  QuicRoundTripCount max_bw_round = 0;
+  QuicBandwidth max_bw(QuicBandwidth::Zero());
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this, &max_bw, &max_bw_round]() {
+        if (max_bw < sender_->ExportDebugState().bandwidth_hi) {
+          max_bw = sender_->ExportDebugState().bandwidth_hi;
+          max_bw_round = sender_->ExportDebugState().round_trip_count;
+        }
+        return sender_->ExportDebugState().startup.full_bandwidth_reached;
+      },
+      QuicTime::Delta::FromSeconds(5));
+  ASSERT_TRUE(simulator_result);
+  EXPECT_EQ(Bbr2Mode::DRAIN, sender_->ExportDebugState().mode);
+  EXPECT_GE(2u, sender_->ExportDebugState().round_trip_count - max_bw_round);
+  EXPECT_EQ(
+      1u,
+      sender_->ExportDebugState().startup.round_trips_without_bandwidth_growth);
+  EXPECT_NE(0u, sender_connection_stats().packets_lost);
+  EXPECT_FALSE(sender_->ExportDebugState().last_sample_is_app_limited);
+
+  EXPECT_APPROX_EQ(sender_->ExportDebugState().inflight_hi, params.BDP(), 0.1f);
+}
+
+// Verifies that in STARTUP, if we exceed loss threshold in a round, we exit
+// STARTUP at the end of the round even if there's enough bandwidth growth.
+TEST_F(Bbr3DefaultTopologyTest, ExitStartupDueToLossB2NE) {
+  // Set up flags such that any loss will be considered "too high".
+  SetQuicFlag(quic_bbr2_default_startup_full_loss_count, 0);
+  SetQuicFlag(quic_bbr2_default_loss_threshold, 0.0);
+
+  sender_ = SetupBbr3Sender(&sender_endpoint_, /*old_sender=*/nullptr);
+
+  SetConnectionOption(kB2NE);
+  DefaultTopologyParams params;
+  params.switch_queue_capacity_in_bdp = 0.5;
+  CreateNetwork(params);
+
+  // Run until the full bandwidth is reached and check how many rounds it was.
+  sender_endpoint_.AddBytesToTransfer(12 * 1024 * 1024);
+  QuicRoundTripCount max_bw_round = 0;
+  QuicBandwidth max_bw(QuicBandwidth::Zero());
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this, &max_bw, &max_bw_round]() {
+        if (max_bw < sender_->ExportDebugState().bandwidth_hi) {
+          max_bw = sender_->ExportDebugState().bandwidth_hi;
+          max_bw_round = sender_->ExportDebugState().round_trip_count;
+        }
+        return sender_->ExportDebugState().startup.full_bandwidth_reached;
+      },
+      QuicTime::Delta::FromSeconds(5));
+  ASSERT_TRUE(simulator_result);
+  EXPECT_EQ(Bbr2Mode::DRAIN, sender_->ExportDebugState().mode);
+  EXPECT_EQ(sender_->ExportDebugState().round_trip_count, max_bw_round);
+  EXPECT_EQ(
+      0u,
+      sender_->ExportDebugState().startup.round_trips_without_bandwidth_growth);
+  EXPECT_NE(0u, sender_connection_stats().packets_lost);
+}
+
+TEST_F(Bbr3DefaultTopologyTest, SenderPoliced) {
+  DefaultTopologyParams params;
+  params.sender_policer_params = TrafficPolicerParams();
+  params.sender_policer_params->initial_burst_size = 1000 * 10;
+  params.sender_policer_params->max_bucket_size = 1000 * 100;
+  params.sender_policer_params->target_bandwidth =
+      params.BottleneckBandwidth() * 0.25;
+
+  CreateNetwork(params);
+
+  ASSERT_GE(params.BDP(), kDefaultInitialCwndBytes + kDefaultTCPMSS);
+
+  DoSimpleTransfer(3 * 1024 * 1024, QuicTime::Delta::FromSeconds(30));
+  EXPECT_TRUE(Bbr3ModeIsOneOf({Bbr2Mode::PROBE_BW, Bbr2Mode::PROBE_RTT}));
+  // TODO(wub): Fix (long-term) bandwidth overestimation in policer mode, then
+  // reduce the loss rate upper bound.
+  EXPECT_LE(sender_loss_rate_in_packets(), 0.30);
+}
+
+// TODO(wub): Add other slowstart stats to BBRv2.
+TEST_F(Bbr3DefaultTopologyTest, StartupStats) {
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  DriveOutOfStartup(params);
+  ASSERT_FALSE(sender_->InSlowStart());
+
+  const QuicConnectionStats& stats = sender_connection_stats();
+  // The test explicitly replaces the default-created send algorithm with the
+  // one created by the test. slowstart_count increaments every time a BBR
+  // sender is created.
+  EXPECT_GE(stats.slowstart_count, 1u);
+  EXPECT_FALSE(stats.slowstart_duration.IsRunning());
+  EXPECT_THAT(stats.slowstart_duration.GetTotalElapsedTime(),
+              AllOf(Ge(QuicTime::Delta::FromMilliseconds(500)),
+                    Le(QuicTime::Delta::FromMilliseconds(1500))));
+  EXPECT_EQ(stats.slowstart_duration.GetTotalElapsedTime(),
+            QuicConnectionPeer::GetSentPacketManager(sender_connection())
+                ->GetSlowStartDuration());
+}
+
+TEST_F(Bbr3DefaultTopologyTest, ProbeUpAdaptInflightHiGradually) {
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  DriveOutOfStartup(params);
+
+  AckedPacketVector acked_packets;
+  QuicPacketNumber acked_packet_number =
+      sender_unacked_map()->GetLeastUnacked();
+  for (auto& info : *sender_unacked_map()) {
+    acked_packets.emplace_back(acked_packet_number++, info.bytes_sent,
+                               SimulatedNow());
+  }
+
+  // Advance time significantly so the OnCongestionEvent enters PROBE_REFILL.
+  QuicTime now = SimulatedNow() + QuicTime::Delta::FromSeconds(5);
+  auto next_packet_number = sender_unacked_map()->largest_sent_packet() + 1;
+  sender_->OnCongestionEvent(
+      /*rtt_updated=*/true, sender_unacked_map()->bytes_in_flight(), now,
+      acked_packets, {}, 0, 0);
+  ASSERT_EQ(ProbePhase::PROBE_REFILL,
+            sender_->ExportDebugState().probe_bw.phase);
+
+  // Send and Ack one packet to exit app limited and enter PROBE_UP.
+  sender_->OnPacketSent(now, /*bytes_in_flight=*/0, next_packet_number++,
+                        kDefaultMaxPacketSize, HAS_RETRANSMITTABLE_DATA);
+  now = now + params.RTT();
+  sender_->OnCongestionEvent(
+      /*rtt_updated=*/true, kDefaultMaxPacketSize, now,
+      {AckedPacket(next_packet_number - 1, kDefaultMaxPacketSize, now)}, {}, 0,
+      0);
+  ASSERT_EQ(ProbePhase::PROBE_UP, sender_->ExportDebugState().probe_bw.phase);
+
+  // Send 2 packets and lose the first one(50% loss) to exit PROBE_UP.
+  for (uint64_t i = 0; i < 2; ++i) {
+    sender_->OnPacketSent(now, /*bytes_in_flight=*/i * kDefaultMaxPacketSize,
+                          next_packet_number++, kDefaultMaxPacketSize,
+                          HAS_RETRANSMITTABLE_DATA);
+  }
+  now = now + params.RTT();
+  sender_->OnCongestionEvent(
+      /*rtt_updated=*/true, 2 * kDefaultMaxPacketSize, now,
+      {AckedPacket(next_packet_number - 1, kDefaultMaxPacketSize, now)},
+      {LostPacket(next_packet_number - 2, kDefaultMaxPacketSize)}, 0, 0);
+
+  QuicByteCount inflight_hi = sender_->ExportDebugState().inflight_hi;
+  EXPECT_LT(2 * kDefaultMaxPacketSize, inflight_hi);
+}
+
+// Ensures bandwidth estimate does not change after a loss only event.
+TEST_F(Bbr3DefaultTopologyTest, LossOnlyCongestionEvent) {
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  DriveOutOfStartup(params);
+  EXPECT_FALSE(sender_->ExportDebugState().last_sample_is_app_limited);
+
+  // Send some bursts, each burst increments round count by 1, since it only
+  // generates small, app-limited samples, the max_bandwidth_filter_ will not be
+  // updated.
+  SendBursts(params, 20, 512, QuicTime::Delta::FromSeconds(3));
+
+  // Run until we have something in flight.
+  sender_endpoint_.AddBytesToTransfer(50 * 1024 * 1024);
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [&]() { return sender_unacked_map()->bytes_in_flight() > 0; },
+      QuicTime::Delta::FromSeconds(5));
+  ASSERT_TRUE(simulator_result);
+
+  const QuicBandwidth prior_bandwidth_estimate = sender_->BandwidthEstimate();
+  EXPECT_APPROX_EQ(params.BottleneckBandwidth(), prior_bandwidth_estimate,
+                   0.01f);
+
+  // Lose the least unacked packet.
+  LostPacketVector lost_packets;
+  lost_packets.emplace_back(
+      sender_connection()->sent_packet_manager().GetLeastUnacked(),
+      kDefaultMaxPacketSize);
+
+  QuicTime now = simulator_.GetClock()->Now() + params.RTT() * 0.25;
+  sender_->OnCongestionEvent(false, sender_unacked_map()->bytes_in_flight(),
+                             now, {}, lost_packets, 0, 0);
+
+  // Bandwidth estimate should not change for the loss only event.
+  EXPECT_EQ(prior_bandwidth_estimate, sender_->BandwidthEstimate());
+}
+
+// Simulate the case where a packet is considered lost but then acked.
+TEST_F(Bbr3DefaultTopologyTest, SpuriousLossEvent) {
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  DriveOutOfStartup(params);
+
+  // Make sure we have something in flight.
+  if (sender_unacked_map()->bytes_in_flight() == 0) {
+    sender_endpoint_.AddBytesToTransfer(50 * 1024 * 1024);
+    bool simulator_result = simulator_.RunUntilOrTimeout(
+        [&]() { return sender_unacked_map()->bytes_in_flight() > 0; },
+        QuicTime::Delta::FromSeconds(5));
+    ASSERT_TRUE(simulator_result);
+  }
+
+  // Lose all in flight packets.
+  QuicTime now = simulator_.GetClock()->Now() + params.RTT() * 0.25;
+  const QuicByteCount prior_inflight = sender_unacked_map()->bytes_in_flight();
+  LostPacketVector lost_packets;
+  for (QuicPacketNumber packet_number = sender_unacked_map()->GetLeastUnacked();
+       sender_unacked_map()->HasInFlightPackets(); packet_number++) {
+    const auto& info = sender_unacked_map()->GetTransmissionInfo(packet_number);
+    if (!info.in_flight) {
+      continue;
+    }
+    lost_packets.emplace_back(packet_number, info.bytes_sent);
+    sender_unacked_map()->RemoveFromInFlight(packet_number);
+  }
+  ASSERT_FALSE(lost_packets.empty());
+  sender_->OnCongestionEvent(false, prior_inflight, now, {}, lost_packets, 0,
+                             0);
+
+  // Pretend the first lost packet number is acked.
+  now = now + params.RTT() * 0.5;
+  AckedPacketVector acked_packets;
+  acked_packets.emplace_back(lost_packets[0].packet_number, 0, now);
+  acked_packets.back().spurious_loss = true;
+  EXPECT_EQ(sender_unacked_map()->bytes_in_flight(), 0);
+  sender_->OnCongestionEvent(false, sender_unacked_map()->bytes_in_flight(),
+                             now, acked_packets, {}, 0, 0);
+
+  EXPECT_EQ(sender_->GetNetworkModel().total_bytes_sent(),
+            sender_->GetNetworkModel().total_bytes_acked() +
+                sender_->GetNetworkModel().total_bytes_lost());
+}
+
+// After quiescence, if the sender is in PROBE_RTT, it should transition to
+// PROBE_BW immediately on the first sent packet after quiescence.
+TEST_F(Bbr3DefaultTopologyTest, ProbeRttAfterQuiescenceImmediatelyExits) {
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  DriveOutOfStartup(params);
+
+  const QuicTime::Delta timeout = QuicTime::Delta::FromSeconds(15);
+  bool simulator_result;
+
+  // Keep sending until reach PROBE_RTT.
+  simulator_result = SendUntilOrTimeout(
+      [this]() {
+        return sender_->ExportDebugState().mode == Bbr2Mode::PROBE_RTT;
+      },
+      timeout);
+  ASSERT_TRUE(simulator_result);
+
+  // Wait for entering a quiescence of 5 seconds.
+  ASSERT_TRUE(simulator_.RunUntilOrTimeout(
+      [this]() {
+        return sender_unacked_map()->bytes_in_flight() == 0 &&
+               sender_->ExportDebugState().mode == Bbr2Mode::PROBE_RTT;
+      },
+      timeout));
+
+  simulator_.RunFor(QuicTime::Delta::FromSeconds(5));
+
+  // Send one packet to exit quiescence.
+  EXPECT_EQ(sender_->ExportDebugState().mode, Bbr2Mode::PROBE_RTT);
+  sender_->OnPacketSent(SimulatedNow(), /*bytes_in_flight=*/0,
+                        sender_unacked_map()->largest_sent_packet() + 1,
+                        kDefaultMaxPacketSize, HAS_RETRANSMITTABLE_DATA);
+
+  EXPECT_EQ(sender_->ExportDebugState().mode, Bbr2Mode::PROBE_BW);
+}
+
+TEST_F(Bbr3DefaultTopologyTest, ProbeBwAfterQuiescencePostponeMinRttTimestamp) {
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  DriveOutOfStartup(params);
+
+  const QuicTime::Delta timeout = QuicTime::Delta::FromSeconds(5);
+  bool simulator_result;
+
+  // Keep sending until reach PROBE_REFILL.
+  simulator_result = SendUntilOrTimeout(
+      [this]() {
+        return sender_->ExportDebugState().probe_bw.phase ==
+               ProbePhase::PROBE_REFILL;
+      },
+      timeout);
+  ASSERT_TRUE(simulator_result);
+
+  const QuicTime min_rtt_timestamp_before_idle =
+      sender_->ExportDebugState().min_rtt_timestamp;
+
+  // Wait for entering a quiescence of 15 seconds.
+  ASSERT_TRUE(simulator_.RunUntilOrTimeout(
+      [this]() { return sender_unacked_map()->bytes_in_flight() == 0; },
+      params.RTT() + timeout));
+
+  simulator_.RunFor(QuicTime::Delta::FromSeconds(15));
+
+  // Send some data to exit quiescence.
+  SendBursts(params, 1, kDefaultTCPMSS, QuicTime::Delta::Zero());
+  const QuicTime min_rtt_timestamp_after_idle =
+      sender_->ExportDebugState().min_rtt_timestamp;
+
+  EXPECT_LT(min_rtt_timestamp_before_idle + QuicTime::Delta::FromSeconds(14),
+            min_rtt_timestamp_after_idle);
+}
+
+TEST_F(Bbr3DefaultTopologyTest, SwitchToBbr2MidConnection) {
+  QuicTime now = QuicTime::Zero();
+  BbrSender old_sender(sender_connection()->clock()->Now(),
+                       sender_connection()->sent_packet_manager().GetRttStats(),
+                       GetUnackedMap(sender_connection()),
+                       kDefaultInitialCwndPackets + 1,
+                       GetQuicFlag(quic_max_congestion_window), &random_,
+                       QuicConnectionPeer::GetStats(sender_connection()));
+
+  QuicPacketNumber next_packet_number(1);
+
+  // Send packets 1-4.
+  while (next_packet_number < QuicPacketNumber(5)) {
+    now = now + QuicTime::Delta::FromMilliseconds(10);
+
+    old_sender.OnPacketSent(now, /*bytes_in_flight=*/0, next_packet_number++,
+                            /*bytes=*/1350, HAS_RETRANSMITTABLE_DATA);
+  }
+
+  // Switch from |old_sender| to |sender_|.
+  const QuicByteCount old_sender_cwnd = old_sender.GetCongestionWindow();
+  sender_ = SetupBbr3Sender(&sender_endpoint_, &old_sender);
+  EXPECT_EQ(old_sender_cwnd, sender_->GetCongestionWindow());
+
+  // Send packets 5-7.
+  now = now + QuicTime::Delta::FromMilliseconds(10);
+  sender_->OnPacketSent(now, /*bytes_in_flight=*/1350, next_packet_number++,
+                        /*bytes=*/23, NO_RETRANSMITTABLE_DATA);
+
+  now = now + QuicTime::Delta::FromMilliseconds(10);
+  sender_->OnPacketSent(now, /*bytes_in_flight=*/1350, next_packet_number++,
+                        /*bytes=*/767, HAS_RETRANSMITTABLE_DATA);
+
+  QuicByteCount bytes_in_flight = 767;
+  while (next_packet_number < QuicPacketNumber(30)) {
+    now = now + QuicTime::Delta::FromMilliseconds(10);
+    bytes_in_flight += 1350;
+    sender_->OnPacketSent(now, bytes_in_flight, next_packet_number++,
+                          /*bytes=*/1350, HAS_RETRANSMITTABLE_DATA);
+  }
+
+  // Ack 1 & 2.
+  AckedPacketVector acked = {
+      AckedPacket(QuicPacketNumber(1), /*bytes_acked=*/0, QuicTime::Zero()),
+      AckedPacket(QuicPacketNumber(2), /*bytes_acked=*/0, QuicTime::Zero()),
+  };
+  now = now + QuicTime::Delta::FromMilliseconds(2000);
+  sender_->OnCongestionEvent(true, bytes_in_flight, now, acked, {}, 0, 0);
+
+  // Send 30-41.
+  while (next_packet_number < QuicPacketNumber(42)) {
+    now = now + QuicTime::Delta::FromMilliseconds(10);
+    bytes_in_flight += 1350;
+    sender_->OnPacketSent(now, bytes_in_flight, next_packet_number++,
+                          /*bytes=*/1350, HAS_RETRANSMITTABLE_DATA);
+  }
+
+  // Ack 3.
+  acked = {
+      AckedPacket(QuicPacketNumber(3), /*bytes_acked=*/0, QuicTime::Zero()),
+  };
+  now = now + QuicTime::Delta::FromMilliseconds(2000);
+  sender_->OnCongestionEvent(true, bytes_in_flight, now, acked, {}, 0, 0);
+
+  // Send 42.
+  now = now + QuicTime::Delta::FromMilliseconds(10);
+  bytes_in_flight += 1350;
+  sender_->OnPacketSent(now, bytes_in_flight, next_packet_number++,
+                        /*bytes=*/1350, HAS_RETRANSMITTABLE_DATA);
+
+  // Ack 4-7.
+  acked = {
+      AckedPacket(QuicPacketNumber(4), /*bytes_acked=*/0, QuicTime::Zero()),
+      AckedPacket(QuicPacketNumber(5), /*bytes_acked=*/0, QuicTime::Zero()),
+      AckedPacket(QuicPacketNumber(6), /*bytes_acked=*/767, QuicTime::Zero()),
+      AckedPacket(QuicPacketNumber(7), /*bytes_acked=*/1350, QuicTime::Zero()),
+  };
+  now = now + QuicTime::Delta::FromMilliseconds(2000);
+  sender_->OnCongestionEvent(true, bytes_in_flight, now, acked, {}, 0, 0);
+  EXPECT_FALSE(sender_->BandwidthEstimate().IsZero());
+}
+
+TEST_F(Bbr3DefaultTopologyTest, AdjustNetworkParameters) {
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  QUIC_LOG(INFO) << "Initial cwnd: " << sender_debug_state().congestion_window
+                 << "\nInitial pacing rate: " << sender_->PacingRate(0)
+                 << "\nInitial bandwidth estimate: "
+                 << sender_->BandwidthEstimate()
+                 << "\nInitial rtt: " << sender_debug_state().min_rtt;
+
+  sender_connection()->AdjustNetworkParameters(
+      SendAlgorithmInterface::NetworkParams(params.BottleneckBandwidth(),
+                                            params.RTT(),
+                                            /*allow_cwnd_to_decrease=*/false));
+
+  EXPECT_EQ(params.BDP(), sender_->ExportDebugState().congestion_window);
+
+  EXPECT_EQ(params.BottleneckBandwidth(),
+            sender_->PacingRate(/*bytes_in_flight=*/0));
+  EXPECT_NE(params.BottleneckBandwidth(), sender_->BandwidthEstimate());
+
+  EXPECT_APPROX_EQ(params.RTT(), sender_->ExportDebugState().min_rtt, 0.01f);
+
+  DriveOutOfStartup(params);
+}
+
+TEST_F(Bbr3DefaultTopologyTest,
+       200InitialCongestionWindowWithNetworkParameterAdjusted) {
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  sender_endpoint_.AddBytesToTransfer(1 * 1024 * 1024);
+
+  // Wait until an ACK comes back.
+  const QuicTime::Delta timeout = QuicTime::Delta::FromSeconds(5);
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() { return !sender_->ExportDebugState().min_rtt.IsZero(); },
+      timeout);
+  ASSERT_TRUE(simulator_result);
+
+  // Bootstrap cwnd by a overly large bandwidth sample.
+  sender_connection()->AdjustNetworkParameters(
+      SendAlgorithmInterface::NetworkParams(1024 * params.BottleneckBandwidth(),
+                                            QuicTime::Delta::Zero(), false));
+
+  // Verify cwnd is capped at 200.
+  EXPECT_EQ(200 * kDefaultTCPMSS,
+            sender_->ExportDebugState().congestion_window);
+  EXPECT_GT(1024 * params.BottleneckBandwidth(), sender_->PacingRate(0));
+}
+
+TEST_F(Bbr3DefaultTopologyTest,
+       100InitialCongestionWindowFromNetworkParameter) {
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  sender_endpoint_.AddBytesToTransfer(1 * 1024 * 1024);
+  // Wait until an ACK comes back.
+  const QuicTime::Delta timeout = QuicTime::Delta::FromSeconds(5);
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() { return !sender_->ExportDebugState().min_rtt.IsZero(); },
+      timeout);
+  ASSERT_TRUE(simulator_result);
+
+  // Bootstrap cwnd by a overly large bandwidth sample.
+  SendAlgorithmInterface::NetworkParams network_params(
+      1024 * params.BottleneckBandwidth(), QuicTime::Delta::Zero(), false);
+  network_params.max_initial_congestion_window = 100;
+  sender_connection()->AdjustNetworkParameters(network_params);
+
+  // Verify cwnd is capped at 100.
+  EXPECT_EQ(100 * kDefaultTCPMSS,
+            sender_->ExportDebugState().congestion_window);
+  EXPECT_GT(1024 * params.BottleneckBandwidth(), sender_->PacingRate(0));
+}
+
+TEST_F(Bbr3DefaultTopologyTest,
+       100InitialCongestionWindowWithNetworkParameterAdjusted) {
+  SetConnectionOption(kICW1);
+  DefaultTopologyParams params;
+  CreateNetwork(params);
+
+  sender_endpoint_.AddBytesToTransfer(1 * 1024 * 1024);
+  // Wait until an ACK comes back.
+  const QuicTime::Delta timeout = QuicTime::Delta::FromSeconds(5);
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() { return !sender_->ExportDebugState().min_rtt.IsZero(); },
+      timeout);
+  ASSERT_TRUE(simulator_result);
+
+  // Bootstrap cwnd by a overly large bandwidth sample.
+  sender_connection()->AdjustNetworkParameters(
+      SendAlgorithmInterface::NetworkParams(1024 * params.BottleneckBandwidth(),
+                                            QuicTime::Delta::Zero(), false));
+
+  // Verify cwnd is capped at 100.
+  EXPECT_EQ(100 * kDefaultTCPMSS,
+            sender_->ExportDebugState().congestion_window);
+  EXPECT_GT(1024 * params.BottleneckBandwidth(), sender_->PacingRate(0));
+}
+
+// All Bbr2MultiSenderTests uses the following network topology:
+//
+//   Sender 0  (A Bbr3Sender)
+//       |
+//       | <-- local_links[0]
+//       |
+//       |  Sender N (1 <= N < kNumLocalLinks) (May or may not be a Bbr3Sender)
+//       |      |
+//       |      | <-- local_links[N]
+//       |      |
+//    Network switch
+//           *  <-- the bottleneck queue in the direction
+//           |          of the receiver
+//           |
+//           |  <-- test_link
+//           |
+//           |
+//       Receiver
+class MultiSenderTopologyParams {
+ public:
+  static constexpr size_t kNumLocalLinks = 8;
+  std::array<LinkParams, kNumLocalLinks> local_links = {
+      LinkParams(10000, 1987), LinkParams(10000, 1993), LinkParams(10000, 1997),
+      LinkParams(10000, 1999), LinkParams(10000, 2003), LinkParams(10000, 2011),
+      LinkParams(10000, 2017), LinkParams(10000, 2027),
+  };
+
+  LinkParams test_link = LinkParams(4000, 30000);
+
+  const simulator::SwitchPortNumber switch_port_count = kNumLocalLinks + 1;
+
+  // Network switch queue capacity, in number of BDPs.
+  float switch_queue_capacity_in_bdp = 2;
+
+  QuicBandwidth BottleneckBandwidth() const {
+    // Make sure all local links have a higher bandwidth than the test link.
+    for (size_t i = 0; i < local_links.size(); ++i) {
+      QUICHE_CHECK_GT(local_links[i].bandwidth, test_link.bandwidth);
+    }
+    return test_link.bandwidth;
+  }
+
+  // Sender n's round trip time of a single full size packet.
+  QuicTime::Delta Rtt(size_t n) const {
+    return 2 * (local_links[n].delay + test_link.delay +
+                local_links[n].bandwidth.TransferTime(kMaxOutgoingPacketSize) +
+                test_link.bandwidth.TransferTime(kMaxOutgoingPacketSize));
+  }
+
+  QuicByteCount Bdp(size_t n) const { return BottleneckBandwidth() * Rtt(n); }
+
+  QuicByteCount SwitchQueueCapacity() const {
+    return switch_queue_capacity_in_bdp * Bdp(1);
+  }
+
+  std::string ToString() const {
+    std::ostringstream os;
+    os << "{ BottleneckBandwidth: " << BottleneckBandwidth();
+    for (size_t i = 0; i < local_links.size(); ++i) {
+      os << " RTT_" << i << ": " << Rtt(i) << " BDP_" << i << ": " << Bdp(i);
+    }
+    os << " BottleneckQueueSize: " << SwitchQueueCapacity() << "}";
+    return os.str();
+  }
+};
+
+class Bbr2MultiSenderTest : public Bbr3SimulatorTest {
+ protected:
+  Bbr2MultiSenderTest() {
+    uint64_t first_connection_id = 42;
+    std::vector<simulator::QuicEndpointBase*> receiver_endpoint_pointers;
+    for (size_t i = 0; i < MultiSenderTopologyParams::kNumLocalLinks; ++i) {
+      std::string sender_name = absl::StrCat("Sender", i + 1);
+      std::string receiver_name = absl::StrCat("Receiver", i + 1);
+      sender_endpoints_.push_back(std::make_unique<simulator::QuicEndpoint>(
+          &simulator_, sender_name, receiver_name, Perspective::IS_CLIENT,
+          TestConnectionId(first_connection_id + i)));
+      receiver_endpoints_.push_back(std::make_unique<simulator::QuicEndpoint>(
+          &simulator_, receiver_name, sender_name, Perspective::IS_SERVER,
+          TestConnectionId(first_connection_id + i)));
+      receiver_endpoint_pointers.push_back(receiver_endpoints_.back().get());
+    }
+    receiver_multiplexer_ =
+        std::make_unique<simulator::QuicEndpointMultiplexer>(
+            "Receiver multiplexer", receiver_endpoint_pointers);
+    sender_0_ = SetupBbr3Sender(sender_endpoints_[0].get());
+  }
+
+  ~Bbr2MultiSenderTest() {
+    const auto* test_info =
+        ::testing::UnitTest::GetInstance()->current_test_info();
+    QUIC_LOG(INFO) << "Bbr2MultiSenderTest." << test_info->name()
+                   << " completed at simulated time: "
+                   << SimulatedNow().ToDebuggingValue() / 1e6
+                   << " sec. Per sender stats:";
+    for (size_t i = 0; i < sender_endpoints_.size(); ++i) {
+      QUIC_LOG(INFO) << "sender[" << i << "]: "
+                     << sender_connection(i)
+                            ->sent_packet_manager()
+                            .GetSendAlgorithm()
+                            ->GetCongestionControlType()
+                     << ", packet_loss:"
+                     << 100.0 * sender_loss_rate_in_packets(i) << "%";
+    }
+  }
+
+  Bbr3Sender* SetupBbr3Sender(simulator::QuicEndpoint* endpoint) {
+    // Ownership of the sender will be overtaken by the endpoint.
+    Bbr3Sender* sender = new Bbr3Sender(
+        endpoint->connection()->clock()->Now(),
+        endpoint->connection()->sent_packet_manager().GetRttStats(),
+        QuicSentPacketManagerPeer::GetUnackedPacketMap(
+            QuicConnectionPeer::GetSentPacketManager(endpoint->connection())),
+        kDefaultInitialCwndPackets, GetQuicFlag(quic_max_congestion_window),
+        &random_, QuicConnectionPeer::GetStats(endpoint->connection()),
+        nullptr);
+    // TODO(ianswett): Add dedicated tests for this option until it becomes
+    // the default behavior.
+    SetConnectionOption(sender, kBBRA);
+
+    QuicConnectionPeer::SetSendAlgorithm(endpoint->connection(), sender);
+    endpoint->RecordTrace();
+    return sender;
+  }
+
+  BbrSender* SetupBbrSender(simulator::QuicEndpoint* endpoint) {
+    // Ownership of the sender will be overtaken by the endpoint.
+    BbrSender* sender = new BbrSender(
+        endpoint->connection()->clock()->Now(),
+        endpoint->connection()->sent_packet_manager().GetRttStats(),
+        QuicSentPacketManagerPeer::GetUnackedPacketMap(
+            QuicConnectionPeer::GetSentPacketManager(endpoint->connection())),
+        kDefaultInitialCwndPackets, GetQuicFlag(quic_max_congestion_window),
+        &random_, QuicConnectionPeer::GetStats(endpoint->connection()));
+    QuicConnectionPeer::SetSendAlgorithm(endpoint->connection(), sender);
+    endpoint->RecordTrace();
+    return sender;
+  }
+
+  // reno => Reno. !reno => Cubic.
+  TcpCubicSenderBytes* SetupTcpSender(simulator::QuicEndpoint* endpoint,
+                                      bool reno) {
+    // Ownership of the sender will be overtaken by the endpoint.
+    TcpCubicSenderBytes* sender = new TcpCubicSenderBytes(
+        endpoint->connection()->clock(),
+        endpoint->connection()->sent_packet_manager().GetRttStats(), reno,
+        kDefaultInitialCwndPackets, GetQuicFlag(quic_max_congestion_window),
+        QuicConnectionPeer::GetStats(endpoint->connection()));
+    QuicConnectionPeer::SetSendAlgorithm(endpoint->connection(), sender);
+    endpoint->RecordTrace();
+    return sender;
+  }
+
+  void SetConnectionOption(SendAlgorithmInterface* sender, QuicTag option) {
+    QuicConfig config;
+    QuicTagVector options;
+    options.push_back(option);
+    QuicConfigPeer::SetReceivedConnectionOptions(&config, options);
+    sender->SetFromConfig(config, Perspective::IS_SERVER);
+  }
+
+  void CreateNetwork(const MultiSenderTopologyParams& params) {
+    QUIC_LOG(INFO) << "CreateNetwork with parameters: " << params.ToString();
+    switch_ = std::make_unique<simulator::Switch>(&simulator_, "Switch",
+                                                  params.switch_port_count,
+                                                  params.SwitchQueueCapacity());
+
+    network_links_.push_back(std::make_unique<simulator::SymmetricLink>(
+        receiver_multiplexer_.get(), switch_->port(1),
+        params.test_link.bandwidth, params.test_link.delay));
+    for (size_t i = 0; i < MultiSenderTopologyParams::kNumLocalLinks; ++i) {
+      simulator::SwitchPortNumber port_number = i + 2;
+      network_links_.push_back(std::make_unique<simulator::SymmetricLink>(
+          sender_endpoints_[i].get(), switch_->port(port_number),
+          params.local_links[i].bandwidth, params.local_links[i].delay));
+    }
+  }
+
+  QuicConnection* sender_connection(size_t which) {
+    return sender_endpoints_[which]->connection();
+  }
+
+  const QuicConnectionStats& sender_connection_stats(size_t which) {
+    return sender_connection(which)->GetStats();
+  }
+
+  float sender_loss_rate_in_packets(size_t which) {
+    return static_cast<float>(sender_connection_stats(which).packets_lost) /
+           sender_connection_stats(which).packets_sent;
+  }
+
+  std::vector<std::unique_ptr<simulator::QuicEndpoint>> sender_endpoints_;
+  std::vector<std::unique_ptr<simulator::QuicEndpoint>> receiver_endpoints_;
+  std::unique_ptr<simulator::QuicEndpointMultiplexer> receiver_multiplexer_;
+  Bbr3Sender* sender_0_;
+
+  std::unique_ptr<simulator::Switch> switch_;
+  std::vector<std::unique_ptr<simulator::SymmetricLink>> network_links_;
+};
+
+TEST_F(Bbr2MultiSenderTest, Bbr2VsBbr2) {
+  SetupBbr3Sender(sender_endpoints_[1].get());
+
+  MultiSenderTopologyParams params;
+  CreateNetwork(params);
+
+  const QuicByteCount transfer_size = 10 * 1024 * 1024;
+  const QuicTime::Delta transfer_time =
+      params.BottleneckBandwidth().TransferTime(transfer_size);
+  QUIC_LOG(INFO) << "Single flow transfer time: " << transfer_time;
+
+  // Transfer 10% of data in first transfer.
+  sender_endpoints_[0]->AddBytesToTransfer(transfer_size);
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() {
+        return receiver_endpoints_[0]->bytes_received() >= 0.1 * transfer_size;
+      },
+      transfer_time);
+  ASSERT_TRUE(simulator_result);
+
+  // Start the second transfer and wait until both finish.
+  sender_endpoints_[1]->AddBytesToTransfer(transfer_size);
+  simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() {
+        return receiver_endpoints_[0]->bytes_received() == transfer_size &&
+               receiver_endpoints_[1]->bytes_received() == transfer_size;
+      },
+      3 * transfer_time);
+  ASSERT_TRUE(simulator_result);
+}
+
+TEST_F(Bbr2MultiSenderTest, QUIC_SLOW_TEST(MultipleBbr2s)) {
+  const int kTotalNumSenders = 6;
+  for (int i = 1; i < kTotalNumSenders; ++i) {
+    SetupBbr3Sender(sender_endpoints_[i].get());
+  }
+
+  MultiSenderTopologyParams params;
+  CreateNetwork(params);
+
+  const QuicByteCount transfer_size = 10 * 1024 * 1024;
+  const QuicTime::Delta transfer_time =
+      params.BottleneckBandwidth().TransferTime(transfer_size);
+  QUIC_LOG(INFO) << "Single flow transfer time: " << transfer_time
+                 << ". Now: " << SimulatedNow();
+
+  // Start all transfers.
+  for (int i = 0; i < kTotalNumSenders; ++i) {
+    if (i != 0) {
+      const QuicTime sender_start_time =
+          SimulatedNow() + QuicTime::Delta::FromSeconds(2);
+      bool simulator_result = simulator_.RunUntilOrTimeout(
+          [&]() { return SimulatedNow() >= sender_start_time; }, transfer_time);
+      ASSERT_TRUE(simulator_result);
+    }
+
+    sender_endpoints_[i]->AddBytesToTransfer(transfer_size);
+  }
+
+  // Wait for all transfers to finish.
+  QuicTime::Delta expected_total_transfer_time_upper_bound =
+      QuicTime::Delta::FromMicroseconds(kTotalNumSenders *
+                                        transfer_time.ToMicroseconds() * 1.1);
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() {
+        for (int i = 0; i < kTotalNumSenders; ++i) {
+          if (receiver_endpoints_[i]->bytes_received() < transfer_size) {
+            return false;
+          }
+        }
+        return true;
+      },
+      expected_total_transfer_time_upper_bound);
+  ASSERT_TRUE(simulator_result)
+      << "Expected upper bound: " << expected_total_transfer_time_upper_bound;
+}
+
+/* The first 11 packets are sent at the same time, but the duration between the
+ * acks of the 1st and the 11th packet is 49 milliseconds, causing very low bw
+ * samples. This happens for both large and small buffers.
+ */
+/*
+TEST_F(Bbr2MultiSenderTest, Bbr2VsBbr2LargeRttTinyBuffer) {
+  SetupBbr3Sender(sender_endpoints_[1].get());
+
+  MultiSenderTopologyParams params;
+  params.switch_queue_capacity_in_bdp = 0.05;
+  params.test_link.delay = QuicTime::Delta::FromSeconds(1);
+  CreateNetwork(params);
+
+  const QuicByteCount transfer_size = 10 * 1024 * 1024;
+  const QuicTime::Delta transfer_time =
+      params.BottleneckBandwidth().TransferTime(transfer_size);
+  QUIC_LOG(INFO) << "Single flow transfer time: " << transfer_time;
+
+  // Transfer 10% of data in first transfer.
+  sender_endpoints_[0]->AddBytesToTransfer(transfer_size);
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() {
+        return receiver_endpoints_[0]->bytes_received() >= 0.1 * transfer_size;
+      },
+      transfer_time);
+  ASSERT_TRUE(simulator_result);
+
+  // Start the second transfer and wait until both finish.
+  sender_endpoints_[1]->AddBytesToTransfer(transfer_size);
+  simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() {
+        return receiver_endpoints_[0]->bytes_received() == transfer_size &&
+               receiver_endpoints_[1]->bytes_received() == transfer_size;
+      },
+      3 * transfer_time);
+  ASSERT_TRUE(simulator_result);
+}
+*/
+
+TEST_F(Bbr2MultiSenderTest, Bbr2VsBbr1) {
+  SetupBbrSender(sender_endpoints_[1].get());
+
+  MultiSenderTopologyParams params;
+  CreateNetwork(params);
+
+  const QuicByteCount transfer_size = 10 * 1024 * 1024;
+  const QuicTime::Delta transfer_time =
+      params.BottleneckBandwidth().TransferTime(transfer_size);
+  QUIC_LOG(INFO) << "Single flow transfer time: " << transfer_time;
+
+  // Transfer 10% of data in first transfer.
+  sender_endpoints_[0]->AddBytesToTransfer(transfer_size);
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() {
+        return receiver_endpoints_[0]->bytes_received() >= 0.1 * transfer_size;
+      },
+      transfer_time);
+  ASSERT_TRUE(simulator_result);
+
+  // Start the second transfer and wait until both finish.
+  sender_endpoints_[1]->AddBytesToTransfer(transfer_size);
+  simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() {
+        return receiver_endpoints_[0]->bytes_received() == transfer_size &&
+               receiver_endpoints_[1]->bytes_received() == transfer_size;
+      },
+      3 * transfer_time);
+  ASSERT_TRUE(simulator_result);
+}
+
+TEST_F(Bbr2MultiSenderTest, QUIC_SLOW_TEST(Bbr2VsReno)) {
+  SetupTcpSender(sender_endpoints_[1].get(), /*reno=*/true);
+
+  MultiSenderTopologyParams params;
+  CreateNetwork(params);
+
+  const QuicByteCount transfer_size = 10 * 1024 * 1024;
+  const QuicTime::Delta transfer_time =
+      params.BottleneckBandwidth().TransferTime(transfer_size);
+  QUIC_LOG(INFO) << "Single flow transfer time: " << transfer_time;
+
+  // Transfer 10% of data in first transfer.
+  sender_endpoints_[0]->AddBytesToTransfer(transfer_size);
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() {
+        return receiver_endpoints_[0]->bytes_received() >= 0.1 * transfer_size;
+      },
+      transfer_time);
+  ASSERT_TRUE(simulator_result);
+
+  // Start the second transfer and wait until both finish.
+  sender_endpoints_[1]->AddBytesToTransfer(transfer_size);
+  simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() {
+        return receiver_endpoints_[0]->bytes_received() == transfer_size &&
+               receiver_endpoints_[1]->bytes_received() == transfer_size;
+      },
+      3 * transfer_time);
+  ASSERT_TRUE(simulator_result);
+}
+
+TEST_F(Bbr2MultiSenderTest, QUIC_SLOW_TEST(Bbr2VsRenoB2RC)) {
+  SetConnectionOption(sender_0_, kB2RC);
+  SetupTcpSender(sender_endpoints_[1].get(), /*reno=*/true);
+
+  MultiSenderTopologyParams params;
+  CreateNetwork(params);
+
+  const QuicByteCount transfer_size = 10 * 1024 * 1024;
+  const QuicTime::Delta transfer_time =
+      params.BottleneckBandwidth().TransferTime(transfer_size);
+  QUIC_LOG(INFO) << "Single flow transfer time: " << transfer_time;
+
+  // Transfer 10% of data in first transfer.
+  sender_endpoints_[0]->AddBytesToTransfer(transfer_size);
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() {
+        return receiver_endpoints_[0]->bytes_received() >= 0.1 * transfer_size;
+      },
+      transfer_time);
+  ASSERT_TRUE(simulator_result);
+
+  // Start the second transfer and wait until both finish.
+  sender_endpoints_[1]->AddBytesToTransfer(transfer_size);
+  simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() {
+        return receiver_endpoints_[0]->bytes_received() == transfer_size &&
+               receiver_endpoints_[1]->bytes_received() == transfer_size;
+      },
+      3 * transfer_time);
+  ASSERT_TRUE(simulator_result);
+}
+
+TEST_F(Bbr2MultiSenderTest, QUIC_SLOW_TEST(Bbr2VsCubic)) {
+  SetupTcpSender(sender_endpoints_[1].get(), /*reno=*/false);
+
+  MultiSenderTopologyParams params;
+  CreateNetwork(params);
+
+  const QuicByteCount transfer_size = 50 * 1024 * 1024;
+  const QuicTime::Delta transfer_time =
+      params.BottleneckBandwidth().TransferTime(transfer_size);
+  QUIC_LOG(INFO) << "Single flow transfer time: " << transfer_time;
+
+  // Transfer 10% of data in first transfer.
+  sender_endpoints_[0]->AddBytesToTransfer(transfer_size);
+  bool simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() {
+        return receiver_endpoints_[0]->bytes_received() >= 0.1 * transfer_size;
+      },
+      transfer_time);
+  ASSERT_TRUE(simulator_result);
+
+  // Start the second transfer and wait until both finish.
+  sender_endpoints_[1]->AddBytesToTransfer(transfer_size);
+  simulator_result = simulator_.RunUntilOrTimeout(
+      [this]() {
+        return receiver_endpoints_[0]->bytes_received() == transfer_size &&
+               receiver_endpoints_[1]->bytes_received() == transfer_size;
+      },
+      3 * transfer_time);
+  ASSERT_TRUE(simulator_result);
+}
+
+TEST(MinRttFilter, BadRttSample) {
+  auto time_in_seconds = [](int64_t seconds) {
+    return QuicTime::Zero() + QuicTime::Delta::FromSeconds(seconds);
+  };
+
+  MinRttFilter filter(QuicTime::Delta::FromMilliseconds(10),
+                      time_in_seconds(100));
+  ASSERT_EQ(filter.Get(), QuicTime::Delta::FromMilliseconds(10));
+
+  filter.Update(QuicTime::Delta::FromMilliseconds(-1), time_in_seconds(150));
+
+  EXPECT_EQ(filter.Get(), QuicTime::Delta::FromMilliseconds(10));
+  EXPECT_EQ(filter.GetTimestamp(), time_in_seconds(100));
+
+  filter.ForceUpdate(QuicTime::Delta::FromMilliseconds(-2),
+                     time_in_seconds(200));
+
+  EXPECT_EQ(filter.Get(), QuicTime::Delta::FromMilliseconds(10));
+  EXPECT_EQ(filter.GetTimestamp(), time_in_seconds(100));
+}
+
+}  // namespace test
+}  // namespace quic
diff --git a/quiche/quic/core/congestion_control/bbr_sender.h b/quiche/quic/core/congestion_control/bbr_sender.h
index e8b819b..eb887eb 100644
--- a/quiche/quic/core/congestion_control/bbr_sender.h
+++ b/quiche/quic/core/congestion_control/bbr_sender.h
@@ -1,4 +1,4 @@
-// Copyright 2016 The Chromium Authors. All rights reserved.
+// Copyright 2026 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.
 
@@ -183,6 +183,7 @@
  private:
   // For switching send algorithm mid connection.
   friend class Bbr2Sender;
+  friend class Bbr3Sender;
 
   using MaxBandwidthFilter =
       WindowedFilter<QuicBandwidth, MaxFilter<QuicBandwidth>,