Disable batched write in QuicWriteBlockedList.

As a reminder, HTTP/3 streams have two priority-related parameters received from
the clien in PRIORITY_UPDATE frames: urgency (int) and incremental (bool).

Writes can be continuous or round-robin. With continuous writes, if data is
written on a given stream, writing contiues as long as there is data for that
stream. Round-robin writes can be batched or not. If batched, writing on a
stream can continue if less than 16 kB of data has been written continuously on
that stream. If not batched, other streams with same urgency get a turn after a
single write. In any case, the size of a single write is not limited by
QuicWriteBlockedList.

This applies to streams with the same urgency value. Streams always yield to
streams with higher priority (lower urgency value).

Incremental streams are always written round-robin. If
--gfe2_reloadable_flag_quic_priority_respect_incremental is
false, non-incremental streams are also written round-robin. If it is true, they
are written continuously. This was introduced in cl/511587715.

This CL introduces --gfe2_reloadable_flag_quic_disable_batch_write. If false,
round-robin writes are batched. If true, they are not batched. This applies to
incremental streams, and if
--gfe2_reloadable_flag_quic_priority_respect_incremental is false, then to
non-incremental streams as well.

An additional optimization is to prioritize the non-incremental stream with the
last write over incremental streams even if there was a write on the incremental
stream in the meanwhile (due to the non-incremental stream not having data to
write).  While this sounds unfair (in the sense that incremental streams may be
starved), this is no worse than incremental streams being blocked by a
non-incremental stream that can provide write data continuously.  The rationale
is that data on a non-incremental stream is useless for the client until all of
it is received, so once some of it was sent, send all of it.  (Note that
incremental streams do not immediately yield to other non-incremental streams
that have started writing, only to the most recent one.  They get at least one
turn to write their own data.)

Protected by FLAGS_quic_reloadable_flag_quic_disable_batch_write.

PiperOrigin-RevId: 516822305
diff --git a/quiche/quic/core/http/quic_spdy_session_test.cc b/quiche/quic/core/http/quic_spdy_session_test.cc
index e3db301..67ed9ea 100644
--- a/quiche/quic/core/http/quic_spdy_session_test.cc
+++ b/quiche/quic/core/http/quic_spdy_session_test.cc
@@ -859,79 +859,6 @@
   session_.OnStreamsBlockedFrame(frame);
 }
 
