Fix a byte counting bug in QUIC BBR2.

If a packet is detected as lost and then acked, its size is double counted towards both bytes_lost and bytes_acked in BandwidthSampler. With the fix it will only be counted as lost.

Credit to slusnys@gmail.com from https://groups.google.com/g/bbr-dev/c/fJYeAawFD90

Protected by FLAGS_quic_bbr2_fix_spurious_loss_bytes_counting.

PiperOrigin-RevId: 592228843
diff --git a/quiche/quic/core/congestion_control/bandwidth_sampler.cc b/quiche/quic/core/congestion_control/bandwidth_sampler.cc
index fe42e08..e79a106 100644
--- a/quiche/quic/core/congestion_control/bandwidth_sampler.cc
+++ b/quiche/quic/core/congestion_control/bandwidth_sampler.cc
@@ -301,6 +301,15 @@
   SendTimeState last_acked_packet_send_state;
   QuicBandwidth max_send_rate = QuicBandwidth::Zero();
   for (const auto& packet : acked_packets) {
+    if (GetQuicReloadableFlag(quic_bbr2_fix_spurious_loss_bytes_counting) &&
+        packet.spurious_loss) {
+      QUIC_RELOADABLE_FLAG_COUNT(quic_bbr2_fix_spurious_loss_bytes_counting);
+      // If the packet has been detected as lost before, QuicSentPacketManager
+      // should set the AckedPacket.bytes_acked to 0 before passing the packet
+      // to the congestion controller.
+      QUICHE_DCHECK_EQ(packet.bytes_acked, 0);
+      continue;
+    }
     BandwidthSample sample =
         OnPacketAcknowledged(ack_time, packet.packet_number);
     if (!sample.state_at_send.is_valid) {
diff --git a/quiche/quic/core/congestion_control/bbr2_misc.cc b/quiche/quic/core/congestion_control/bbr2_misc.cc
index f3306ec..75be707 100644
--- a/quiche/quic/core/congestion_control/bbr2_misc.cc
+++ b/quiche/quic/core/congestion_control/bbr2_misc.cc
@@ -8,6 +8,7 @@
 #include "quiche/quic/core/quic_bandwidth.h"
 #include "quiche/quic/core/quic_time.h"
 #include "quiche/quic/core/quic_types.h"
+#include "quiche/quic/platform/api/quic_bug_tracker.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"
@@ -152,11 +153,19 @@
         congestion_event->prior_bytes_in_flight -
         congestion_event->bytes_acked - congestion_event->bytes_lost;
   } else {
-    QUIC_LOG_FIRST_N(ERROR, 1)
-        << "prior_bytes_in_flight:" << congestion_event->prior_bytes_in_flight
-        << " is smaller than the sum of bytes_acked:"
-        << congestion_event->bytes_acked
-        << " and bytes_lost:" << congestion_event->bytes_lost;
+    if (GetQuicReloadableFlag(quic_bbr2_fix_spurious_loss_bytes_counting)) {
+      QUIC_BUG(quic_bbr2_prior_in_flight_too_small)
+          << "prior_bytes_in_flight:" << congestion_event->prior_bytes_in_flight
+          << " is smaller than the sum of bytes_acked:"
+          << congestion_event->bytes_acked
+          << " and bytes_lost:" << congestion_event->bytes_lost;
+    } else {
+      QUIC_LOG_FIRST_N(ERROR, 1)
+          << "prior_bytes_in_flight:" << congestion_event->prior_bytes_in_flight
+          << " is smaller than the sum of bytes_acked:"
+          << congestion_event->bytes_acked
+          << " and bytes_lost:" << congestion_event->bytes_lost;
+    }
     congestion_event->bytes_in_flight = 0;
   }
 
diff --git a/quiche/quic/core/congestion_control/bbr2_sender.h b/quiche/quic/core/congestion_control/bbr2_sender.h
index a10d24b..636d340 100644
--- a/quiche/quic/core/congestion_control/bbr2_sender.h
+++ b/quiche/quic/core/congestion_control/bbr2_sender.h
@@ -139,6 +139,8 @@
 
   DebugState ExportDebugState() const;
 
+  const Bbr2NetworkModel& GetNetworkModel() const { return model_; }
+
  private:
   void UpdatePacingRate(QuicByteCount bytes_acked);
   void UpdateCongestionWindow(QuicByteCount bytes_acked);
diff --git a/quiche/quic/core/congestion_control/bbr2_simulator_test.cc b/quiche/quic/core/congestion_control/bbr2_simulator_test.cc
index 9eadce2..f9bc24a 100644
--- a/quiche/quic/core/congestion_control/bbr2_simulator_test.cc
+++ b/quiche/quic/core/congestion_control/bbr2_simulator_test.cc
@@ -15,6 +15,7 @@
 #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"
@@ -1799,7 +1800,7 @@
   }
   now = now + params.RTT();
   sender_->OnCongestionEvent(
-      /*rtt_updated=*/true, kDefaultMaxPacketSize, now,
+      /*rtt_updated=*/true, 2 * kDefaultMaxPacketSize, now,
       {AckedPacket(next_packet_number - 1, kDefaultMaxPacketSize, now)},
       {LostPacket(next_packet_number - 2, kDefaultMaxPacketSize)}, 0, 0);
 
