Use dynamic table for QPACK encoding.

gfe-relnote: n/a, change to QUIC v99-only code.  Protected by existing disabled gfe2_reloadable_flag_quic_enable_version_99.
PiperOrigin-RevId: 264499016
Change-Id: I64289eca7487d0e84764ef30f4b250ab45e01833
diff --git a/quic/core/qpack/qpack_encoder.cc b/quic/core/qpack/qpack_encoder.cc
index b6be648..6485071 100644
--- a/quic/core/qpack/qpack_encoder.cc
+++ b/quic/core/qpack/qpack_encoder.cc
@@ -4,9 +4,11 @@
 
 #include "net/third_party/quiche/src/quic/core/qpack/qpack_encoder.h"
 
+#include <algorithm>
 #include <utility>
 
 #include "net/third_party/quiche/src/quic/core/qpack/qpack_constants.h"
+#include "net/third_party/quiche/src/quic/core/qpack/qpack_index_conversions.h"
 #include "net/third_party/quiche/src/quic/core/qpack/qpack_instruction_encoder.h"
 #include "net/third_party/quiche/src/quic/core/qpack/qpack_required_insert_count.h"
 #include "net/third_party/quiche/src/quic/core/qpack/value_splitting_header_list.h"
@@ -16,6 +18,19 @@
 
 namespace quic {
 
+namespace {
+
+// Fraction to calculate draining index.  The oldest |kDrainingFraction| entries
+// will not be referenced in header blocks.  A new entry (duplicate or literal
+// with name reference) will be added to the dynamic table instead.  This allows
+// the number of references to the draining entry to go to zero faster, so that
+// it can be evicted.  See
+// https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#avoiding-blocked-insertions.
+// TODO(bnc): Fine tune.
+const float kDrainingFraction = 0.25;
+
+}  // anonymous namespace
+
 QpackEncoder::QpackEncoder(
     DecoderStreamErrorDelegate* decoder_stream_error_delegate)
     : decoder_stream_error_delegate_(decoder_stream_error_delegate),
@@ -26,12 +41,75 @@
 
 QpackEncoder::~QpackEncoder() {}
 
+// static
+QpackEncoder::InstructionWithValues QpackEncoder::EncodeIndexedHeaderField(
+    bool is_static,
+    uint64_t index,
+    QpackBlockingManager::IndexSet* referred_indices) {
+  InstructionWithValues instruction{QpackIndexedHeaderFieldInstruction(), {}};
+  instruction.values.s_bit = is_static;
+  instruction.values.varint = index;
+  // Add |index| to |*referred_indices| only if entry is in the dynamic table.
+  if (!is_static) {
+    referred_indices->insert(index);
+  }
+  return instruction;
+}
+
+// static
+QpackEncoder::InstructionWithValues
+QpackEncoder::EncodeLiteralHeaderFieldWithNameReference(
+    bool is_static,
+    uint64_t index,
+    QuicStringPiece value,
+    QpackBlockingManager::IndexSet* referred_indices) {
+  InstructionWithValues instruction{
+      QpackLiteralHeaderFieldNameReferenceInstruction(), {}};
+  instruction.values.s_bit = is_static;
+  instruction.values.varint = index;
+  instruction.values.value = value;
+  // Add |index| to |*referred_indices| only if entry is in the dynamic table.
+  if (!is_static) {
+    referred_indices->insert(index);
+  }
+  return instruction;
+}
+
+// static
+QpackEncoder::InstructionWithValues QpackEncoder::EncodeLiteralHeaderField(
+    QuicStringPiece name,
+    QuicStringPiece value) {
+  InstructionWithValues instruction{QpackLiteralHeaderFieldInstruction(), {}};
+  instruction.values.name = name;
+  instruction.values.value = value;
+  return instruction;
+}
+
 QpackEncoder::Instructions QpackEncoder::FirstPassEncode(
-    const spdy::SpdyHeaderBlock* header_list) {
+    const spdy::SpdyHeaderBlock* header_list,
+    QpackBlockingManager::IndexSet* referred_indices) {
   Instructions instructions;
   instructions.reserve(header_list->size());
 
+  // The index of the oldest entry that must not be evicted.
+  uint64_t smallest_blocking_index =
+      blocking_manager_.smallest_blocking_index();
+  // Entries with index larger than or equal to |known_received_count| are
+  // blocking.
+  const uint64_t known_received_count =
+      blocking_manager_.known_received_count();
+  // Only entries with index greater than or equal to |draining_index| are
+  // allowed to be referenced.
+  const uint64_t draining_index =
+      header_table_.draining_index(kDrainingFraction);
+  // Blocking references are allowed if the number of blocked streams is less
+  // than the limit.
+  // TODO(b/112770235): Also allow blocking if given stream is already blocked.
+  const bool blocking_allowed =
+      maximum_blocked_streams_ > blocking_manager_.blocked_stream_count();
+
   for (const auto& header : ValueSplittingHeaderList(header_list)) {
+    // These strings are owned by |*header_list|.
     QuicStringPiece name = header.first;
     QuicStringPiece value = header.second;
 
@@ -43,27 +121,136 @@
 
     switch (match_type) {
       case QpackHeaderTable::MatchType::kNameAndValue:
-        DCHECK(is_static) << "Dynamic table entries not supported yet.";
+        if (is_static) {
+          // Refer to entry directly.
+          instructions.push_back(
+              EncodeIndexedHeaderField(is_static, index, referred_indices));
 
-        instructions.push_back({QpackIndexedHeaderFieldInstruction(), {}});
-        instructions.back().values.s_bit = is_static;
-        instructions.back().values.varint = index;
+          break;
+        }
+
+        if (blocking_allowed || index < known_received_count) {
+          if (index >= draining_index) {
+            // If allowed, refer to entry directly.
+            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))) {
+            // If allowed, duplicate entry and refer to it.
+            encoder_stream_sender_.SendDuplicate(
+                QpackAbsoluteIndexToEncoderStreamRelativeIndex(
+                    index, header_table_.inserted_entry_count()));
+            auto entry = header_table_.InsertEntry(name, value);
+            blocking_manager_.OnReferenceSentOnEncoderStream(
+                entry->InsertionIndex(), index);
+            instructions.push_back(EncodeIndexedHeaderField(
+                is_static, entry->InsertionIndex(), referred_indices));
+            smallest_blocking_index = std::min(smallest_blocking_index, index);
+
+            break;
+          }
+        }
+
+        // Encode entry as string literals.
+        // TODO(b/112770235): Use already acknowledged entry with lower index if
+        // exists.
+        // TODO(b/112770235): Use static entry name with literal value if
+        // dynamic entry exists but cannot be used.
+        instructions.push_back(EncodeLiteralHeaderField(name, value));
 
         break;
+
       case QpackHeaderTable::MatchType::kName:
-        DCHECK(is_static) << "Dynamic table entries not supported yet.";
+        if (is_static) {
+          if (blocking_allowed &&
+              QpackEntry::Size(name, value) <=
+                  header_table_.MaxInsertSizeWithoutEvictingGivenEntry(
+                      smallest_blocking_index)) {
+            // If allowed, insert entry into dynamic table and refer to it.
+            encoder_stream_sender_.SendInsertWithNameReference(is_static, index,
+                                                               value);
+            auto entry = header_table_.InsertEntry(name, value);
+            instructions.push_back(EncodeIndexedHeaderField(
+                /* is_static = */ false, entry->InsertionIndex(),
+                referred_indices));
+            smallest_blocking_index =
+                std::min(smallest_blocking_index, entry->InsertionIndex());
 
-        instructions.push_back(
-            {QpackLiteralHeaderFieldNameReferenceInstruction(), {}});
-        instructions.back().values.s_bit = is_static;
-        instructions.back().values.varint = index;
-        instructions.back().values.value = value;
+            break;
+          }
+
+          // Emit literal field with name reference.
+          instructions.push_back(EncodeLiteralHeaderFieldWithNameReference(
+              is_static, index, value, referred_indices));
+
+          break;
+        }
+
+        if (blocking_allowed &&
+            QpackEntry::Size(name, value) <=
+                header_table_.MaxInsertSizeWithoutEvictingGivenEntry(
+                    std::min(smallest_blocking_index, index))) {
+          // If allowed, insert entry with name reference and refer to it.
+          encoder_stream_sender_.SendInsertWithNameReference(
+              is_static,
+              QpackAbsoluteIndexToEncoderStreamRelativeIndex(
+                  index, header_table_.inserted_entry_count()),
+              value);
+          auto entry = header_table_.InsertEntry(name, value);
+          blocking_manager_.OnReferenceSentOnEncoderStream(
+              entry->InsertionIndex(), index);
+          instructions.push_back(EncodeIndexedHeaderField(
+              is_static, entry->InsertionIndex(), referred_indices));
+          smallest_blocking_index = std::min(smallest_blocking_index, index);
+
+          break;
+        }
+
+        if ((blocking_allowed || index < known_received_count) &&
+            index >= draining_index) {
+          // If allowed, refer to entry name directly, with literal value.
+          instructions.push_back(EncodeLiteralHeaderFieldWithNameReference(
+              is_static, index, value, referred_indices));
+          smallest_blocking_index = std::min(smallest_blocking_index, index);
+
+          break;
+        }
+
+        // Encode entry as string literals.
+        // TODO(b/112770235): Use already acknowledged entry with lower index if
+        // exists.
+        // TODO(b/112770235): Use static entry name with literal value if
+        // dynamic entry exists but cannot be used.
+        instructions.push_back(EncodeLiteralHeaderField(name, value));
 
         break;
+
       case QpackHeaderTable::MatchType::kNoMatch:
-        instructions.push_back({QpackLiteralHeaderFieldInstruction(), {}});
-        instructions.back().values.name = name;
-        instructions.back().values.value = value;
+        if (blocking_allowed &&
+            QpackEntry::Size(name, value) <=
+                header_table_.MaxInsertSizeWithoutEvictingGivenEntry(
+                    smallest_blocking_index)) {
+          // If allowed, insert entry and refer to it.
+          encoder_stream_sender_.SendInsertWithoutNameReference(name, value);
+          auto entry = header_table_.InsertEntry(name, value);
+          instructions.push_back(EncodeIndexedHeaderField(
+              /* is_static = */ false, entry->InsertionIndex(),
+              referred_indices));
+          smallest_blocking_index =
+              std::min(smallest_blocking_index, entry->InsertionIndex());
+
+          break;
+        }
+
+        // Encode entry as string literals.
+        instructions.push_back(EncodeLiteralHeaderField(name, value));
 
         break;
     }
@@ -84,11 +271,22 @@
                                                  header_table_.max_entries());
   values.varint2 = 0;    // Delta Base.
   values.s_bit = false;  // Delta Base sign.
