dcsctp: Add retransmission counters to metrics

Bug: webrtc:15458
Change-Id: Ib90cb0b9a94e1f358685ed319538654b0c8ed5c4
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/318581
Reviewed-by: Florent Castelli <orphis@webrtc.org>
Commit-Queue: Victor Boivie <boivie@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#40683}
diff --git a/net/dcsctp/public/dcsctp_socket.h b/net/dcsctp/public/dcsctp_socket.h
index 2df6a2c..9fda56a 100644
--- a/net/dcsctp/public/dcsctp_socket.h
+++ b/net/dcsctp/public/dcsctp_socket.h
@@ -211,6 +211,16 @@
   // Number of messages requested to be sent.
   size_t tx_messages_count = 0;
 
+  // Number of packets retransmitted. Since SCTP packets can contain both
+  // retransmitted DATA chunks and DATA chunks that are transmitted for the
+  // first time, this represents an upper bound as it's incremented every time a
+  // packet contains a retransmitted DATA chunk.
+  size_t rtx_packets_count = 0;
+
+  // Total number of bytes retransmitted. This includes the payload and
+  // DATA/I-DATA headers, but not SCTP packet headers.
+  uint64_t rtx_bytes_count = 0;
+
   // The current congestion window (cwnd) in bytes, corresponding to spinfo_cwnd
   // defined in RFC6458.
   size_t cwnd_bytes = 0;
@@ -583,7 +593,9 @@
                                              size_t bytes) = 0;
 
   // Retrieves the latest metrics. If the socket is not fully connected,
-  // `absl::nullopt` will be returned.
+  // `absl::nullopt` will be returned. Note that metrics are not guaranteed to
+  // be carried over if this socket is handed over by calling
+  // `GetHandoverStateAndClose`.
   virtual absl::optional<Metrics> GetMetrics() const = 0;
 
   // Returns empty bitmask if the socket is in the state in which a snapshot of
diff --git a/net/dcsctp/socket/BUILD.gn b/net/dcsctp/socket/BUILD.gn
index aeb157b..681ddd4 100644
--- a/net/dcsctp/socket/BUILD.gn
+++ b/net/dcsctp/socket/BUILD.gn
@@ -244,6 +244,7 @@
       "../../../test:test_support",
       "../common:handover_testing",
       "../common:internal_types",
+      "../common:math",
       "../packet:chunk",
       "../packet:error_cause",
       "../packet:parameter",
diff --git a/net/dcsctp/socket/dcsctp_socket.cc b/net/dcsctp/socket/dcsctp_socket.cc
index 139ec280..712fcea 100644
--- a/net/dcsctp/socket/dcsctp_socket.cc
+++ b/net/dcsctp/socket/dcsctp_socket.cc
@@ -598,6 +598,8 @@
       tcb_->capabilities().negotiated_maximum_incoming_streams;
   metrics.negotiated_maximum_incoming_streams =
       tcb_->capabilities().negotiated_maximum_incoming_streams;
+  metrics.rtx_packets_count = tcb_->retransmission_queue().rtx_packets_count();
+  metrics.rtx_bytes_count = tcb_->retransmission_queue().rtx_bytes_count();
 
   return metrics;
 }
diff --git a/net/dcsctp/socket/dcsctp_socket_test.cc b/net/dcsctp/socket/dcsctp_socket_test.cc
index 0da893d..c31c048 100644
--- a/net/dcsctp/socket/dcsctp_socket_test.cc
+++ b/net/dcsctp/socket/dcsctp_socket_test.cc
@@ -22,6 +22,7 @@
 #include "absl/types/optional.h"
 #include "api/array_view.h"
 #include "net/dcsctp/common/handover_testing.h"
+#include "net/dcsctp/common/math.h"
 #include "net/dcsctp/packet/chunk/chunk.h"
 #include "net/dcsctp/packet/chunk/cookie_echo_chunk.h"
 #include "net/dcsctp/packet/chunk/data_chunk.h"
@@ -2032,6 +2033,44 @@
   EXPECT_EQ(a.socket.GetMetrics()->peer_rwnd_bytes, initial_a_rwnd);
 }
 
