gfe-relnote: In QUIC, add probe timeout mode, which unifies TLP and RTO. Protected by gfe2_reloadable_flag_quic_enable_pto.

PTO mode is enabled on client side if gfe2_reloadable_flag_quic_enable_pto is true and client sends 1PTO or 2PTO. PTO mode is enabled on server side if gfe2_reloadable_flag_quic_enable_pto is true and server receives 1PTO or 2PTO from client.

Connection is closed after 7 or 8 PTO depending on connection option 7PTO or 8PTO, respectively.

PiperOrigin-RevId: 263574963
Change-Id: Id952a3e4640146c3fe72e3d6745cbac5ee16dcdc
diff --git a/quic/core/crypto/crypto_protocol.h b/quic/core/crypto/crypto_protocol.h
index bbd0b18..812aae0 100644
--- a/quic/core/crypto/crypto_protocol.h
+++ b/quic/core/crypto/crypto_protocol.h
@@ -170,6 +170,14 @@
 const QuicTag kNSTP = TAG('N', 'S', 'T', 'P');   // No stop waiting frames.
 const QuicTag kNRTT = TAG('N', 'R', 'T', 'T');   // Ignore initial RTT
 
+const QuicTag k1PTO = TAG('1', 'P', 'T', 'O');   // Send 1 packet upon PTO.
+const QuicTag k2PTO = TAG('2', 'P', 'T', 'O');   // Send 2 packets upon PTO.
+
+const QuicTag k7PTO = TAG('7', 'P', 'T', 'O');   // Closes connection on 7
+                                                 // consecutive PTOs.
+const QuicTag k8PTO = TAG('8', 'P', 'T', 'O');   // Closes connection on 8
+                                                 // consecutive PTOs.
+
 // Optional support of truncated Connection IDs.  If sent by a peer, the value
 // is the minimum number of bytes allowed for the connection ID sent to the
 // peer.
diff --git a/quic/core/http/end_to_end_test.cc b/quic/core/http/end_to_end_test.cc
index d6c8486..4d7d1df 100644
--- a/quic/core/http/end_to_end_test.cc
+++ b/quic/core/http/end_to_end_test.cc
@@ -362,7 +362,9 @@
       copt.push_back(kTPCC);
     }
     copt.push_back(GetParam().priority_tag);
-
+    if (GetQuicReloadableFlag(quic_enable_pto)) {
+      copt.push_back(k2PTO);
+    }
     client_config_.SetConnectionOptionsToSend(copt);
 
     // Start the server first, because CreateQuicClient() attempts
diff --git a/quic/core/quic_connection.cc b/quic/core/quic_connection.cc
index 581709b..73ad66e 100644
--- a/quic/core/quic_connection.cc
+++ b/quic/core/quic_connection.cc
@@ -321,7 +321,8 @@
       processing_ack_frame_(false),
       supports_release_time_(false),
       release_time_into_future_(QuicTime::Delta::Zero()),
-      retry_has_been_parsed_(false) {
+      retry_has_been_parsed_(false),
+      max_consecutive_ptos_(0) {
   QUIC_DLOG(INFO) << ENDPOINT << "Created connection with server connection ID "
                   << server_connection_id
                   << " and version: " << ParsedQuicVersionToString(version());
@@ -419,6 +420,16 @@
   if (config.HasClientSentConnectionOption(k5RTO, perspective_)) {
     close_connection_after_five_rtos_ = true;
   }
+  if (sent_packet_manager_.enable_pto()) {
+    if (config.HasClientSentConnectionOption(k7PTO, perspective_)) {
+      max_consecutive_ptos_ = 6;
+      QUIC_RELOADABLE_FLAG_COUNT_N(quic_enable_pto, 3, 4);
+    }
+    if (config.HasClientSentConnectionOption(k8PTO, perspective_)) {
+      max_consecutive_ptos_ = 7;
+      QUIC_RELOADABLE_FLAG_COUNT_N(quic_enable_pto, 4, 4);
+    }
+  }
   if (config.HasClientSentConnectionOption(kNSTP, perspective_)) {
     no_stop_waiting_frames_ = true;
   }
@@ -2438,11 +2449,6 @@
   DCHECK(!sent_packet_manager_.unacked_packets().empty());
   const QuicPacketNumber previous_created_packet_number =
       packet_generator_.packet_number();
-  const size_t previous_crypto_retransmit_count =
-      stats_.crypto_retransmit_count;
-  const size_t previous_loss_timeout_count = stats_.loss_timeout_count;
-  const size_t previous_tlp_count = stats_.tlp_count;
-  const size_t pervious_rto_count = stats_.rto_count;
   if (close_connection_after_five_rtos_ &&
       sent_packet_manager_.GetConsecutiveRtoCount() >= 4) {
     // Close on the 5th consecutive RTO, so after 4 previous RTOs have occurred.
@@ -2450,6 +2456,14 @@
                     ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET);
     return;
   }
+  if (sent_packet_manager_.enable_pto() && max_consecutive_ptos_ > 0 &&
+      sent_packet_manager_.GetConsecutivePtoCount() >= max_consecutive_ptos_) {
+    CloseConnection(QUIC_TOO_MANY_RTOS,
+                    QuicStrCat(max_consecutive_ptos_ + 1,
+                               "consecutive retransmission timeouts"),
+                    ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET);
+    return;
+  }
 
   const auto retransmission_mode =
       sent_packet_manager_.OnRetransmissionTimeout();
@@ -2461,23 +2475,29 @@
     return;
   }
 