+  const uint64_t base = required_insert_count;
 
   instruction_encoder.Encode(QpackPrefixInstruction(), values,
                              &encoded_headers);
 
-  for (const auto& instruction : instructions) {
+  for (auto& instruction : instructions) {
+    // Dynamic table references must be transformed from absolute to relative
+    // indices.
+    if ((instruction.instruction == QpackIndexedHeaderFieldInstruction() ||
+         instruction.instruction ==
+             QpackLiteralHeaderFieldNameReferenceInstruction()) &&
+        !instruction.values.s_bit) {
+      instruction.values.varint =
+          QpackAbsoluteIndexToRequestStreamRelativeIndex(
+              instruction.values.varint, base);
+    }
     instruction_encoder.Encode(instruction.instruction, instruction.values,
                                &encoded_headers);
   }
@@ -97,14 +295,22 @@
 }
 
 std::string QpackEncoder::EncodeHeaderList(
-    QuicStreamId /* stream_id */,
+    QuicStreamId stream_id,
     const spdy::SpdyHeaderBlock* header_list) {
-  // First pass: encode into |instructions|.
-  Instructions instructions = FirstPassEncode(header_list);
+  // Keep track of all dynamic table indices that this header block refers to so
+  // that it can be passed to QpackBlockingManager.
+  QpackBlockingManager::IndexSet referred_indices;
 
-  // TODO(bnc): Implement dynamic entries and set Required Insert Count
-  // accordingly.
-  const uint64_t required_insert_count = 0;
+  // First pass: encode into |instructions|.
+  Instructions instructions = FirstPassEncode(header_list, &referred_indices);
+
+  const uint64_t required_insert_count =
+      referred_indices.empty()
+          ? 0
+          : QpackBlockingManager::RequiredInsertCount(referred_indices);
+  if (!referred_indices.empty()) {
+    blocking_manager_.OnHeaderBlockSent(stream_id, std::move(referred_indices));
+  }
 
   // Second pass.
   return SecondPassEncode(std::move(instructions), required_insert_count);
diff --git a/quic/core/qpack/qpack_encoder.h b/quic/core/qpack/qpack_encoder.h
index a1b6427..55dd1e9 100644
--- a/quic/core/qpack/qpack_encoder.h
+++ b/quic/core/qpack/qpack_encoder.h
@@ -50,6 +50,7 @@
   ~QpackEncoder() override;
 
   // Encode a header list.
+  // TODO(bnc): Take |header_list| by const reference instead of pointer.
   std::string EncodeHeaderList(QuicStreamId stream_id,
                                const spdy::SpdyHeaderBlock* header_list);
 
@@ -97,13 +98,41 @@
   };
   using Instructions = std::vector<InstructionWithValues>;
 