-TEST_P(QuicSpdySessionTestServer, TestBatchedWrites) {
-  session_.set_writev_consumes_all_data(true);
-  TestStream* stream2 = session_.CreateOutgoingBidirectionalStream();
-  TestStream* stream4 = session_.CreateOutgoingBidirectionalStream();
-  TestStream* stream6 = session_.CreateOutgoingBidirectionalStream();
-
-  const QuicStreamPriority priority{QuicStreamPriority::kDefaultUrgency,
-                                    /* incremental = */ true};
-  stream2->SetPriority(priority);
-  stream4->SetPriority(priority);
-  stream6->SetPriority(priority);
-
-  session_.set_writev_consumes_all_data(true);
-  session_.MarkConnectionLevelWriteBlocked(stream2->id());
-  session_.MarkConnectionLevelWriteBlocked(stream4->id());
-
-  // With two sessions blocked, we should get two write calls.  They should both
-  // go to the first stream as it will only write 6k and mark itself blocked
-  // again.
-  InSequence s;
-  EXPECT_CALL(*stream2, OnCanWrite()).WillOnce(Invoke([this, stream2]() {
-    session_.SendLargeFakeData(stream2, 6000);
-    session_.MarkConnectionLevelWriteBlocked(stream2->id());
-  }));
-  EXPECT_CALL(*stream2, OnCanWrite()).WillOnce(Invoke([this, stream2]() {
-    session_.SendLargeFakeData(stream2, 6000);
-    session_.MarkConnectionLevelWriteBlocked(stream2->id());
-  }));
-  session_.OnCanWrite();
-
-  // We should get one more call for stream2, at which point it has used its
-  // write quota and we move over to stream 4.
-  EXPECT_CALL(*stream2, OnCanWrite()).WillOnce(Invoke([this, stream2]() {
-    session_.SendLargeFakeData(stream2, 6000);
-    session_.MarkConnectionLevelWriteBlocked(stream2->id());
-  }));
-  EXPECT_CALL(*stream4, OnCanWrite()).WillOnce(Invoke([this, stream4]() {
-    session_.SendLargeFakeData(stream4, 6000);
-    session_.MarkConnectionLevelWriteBlocked(stream4->id());
-  }));
-  session_.OnCanWrite();
-
-  // Now let stream 4 do the 2nd of its 3 writes, but add a block for a high
-  // priority stream 6.  4 should be preempted.  6 will write but *not* block so
-  // will cede back to 4.
-  stream6->SetPriority(QuicStreamPriority{
-      kV3HighestPriority, QuicStreamPriority::kDefaultIncremental});
-  EXPECT_CALL(*stream4, OnCanWrite())
-      .WillOnce(Invoke([this, stream4, stream6]() {
-        session_.SendLargeFakeData(stream4, 6000);
-        session_.MarkConnectionLevelWriteBlocked(stream4->id());
-        session_.MarkConnectionLevelWriteBlocked(stream6->id());
-      }));
-  EXPECT_CALL(*stream6, OnCanWrite())
-      .WillOnce(Invoke([this, stream4, stream6]() {
-        session_.SendStreamData(stream6);
-        session_.SendLargeFakeData(stream4, 6000);
-      }));
-  session_.OnCanWrite();
-
-  // Stream4 alread did 6k worth of writes, so after doing another 12k it should
-  // cede and 2 should resume.
-  EXPECT_CALL(*stream4, OnCanWrite()).WillOnce(Invoke([this, stream4]() {
-    session_.SendLargeFakeData(stream4, 12000);
-    session_.MarkConnectionLevelWriteBlocked(stream4->id());
-  }));
-  EXPECT_CALL(*stream2, OnCanWrite()).WillOnce(Invoke([this, stream2]() {
-    session_.SendLargeFakeData(stream2, 6000);
-    session_.MarkConnectionLevelWriteBlocked(stream2->id());
-  }));
-  session_.OnCanWrite();
-}
-
 TEST_P(QuicSpdySessionTestServer, OnCanWriteBundlesStreams) {
   // Encryption needs to be established before data can be sent.
   CompleteHandshake();
diff --git a/quiche/quic/core/quic_flags_list.h b/quiche/quic/core/quic_flags_list.h
index 09b3b4b..08b6902 100644
--- a/quiche/quic/core/quic_flags_list.h
+++ b/quiche/quic/core/quic_flags_list.h
@@ -65,6 +65,8 @@
 QUIC_FLAG(quic_reloadable_flag_quic_require_handshake_confirmation, false)
 // If true, respect the incremental parameter of each stream in QuicWriteBlockedList.
 QUIC_FLAG(quic_reloadable_flag_quic_priority_respect_incremental, false)
+// If true, round-robin stream writes instead of batching in QuicWriteBlockedList.
+QUIC_FLAG(quic_reloadable_flag_quic_disable_batch_write, false)
 // If true, server proactively retires client issued connection ID on reverse path validation failure. 
 QUIC_FLAG(quic_reloadable_flag_quic_retire_cid_on_reverse_path_validation_failure, true)
 // If true, server sends bandwidth eastimate when network is idle for a while.
diff --git a/quiche/quic/core/quic_session_test.cc b/quiche/quic/core/quic_session_test.cc
index 3906937..750ffac 100644
--- a/quiche/quic/core/quic_session_test.cc
+++ b/quiche/quic/core/quic_session_test.cc
@@ -1037,25 +1037,33 @@
   stream6->SetPriority(priority);
 
   session_.set_writev_consumes_all_data(true);
+  // Tell the session that stream2 and stream4 have data to write.
   session_.MarkConnectionLevelWriteBlocked(stream2->id());
   session_.MarkConnectionLevelWriteBlocked(stream4->id());
 
-  // With two sessions blocked, we should get two write calls.  They should both
-  // go to the first stream as it will only write 6k and mark itself blocked
-  // again.
+  // With two sessions blocked, we should get two write calls.
   InSequence s;
   EXPECT_CALL(*stream2, OnCanWrite()).WillOnce(Invoke([this, stream2]() {
     session_.SendLargeFakeData(stream2, 6000);
     session_.MarkConnectionLevelWriteBlocked(stream2->id());
   }));
-  EXPECT_CALL(*stream2, OnCanWrite()).WillOnce(Invoke([this, stream2]() {
-    session_.SendLargeFakeData(stream2, 6000);
-    session_.MarkConnectionLevelWriteBlocked(stream2->id());
-  }));
+  if (GetQuicReloadableFlag(quic_disable_batch_write)) {
+    EXPECT_CALL(*stream4, OnCanWrite()).WillOnce(Invoke([this, stream4]() {
+      session_.SendLargeFakeData(stream4, 6000);
+      session_.MarkConnectionLevelWriteBlocked(stream4->id());
+    }));
+  } else {
+    // Since stream2 only wrote 6 kB and marked itself blocked again,
+    // the second write happens on the same stream.
+    EXPECT_CALL(*stream2, OnCanWrite()).WillOnce(Invoke([this, stream2]() {
+      session_.SendLargeFakeData(stream2, 6000);
+      session_.MarkConnectionLevelWriteBlocked(stream2->id());
+    }));
+  }
   session_.OnCanWrite();
 
-  // We should get one more call for stream2, at which point it has used its
-  // write quota and we move over to stream 4.
+  // If batched write is enabled, stream2 can write a third time in a row.
+  // If batched write is disabled, stream2 has a turn again after stream4.
   EXPECT_CALL(*stream2, OnCanWrite()).WillOnce(Invoke([this, stream2]() {
     session_.SendLargeFakeData(stream2, 6000);
     session_.MarkConnectionLevelWriteBlocked(stream2->id());
@@ -1066,17 +1074,26 @@
   }));
   session_.OnCanWrite();
 
-  // Now let stream 4 do the 2nd of its 3 writes, but add a block for a high
-  // priority stream 6.  4 should be preempted.  6 will write but *not* block so
-  // will cede back to 4.
+  // The next write adds a block for stream 6.
   stream6->SetPriority(QuicStreamPriority{
       kV3HighestPriority, QuicStreamPriority::kDefaultIncremental});
-  EXPECT_CALL(*stream4, OnCanWrite())
-      .WillOnce(Invoke([this, stream4, stream6]() {
-        session_.SendLargeFakeData(stream4, 6000);
-        session_.MarkConnectionLevelWriteBlocked(stream4->id());
-        session_.MarkConnectionLevelWriteBlocked(stream6->id());
-      }));
+  if (GetQuicReloadableFlag(quic_disable_batch_write)) {
+    EXPECT_CALL(*stream2, OnCanWrite())
+        .WillOnce(Invoke([this, stream2, stream6]() {
+          session_.SendLargeFakeData(stream2, 6000);
+          session_.MarkConnectionLevelWriteBlocked(stream2->id());
+          session_.MarkConnectionLevelWriteBlocked(stream6->id());
+        }));
+  } else {
+    EXPECT_CALL(*stream4, OnCanWrite())
+        .WillOnce(Invoke([this, stream4, stream6]() {
+          session_.SendLargeFakeData(stream4, 6000);
+          session_.MarkConnectionLevelWriteBlocked(stream4->id());
+          session_.MarkConnectionLevelWriteBlocked(stream6->id());
+        }));
+  }
+  // Stream 6 will write next, because it has higher priority.
+  // It does not mark itself as blocked.
   EXPECT_CALL(*stream6, OnCanWrite())
       .WillOnce(Invoke([this, stream4, stream6]() {
         session_.SendStreamData(stream6);
@@ -1084,8 +1101,9 @@
       }));
   session_.OnCanWrite();
 
-  // Stream4 alread did 6k worth of writes, so after doing another 12k it should
-  // cede and 2 should resume.
+  // If batched write is enabled, stream4 can continue to write, but will
+  // exhaust its write limit, so the last write is on stream2.
+  // If batched write is disabled, stream4 has a turn again, then stream2.
   EXPECT_CALL(*stream4, OnCanWrite()).WillOnce(Invoke([this, stream4]() {
     session_.SendLargeFakeData(stream4, 12000);
     session_.MarkConnectionLevelWriteBlocked(stream4->id());
diff --git a/quiche/quic/core/quic_write_blocked_list.cc b/quiche/quic/core/quic_write_blocked_list.cc
index 86ed31d..3f09167 100644
--- a/quiche/quic/core/quic_write_blocked_list.cc
+++ b/quiche/quic/core/quic_write_blocked_list.cc
@@ -12,7 +12,8 @@
 QuicWriteBlockedList::QuicWriteBlockedList()
     : last_priority_popped_(0),
       respect_incremental_(
-          GetQuicReloadableFlag(quic_priority_respect_incremental)) {
+          GetQuicReloadableFlag(quic_priority_respect_incremental)),
+      disable_batch_write_(GetQuicReloadableFlag(quic_disable_batch_write)) {
   memset(batch_write_stream_id_, 0, sizeof(batch_write_stream_id_));
   memset(bytes_left_for_batch_write_, 0, sizeof(bytes_left_for_batch_write_));
 }
@@ -41,18 +42,35 @@
   const auto id_and_priority =
       priority_write_scheduler_.PopNextReadyStreamAndPriority();
   const QuicStreamId id = std::get<0>(id_and_priority);
-  const spdy::SpdyPriority priority = std::get<1>(id_and_priority).urgency;
+  const QuicStreamPriority& priority = std::get<1>(id_and_priority);
+  const spdy::SpdyPriority urgency = priority.urgency;
+  const bool incremental = priority.incremental;
+
+  last_priority_popped_ = urgency;
+
+  if (disable_batch_write_) {
+    QUIC_RELOADABLE_FLAG_COUNT_N(quic_disable_batch_write, 1, 3);
+
+    // Writes on incremental streams are not batched.  Not setting
+    // `batch_write_stream_id_` if the current write is incremental allows the
+    // write on the last non-incremental stream to continue if only incremental
+    // writes happened within this urgency bucket while that stream had no data
+    // to write.
+    if (!respect_incremental_ || !incremental) {
+      batch_write_stream_id_[urgency] = id;
+    }
+
+    return id;
+  }
 
   if (!priority_write_scheduler_.HasReadyStreams()) {
     // If no streams are blocked, don't bother latching.  This stream will be
-    // the first popped for its priority anyway.
-    batch_write_stream_id_[priority] = 0;
-    last_priority_popped_ = priority;
-  } else if (batch_write_stream_id_[priority] != id) {
+    // the first popped for its urgency anyway.
+    batch_write_stream_id_[urgency] = 0;
+  } else if (batch_write_stream_id_[urgency] != id) {
     // If newly latching this batch write stream, let it write 16k.
-    batch_write_stream_id_[priority] = id;
-    bytes_left_for_batch_write_[priority] = 16000;
-    last_priority_popped_ = priority;
+    batch_write_stream_id_[urgency] = id;
+    bytes_left_for_batch_write_[urgency] = 16000;
   }
 
   return id;
@@ -86,6 +104,11 @@
 
 void QuicWriteBlockedList::UpdateBytesForStream(QuicStreamId stream_id,
                                                 size_t bytes) {
+  if (disable_batch_write_) {
+    QUIC_RELOADABLE_FLAG_COUNT_N(quic_disable_batch_write, 2, 3);
+    return;
+  }
+
   if (batch_write_stream_id_[last_priority_popped_] == stream_id) {
     // If this was the last data stream popped by PopFront, update the
     // bytes remaining in its batch write.
@@ -99,19 +122,26 @@
     return;
   }
 
-  bool incremental = true;
-
   if (respect_incremental_) {
     QUIC_RELOADABLE_FLAG_COUNT(quic_priority_respect_incremental);
-    incremental =
-        priority_write_scheduler_.GetStreamPriority(stream_id).incremental;
+    if (!priority_write_scheduler_.GetStreamPriority(stream_id).incremental) {
+      const bool push_front =
+          stream_id == batch_write_stream_id_[last_priority_popped_];
+      priority_write_scheduler_.MarkStreamReady(stream_id, push_front);
+      return;
+    }
   }
 
-  // Writes on incremental streams are batched up to 16 kB.
-  // Writes on non-incremental streams continue whenever possible.
-  bool push_front =
+  if (disable_batch_write_) {
+    QUIC_RELOADABLE_FLAG_COUNT_N(quic_disable_batch_write, 3, 3);
+    priority_write_scheduler_.MarkStreamReady(stream_id,
+                                              /* push_front = */ false);
+    return;
+  }
+
+  const bool push_front =
       stream_id == batch_write_stream_id_[last_priority_popped_] &&
-      (!incremental || bytes_left_for_batch_write_[last_priority_popped_] > 0);
+      bytes_left_for_batch_write_[last_priority_popped_] > 0;
 
   priority_write_scheduler_.MarkStreamReady(stream_id, push_front);
 }
diff --git a/quiche/quic/core/quic_write_blocked_list.h b/quiche/quic/core/quic_write_blocked_list.h
index 422b79c..ba1fd18 100644
--- a/quiche/quic/core/quic_write_blocked_list.h
+++ b/quiche/quic/core/quic_write_blocked_list.h
@@ -72,6 +72,8 @@
   void UpdateStreamPriority(QuicStreamId stream_id,
                             const QuicStreamPriority& new_priority);
 
+  // TODO(b/147306124): Remove when deprecating
+  // reloadable_flag_quic_disable_batch_write.
   void UpdateBytesForStream(QuicStreamId stream_id, size_t bytes);
 
   // Pushes a stream to the back of the list for its priority level *unless* it
@@ -98,8 +100,10 @@
   // Set to kBatchWriteSize when we set a new batch_write_stream_id_ for a given
   // priority.  This is decremented with each write the stream does until it is
   // done with its batch write.
+  // TODO(b/147306124): Remove when deprecating
+  // reloadable_flag_quic_disable_batch_write.
   size_t bytes_left_for_batch_write_[spdy::kV3LowestPriority + 1];
-  // Tracks the last priority popped for UpdateBytesForStream.
+  // Tracks the last priority popped for UpdateBytesForStream() and AddStream().
   spdy::SpdyPriority last_priority_popped_;
 
   // A StaticStreamCollection is a vector of <QuicStreamId, bool> pairs plus a
@@ -149,6 +153,8 @@
 
   // Latched value of reloadable_flag_quic_priority_respect_incremental.
   const bool respect_incremental_;
+  // Latched value of reloadable_flag_quic_disable_batch_write.
+  const bool disable_batch_write_;
 };
 
 }  // namespace quic
diff --git a/quiche/quic/core/quic_write_blocked_list_test.cc b/quiche/quic/core/quic_write_blocked_list_test.cc
index b13edec..812e936 100644
--- a/quiche/quic/core/quic_write_blocked_list_test.cc
+++ b/quiche/quic/core/quic_write_blocked_list_test.cc
@@ -23,60 +23,67 @@
 
 class QuicWriteBlockedListTest : public QuicTest {
  protected:
+  void SetUp() override {
+    // Delay construction of QuicWriteBlockedList object to allow constructor of
+    // derived test classes to manipulate reloadable flags that are latched in
+    // QuicWriteBlockedList constructor.
+    write_blocked_list_.emplace();
+  }
+
   bool HasWriteBlockedDataStreams() const {
-    return write_blocked_list_.HasWriteBlockedDataStreams();
+    return write_blocked_list_->HasWriteBlockedDataStreams();
   }
 
   bool HasWriteBlockedSpecialStream() const {
-    return write_blocked_list_.HasWriteBlockedSpecialStream();
+    return write_blocked_list_->HasWriteBlockedSpecialStream();
   }
 
   size_t NumBlockedSpecialStreams() const {
-    return write_blocked_list_.NumBlockedSpecialStreams();
+    return write_blocked_list_->NumBlockedSpecialStreams();
   }
 
   size_t NumBlockedStreams() const {
-    return write_blocked_list_.NumBlockedStreams();
+    return write_blocked_list_->NumBlockedStreams();
   }
 
   bool ShouldYield(QuicStreamId id) const {
-    return write_blocked_list_.ShouldYield(id);
+    return write_blocked_list_->ShouldYield(id);
   }
 
   QuicStreamPriority GetPriorityofStream(QuicStreamId id) const {
-    return write_blocked_list_.GetPriorityofStream(id);
+    return write_blocked_list_->GetPriorityofStream(id);
   }
 
-  QuicStreamId PopFront() { return write_blocked_list_.PopFront(); }
+  QuicStreamId PopFront() { return write_blocked_list_->PopFront(); }
 
   void RegisterStream(QuicStreamId stream_id, bool is_static_stream,
                       const QuicStreamPriority& priority) {
-    write_blocked_list_.RegisterStream(stream_id, is_static_stream, priority);
+    write_blocked_list_->RegisterStream(stream_id, is_static_stream, priority);
   }
 
   void UnregisterStream(QuicStreamId stream_id) {
-    write_blocked_list_.UnregisterStream(stream_id);
+    write_blocked_list_->UnregisterStream(stream_id);
   }
 
   void UpdateStreamPriority(QuicStreamId stream_id,
                             const QuicStreamPriority& new_priority) {
-    write_blocked_list_.UpdateStreamPriority(stream_id, new_priority);
+    write_blocked_list_->UpdateStreamPriority(stream_id, new_priority);
   }
 
   void UpdateBytesForStream(QuicStreamId stream_id, size_t bytes) {
-    write_blocked_list_.UpdateBytesForStream(stream_id, bytes);
+    write_blocked_list_->UpdateBytesForStream(stream_id, bytes);
   }
 
   void AddStream(QuicStreamId stream_id) {
-    write_blocked_list_.AddStream(stream_id);
+    write_blocked_list_->AddStream(stream_id);
   }
 
   bool IsStreamBlocked(QuicStreamId stream_id) const {
-    return write_blocked_list_.IsStreamBlocked(stream_id);
+    return write_blocked_list_->IsStreamBlocked(stream_id);
   }
 
  private:
-  QuicWriteBlockedList write_blocked_list_;
+  absl::optional<QuicWriteBlockedList> write_blocked_list_;
 };
 
 TEST_F(QuicWriteBlockedListTest, PriorityOrder) {
@@ -186,7 +193,74 @@
   EXPECT_FALSE(HasWriteBlockedDataStreams());
 }
 
-TEST_F(QuicWriteBlockedListTest, BatchingWrites) {
+TEST_F(QuicWriteBlockedListTest, IncrementalStreamsRoundRobin) {
+  const QuicStreamId id1 = 5;
+  const QuicStreamId id2 = 7;
+  const QuicStreamId id3 = 9;
+  RegisterStream(id1, kNotStatic, {kV3LowestPriority, kIncremental});
+  RegisterStream(id2, kNotStatic, {kV3LowestPriority, kIncremental});
+  RegisterStream(id3, kNotStatic, {kV3LowestPriority, kIncremental});
+
+  AddStream(id1);
+  AddStream(id2);
+  AddStream(id3);
+
+  EXPECT_EQ(id1, PopFront());
+  const size_t kLargeWriteSize = 1000 * 1000 * 1000;
+  UpdateBytesForStream(id1, kLargeWriteSize);
+  AddStream(id1);
+
+  EXPECT_EQ(id2, PopFront());
+  UpdateBytesForStream(id2, kLargeWriteSize);
+  EXPECT_EQ(id3, PopFront());
+  UpdateBytesForStream(id3, kLargeWriteSize);
+
+  AddStream(id3);
+  AddStream(id2);
+
+  EXPECT_EQ(id1, PopFront());
+  UpdateBytesForStream(id1, kLargeWriteSize);
+  EXPECT_EQ(id3, PopFront());
+  UpdateBytesForStream(id3, kLargeWriteSize);
+  AddStream(id3);
+
+  EXPECT_EQ(id2, PopFront());
+  EXPECT_EQ(id3, PopFront());
+}
+
+class QuicWriteBlockedListParameterizedTest
+    : public QuicWriteBlockedListTest,
+      public ::testing::WithParamInterface<std::tuple<bool, bool>> {
+ protected:
+  QuicWriteBlockedListParameterizedTest()
+      : priority_respect_incremental_(std::get<0>(GetParam())),
+        disable_batch_write_(std::get<1>(GetParam())) {
+    SetQuicReloadableFlag(quic_priority_respect_incremental,
+                          priority_respect_incremental_);
+    SetQuicReloadableFlag(quic_disable_batch_write, disable_batch_write_);
+  }
+
+  const bool priority_respect_incremental_;
+  const bool disable_batch_write_;
+};
+
+INSTANTIATE_TEST_SUITE_P(
+    BatchWrite, QuicWriteBlockedListParameterizedTest,
+    ::testing::Combine(::testing::Bool(), ::testing::Bool()),
+    [](const testing::TestParamInfo<
+        QuicWriteBlockedListParameterizedTest::ParamType>& info) {
+      return absl::StrCat(std::get<0>(info.param) ? "RespectIncrementalTrue"
+                                                  : "RespectIncrementalFalse",
+                          std::get<1>(info.param) ? "DisableBatchWriteTrue"
+                                                  : "DisableBatchWriteFalse");
+    });
+
+// If reloadable_flag_quic_disable_batch_write is false, writes are batched.
+TEST_P(QuicWriteBlockedListParameterizedTest, BatchingWrites) {
+  if (disable_batch_write_) {
+    return;
+  }
+
   const QuicStreamId id1 = 5;
   const QuicStreamId id2 = 7;
   const QuicStreamId id3 = 9;
@@ -236,7 +310,13 @@
   EXPECT_EQ(id1, PopFront());
 }
 
-TEST_F(QuicWriteBlockedListTest, IncrementalStreamsRoundRobin) {
+// If reloadable_flag_quic_disable_batch_write is true, writes are performed
+// round-robin regardless of how little data is written on each stream.
+TEST_P(QuicWriteBlockedListParameterizedTest, RoundRobin) {
+  if (!disable_batch_write_) {
+    return;
+  }
+
   const QuicStreamId id1 = 5;
   const QuicStreamId id2 = 7;
   const QuicStreamId id3 = 9;
@@ -249,30 +329,25 @@
   AddStream(id3);
 
   EXPECT_EQ(id1, PopFront());
-  const size_t kLargeWriteSize = 1000 * 1000 * 1000;
-  UpdateBytesForStream(id1, kLargeWriteSize);
   AddStream(id1);
 
   EXPECT_EQ(id2, PopFront());
-  UpdateBytesForStream(id2, kLargeWriteSize);
   EXPECT_EQ(id3, PopFront());
-  UpdateBytesForStream(id3, kLargeWriteSize);
 
   AddStream(id3);
   AddStream(id2);
 
   EXPECT_EQ(id1, PopFront());
-  UpdateBytesForStream(id1, kLargeWriteSize);
   EXPECT_EQ(id3, PopFront());
-  UpdateBytesForStream(id3, kLargeWriteSize);
   AddStream(id3);
 
   EXPECT_EQ(id2, PopFront());
   EXPECT_EQ(id3, PopFront());
 }
 
-TEST_F(QuicWriteBlockedListTest, NonIncrementalStreamsKeepWriting) {
-  if (!GetQuicReloadableFlag(quic_priority_respect_incremental)) {
+TEST_P(QuicWriteBlockedListParameterizedTest,
+       NonIncrementalStreamsKeepWriting) {
+  if (!priority_respect_incremental_) {
     return;
   }
 
@@ -347,8 +422,9 @@
   UpdateBytesForStream(id1, kLargeWriteSize);
 }
 
-TEST_F(QuicWriteBlockedListTest, IncrementalAndNonIncrementalStreams) {
-  if (!GetQuicReloadableFlag(quic_priority_respect_incremental)) {
+TEST_P(QuicWriteBlockedListParameterizedTest,
+       IncrementalAndNonIncrementalStreams) {
+  if (!priority_respect_incremental_) {
     return;
   }
 
@@ -360,9 +436,7 @@
   AddStream(id1);
   AddStream(id2);
 
-  // Small writes do not exceed the batch limit.  Writes continue on streams
-  // with most recently written data, regardless of the incremental parameter
-  // value.
+  // A non-incremental stream can continue writing as long as it has data.
   EXPECT_EQ(id1, PopFront());
   const size_t kSmallWriteSize = 1000;
   UpdateBytesForStream(id1, kSmallWriteSize);
@@ -380,14 +454,17 @@
   AddStream(id2);
   AddStream(id1);
 
-  EXPECT_EQ(id2, PopFront());
-  UpdateBytesForStream(id2, kSmallWriteSize);
-  AddStream(id2);
+  if (!disable_batch_write_) {
+    // Small writes do not exceed the batch limit.
+    // Writes continue even on an incremental stream.
+    EXPECT_EQ(id2, PopFront());
+    UpdateBytesForStream(id2, kSmallWriteSize);
+    AddStream(id2);
 
-  EXPECT_EQ(id2, PopFront());
-  UpdateBytesForStream(id2, kSmallWriteSize);
+    EXPECT_EQ(id2, PopFront());
+    UpdateBytesForStream(id2, kSmallWriteSize);
+  }
 
-  // A non-incremental stream can continue writing as long as it has data.
   EXPECT_EQ(id1, PopFront());
   const size_t kLargeWriteSize = 1000 * 1000 * 1000;
   UpdateBytesForStream(id1, kLargeWriteSize);
@@ -402,11 +479,15 @@
   AddStream(id2);
   AddStream(id1);
 
-  // However, if the batching limit is exceeded on stream 2, this stream will
-  // yield to stream 1.
-  EXPECT_EQ(id2, PopFront());
-  UpdateBytesForStream(id2, kLargeWriteSize);
-  AddStream(id2);
+  // When batch writing is disabled, stream 2 immediately yields to stream 1,
+  // which is the non-incremental stream with most recent writes.
+  // When batch writing is enabled, stream 2 only yields to stream 1 after
+  // exceeding the batching limit.
+  if (!disable_batch_write_) {
+    EXPECT_EQ(id2, PopFront());
+    UpdateBytesForStream(id2, kLargeWriteSize);
+    AddStream(id2);
+  }
 
   EXPECT_EQ(id1, PopFront());
   UpdateBytesForStream(id1, kLargeWriteSize);