-  // In the TLP case, the SentPacketManager gives the connection the opportunity
-  // to send new data before retransmitting.
-  if (sent_packet_manager_.MaybeRetransmitTailLossProbe()) {
+  // In the PTO and TLP cases, the SentPacketManager gives the connection the
+  // opportunity to send new data before retransmitting.
+  if (sent_packet_manager_.enable_pto()) {
+    sent_packet_manager_.MaybeSendProbePackets();
+  } else if (sent_packet_manager_.MaybeRetransmitTailLossProbe()) {
     // Send the pending retransmission now that it's been queued.
     WriteIfNotBlocked();
   }
 
   if (sent_packet_manager_.fix_rto_retransmission()) {
     if (packet_generator_.packet_number() == previous_created_packet_number &&
-        retransmission_mode == QuicSentPacketManager::RTO_MODE &&
+        (retransmission_mode == QuicSentPacketManager::RTO_MODE ||
+         retransmission_mode == QuicSentPacketManager::PTO_MODE) &&
         !visitor_->WillingAndAbleToWrite()) {
-      // Send PING if timer fires in RTO mode but there is no data to send.
+      // Send PING if timer fires in RTO or PTO mode but there is no data to
+      // send.
       DCHECK_LT(0u, sent_packet_manager_.pending_timer_transmission_count());
       visitor_->SendPing();
     }
-    if (retransmission_mode != QuicSentPacketManager::LOSS_MODE &&
-        retransmission_mode != QuicSentPacketManager::HANDSHAKE_MODE) {
+    if (retransmission_mode == QuicSentPacketManager::PTO_MODE) {
+      sent_packet_manager_.AdjustPendingTimerTransmissions();
+    }
+    if (retransmission_mode != QuicSentPacketManager::LOSS_MODE) {
       // When timer fires in TLP or RTO mode, ensure 1) at least one packet is
       // created, or there is data to send and available credit (such that
       // packets will be sent eventually).
@@ -2485,21 +2505,13 @@
           packet_generator_.packet_number() == previous_created_packet_number &&
           (!visitor_->WillingAndAbleToWrite() ||
            sent_packet_manager_.pending_timer_transmission_count() == 0u))
-          << "previous_crypto_retransmit_count: "
-          << previous_crypto_retransmit_count
-          << ", crypto_retransmit_count: " << stats_.crypto_retransmit_count
-          << ", previous_loss_timeout_count: " << previous_loss_timeout_count
-          << ", loss_timeout_count: " << stats_.loss_timeout_count
-          << ", previous_tlp_count: " << previous_tlp_count
-          << ", tlp_count: " << stats_.tlp_count
-          << ", pervious_rto_count: " << pervious_rto_count
-          << ", rto_count: " << stats_.rto_count
-          << ", previous_created_packet_number: "
-          << previous_created_packet_number
+          << "retransmission_mode: " << retransmission_mode
           << ", packet_number: " << packet_generator_.packet_number()
           << ", session has data to write: "
           << visitor_->WillingAndAbleToWrite()
-          << ", writer is blocked: " << writer_->IsWriteBlocked();
+          << ", writer is blocked: " << writer_->IsWriteBlocked()
+          << ", pending_timer_transmission_count: "
+          << sent_packet_manager_.pending_timer_transmission_count();
     }
   }
 
diff --git a/quic/core/quic_connection.h b/quic/core/quic_connection.h
index 118e13b..7637584 100644
--- a/quic/core/quic_connection.h
+++ b/quic/core/quic_connection.h
@@ -1429,6 +1429,10 @@
 
   // Indicates whether a RETRY packet has been parsed.
   bool retry_has_been_parsed_;
+
+  // If max_consecutive_ptos_ > 0, close connection if consecutive PTOs is
+  // greater than max_consecutive_ptos.
+  size_t max_consecutive_ptos_;
 };
 
 }  // namespace quic