-  // Perform first pass of two-pass encoding: represent each header field as a
-  // reference to an existing entry, the name of an existing entry with a
-  // literal value, or a literal name and value pair.
-  Instructions FirstPassEncode(const spdy::SpdyHeaderBlock* header_list);
+  // Generate indexed header field instruction
+  // and optionally update |*referred_indices|.
+  static InstructionWithValues EncodeIndexedHeaderField(
+      bool is_static,
+      uint64_t index,
+      QpackBlockingManager::IndexSet* referred_indices);
 
-  // Perform second pass of two-pass encoding: serialize representations
-  // generated in first pass.
+  // Generate literal header field with name reference instruction
+  // and optionally update |*referred_indices|.
+  static InstructionWithValues EncodeLiteralHeaderFieldWithNameReference(
+      bool is_static,
+      uint64_t index,
+      QuicStringPiece value,
+      QpackBlockingManager::IndexSet* referred_indices);
+
+  // Generate literal header field instruction.
+  static InstructionWithValues EncodeLiteralHeaderField(QuicStringPiece name,
+                                                        QuicStringPiece value);
+
+  // Performs first pass of two-pass encoding: represent each header field in
+  // |*header_list| as a reference to an existing entry, the name of an existing
+  // entry with a literal value, or a literal name and value pair.  Sends
+  // necessary instructions on the encoder stream.  Records absolute indices of
+  // referred dynamic table entries in |*referred_indices|.  Returns list of
+  // header field representations, with all dynamic table entries referred to
+  // with absolute indices.  Returned Instructions object may have
+  // QuicStringPieces pointing to strings owned by |*header_list|.
+  // TODO(bnc): Take |header_list| by const reference instead of pointer.
+  Instructions FirstPassEncode(
+      const spdy::SpdyHeaderBlock* header_list,
+      QpackBlockingManager::IndexSet* referred_indices);
+
+  // Performs second pass of two-pass encoding: serializes representations
+  // generated in first pass, transforming absolute indices of dynamic table
+  // entries to relative indices.
   std::string SecondPassEncode(Instructions instructions,
                                uint64_t required_insert_count) const;
 