@@ -1845,6 +1846,60 @@
   EXPECT_EQ(prior_bandwidth_estimate, sender_->BandwidthEstimate());
 }
 
+// Simulate the case where a packet is considered lost but then acked.
+TEST_F(Bbr2DefaultTopologyTest, 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);
+
+  if (GetQuicReloadableFlag(quic_bbr2_fix_spurious_loss_bytes_counting)) {
+    EXPECT_EQ(sender_->GetNetworkModel().total_bytes_sent(),
+              sender_->GetNetworkModel().total_bytes_acked() +
+                  sender_->GetNetworkModel().total_bytes_lost());
+  } else {
+    // This is the bug fixed by the flag.
+    EXPECT_LT(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(Bbr2DefaultTopologyTest, ProbeRttAfterQuiescenceImmediatelyExits) {
diff --git a/quiche/quic/core/quic_flags_list.h b/quiche/quic/core/quic_flags_list.h
index 3824bff..79a1e21 100644
--- a/quiche/quic/core/quic_flags_list.h
+++ b/quiche/quic/core/quic_flags_list.h
@@ -79,6 +79,8 @@
 QUIC_FLAG(quic_reloadable_flag_quic_limit_sending_max_streams2, true)
 // If true, enable server retransmittable on wire PING.
 QUIC_FLAG(quic_reloadable_flag_quic_enable_server_on_wire_ping, true)
+// If true, fix a QUIC BBR2 bytes counting issue caused by spurious losses.
+QUIC_FLAG(quic_reloadable_flag_quic_bbr2_fix_spurious_loss_bytes_counting, false)
 // If true, include stream information in idle timeout connection close detail.
 QUIC_FLAG(quic_reloadable_flag_quic_add_stream_info_to_idle_close_detail, true)
 // If true, reject or send error response code upon receiving invalid request or response headers.
diff --git a/quiche/quic/core/quic_sent_packet_manager.cc b/quiche/quic/core/quic_sent_packet_manager.cc
index 23e5e06..9dc1048 100644
--- a/quiche/quic/core/quic_sent_packet_manager.cc
+++ b/quiche/quic/core/quic_sent_packet_manager.cc
@@ -1394,6 +1394,7 @@
     if (info->in_flight) {
       acked_packet.bytes_acked = info->bytes_sent;
     } else {
+      acked_packet.spurious_loss = (info->state == LOST);
       // Unackable packets are skipped earlier.
       largest_newly_acked_ = acked_packet.packet_number;
     }
diff --git a/quiche/quic/core/quic_types.h b/quiche/quic/core/quic_types.h
index 0389e74..b626531 100644
--- a/quiche/quic/core/quic_types.h
+++ b/quiche/quic/core/quic_types.h
@@ -571,6 +571,9 @@
   QuicPacketNumber packet_number;
   // Number of bytes sent in the packet that was acknowledged.
   QuicPacketLength bytes_acked;
+  // Whether the packet has been marked as lost before the ack. |bytes_acked|
+  // should be 0 if this is true.
+  bool spurious_loss = false;
   // The time |packet_number| was received by the peer, according to the
   // optional timestamp the peer included in the ACK frame which acknowledged
   // |packet_number|. Zero if no timestamp was available for this packet.