diff --git a/quic/core/quic_connection_stats.cc b/quic/core/quic_connection_stats.cc
index 246b9f1..98859ac 100644
--- a/quic/core/quic_connection_stats.cc
+++ b/quic/core/quic_connection_stats.cc
@@ -34,6 +34,7 @@
       loss_timeout_count(0),
       tlp_count(0),
       rto_count(0),
+      pto_count(0),
       min_rtt_us(0),
       srtt_us(0),
       max_packet_size(0),
@@ -77,6 +78,7 @@
   os << " loss_timeout_count: " << s.loss_timeout_count;
   os << " tlp_count: " << s.tlp_count;
   os << " rto_count: " << s.rto_count;
+  os << " pto_count: " << s.pto_count;
   os << " min_rtt_us: " << s.min_rtt_us;
   os << " srtt_us: " << s.srtt_us;
   os << " max_packet_size: " << s.max_packet_size;
diff --git a/quic/core/quic_connection_stats.h b/quic/core/quic_connection_stats.h
index 5317c7a..56bb510 100644
--- a/quic/core/quic_connection_stats.h
+++ b/quic/core/quic_connection_stats.h
@@ -76,6 +76,7 @@
   size_t loss_timeout_count;
   size_t tlp_count;
   size_t rto_count;  // Count of times the rto timer fired.
+  size_t pto_count;
 
   int64_t min_rtt_us;  // Minimum RTT in microseconds.
   int64_t srtt_us;     // Smoothed RTT in microseconds.
diff --git a/quic/core/quic_connection_test.cc b/quic/core/quic_connection_test.cc
index f90a9db..5546419 100644
--- a/quic/core/quic_connection_test.cc
+++ b/quic/core/quic_connection_test.cc
@@ -8794,6 +8794,121 @@
   EXPECT_EQ(1u, writer_->ping_frames().size());
 }
 
