diff --git a/net/dcsctp/public/dcsctp_socket.h b/net/dcsctp/public/dcsctp_socket.h index 2df6a2c009..9fda56a3ad 100644 --- a/net/dcsctp/public/dcsctp_socket.h +++ b/net/dcsctp/public/dcsctp_socket.h @@ -211,6 +211,16 @@ struct Metrics { // 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 @@ class DcSctpSocketInterface { 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 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 aeb157b56a..681ddd47e9 100644 --- a/net/dcsctp/socket/BUILD.gn +++ b/net/dcsctp/socket/BUILD.gn @@ -244,6 +244,7 @@ if (rtc_include_tests) { "../../../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 139ec28055..712fceaa66 100644 --- a/net/dcsctp/socket/dcsctp_socket.cc +++ b/net/dcsctp/socket/dcsctp_socket.cc @@ -598,6 +598,8 @@ absl::optional DcSctpSocket::GetMetrics() const { 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 0da893d9bd..c31c048582 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 @@ TEST(DcSctpSocketTest, RxAndTxPacketMetricsIncrease) { 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 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 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("Z"); diff --git a/net/dcsctp/tx/retransmission_queue.cc b/net/dcsctp/tx/retransmission_queue.cc index 05bf6dae1f..93084cc27b 100644 --- a/net/dcsctp/tx/retransmission_queue.cc +++ b/net/dcsctp/tx/retransmission_queue.cc @@ -426,18 +426,21 @@ RetransmissionQueue::GetChunksForFastRetransmit(size_t bytes_in_packet) { 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& 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& c) { sb << *c.first; }) - << " - " - << absl::c_accumulate( - to_be_sent, 0, - [&](size_t r, const std::pair& d) { - return r + GetSerializedChunkSize(d.second); - }) + << " - " << bytes_retransmitted << " bytes. outstanding_bytes=" << outstanding_bytes() << " (" << old_outstanding_bytes << ")"; @@ -463,10 +466,17 @@ std::vector> RetransmissionQueue::GetChunksToSend( 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& d) { - return r + GetSerializedChunkSize(d.second); - }); + + size_t bytes_retransmitted = absl::c_accumulate( + to_be_sent, 0, [&](size_t r, const std::pair& 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 b4350e9625..51e9c5b318 100644 --- a/net/dcsctp/tx/retransmission_queue.h +++ b/net/dcsctp/tx/retransmission_queue.h @@ -113,6 +113,9 @@ class RetransmissionQueue { // 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 @@ class RetransmissionQueue { 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 fast_recovery_exit_tsn_ = absl::nullopt;