+TEST(DcSctpSocketTest, RetransmissionMetricsAreSetForFastRetransmit) {
+  SocketUnderTest a("A");
+  SocketUnderTest z("Z");
+  ConnectSockets(a, z);
+
+  // Enough to trigger fast retransmit of the missing second packet.
+  std::vector<uint8_t> payload(DcSctpOptions::kMaxSafeMTUSize * 5);
+  a.socket.Send(DcSctpMessage(StreamID(1), PPID(53), payload), kSendOptions);
+
+  // Receive first packet, drop second, receive and retransmit the remaining.
+  z.socket.ReceivePacket(a.cb.ConsumeSentPacket());
+  a.cb.ConsumeSentPacket();
+  ExchangeMessages(a, z);
+
+  EXPECT_EQ(a.socket.GetMetrics()->rtx_packets_count, 1u);
+  size_t expected_data_size =
+      RoundDownTo4(DcSctpOptions::kMaxSafeMTUSize - SctpPacket::kHeaderSize);
+  EXPECT_EQ(a.socket.GetMetrics()->rtx_bytes_count, expected_data_size);
+}
+
+TEST(DcSctpSocketTest, RetransmissionMetricsAreSetForNormalRetransmit) {
+  SocketUnderTest a("A");
+  SocketUnderTest z("Z");
+  ConnectSockets(a, z);
+
+  std::vector<uint8_t> payload(kSmallMessageSize);
+  a.socket.Send(DcSctpMessage(StreamID(1), PPID(53), payload), kSendOptions);
+
+  a.cb.ConsumeSentPacket();
+  AdvanceTime(a, z, a.options.rto_initial);
+  ExchangeMessages(a, z);
+
+  EXPECT_EQ(a.socket.GetMetrics()->rtx_packets_count, 1u);
+  size_t expected_data_size =
+      RoundUpTo4(kSmallMessageSize + DataChunk::kHeaderSize);
+  EXPECT_EQ(a.socket.GetMetrics()->rtx_bytes_count, expected_data_size);
+}
+
 TEST_P(DcSctpSocketParametrizedTest, UnackDataAlsoIncludesSendQueue) {
   SocketUnderTest a("A");
   auto z = std::make_unique<SocketUnderTest>("Z");
diff --git a/net/dcsctp/tx/retransmission_queue.cc b/net/dcsctp/tx/retransmission_queue.cc
index 05bf6da..93084cc 100644
--- a/net/dcsctp/tx/retransmission_queue.cc
+++ b/net/dcsctp/tx/retransmission_queue.cc
@@ -426,18 +426,21 @@
   if (!t3_rtx_.is_running()) {
     t3_rtx_.Start();
   }
+
+  size_t bytes_retransmitted = absl::c_accumulate(
+      to_be_sent, 0, [&](size_t r, const std::pair<TSN, Data>& d) {
+        return r + GetSerializedChunkSize(d.second);
+      });
+  ++rtx_packets_count_;
+  rtx_bytes_count_ += bytes_retransmitted;
+
   RTC_DLOG(LS_VERBOSE) << log_prefix_ << "Fast-retransmitting TSN "
                        << StrJoin(to_be_sent, ",",
                                   [&](rtc::StringBuilder& sb,
                                       const std::pair<TSN, Data>& c) {
                                     sb << *c.first;
                                   })
-                       << " - "
-                       << absl::c_accumulate(
-                              to_be_sent, 0,
-                              [&](size_t r, const std::pair<TSN, Data>& d) {
-                                return r + GetSerializedChunkSize(d.second);
-                              })
+                       << " - " << bytes_retransmitted
                        << " bytes. outstanding_bytes=" << outstanding_bytes()
                        << " (" << old_outstanding_bytes << ")";
 
@@ -463,10 +466,17 @@
       RoundDownTo4(std::min(max_bytes_to_send(), bytes_remaining_in_packet));
 
   to_be_sent = outstanding_data_.GetChunksToBeRetransmitted(max_bytes);
-  max_bytes -= absl::c_accumulate(to_be_sent, 0,
-                                  [&](size_t r, const std::pair<TSN, Data>& d) {
-                                    return r + GetSerializedChunkSize(d.second);
-                                  });
+
+  size_t bytes_retransmitted = absl::c_accumulate(
+      to_be_sent, 0, [&](size_t r, const std::pair<TSN, Data>& d) {
+        return r + GetSerializedChunkSize(d.second);
+      });
+  max_bytes -= bytes_retransmitted;
+
+  if (!to_be_sent.empty()) {
+    ++rtx_packets_count_;
+    rtx_bytes_count_ += bytes_retransmitted;
+  }
 
   while (max_bytes > data_chunk_header_size_) {
     RTC_DCHECK(IsDivisibleBy4(max_bytes));
diff --git a/net/dcsctp/tx/retransmission_queue.h b/net/dcsctp/tx/retransmission_queue.h
index b4350e9..51e9c5b 100644
--- a/net/dcsctp/tx/retransmission_queue.h
+++ b/net/dcsctp/tx/retransmission_queue.h
@@ -113,6 +113,9 @@
   // Returns the current receiver window size.
   size_t rwnd() const { return rwnd_; }
 
+  size_t rtx_packets_count() const { return rtx_packets_count_; }
+  uint64_t rtx_bytes_count() const { return rtx_bytes_count_; }
+
   // Returns the number of bytes of packets that are in-flight.
   size_t outstanding_bytes() const {
     return outstanding_data_.outstanding_bytes();
@@ -241,6 +244,11 @@
   size_t ssthresh_;
   // Partial Bytes Acked. See RFC4960.
   size_t partial_bytes_acked_;
+
+  // See `dcsctp::Metrics`.
+  size_t rtx_packets_count_ = 0;
+  uint64_t rtx_bytes_count_ = 0;
+
   // If set, fast recovery is enabled until this TSN has been cumulative
   // acked.
   absl::optional<UnwrappedTSN> fast_recovery_exit_tsn_ = absl::nullopt;