+TEST_P(QuicConnectionTest, ProbeTimeout) {
+  if (!connection_.session_decides_what_to_write() ||
+      !GetQuicReloadableFlag(quic_fix_rto_retransmission3)) {
+    return;
+  }
+  SetQuicReloadableFlag(quic_enable_pto, true);
+  SetQuicReloadableFlag(quic_fix_rto_retransmission3, true);
+  QuicConfig config;
+  QuicTagVector connection_options;
+  connection_options.push_back(k2PTO);
+  config.SetConnectionOptionsToSend(connection_options);
+  EXPECT_CALL(*send_algorithm_, SetFromConfig(_, _));
+  connection_.SetFromConfig(config);
+  EXPECT_FALSE(connection_.GetRetransmissionAlarm()->IsSet());
+
+  QuicStreamId stream_id = 2;
+  QuicPacketNumber last_packet;
+  SendStreamDataToPeer(stream_id, "foooooo", 0, NO_FIN, &last_packet);
+  SendStreamDataToPeer(stream_id, "foooooo", 7, NO_FIN, &last_packet);
+  EXPECT_TRUE(connection_.GetRetransmissionAlarm()->IsSet());
+
+  // Reset stream.
+  EXPECT_CALL(*send_algorithm_, OnPacketSent(_, _, _, _, _)).Times(1);
+  SendRstStream(stream_id, QUIC_ERROR_PROCESSING_STREAM, 3);
+
+  // Fire the PTO and verify only the RST_STREAM is resent, not stream data.
+  EXPECT_CALL(*send_algorithm_, OnPacketSent(_, _, _, _, _)).Times(1);
+  connection_.GetRetransmissionAlarm()->Fire();
+  EXPECT_EQ(0u, writer_->stream_frames().size());
+  EXPECT_EQ(1u, writer_->rst_stream_frames().size());
+  EXPECT_TRUE(connection_.GetRetransmissionAlarm()->IsSet());
+}
+
+TEST_P(QuicConnectionTest, CloseConnectionAfter7ClientPTOs) {
+  if (!connection_.session_decides_what_to_write() ||
+      !GetQuicReloadableFlag(quic_fix_rto_retransmission3)) {
+    return;
+  }
+  SetQuicReloadableFlag(quic_enable_pto, true);
+  QuicConfig config;
+  QuicTagVector connection_options;
+  connection_options.push_back(k2PTO);
+  connection_options.push_back(k7PTO);
+  config.SetConnectionOptionsToSend(connection_options);
+  EXPECT_CALL(*send_algorithm_, SetFromConfig(_, _));
+  connection_.SetFromConfig(config);
+  EXPECT_FALSE(connection_.GetRetransmissionAlarm()->IsSet());
+
+  // Send stream data.
+  SendStreamDataToPeer(
+      GetNthClientInitiatedStreamId(1, connection_.transport_version()), "foo",
+      0, FIN, nullptr);
+
+  // Fire the retransmission alarm 6 times.
+  for (int i = 0; i < 6; ++i) {
+    EXPECT_CALL(*send_algorithm_, OnPacketSent(_, _, _, _, _));
+    connection_.GetRetransmissionAlarm()->Fire();
+    EXPECT_TRUE(connection_.GetTimeoutAlarm()->IsSet());
+    EXPECT_TRUE(connection_.connected());
+  }
+
+  EXPECT_EQ(0u, connection_.sent_packet_manager().GetConsecutiveTlpCount());
+  EXPECT_EQ(0u, connection_.sent_packet_manager().GetConsecutiveRtoCount());
+  EXPECT_EQ(6u, connection_.sent_packet_manager().GetConsecutivePtoCount());
+  // Closes connection on 7th PTO.
+  EXPECT_CALL(visitor_,
+              OnConnectionClosed(_, ConnectionCloseSource::FROM_SELF));
+  EXPECT_CALL(*send_algorithm_, OnPacketSent(_, _, _, _, _));
+  connection_.GetRetransmissionAlarm()->Fire();
+  EXPECT_FALSE(connection_.GetTimeoutAlarm()->IsSet());
+  EXPECT_FALSE(connection_.connected());
+  TestConnectionCloseQuicErrorCode(QUIC_TOO_MANY_RTOS);
+}
+
+TEST_P(QuicConnectionTest, CloseConnectionAfter8ClientPTOs) {
+  if (!connection_.session_decides_what_to_write() ||
+      !GetQuicReloadableFlag(quic_fix_rto_retransmission3)) {
+    return;
+  }
+  SetQuicReloadableFlag(quic_enable_pto, true);
+  QuicConfig config;
+  QuicTagVector connection_options;
+  connection_options.push_back(k2PTO);
+  connection_options.push_back(k8PTO);
+  config.SetConnectionOptionsToSend(connection_options);
+  EXPECT_CALL(*send_algorithm_, SetFromConfig(_, _));
+  connection_.SetFromConfig(config);
+  EXPECT_FALSE(connection_.GetRetransmissionAlarm()->IsSet());
+
+  // Send stream data.
+  SendStreamDataToPeer(
+      GetNthClientInitiatedStreamId(1, connection_.transport_version()), "foo",
+      0, FIN, nullptr);
+
+  // Fire the retransmission alarm 7 times.
+  for (int i = 0; i < 7; ++i) {
+    EXPECT_CALL(*send_algorithm_, OnPacketSent(_, _, _, _, _));
+    connection_.GetRetransmissionAlarm()->Fire();
+    EXPECT_TRUE(connection_.GetTimeoutAlarm()->IsSet());
+    EXPECT_TRUE(connection_.connected());
+  }
+
+  EXPECT_EQ(0u, connection_.sent_packet_manager().GetConsecutiveTlpCount());
+  EXPECT_EQ(0u, connection_.sent_packet_manager().GetConsecutiveRtoCount());
+  EXPECT_EQ(7u, connection_.sent_packet_manager().GetConsecutivePtoCount());
+  // Closes connection on 8th PTO.
+  EXPECT_CALL(visitor_,
+              OnConnectionClosed(_, ConnectionCloseSource::FROM_SELF));
+  EXPECT_CALL(*send_algorithm_, OnPacketSent(_, _, _, _, _));
+  connection_.GetRetransmissionAlarm()->Fire();
+  EXPECT_FALSE(connection_.GetTimeoutAlarm()->IsSet());
+  EXPECT_FALSE(connection_.connected());
+  TestConnectionCloseQuicErrorCode(QUIC_TOO_MANY_RTOS);
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic
diff --git a/quic/core/quic_sent_packet_manager.cc b/quic/core/quic_sent_packet_manager.cc
index 54fbdfa..c1da218 100644
--- a/quic/core/quic_sent_packet_manager.cc
+++ b/quic/core/quic_sent_packet_manager.cc
@@ -115,6 +115,9 @@
           QuicTime::Delta::FromMilliseconds(kDefaultDelayedAckTimeMs)),
       rtt_updated_(false),
       acked_packets_iter_(last_ack_frame_.packets.rbegin()),