diff --git a/quic/core/qpack/qpack_encoder_test.cc b/quic/core/qpack/qpack_encoder_test.cc
index 56cfe84..01452ab 100644
--- a/quic/core/qpack/qpack_encoder_test.cc
+++ b/quic/core/qpack/qpack_encoder_test.cc
@@ -12,6 +12,7 @@
 #include "net/third_party/quiche/src/quic/platform/api/quic_test.h"
 #include "net/third_party/quiche/src/quic/platform/api/quic_text_utils.h"
 
+using ::testing::_;
 using ::testing::Eq;
 using ::testing::StrictMock;
 using ::testing::Values;
@@ -180,6 +181,224 @@
   encoder_.OnHeaderAcknowledgement(/* stream_id = */ 0);
 }
 
+TEST_F(QpackEncoderTest, DynamicTable) {
+  encoder_.SetMaximumDynamicTableCapacity(4096);
+  encoder_.SetMaximumBlockedStreams(1);
+
+  spdy::SpdyHeaderBlock header_list;
+  header_list["foo"] = "bar";
+  header_list.AppendValueOrAddHeader("foo",
+                                     "baz");  // name matches dynamic entry
+  header_list["cookie"] = "baz";              // name matches static entry
+
+  // Insert three entries into the dynamic table.
+  EXPECT_CALL(encoder_stream_sender_delegate_,
+              WriteStreamData(Eq(QuicTextUtils::HexDecode(
+                  "62"             // insert without name reference
+                  "94e7"           // Huffman-encoded name "foo"
+                  "03626172"))));  // value "bar"
+  EXPECT_CALL(encoder_stream_sender_delegate_,
+              WriteStreamData(Eq(QuicTextUtils::HexDecode(
+                  "80"  // insert with name reference, dynamic index 0
+                  "0362617a"))));  // value "baz"
+  EXPECT_CALL(encoder_stream_sender_delegate_,
+              WriteStreamData(Eq(QuicTextUtils::HexDecode(
+                  "c5"             // insert with name reference, static index 5
+                  "0362617a"))));  // value "baz"
+
+  EXPECT_EQ(QuicTextUtils::HexDecode(
+                "0400"      // prefix
+                "828180"),  // dynamic entries with relative index 0, 1, and 2
+            Encode(&header_list));
+}
+
+// There is no room in the dynamic table after inserting the first entry.
+TEST_F(QpackEncoderTest, SmallDynamicTable) {
+  encoder_.SetMaximumDynamicTableCapacity(QpackEntry::Size("foo", "bar"));
+  encoder_.SetMaximumBlockedStreams(1);
+
+  spdy::SpdyHeaderBlock header_list;
+  header_list["foo"] = "bar";
+  header_list.AppendValueOrAddHeader("foo",
+                                     "baz");  // name matches dynamic entry
+  header_list["cookie"] = "baz";              // name matches static entry
+  header_list["bar"] = "baz";                 // no match
+
+  // Insert one entry into the dynamic table.
+  EXPECT_CALL(encoder_stream_sender_delegate_,
+              WriteStreamData(Eq(QuicTextUtils::HexDecode(
+                  "62"             // insert without name reference
+                  "94e7"           // Huffman-encoded name "foo"
+                  "03626172"))));  // value "bar"
+
+  EXPECT_EQ(QuicTextUtils::HexDecode("0200"  // prefix
+                                     "80"    // dynamic entry 0
+                                     "40"  // reference to dynamic entry 0 name
+                                     "0362617a"  // with literal value "baz"
+                                     "55"  // reference to static entry 5 name
+                                     "0362617a"    // with literal value "baz"
+                                     "23626172"    // literal name "bar"
+                                     "0362617a"),  // with literal value "baz"
+            Encode(&header_list));
+}
+
+TEST_F(QpackEncoderTest, BlockedStream) {
+  encoder_.SetMaximumDynamicTableCapacity(4096);
+  encoder_.SetMaximumBlockedStreams(1);
+
+  spdy::SpdyHeaderBlock header_list1;
+  header_list1["foo"] = "bar";
+
+  // Insert one entry into the dynamic table.
+  EXPECT_CALL(encoder_stream_sender_delegate_,
+              WriteStreamData(Eq(QuicTextUtils::HexDecode(
+                  "62"             // insert without name reference
+                  "94e7"           // Huffman-encoded name "foo"
+                  "03626172"))));  // value "bar"
+
+  EXPECT_EQ(QuicTextUtils::HexDecode("0200"  // prefix
+                                     "80"),  // dynamic entry 0
+            encoder_.EncodeHeaderList(/* stream_id = */ 1, &header_list1));
+
+  // Stream 1 is blocked.  Stream 2 is not allowed to block.
+  spdy::SpdyHeaderBlock header_list2;
+  header_list2["foo"] = "bar";  // name and value match dynamic entry
+  header_list2.AppendValueOrAddHeader("foo",
+                                      "baz");  // name matches dynamic entry
+  header_list2["cookie"] = "baz";              // name matches static entry
+  header_list2["bar"] = "baz";                 // no match
+
+  EXPECT_EQ(QuicTextUtils::HexDecode("0000"        // prefix
+                                     "2a94e7"      // literal name "foo"
+                                     "03626172"    // with literal value "bar"
+                                     "2a94e7"      // literal name "foo"
+                                     "0362617a"    // with literal value "baz"
+                                     "55"          // name of static entry 5
+                                     "0362617a"    // with literal value "baz"
+                                     "23626172"    // literal name "bar"
+                                     "0362617a"),  // with literal value "baz"
+            encoder_.EncodeHeaderList(/* stream_id = */ 2, &header_list2));
+
+  // Peer acknowledges receipt of one dynamic table entry.
+  // Stream 1 is no longer blocked.
+  encoder_.OnInsertCountIncrement(1);
+
+  // Insert three entries into the dynamic table.
+  EXPECT_CALL(encoder_stream_sender_delegate_,
+              WriteStreamData(Eq(QuicTextUtils::HexDecode(
+                  "80"  // insert with name reference, dynamic index 0
+                  "0362617a"))));  // value "baz"
+  EXPECT_CALL(encoder_stream_sender_delegate_,
+              WriteStreamData(Eq(QuicTextUtils::HexDecode(
+                  "c5"             // insert with name reference, static index 5
+                  "0362617a"))));  // value "baz"
+  EXPECT_CALL(encoder_stream_sender_delegate_,
+              WriteStreamData(Eq(QuicTextUtils::HexDecode(
+                  "43"                       // insert without name reference
+                  "626172"                   // name "bar"
+                  "0362617a"))));            // value "baz"
+  EXPECT_EQ(QuicTextUtils::HexDecode("0500"  // prefix
+                                     "83828180"),  // dynamic entries
+            encoder_.EncodeHeaderList(/* stream_id = */ 3, &header_list2));
+
+  // Stream 3 is blocked.  Stream 4 is not allowed to block, but it can
+  // reference already acknowledged dynamic entry 0.
+  EXPECT_EQ(QuicTextUtils::HexDecode("0200"        // prefix
+                                     "80"          // dynamic entry 0
+                                     "2a94e7"      // literal name "foo"
+                                     "0362617a"    // with literal value "baz"
+                                     "2c21cfd4c5"  // literal name "cookie"
+                                     "0362617a"    // with literal value "baz"
+                                     "23626172"    // literal name "bar"
+                                     "0362617a"),  // with literal value "baz"
+            encoder_.EncodeHeaderList(/* stream_id = */ 4, &header_list2));
+
+  // Peer acknowledges receipt of two more dynamic table entries.
+  // Stream 3 is still blocked.
+  encoder_.OnInsertCountIncrement(2);
+
+  // Stream 5 is not allowed to block, but it can reference already acknowledged
+  // dynamic entries 0, 1, and 2.
+  EXPECT_EQ(QuicTextUtils::HexDecode("0400"        // prefix
+                                     "828180"      // dynamic entries
+                                     "23626172"    // literal name "bar"
+                                     "0362617a"),  // with literal value "baz"
+            encoder_.EncodeHeaderList(/* stream_id = */ 5, &header_list2));
+
+  // Peer acknowledges decoding header block on stream 3.
+  // Stream 3 is not blocked any longer.
+  encoder_.OnHeaderAcknowledgement(3);
+
+  EXPECT_EQ(QuicTextUtils::HexDecode("0500"        // prefix
+                                     "83828180"),  // dynamic entries
+            encoder_.EncodeHeaderList(/* stream_id = */ 6, &header_list2));
+}
+
+TEST_F(QpackEncoderTest, Draining) {
+  // TODO(b/112770235): Remove when already blocking stream can emit blocking
+  // references.
+  encoder_.SetMaximumBlockedStreams(2);
+
+  spdy::SpdyHeaderBlock header_list1;
+  header_list1["one"] = "foo";
+  header_list1["two"] = "foo";
+  header_list1["three"] = "foo";
+  header_list1["four"] = "foo";
+  header_list1["five"] = "foo";
+  header_list1["six"] = "foo";
+  header_list1["seven"] = "foo";
+  header_list1["eight"] = "foo";
+  header_list1["nine"] = "foo";
+  header_list1["ten"] = "foo";
+
+  // Make just enough room in the dynamic table for the header list plus the
+  // first entry duplicated.  This will ensure that the oldest entries are
+  // draining.
+  uint64_t maximum_dynamic_table_capacity = 0;
+  for (const auto& header_field : header_list1) {
+    maximum_dynamic_table_capacity +=
+        QpackEntry::Size(header_field.first, header_field.second);
+  }
+  maximum_dynamic_table_capacity += QpackEntry::Size("one", "foo");
+  encoder_.SetMaximumDynamicTableCapacity(maximum_dynamic_table_capacity);
+
+  // Insert ten entries into the dynamic table.
+  EXPECT_CALL(encoder_stream_sender_delegate_, WriteStreamData(_)).Times(10);
+
+  EXPECT_EQ(
+      QuicTextUtils::HexDecode("0b00"                    // prefix
+                               "89888786858483828180"),  // dynamic entries
+      Encode(&header_list1));
+
+  // Entry is identical to oldest one, which is draining.  It will be
+  // duplicated and referenced.
+  spdy::SpdyHeaderBlock header_list2;
+  header_list2["one"] = "foo";
+
+  // Duplicate oldest entry.
+  EXPECT_CALL(encoder_stream_sender_delegate_,
+              WriteStreamData(Eq(QuicTextUtils::HexDecode("09"))));
+
+  EXPECT_EQ(QuicTextUtils::HexDecode("0c00"  // prefix
+                                     "80"),  // most recent dynamic table entry
+            Encode(&header_list2));
+
+  spdy::SpdyHeaderBlock header_list3;
+  // Entry is identical to second oldest one, which is draining.  There is no
+  // room to duplicate, it will be encoded with string literals.
+  header_list3.AppendValueOrAddHeader("two", "foo");
+  // Entry has name identical to second oldest one, which is draining.  There is
+  // no room to insert new entry, it will be encoded with string literals.
+  header_list3.AppendValueOrAddHeader("two", "bar");
+
+  EXPECT_EQ(QuicTextUtils::HexDecode("0000"        // prefix
+                                     "2374776f"    // literal name "two"
+                                     "8294e7"      // literal value "foo"
+                                     "2374776f"    // literal name "two"
+                                     "03626172"),  // literal value "bar"
+            Encode(&header_list3));
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic