Fix QUIC connection retransmission stats
Because QUIC retransmits data at the stream level (rather than packet level) since QUIC version > 39, the current implementation of bytes_retransmitted and packets_retransmitted is incorrect.
With this change:
- The packet now has a running total of "not retransmission" bytes. We use this new field to populate the `bytes_retransmitted` fields on the connection stats.
- The packet is considered a "retransmission" packet if ANY frame is a retransmission frame (rather than looking at the latest retransmittable frame).
PiperOrigin-RevId: 454625064
diff --git a/quiche/quic/core/quic_connection.cc b/quiche/quic/core/quic_connection.cc
index 92db728..2645c58 100644
--- a/quiche/quic/core/quic_connection.cc
+++ b/quiche/quic/core/quic_connection.cc
@@ -8,6 +8,7 @@
#include <sys/types.h>
#include <algorithm>
+#include <cstdint>
#include <iterator>
#include <limits>
#include <memory>
@@ -3655,10 +3656,24 @@
sent_packet_manager_.GetLeastPacketAwaitedByPeer(encryption_level_),
sent_packet_manager_.EstimateMaxPacketsInFlight(max_packet_length()));
- stats_.bytes_sent += result.bytes_written;
+ stats_.bytes_sent += encrypted_length;
++stats_.packets_sent;
+
+ QuicByteCount bytes_not_retransmitted =
+ packet->bytes_not_retransmitted.value_or(0);
if (packet->transmission_type != NOT_RETRANSMISSION) {
- stats_.bytes_retransmitted += result.bytes_written;
+ if (static_cast<uint64_t>(encrypted_length) < bytes_not_retransmitted) {
+ QUIC_BUG(quic_packet_bytes_written_lt_bytes_not_retransmitted)
+ << "Total bytes written to the packet should be larger than the "
+ "bytes in not-retransmitted frames. Bytes written: "
+ << encrypted_length
+ << ", bytes not retransmitted: " << bytes_not_retransmitted;
+ } else {
+ // bytes_retransmitted includes packet's headers and encryption
+ // overhead.
+ stats_.bytes_retransmitted +=
+ (encrypted_length - bytes_not_retransmitted);
+ }
++stats_.packets_retransmitted;
}
diff --git a/quiche/quic/core/quic_connection_test.cc b/quiche/quic/core/quic_connection_test.cc
index ff84538..b3c01d5 100644
--- a/quiche/quic/core/quic_connection_test.cc
+++ b/quiche/quic/core/quic_connection_test.cc
@@ -240,10 +240,19 @@
absl::string_view data,
QuicStreamOffset offset,
StreamSendingState state) {
+ return SaveAndSendStreamData(id, data, offset, state, NOT_RETRANSMISSION);
+ }
+
+ QuicConsumedData SaveAndSendStreamData(QuicStreamId id,
+ absl::string_view data,
+ QuicStreamOffset offset,
+ StreamSendingState state,
+ TransmissionType transmission_type) {
ScopedPacketFlusher flusher(this);
producer_.SaveStreamData(id, data);
if (notifier_ != nullptr) {
- return notifier_->WriteOrBufferData(id, data.length(), state);
+ return notifier_->WriteOrBufferData(id, data.length(), state,
+ transmission_type);
}
return QuicConnection::SendStreamData(id, data.length(), offset, state);
}
@@ -3545,6 +3554,86 @@
<< ". Actual time = " << actual_recorded_send_time.ToDebuggingValue();
}
+TEST_P(QuicConnectionTest, ConnectionStatsRetransmission_WithRetransmissions) {
+ // Send two stream frames in 1 packet by queueing them.
+ connection_.SetDefaultEncryptionLevel(ENCRYPTION_FORWARD_SECURE);
+
+ {
+ QuicConnection::ScopedPacketFlusher flusher(&connection_);
+ connection_.SaveAndSendStreamData(
+ GetNthClientInitiatedStreamId(1, connection_.transport_version()),
+ "helloworld", 0, NO_FIN, PTO_RETRANSMISSION);
+ connection_.SaveAndSendStreamData(
+ GetNthClientInitiatedStreamId(2, connection_.transport_version()),
+ "helloworld", 0, NO_FIN, LOSS_RETRANSMISSION);
+ EXPECT_CALL(*send_algorithm_, OnPacketSent(_, _, _, _, _)).Times(1);
+ }
+
+ EXPECT_EQ(0u, connection_.NumQueuedPackets());
+ EXPECT_FALSE(connection_.HasQueuedData());
+
+ EXPECT_EQ(2u, writer_->frame_count());
+ for (auto& frame : writer_->stream_frames()) {
+ EXPECT_EQ(frame->data_length, 10u);
+ }
+
+ ASSERT_EQ(connection_.GetStats().packets_retransmitted, 1u);
+ ASSERT_GE(connection_.GetStats().bytes_retransmitted, 20u);
+}
+
+TEST_P(QuicConnectionTest, ConnectionStatsRetransmission_WithMixedFrames) {
+ // Send two stream frames in 1 packet by queueing them.
+ connection_.SetDefaultEncryptionLevel(ENCRYPTION_FORWARD_SECURE);
+
+ {
+ QuicConnection::ScopedPacketFlusher flusher(&connection_);
+ // First frame is retransmission. Second is NOT_RETRANSMISSION but the
+ // packet retains the PTO_RETRANSMISSION type.
+ connection_.SaveAndSendStreamData(
+ GetNthClientInitiatedStreamId(1, connection_.transport_version()),
+ "helloworld", 0, NO_FIN, PTO_RETRANSMISSION);
+ connection_.SaveAndSendStreamData(
+ GetNthClientInitiatedStreamId(2, connection_.transport_version()),
+ "helloworld", 0, NO_FIN, NOT_RETRANSMISSION);
+ EXPECT_CALL(*send_algorithm_, OnPacketSent(_, _, _, _, _)).Times(1);
+ }
+
+ EXPECT_EQ(0u, connection_.NumQueuedPackets());
+ EXPECT_FALSE(connection_.HasQueuedData());
+
+ EXPECT_EQ(2u, writer_->frame_count());
+ for (auto& frame : writer_->stream_frames()) {
+ EXPECT_EQ(frame->data_length, 10u);
+ }
+
+ ASSERT_EQ(connection_.GetStats().packets_retransmitted, 1u);
+ ASSERT_GE(connection_.GetStats().bytes_retransmitted, 10u);
+}
+
+TEST_P(QuicConnectionTest, ConnectionStatsRetransmission_NoRetransmission) {
+ // Send two stream frames in 1 packet by queueing them.
+ connection_.SetDefaultEncryptionLevel(ENCRYPTION_FORWARD_SECURE);
+
+ {
+ QuicConnection::ScopedPacketFlusher flusher(&connection_);
+ // Both frames are NOT_RETRANSMISSION
+ connection_.SaveAndSendStreamData(
+ GetNthClientInitiatedStreamId(1, connection_.transport_version()),
+ "helloworld", 0, NO_FIN, NOT_RETRANSMISSION);
+ connection_.SaveAndSendStreamData(
+ GetNthClientInitiatedStreamId(2, connection_.transport_version()),
+ "helloworld", 0, NO_FIN, NOT_RETRANSMISSION);
+ EXPECT_CALL(*send_algorithm_, OnPacketSent(_, _, _, _, _)).Times(1);
+ }
+
+ EXPECT_EQ(0u, connection_.NumQueuedPackets());
+ EXPECT_FALSE(connection_.HasQueuedData());
+
+ EXPECT_EQ(2u, writer_->frame_count());
+ ASSERT_EQ(connection_.GetStats().packets_retransmitted, 0u);
+ ASSERT_EQ(connection_.GetStats().bytes_retransmitted, 0u);
+}
+
TEST_P(QuicConnectionTest, FramePacking) {
// Send two stream frames in 1 packet by queueing them.
connection_.SetDefaultEncryptionLevel(ENCRYPTION_FORWARD_SECURE);
diff --git a/quiche/quic/core/quic_packet_creator.cc b/quiche/quic/core/quic_packet_creator.cc
index 66c7d14..98d6314 100644
--- a/quiche/quic/core/quic_packet_creator.cc
+++ b/quiche/quic/core/quic_packet_creator.cc
@@ -476,6 +476,12 @@
QUIC_BUG_IF(quic_bug_12398_5, packet_.encrypted_buffer == nullptr)
<< ENDPOINT;
+ // Clear bytes_not_retransmitted for packets containing only
+ // NOT_RETRANSMISSION frames.
+ if (packet_.transmission_type == NOT_RETRANSMISSION) {
+ packet_.bytes_not_retransmitted.reset();
+ }
+
SerializedPacket packet(std::move(packet_));
ClearPacket();
RemoveSoftMaxPacketLength();
@@ -499,6 +505,7 @@
QUICHE_DCHECK(packet_.nonretransmittable_frames.empty()) << ENDPOINT;
packet_.largest_acked.Clear();
needs_full_padding_ = false;
+ packet_.bytes_not_retransmitted.reset();
}
size_t QuicPacketCreator::ReserializeInitialPacketInCoalescedPacket(
@@ -1795,9 +1802,13 @@
debug_delegate_->OnFrameAddedToPacket(frame);
}
- // Packet transmission type is determined by the last added retransmittable
- // frame.
- if (QuicUtils::IsRetransmittableFrame(frame.type)) {
+ if (transmission_type == NOT_RETRANSMISSION) {
+ packet_.bytes_not_retransmitted.emplace(
+ packet_.bytes_not_retransmitted.value_or(0) + frame_len);
+ } else if (QuicUtils::IsRetransmittableFrame(frame.type)) {
+ // Packet transmission type is determined by the last added retransmittable
+ // frame of a retransmission type. If a packet has no retransmittable
+ // retransmission frames, it has type NOT_RETRANSMISSION.
packet_.transmission_type = transmission_type;
}
return true;
diff --git a/quiche/quic/core/quic_packet_creator_test.cc b/quiche/quic/core/quic_packet_creator_test.cc
index c9f8ecc..f6a81b2 100644
--- a/quiche/quic/core/quic_packet_creator_test.cc
+++ b/quiche/quic/core/quic_packet_creator_test.cc
@@ -18,6 +18,7 @@
#include "quiche/quic/core/crypto/null_encrypter.h"
#include "quiche/quic/core/crypto/quic_decrypter.h"
#include "quiche/quic/core/crypto/quic_encrypter.h"
+#include "quiche/quic/core/frames/quic_frame.h"
#include "quiche/quic/core/frames/quic_stream_frame.h"
#include "quiche/quic/core/quic_connection_id.h"
#include "quiche/quic/core/quic_data_writer.h"
@@ -1927,6 +1928,10 @@
absl::string_view()));
ASSERT_TRUE(QuicUtils::IsRetransmittableFrame(stream_frame.type));
+ QuicFrame stream_frame_2(QuicStreamFrame(stream_id,
+ /*fin=*/false, 1u,
+ absl::string_view()));
+
QuicFrame padding_frame{QuicPaddingFrame()};
ASSERT_FALSE(QuicUtils::IsRetransmittableFrame(padding_frame.type));
@@ -1939,16 +1944,154 @@
EXPECT_TRUE(creator_.AddFrame(stream_frame, PTO_RETRANSMISSION));
ASSERT_EQ(serialized_packet_, nullptr);
- EXPECT_TRUE(creator_.AddFrame(padding_frame, LOSS_RETRANSMISSION));
+ EXPECT_TRUE(creator_.AddFrame(stream_frame_2, PATH_RETRANSMISSION));
+ ASSERT_EQ(serialized_packet_, nullptr);
+
+ EXPECT_TRUE(creator_.AddFrame(padding_frame, PTO_RETRANSMISSION));
creator_.FlushCurrentPacket();
ASSERT_TRUE(serialized_packet_->encrypted_buffer);
// The last retransmittable frame on packet is a stream frame, the packet's
// transmission type should be the same as the stream frame's.
- EXPECT_EQ(serialized_packet_->transmission_type, PTO_RETRANSMISSION);
+ EXPECT_EQ(serialized_packet_->transmission_type, PATH_RETRANSMISSION);
DeleteSerializedPacket();
}
+TEST_P(QuicPacketCreatorTest,
+ PacketBytesRetransmitted_AddFrame_Retransmission) {
+ creator_.set_encryption_level(ENCRYPTION_FORWARD_SECURE);
+
+ QuicAckFrame temp_ack_frame = InitAckFrame(1);
+ QuicFrame ack_frame(&temp_ack_frame);
+ EXPECT_TRUE(creator_.AddFrame(ack_frame, LOSS_RETRANSMISSION));
+
+ QuicStreamId stream_id = QuicUtils::GetFirstBidirectionalStreamId(
+ client_framer_.transport_version(), Perspective::IS_CLIENT);
+
+ QuicFrame stream_frame;
+ const std::string data("data");
+ // ConsumeDataToFillCurrentPacket calls AddFrame
+ ASSERT_TRUE(creator_.ConsumeDataToFillCurrentPacket(
+ stream_id, data, 0u, false, false, PTO_RETRANSMISSION, &stream_frame));
+ EXPECT_EQ(4u, stream_frame.stream_frame.data_length);
+
+ EXPECT_CALL(delegate_, OnSerializedPacket(_))
+ .WillOnce(Invoke(this, &QuicPacketCreatorTest::SaveSerializedPacket));
+
+ creator_.FlushCurrentPacket();
+ ASSERT_TRUE(serialized_packet_->encrypted_buffer);
+ ASSERT_FALSE(serialized_packet_->bytes_not_retransmitted.has_value());
+
+ DeleteSerializedPacket();
+}
+
+TEST_P(QuicPacketCreatorTest,
+ PacketBytesRetransmitted_AddFrame_NotRetransmission) {
+ creator_.set_encryption_level(ENCRYPTION_FORWARD_SECURE);
+
+ QuicAckFrame temp_ack_frame = InitAckFrame(1);
+ QuicFrame ack_frame(&temp_ack_frame);
+ EXPECT_TRUE(creator_.AddFrame(ack_frame, NOT_RETRANSMISSION));
+
+ QuicStreamId stream_id = QuicUtils::GetFirstBidirectionalStreamId(
+ client_framer_.transport_version(), Perspective::IS_CLIENT);
+
+ QuicFrame stream_frame;
+ const std::string data("data");
+ // ConsumeDataToFillCurrentPacket calls AddFrame
+ ASSERT_TRUE(creator_.ConsumeDataToFillCurrentPacket(
+ stream_id, data, 0u, false, false, NOT_RETRANSMISSION, &stream_frame));
+ EXPECT_EQ(4u, stream_frame.stream_frame.data_length);
+
+ EXPECT_CALL(delegate_, OnSerializedPacket(_))
+ .WillOnce(Invoke(this, &QuicPacketCreatorTest::SaveSerializedPacket));
+
+ creator_.FlushCurrentPacket();
+ ASSERT_TRUE(serialized_packet_->encrypted_buffer);
+ ASSERT_FALSE(serialized_packet_->bytes_not_retransmitted.has_value());
+
+ DeleteSerializedPacket();
+}
+
+TEST_P(QuicPacketCreatorTest, PacketBytesRetransmitted_AddFrame_MixedFrames) {
+ creator_.set_encryption_level(ENCRYPTION_FORWARD_SECURE);
+
+ QuicAckFrame temp_ack_frame = InitAckFrame(1);
+ QuicFrame ack_frame(&temp_ack_frame);
+ EXPECT_TRUE(creator_.AddFrame(ack_frame, NOT_RETRANSMISSION));
+
+ QuicStreamId stream_id = QuicUtils::GetFirstBidirectionalStreamId(
+ client_framer_.transport_version(), Perspective::IS_CLIENT);
+
+ QuicFrame stream_frame;
+ const std::string data("data");
+ // ConsumeDataToFillCurrentPacket calls AddFrame
+ ASSERT_TRUE(creator_.ConsumeDataToFillCurrentPacket(
+ stream_id, data, 0u, false, false, NOT_RETRANSMISSION, &stream_frame));
+ EXPECT_EQ(4u, stream_frame.stream_frame.data_length);
+
+ QuicFrame stream_frame2;
+ // ConsumeDataToFillCurrentPacket calls AddFrame
+ ASSERT_TRUE(creator_.ConsumeDataToFillCurrentPacket(
+ stream_id, data, 0u, false, false, LOSS_RETRANSMISSION, &stream_frame2));
+ EXPECT_EQ(4u, stream_frame2.stream_frame.data_length);
+
+ EXPECT_CALL(delegate_, OnSerializedPacket(_))
+ .WillOnce(Invoke(this, &QuicPacketCreatorTest::SaveSerializedPacket));
+
+ creator_.FlushCurrentPacket();
+ ASSERT_TRUE(serialized_packet_->encrypted_buffer);
+ ASSERT_TRUE(serialized_packet_->bytes_not_retransmitted.has_value());
+ ASSERT_GE(serialized_packet_->bytes_not_retransmitted.value(), 4u);
+
+ DeleteSerializedPacket();
+}
+
+TEST_P(QuicPacketCreatorTest,
+ PacketBytesRetransmitted_CreateAndSerializeStreamFrame_Retransmission) {
+ creator_.set_encryption_level(ENCRYPTION_FORWARD_SECURE);
+
+ const std::string data("test");
+ producer_.SaveStreamData(GetNthClientInitiatedStreamId(0), data);
+ EXPECT_CALL(delegate_, OnSerializedPacket(_))
+ .WillOnce(Invoke(this, &QuicPacketCreatorTest::SaveSerializedPacket));
+ size_t num_bytes_consumed;
+ // Retransmission frame adds to packet's bytes_retransmitted
+ creator_.CreateAndSerializeStreamFrame(
+ GetNthClientInitiatedStreamId(0), data.length(), 0, 0, true,
+ LOSS_RETRANSMISSION, &num_bytes_consumed);
+ EXPECT_EQ(4u, num_bytes_consumed);
+
+ ASSERT_TRUE(serialized_packet_->encrypted_buffer);
+ ASSERT_FALSE(serialized_packet_->bytes_not_retransmitted.has_value());
+ DeleteSerializedPacket();
+
+ EXPECT_FALSE(creator_.HasPendingFrames());
+}
+
+TEST_P(
+ QuicPacketCreatorTest,
+ PacketBytesRetransmitted_CreateAndSerializeStreamFrame_NotRetransmission) {
+ creator_.set_encryption_level(ENCRYPTION_FORWARD_SECURE);
+
+ const std::string data("test");
+ producer_.SaveStreamData(GetNthClientInitiatedStreamId(0), data);
+ EXPECT_CALL(delegate_, OnSerializedPacket(_))
+ .WillOnce(Invoke(this, &QuicPacketCreatorTest::SaveSerializedPacket));
+ size_t num_bytes_consumed;
+ // Non-retransmission frame does not add to packet's bytes_retransmitted
+ creator_.CreateAndSerializeStreamFrame(
+ GetNthClientInitiatedStreamId(0), data.length(), 0, 0, true,
+ NOT_RETRANSMISSION, &num_bytes_consumed);
+ EXPECT_EQ(4u, num_bytes_consumed);
+
+ ASSERT_TRUE(serialized_packet_->encrypted_buffer);
+ ASSERT_FALSE(serialized_packet_->bytes_not_retransmitted.has_value());
+ DeleteSerializedPacket();
+
+ EXPECT_FALSE(creator_.HasPendingFrames());
+}
+
TEST_P(QuicPacketCreatorTest, RetryToken) {
if (!GetParam().version_serialization ||
!QuicVersionHasLongHeaderLengths(client_framer_.transport_version())) {
diff --git a/quiche/quic/core/quic_packets.cc b/quiche/quic/core/quic_packets.cc
index 57d6ba7..369a32a 100644
--- a/quiche/quic/core/quic_packets.cc
+++ b/quiche/quic/core/quic_packets.cc
@@ -439,7 +439,8 @@
has_ack_frequency(other.has_ack_frequency),
has_message(other.has_message),
fate(other.fate),
- peer_address(other.peer_address) {
+ peer_address(other.peer_address),
+ bytes_not_retransmitted(other.bytes_not_retransmitted) {
if (this != &other) {
if (release_encrypted_buffer && encrypted_buffer != nullptr) {
release_encrypted_buffer(encrypted_buffer);
@@ -486,6 +487,7 @@
copy->has_message = serialized.has_message;
copy->fate = serialized.fate;
copy->peer_address = serialized.peer_address;
+ copy->bytes_not_retransmitted = serialized.bytes_not_retransmitted;
if (copy_buffer) {
copy->encrypted_buffer = CopyBuffer(serialized);
diff --git a/quiche/quic/core/quic_packets.h b/quiche/quic/core/quic_packets.h
index 5246f43..d4f04c8 100644
--- a/quiche/quic/core/quic_packets.h
+++ b/quiche/quic/core/quic_packets.h
@@ -376,6 +376,10 @@
bool has_message;
SerializedPacketFate fate;
QuicSocketAddress peer_address;
+ // Sum of bytes from frames that are not retransmissions. This field is only
+ // populated for packets with "mixed frames": at least one frame of a
+ // retransmission type and at least one frame of NOT_RETRANSMISSION type.
+ absl::optional<QuicByteCount> bytes_not_retransmitted;
};
// Make a copy of |serialized| (including the underlying frames). |copy_buffer|
diff --git a/quiche/quic/test_tools/simple_session_notifier.cc b/quiche/quic/test_tools/simple_session_notifier.cc
index 123eb6e..7a2e705 100644
--- a/quiche/quic/test_tools/simple_session_notifier.cc
+++ b/quiche/quic/test_tools/simple_session_notifier.cc
@@ -37,6 +37,12 @@
QuicConsumedData SimpleSessionNotifier::WriteOrBufferData(
QuicStreamId id, QuicByteCount data_length, StreamSendingState state) {
+ return WriteOrBufferData(id, data_length, state, NOT_RETRANSMISSION);
+}
+
+QuicConsumedData SimpleSessionNotifier::WriteOrBufferData(
+ QuicStreamId id, QuicByteCount data_length, StreamSendingState state,
+ TransmissionType transmission_type) {
if (!stream_map_.contains(id)) {
stream_map_[id] = StreamState();
}
@@ -53,7 +59,7 @@
return {0, false};
}
const size_t length = stream_state.bytes_total - stream_state.bytes_sent;
- connection_->SetTransmissionType(NOT_RETRANSMISSION);
+ connection_->SetTransmissionType(transmission_type);
QuicConsumedData consumed =
connection_->SendStreamData(id, length, stream_state.bytes_sent, state);
QUIC_DVLOG(1) << "consumed: " << consumed;
diff --git a/quiche/quic/test_tools/simple_session_notifier.h b/quiche/quic/test_tools/simple_session_notifier.h
index e846499..f1af586 100644
--- a/quiche/quic/test_tools/simple_session_notifier.h
+++ b/quiche/quic/test_tools/simple_session_notifier.h
@@ -28,6 +28,9 @@
// Tries to write stream data and returns data consumed.
QuicConsumedData WriteOrBufferData(QuicStreamId id, QuicByteCount data_length,
StreamSendingState state);
+ QuicConsumedData WriteOrBufferData(QuicStreamId id, QuicByteCount data_length,
+ StreamSendingState state,
+ TransmissionType transmission_type);
// Tries to write RST_STREAM_FRAME.
void WriteOrBufferRstStream(QuicStreamId id, QuicRstStreamErrorCode error,