+      enable_pto_(false),
+      max_probe_packets_per_pto_(2),
+      consecutive_pto_count_(0),
       loss_removes_from_inflight_(
           GetQuicReloadableFlag(quic_loss_removes_from_inflight)),
       ignore_tlpr_if_no_pending_stream_data_(
@@ -180,6 +183,18 @@
     }
   }
 
+  if (GetQuicReloadableFlag(quic_enable_pto) && fix_rto_retransmission_) {
+    if (config.HasClientSentConnectionOption(k2PTO, perspective)) {
+      enable_pto_ = true;
+      QUIC_RELOADABLE_FLAG_COUNT_N(quic_enable_pto, 2, 4);
+    }
+    if (config.HasClientSentConnectionOption(k1PTO, perspective)) {
+      enable_pto_ = true;
+      max_probe_packets_per_pto_ = 1;
+      QUIC_RELOADABLE_FLAG_COUNT_N(quic_enable_pto, 1, 4);
+    }
+  }
+
   // Configure congestion control.
   if (config.HasClientRequestedIndependentOption(kTBBR, perspective)) {
     SetSendAlgorithm(kBBR);
@@ -340,6 +355,7 @@
     // Reset all retransmit counters any time a new packet is acked.
     consecutive_rto_count_ = 0;
     consecutive_tlp_count_ = 0;
+    consecutive_pto_count_ = 0;
     consecutive_crypto_retransmission_count_ = 0;
   }
 
@@ -470,7 +486,7 @@
       << "transmission_type: "
       << QuicUtils::TransmissionTypeToString(transmission_type);
   // Handshake packets should never be sent as probing retransmissions.
-  DCHECK(!transmission_info->has_crypto_handshake ||
+  DCHECK(enable_pto_ || !transmission_info->has_crypto_handshake ||
          transmission_type != PROBING_RETRANSMISSION);
   if (!loss_removes_from_inflight_ &&
       !RetransmissionLeavesBytesInFlight(transmission_type)) {
@@ -741,6 +757,12 @@
       ++stats_->rto_count;
       RetransmitRtoPackets();
       return RTO_MODE;
+    case PTO_MODE:
+      QUIC_DVLOG(1) << ENDPOINT << "PTO mode";
+      ++stats_->pto_count;
+      ++consecutive_pto_count_;
+      pending_timer_transmission_count_ = max_probe_packets_per_pto_;
+      return PTO_MODE;
   }
 }
 
@@ -776,6 +798,7 @@
 }
 
 bool QuicSentPacketManager::MaybeRetransmitTailLossProbe() {
+  DCHECK(!enable_pto_);
   if (pending_timer_transmission_count_ == 0) {
     return false;
   }
@@ -804,6 +827,7 @@
 }
 
 void QuicSentPacketManager::RetransmitRtoPackets() {
+  DCHECK(!enable_pto_);
   QUIC_BUG_IF(pending_timer_transmission_count_ > 0)
       << "Retransmissions already queued:" << pending_timer_transmission_count_;
   // Mark two packets for retransmission.
@@ -859,6 +883,41 @@
   }
 }
 
