Add debug visitor for logging when QPACK encoder cannot insert into dynamic table or cannot refer to unacknowledged entries.
gfe-relnote: n/a, change to QUIC v99-only code. Protected by existing disabled gfe2_reloadable_flag_quic_enable_version_99.
PiperOrigin-RevId: 270715196
Change-Id: I94cece5ca466ddc58a668805e91acff70e468528
diff --git a/quic/core/qpack/qpack_encoder.cc b/quic/core/qpack/qpack_encoder.cc
index 61ee8fc..bd16f36 100644
--- a/quic/core/qpack/qpack_encoder.cc
+++ b/quic/core/qpack/qpack_encoder.cc
@@ -35,7 +35,8 @@
DecoderStreamErrorDelegate* decoder_stream_error_delegate)
: decoder_stream_error_delegate_(decoder_stream_error_delegate),
decoder_stream_receiver_(this),
- maximum_blocked_streams_(0) {
+ maximum_blocked_streams_(0),
+ debug_visitor_(nullptr) {
DCHECK(decoder_stream_error_delegate_);
}
@@ -110,6 +111,10 @@
const bool blocking_allowed = blocking_manager_.blocking_allowed_on_stream(
stream_id, maximum_blocked_streams_);
+ // Track events for debug visitor.
+ bool dynamic_table_insertion_blocked = false;
+ bool blocked_stream_limit_exhausted = false;
+
for (const auto& header : ValueSplittingHeaderList(&header_list)) {
// These strings are owned by |header_list|.
QuicStringPiece name = header.first;
@@ -131,20 +136,26 @@
break;
}
- if (blocking_allowed || index < known_received_count) {
- if (index >= draining_index) {
- // If allowed, refer to entry directly.
+ if (index >= draining_index) {
+ // If allowed, refer to entry directly.
+ if (!blocking_allowed && index >= known_received_count) {
+ blocked_stream_limit_exhausted = true;
+ } else {
instructions.push_back(
EncodeIndexedHeaderField(is_static, index, referred_indices));
smallest_blocking_index = std::min(smallest_blocking_index, index);
break;
}
-
- if (blocking_allowed &&
- QpackEntry::Size(name, value) <=
- header_table_.MaxInsertSizeWithoutEvictingGivenEntry(
- std::min(smallest_blocking_index, index))) {
+ } else {
+ // Entry is draining, needs to be duplicated.
+ if (!blocking_allowed) {
+ blocked_stream_limit_exhausted = true;
+ } else if (QpackEntry::Size(name, value) >
+ header_table_.MaxInsertSizeWithoutEvictingGivenEntry(
+ std::min(smallest_blocking_index, index))) {
+ dynamic_table_insertion_blocked = true;
+ } else {
// If allowed, duplicate entry and refer to it.
sent_byte_count += encoder_stream_sender_.SendDuplicate(
QpackAbsoluteIndexToEncoderStreamRelativeIndex(
@@ -196,10 +207,13 @@
break;
}
- if (blocking_allowed &&
- QpackEntry::Size(name, value) <=
- header_table_.MaxInsertSizeWithoutEvictingGivenEntry(
- std::min(smallest_blocking_index, index))) {
+ if (!blocking_allowed) {
+ blocked_stream_limit_exhausted = true;
+ } else if (QpackEntry::Size(name, value) >
+ header_table_.MaxInsertSizeWithoutEvictingGivenEntry(
+ std::min(smallest_blocking_index, index))) {
+ dynamic_table_insertion_blocked = true;
+ } else {
// If allowed, insert entry with name reference and refer to it.
sent_byte_count += encoder_stream_sender_.SendInsertWithNameReference(
is_static,
@@ -236,11 +250,14 @@
break;
case QpackHeaderTable::MatchType::kNoMatch:
- if (blocking_allowed &&
- QpackEntry::Size(name, value) <=
- header_table_.MaxInsertSizeWithoutEvictingGivenEntry(
- smallest_blocking_index)) {
- // If allowed, insert entry and refer to it.
+ // If allowed, insert entry and refer to it.
+ if (!blocking_allowed) {
+ blocked_stream_limit_exhausted = true;
+ } else if (QpackEntry::Size(name, value) >
+ header_table_.MaxInsertSizeWithoutEvictingGivenEntry(
+ smallest_blocking_index)) {
+ dynamic_table_insertion_blocked = true;
+ } else {
sent_byte_count +=
encoder_stream_sender_.SendInsertWithoutNameReference(name,
value);
@@ -255,6 +272,9 @@
}
// Encode entry as string literals.
+ // TODO(b/112770235): Consider also adding to dynamic table to improve
+ // compression ratio for subsequent header blocks with peers that do not
+ // allow any blocked streams.
instructions.push_back(EncodeLiteralHeaderField(name, value));
break;
@@ -267,6 +287,11 @@
*encoder_stream_sent_byte_count = sent_byte_count;
}
+ if (debug_visitor_) {
+ debug_visitor_->OnHeaderListEncoded(dynamic_table_insertion_blocked,
+ blocked_stream_limit_exhausted);
+ }
+
return instructions;
}
diff --git a/quic/core/qpack/qpack_encoder.h b/quic/core/qpack/qpack_encoder.h
index 2ffbc99..c4aaced 100644
--- a/quic/core/qpack/qpack_encoder.h
+++ b/quic/core/qpack/qpack_encoder.h
@@ -46,6 +46,25 @@
virtual void OnDecoderStreamError(QuicStringPiece error_message) = 0;
};
+ // Interface for logging whenever dynamic table insertion is blocked and
+ // whenever using unacknowledged entries is not allowed because of the blocked
+ // stream limit.
+ class QUIC_EXPORT_PRIVATE DebugVisitor {
+ public:
+ virtual ~DebugVisitor() = default;
+
+ // Called once for each header list encoded.
+ // |dynamic_table_insertion_blocked| it true if for any header field in this
+ // header list the encoder would have inserted an entry into the dynamic
+ // table, but could not because that insertion would have evicted a blocking
+ // entry. |blocked_stream_limit_exhausted| is true if for any header field
+ // in this header list the encoder would have referred to an unacknowledged
+ // entry in the dynamic table, but could not because that would have
+ // violated the limit on the number of blocked streams.
+ virtual void OnHeaderListEncoded(bool dynamic_table_insertion_blocked,
+ bool blocked_stream_limit_exhausted) = 0;
+ };
+
QpackEncoder(DecoderStreamErrorDelegate* decoder_stream_error_delegate);
~QpackEncoder() override;
@@ -86,6 +105,10 @@
return &decoder_stream_receiver_;
}
+ void set_debug_visitor(DebugVisitor* debug_visitor) {
+ debug_visitor_ = debug_visitor;
+ }
+
private:
friend class test::QpackEncoderPeer;
@@ -148,6 +171,7 @@
QpackHeaderTable header_table_;
uint64_t maximum_blocked_streams_;
QpackBlockingManager blocking_manager_;
+ DebugVisitor* debug_visitor_;
};
} // namespace quic
diff --git a/quic/core/qpack/qpack_encoder_test.cc b/quic/core/qpack/qpack_encoder_test.cc
index b0464c8..e6ee259 100644
--- a/quic/core/qpack/qpack_encoder_test.cc
+++ b/quic/core/qpack/qpack_encoder_test.cc
@@ -23,6 +23,13 @@
namespace test {
namespace {
+class MockDebugVisitor : public QpackEncoder::DebugVisitor {
+ public:
+ virtual ~MockDebugVisitor() = default;
+
+ MOCK_METHOD(void, OnHeaderListEncoded, (bool, bool), (override));
+};
+
class QpackEncoderTest : public QuicTest {
protected:
QpackEncoderTest()
@@ -466,6 +473,70 @@
EXPECT_EQ(30u, QpackHeaderTablePeer::dynamic_table_capacity(header_table));
}
+TEST_F(QpackEncoderTest, DebugVisitor) {
+ StrictMock<MockDebugVisitor> debug_visitor;
+ encoder_.set_debug_visitor(&debug_visitor);
+ encoder_.SetMaximumDynamicTableCapacity(QpackEntry::Size("foo", "bar"));
+
+ // Set Dynamic Table Capacity instruction.
+ EXPECT_CALL(encoder_stream_sender_delegate_, WriteStreamData(_));
+ encoder_.SetDynamicTableCapacity(QpackEntry::Size("foo", "bar"));
+
+ // Static table entry only, no blocking.
+ spdy::SpdyHeaderBlock header_list1;
+ header_list1[":method"] = "GET";
+ EXPECT_CALL(debug_visitor, OnHeaderListEncoded(
+ /* dynamic_table_insertion_blocked = */ false,
+ /* blocked_stream_limit_exhausted = */ false));
+ encoder_.EncodeHeaderList(/* stream_id = */ 1, header_list1, nullptr);
+
+ // Insert single dynamic table entry. First stream is allowed to be blocked.
+ spdy::SpdyHeaderBlock header_list2;
+ header_list2["foo"] = "bar";
+ EXPECT_CALL(debug_visitor, OnHeaderListEncoded(
+ /* dynamic_table_insertion_blocked = */ false,
+ /* blocked_stream_limit_exhausted = */ false));
+ // Add entry to dynamic table.
+ EXPECT_CALL(encoder_stream_sender_delegate_, WriteStreamData(_));
+ encoder_.EncodeHeaderList(/* stream_id = */ 1, header_list2, nullptr);
+
+ // First stream is blocked. Second stream is not allowed to be blocked.
+ // This would have been an insertion with name reference.
+ spdy::SpdyHeaderBlock header_list3;
+ header_list3["foo"] = "baz";
+ EXPECT_CALL(debug_visitor, OnHeaderListEncoded(
+ /* dynamic_table_insertion_blocked = */ false,
+ /* blocked_stream_limit_exhausted = */ true));
+ encoder_.EncodeHeaderList(/* stream_id = */ 2, header_list3, nullptr);
+
+ // First stream is blocked. Second stream is not allowed to be blocked.
+ // This would have been an insertion without name reference.
+ spdy::SpdyHeaderBlock header_list4;
+ header_list4["bar"] = "baz";
+ EXPECT_CALL(debug_visitor, OnHeaderListEncoded(
+ /* dynamic_table_insertion_blocked = */ false,
+ /* blocked_stream_limit_exhausted = */ true));
+ encoder_.EncodeHeaderList(/* stream_id = */ 2, header_list4, nullptr);
+
+ // Cannot insert into dynamic table because first entry takes up all space and
+ // it is blocking. This would have been an insertion with name reference.
+ spdy::SpdyHeaderBlock header_list5;
+ header_list5["foo"] = "baz";
+ EXPECT_CALL(debug_visitor, OnHeaderListEncoded(
+ /* dynamic_table_insertion_blocked = */ true,
+ /* blocked_stream_limit_exhausted = */ false));
+ encoder_.EncodeHeaderList(/* stream_id = */ 1, header_list5, nullptr);
+
+ // Cannot insert into dynamic table because first entry takes up all space and
+ // it is blocking. This would have been an insertion without name reference.
+ spdy::SpdyHeaderBlock header_list6;
+ header_list6["bar"] = "baz";
+ EXPECT_CALL(debug_visitor, OnHeaderListEncoded(
+ /* dynamic_table_insertion_blocked = */ true,
+ /* blocked_stream_limit_exhausted = */ false));
+ encoder_.EncodeHeaderList(/* stream_id = */ 1, header_list6, nullptr);
+}
+
} // namespace
} // namespace test
} // namespace quic