+void QuicSentPacketManager::MaybeSendProbePackets() {
+  if (pending_timer_transmission_count_ == 0) {
+    return;
+  }
+  QuicPacketNumber packet_number = unacked_packets_.GetLeastUnacked();
+  std::vector<QuicPacketNumber> probing_packets;
+  for (QuicUnackedPacketMap::const_iterator it = unacked_packets_.begin();
+       it != unacked_packets_.end(); ++it, ++packet_number) {
+    if (it->state == OUTSTANDING &&
+        unacked_packets_.HasRetransmittableFrames(*it)) {
+      probing_packets.push_back(packet_number);
+      if (probing_packets.size() == pending_timer_transmission_count_) {
+        break;
+      }
+    }
+  }
+
+  for (QuicPacketNumber retransmission : probing_packets) {
+    QUIC_DVLOG(1) << ENDPOINT << "Marking " << retransmission
+                  << " for probing retransmission";
+    MarkForRetransmission(retransmission, PROBING_RETRANSMISSION);
+  }
+  // It is possible that there is not enough outstanding data for probing.
+}
+
+void QuicSentPacketManager::AdjustPendingTimerTransmissions() {
+  if (pending_timer_transmission_count_ < max_probe_packets_per_pto_) {
+    // There are packets sent already, clear credit.
+    pending_timer_transmission_count_ = 0;
+    return;
+  }
+  // No packet gets sent, leave 1 credit to allow data to be write eventually.
+  pending_timer_transmission_count_ = 1;
+}
+
 QuicSentPacketManager::RetransmissionTimeoutMode
 QuicSentPacketManager::GetRetransmissionMode() const {
   DCHECK(unacked_packets_.HasInFlightPackets());
@@ -868,6 +927,9 @@
   if (loss_algorithm_->GetLossTimeout() != QuicTime::Zero()) {
     return LOSS_MODE;
   }
+  if (enable_pto_) {
+    return PTO_MODE;
+  }
   if (consecutive_tlp_count_ < max_tail_loss_probes_) {
     if (unacked_packets_.HasUnackedRetransmittableFrames()) {
       return TLP_MODE;
@@ -962,6 +1024,7 @@
     case LOSS_MODE:
       return loss_algorithm_->GetLossTimeout();
     case TLP_MODE: {
+      DCHECK(!enable_pto_);
       // TODO(ianswett): When CWND is available, it would be preferable to
       // set the timer based on the earliest retransmittable packet.
       // Base the updated timer on the send time of the last packet.
@@ -971,6 +1034,7 @@
       return std::max(clock_->ApproximateNow(), tlp_time);
     }
     case RTO_MODE: {
+      DCHECK(!enable_pto_);
       // The RTO is based on the first outstanding packet.
       const QuicTime sent_time = unacked_packets_.GetLastPacketSentTime();
       QuicTime rto_time = sent_time + GetRetransmissionDelay();
@@ -979,6 +1043,12 @@
           unacked_packets_.GetLastPacketSentTime() + GetTailLossProbeDelay();
       return std::max(tlp_time, rto_time);
     }
+    case PTO_MODE: {
+      // Ensure PTO never gets set to a time in the past.
+      return std::max(
+          clock_->ApproximateNow(),
+          unacked_packets_.GetLastPacketSentTime() + GetProbeTimeoutDelay());
+    }
   }
   DCHECK(false);
   return QuicTime::Zero();
@@ -1070,6 +1140,22 @@
   return retransmission_delay;
 }
 
+const QuicTime::Delta QuicSentPacketManager::GetProbeTimeoutDelay() const {
+  DCHECK(enable_pto_);
+  if (rtt_stats_.smoothed_rtt().IsZero()) {
+    if (rtt_stats_.initial_rtt().IsZero()) {
+      return QuicTime::Delta::FromSeconds(1);
+    }
+    return 2 * rtt_stats_.initial_rtt();
+  }
+  const QuicTime::Delta pto_delay =
+      rtt_stats_.smoothed_rtt() +
+      std::max(4 * rtt_stats_.mean_deviation(),
+               QuicTime::Delta::FromMilliseconds(1)) +
+      peer_max_ack_delay_;
+  return pto_delay * (1 << consecutive_pto_count_);
+}
+
 QuicTime::Delta QuicSentPacketManager::GetSlowStartDuration() const {
   if (send_algorithm_->GetCongestionControlType() != kBBR) {
     return QuicTime::Delta::Infinite();
@@ -1124,6 +1210,7 @@
   }
   consecutive_rto_count_ = 0;
   consecutive_tlp_count_ = 0;
+  consecutive_pto_count_ = 0;
   rtt_stats_.OnConnectionMigration();
   send_algorithm_->OnConnectionMigration();
 }
diff --git a/quic/core/quic_sent_packet_manager.h b/quic/core/quic_sent_packet_manager.h
index 1373646..ee285e8 100644
--- a/quic/core/quic_sent_packet_manager.h
+++ b/quic/core/quic_sent_packet_manager.h
@@ -103,6 +103,9 @@
     // Re-invoke the loss detection when a packet is not acked before the
     // loss detection algorithm expects.
     LOSS_MODE,
+    // A probe timeout. At least one probe packet must be sent when timer
+    // expires.
+    PTO_MODE,
   };
 
   QuicSentPacketManager(Perspective perspective,
@@ -337,6 +340,8 @@
 
   size_t GetConsecutiveTlpCount() const { return consecutive_tlp_count_; }
 
+  size_t GetConsecutivePtoCount() const { return consecutive_pto_count_; }
+
   void OnApplicationLimited();
 
   const SendAlgorithmInterface* GetSendAlgorithm() const {
@@ -390,6 +395,12 @@
   // Setting the send algorithm once the connection is underway is dangerous.
   void SetSendAlgorithm(SendAlgorithmInterface* send_algorithm);
 
+  // Sends up to max_probe_packets_per_pto_ probe packets.
+  void MaybeSendProbePackets();
+
+  // Called to adjust pending_timer_transmission_count_ accordingly.
+  void AdjustPendingTimerTransmissions();
+
   bool supports_multiple_packet_number_spaces() const {
     return unacked_packets_.supports_multiple_packet_number_spaces();
   }
@@ -400,6 +411,8 @@
 
   bool fix_rto_retransmission() const { return fix_rto_retransmission_; }
 
+  bool enable_pto() const { return enable_pto_; }
+
  private:
   friend class test::QuicConnectionPeer;
   friend class test::QuicSentPacketManagerPeer;
@@ -445,6 +458,9 @@
     return GetRetransmissionDelay(consecutive_rto_count_);
   }
 
+  // Returns the probe timeout.
+  const QuicTime::Delta GetProbeTimeoutDelay() const;
+
   // Returns the newest transmission associated with a packet.
   QuicPacketNumber GetNewestRetransmission(
       QuicPacketNumber packet_number,
@@ -623,6 +639,15 @@
   // OnAckRangeStart, and gradually moves in OnAckRange..
   PacketNumberQueue::const_reverse_iterator acked_packets_iter_;
 
+  // If true, enable PTO mode which unifies TLP and RTO modes.
+  bool enable_pto_;
+
+  // Maximum number of probes to send when PTO fires.
+  size_t max_probe_packets_per_pto_;
+
+  // Number of times the PTO timer has fired in a row without receiving an ack.
+  size_t consecutive_pto_count_;
+
   // Latched value of quic_loss_removes_from_inflight.
   const bool loss_removes_from_inflight_;
 
diff --git a/quic/core/quic_sent_packet_manager_test.cc b/quic/core/quic_sent_packet_manager_test.cc
index 56c3d8a..ae5722b 100644
--- a/quic/core/quic_sent_packet_manager_test.cc
+++ b/quic/core/quic_sent_packet_manager_test.cc
@@ -357,6 +357,20 @@
                           pending.transmission_type, HAS_RETRANSMITTABLE_DATA);
   }
 
+  void EnablePto(QuicTag tag) {
+    SetQuicReloadableFlag(quic_fix_rto_retransmission3, true);
+    manager_.SetSessionDecideWhatToWrite(true);
+    SetQuicReloadableFlag(quic_enable_pto, true);
+    QuicConfig config;
+    QuicTagVector options;
+    options.push_back(tag);
+    QuicConfigPeer::SetReceivedConnectionOptions(&config, options);
+    EXPECT_CALL(*send_algorithm_, SetFromConfig(_, _));
+    EXPECT_CALL(*network_change_visitor_, OnCongestionChange());
+    manager_.SetFromConfig(config);
+    EXPECT_TRUE(manager_.enable_pto());
+  }
+
   QuicSentPacketManager manager_;
   MockClock clock_;
   QuicConnectionStats stats_;
@@ -2980,6 +2994,107 @@
   }
 }
 
+TEST_P(QuicSentPacketManagerTest, ComputingProbeTimeout) {
+  EnablePto(k2PTO);
+  EXPECT_CALL(*send_algorithm_, PacingRate(_))
+      .WillRepeatedly(Return(QuicBandwidth::Zero()));
+  EXPECT_CALL(*send_algorithm_, GetCongestionWindow())
+      .WillRepeatedly(Return(10 * kDefaultTCPMSS));
+  RttStats* rtt_stats = const_cast<RttStats*>(manager_.GetRttStats());
+  rtt_stats->UpdateRtt(QuicTime::Delta::FromMilliseconds(100),
+                       QuicTime::Delta::Zero(), QuicTime::Zero());
+  QuicTime::Delta srtt = rtt_stats->smoothed_rtt();
+
+  SendDataPacket(1, ENCRYPTION_FORWARD_SECURE);
+  // Verify PTO is correctly set.
+  QuicTime::Delta expected_pto_delay =
+      srtt + 4 * rtt_stats->mean_deviation() +
+      QuicTime::Delta::FromMilliseconds(kDefaultDelayedAckTimeMs);
+  EXPECT_EQ(clock_.Now() + expected_pto_delay,
+            manager_.GetRetransmissionTime());
+
+  clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(10));
+  SendDataPacket(2, ENCRYPTION_FORWARD_SECURE);
+  // Verify PTO is correctly set based on sent time of packet 2.
+  EXPECT_EQ(clock_.Now() + expected_pto_delay,
+            manager_.GetRetransmissionTime());
+  EXPECT_EQ(0u, stats_.pto_count);
+
+  // Invoke PTO.
+  clock_.AdvanceTime(expected_pto_delay);
+  manager_.OnRetransmissionTimeout();
+  EXPECT_EQ(QuicTime::Delta::Zero(), manager_.TimeUntilSend(clock_.Now()));
+  EXPECT_EQ(1u, stats_.pto_count);
+
+  // Verify two probe packets get sent.
+  EXPECT_CALL(notifier_, RetransmitFrames(_, _))
+      .Times(2)
+      .WillOnce(WithArgs<1>(Invoke([this](TransmissionType type) {
+        RetransmitDataPacket(3, type, ENCRYPTION_FORWARD_SECURE);
+      })))
+      .WillOnce(WithArgs<1>(Invoke([this](TransmissionType type) {
+        RetransmitDataPacket(4, type, ENCRYPTION_FORWARD_SECURE);
+      })));
+  manager_.MaybeSendProbePackets();
+  // Verify PTO period gets set to twice the current value.
+  QuicTime sent_time = clock_.Now();
+  EXPECT_EQ(sent_time + expected_pto_delay * 2,
+            manager_.GetRetransmissionTime());
+
+  // Received ACK for packets 1 and 2.
+  uint64_t acked[] = {1, 2};
+  ExpectAcksAndLosses(true, acked, QUIC_ARRAYSIZE(acked), nullptr, 0);
+  manager_.OnAckFrameStart(QuicPacketNumber(2), QuicTime::Delta::Infinite(),
+                           clock_.Now());
+  manager_.OnAckRange(QuicPacketNumber(1), QuicPacketNumber(3));
+  EXPECT_EQ(PACKETS_NEWLY_ACKED,
+            manager_.OnAckFrameEnd(clock_.Now(), QuicPacketNumber(1),
+                                   ENCRYPTION_FORWARD_SECURE));
+  expected_pto_delay =
+      rtt_stats->SmoothedOrInitialRtt() +
+      std::max(4 * rtt_stats->mean_deviation(),
+               QuicTime::Delta::FromMilliseconds(1)) +
+      QuicTime::Delta::FromMilliseconds(kDefaultDelayedAckTimeMs);
+
+  // Verify PTO is correctly re-armed based on sent time of packet 4.
+  EXPECT_EQ(sent_time + expected_pto_delay, manager_.GetRetransmissionTime());
+}
+
+TEST_P(QuicSentPacketManagerTest, SendOneProbePacket) {
+  EnablePto(k1PTO);
+  EXPECT_CALL(*send_algorithm_, PacingRate(_))
+      .WillRepeatedly(Return(QuicBandwidth::Zero()));
+  EXPECT_CALL(*send_algorithm_, GetCongestionWindow())
+      .WillRepeatedly(Return(10 * kDefaultTCPMSS));
+
+  SendDataPacket(1, ENCRYPTION_FORWARD_SECURE);
+  clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(10));
+  SendDataPacket(2, ENCRYPTION_FORWARD_SECURE);
+
+  RttStats* rtt_stats = const_cast<RttStats*>(manager_.GetRttStats());
+  rtt_stats->UpdateRtt(QuicTime::Delta::FromMilliseconds(100),
+                       QuicTime::Delta::Zero(), QuicTime::Zero());
+  QuicTime::Delta srtt = rtt_stats->smoothed_rtt();
+  // Verify PTO period is correctly set.
+  QuicTime::Delta expected_pto_delay =
+      srtt + 4 * rtt_stats->mean_deviation() +
+      QuicTime::Delta::FromMilliseconds(kDefaultDelayedAckTimeMs);
+  EXPECT_EQ(clock_.Now() + expected_pto_delay,
+            manager_.GetRetransmissionTime());
+
+  // Invoke PTO.
+  clock_.AdvanceTime(expected_pto_delay);
+  manager_.OnRetransmissionTimeout();
+  EXPECT_EQ(QuicTime::Delta::Zero(), manager_.TimeUntilSend(clock_.Now()));
+
+  // Verify one probe packet gets sent.
+  EXPECT_CALL(notifier_, RetransmitFrames(_, _))
+      .WillOnce(WithArgs<1>(Invoke([this](TransmissionType type) {
+        RetransmitDataPacket(3, type, ENCRYPTION_FORWARD_SECURE);
+      })));
+  manager_.MaybeSendProbePackets();
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic