Implement QuicWriteBlockedList version that supports WebTransport priority scheme.
The resulting scheduler can handle both HTTP/3 priorities for HTTP streams, and WebTransport priorities for WebTransport data streams.
PiperOrigin-RevId: 633604441
diff --git a/build/source_list.bzl b/build/source_list.bzl
index 67a610a..b04eb4c 100644
--- a/build/source_list.bzl
+++ b/build/source_list.bzl
@@ -357,6 +357,7 @@
"quic/core/uber_received_packet_manager.h",
"quic/core/web_transport_interface.h",
"quic/core/web_transport_stats.h",
+ "quic/core/web_transport_write_blocked_list.h",
"quic/platform/api/quic_bug_tracker.h",
"quic/platform/api/quic_client_stats.h",
"quic/platform/api/quic_export.h",
@@ -674,6 +675,7 @@
"quic/core/uber_quic_stream_id_manager.cc",
"quic/core/uber_received_packet_manager.cc",
"quic/core/web_transport_stats.cc",
+ "quic/core/web_transport_write_blocked_list.cc",
"quic/platform/api/quic_socket_address.cc",
"spdy/core/array_output_buffer.cc",
"spdy/core/hpack/hpack_constants.cc",
@@ -1303,6 +1305,7 @@
"quic/core/tls_server_handshaker_test.cc",
"quic/core/uber_quic_stream_id_manager_test.cc",
"quic/core/uber_received_packet_manager_test.cc",
+ "quic/core/web_transport_write_blocked_list_test.cc",
"quic/platform/api/quic_socket_address_test.cc",
"quic/test_tools/crypto_test_utils_test.cc",
"quic/test_tools/quic_test_utils_test.cc",
diff --git a/build/source_list.gni b/build/source_list.gni
index afc2300..9af07f6 100644
--- a/build/source_list.gni
+++ b/build/source_list.gni
@@ -357,6 +357,7 @@
"src/quiche/quic/core/uber_received_packet_manager.h",
"src/quiche/quic/core/web_transport_interface.h",
"src/quiche/quic/core/web_transport_stats.h",
+ "src/quiche/quic/core/web_transport_write_blocked_list.h",
"src/quiche/quic/platform/api/quic_bug_tracker.h",
"src/quiche/quic/platform/api/quic_client_stats.h",
"src/quiche/quic/platform/api/quic_export.h",
@@ -674,6 +675,7 @@
"src/quiche/quic/core/uber_quic_stream_id_manager.cc",
"src/quiche/quic/core/uber_received_packet_manager.cc",
"src/quiche/quic/core/web_transport_stats.cc",
+ "src/quiche/quic/core/web_transport_write_blocked_list.cc",
"src/quiche/quic/platform/api/quic_socket_address.cc",
"src/quiche/spdy/core/array_output_buffer.cc",
"src/quiche/spdy/core/hpack/hpack_constants.cc",
@@ -1304,6 +1306,7 @@
"src/quiche/quic/core/tls_server_handshaker_test.cc",
"src/quiche/quic/core/uber_quic_stream_id_manager_test.cc",
"src/quiche/quic/core/uber_received_packet_manager_test.cc",
+ "src/quiche/quic/core/web_transport_write_blocked_list_test.cc",
"src/quiche/quic/platform/api/quic_socket_address_test.cc",
"src/quiche/quic/test_tools/crypto_test_utils_test.cc",
"src/quiche/quic/test_tools/quic_test_utils_test.cc",
diff --git a/build/source_list.json b/build/source_list.json
index b49530e..09f6d11 100644
--- a/build/source_list.json
+++ b/build/source_list.json
@@ -356,6 +356,7 @@
"quiche/quic/core/uber_received_packet_manager.h",
"quiche/quic/core/web_transport_interface.h",
"quiche/quic/core/web_transport_stats.h",
+ "quiche/quic/core/web_transport_write_blocked_list.h",
"quiche/quic/platform/api/quic_bug_tracker.h",
"quiche/quic/platform/api/quic_client_stats.h",
"quiche/quic/platform/api/quic_export.h",
@@ -673,6 +674,7 @@
"quiche/quic/core/uber_quic_stream_id_manager.cc",
"quiche/quic/core/uber_received_packet_manager.cc",
"quiche/quic/core/web_transport_stats.cc",
+ "quiche/quic/core/web_transport_write_blocked_list.cc",
"quiche/quic/platform/api/quic_socket_address.cc",
"quiche/spdy/core/array_output_buffer.cc",
"quiche/spdy/core/hpack/hpack_constants.cc",
@@ -1303,6 +1305,7 @@
"quiche/quic/core/tls_server_handshaker_test.cc",
"quiche/quic/core/uber_quic_stream_id_manager_test.cc",
"quiche/quic/core/uber_received_packet_manager_test.cc",
+ "quiche/quic/core/web_transport_write_blocked_list_test.cc",
"quiche/quic/platform/api/quic_socket_address_test.cc",
"quiche/quic/test_tools/crypto_test_utils_test.cc",
"quiche/quic/test_tools/quic_test_utils_test.cc",
diff --git a/quiche/common/btree_scheduler.h b/quiche/common/btree_scheduler.h
index 75312a3..753eda2 100644
--- a/quiche/common/btree_scheduler.h
+++ b/quiche/common/btree_scheduler.h
@@ -49,6 +49,8 @@
bool HasScheduled() const { return !schedule_.empty(); }
// Returns the number of currently scheduled streams.
size_t NumScheduled() const { return schedule_.size(); }
+ // Returns the total number of currently registered streams.
+ size_t NumRegistered() const { return streams_.size(); }
// Counts the number of scheduled entries in the range [min, max]. If either
// min or max is omitted, negative or positive infinity is assumed.
diff --git a/quiche/quic/core/quic_stream_priority.h b/quiche/quic/core/quic_stream_priority.h
index 8b1df3c..4065662 100644
--- a/quiche/quic/core/quic_stream_priority.h
+++ b/quiche/quic/core/quic_stream_priority.h
@@ -15,6 +15,7 @@
#include "quiche/quic/core/quic_types.h"
#include "quiche/common/platform/api/quiche_bug_tracker.h"
#include "quiche/common/platform/api/quiche_export.h"
+#include "quiche/web_transport/web_transport.h"
namespace quic {
@@ -42,27 +43,22 @@
}
};
-// Represents WebTransport priorities as defined by
+// Represents the priorities of WebTransport nested data streams as defined in
// <https://w3c.github.io/webtransport/>.
struct QUICHE_EXPORT WebTransportStreamPriority {
- enum class StreamType : uint8_t {
- // WebTransport data streams.
- kData = 0,
- // Regular HTTP traffic. Since we're currently only supporting dedicated
- // HTTP/3 transport, this means that all HTTP traffic is control traffic,
- // and thus should always go first.
- kHttp = 1,
- // Streams that the QUIC stack declares as static.
- kStatic = 2,
- };
-
- // Allows prioritizing control streams over the data streams.
- StreamType stream_type = StreamType::kData;
+ // The stream ID of the control stream for the WebTransport session to which
+ // this data stream belongs.
+ QuicStreamId session_id = 0;
+ // Number of the send group with which the stream is associated; see
+ // https://w3c.github.io/webtransport/#dom-webtransportsendstreamoptions-sendgroup
+ uint64_t send_group_number = 0;
// https://w3c.github.io/webtransport/#dom-webtransportsendstreamoptions-sendorder
- int64_t send_order = 0;
+ webtransport::SendOrder send_order = 0;
bool operator==(const WebTransportStreamPriority& other) const {
- return stream_type == other.stream_type && send_order == other.send_order;
+ return session_id == other.session_id &&
+ send_group_number == other.send_group_number &&
+ send_order == other.send_order;
}
bool operator!=(const WebTransportStreamPriority& other) const {
return !(*this == other);
diff --git a/quiche/quic/core/quic_stream_priority_test.cc b/quiche/quic/core/quic_stream_priority_test.cc
index 53f3312..e0e77b4 100644
--- a/quiche/quic/core/quic_stream_priority_test.cc
+++ b/quiche/quic/core/quic_stream_priority_test.cc
@@ -34,21 +34,18 @@
TEST(WebTransportStreamPriority, DefaultConstructed) {
WebTransportStreamPriority priority;
- EXPECT_EQ(priority.stream_type,
- WebTransportStreamPriority::StreamType::kData);
+ EXPECT_EQ(priority.session_id, 0);
+ EXPECT_EQ(priority.send_group_number, 0);
EXPECT_EQ(priority.send_order, 0);
}
TEST(WebTransportStreamPriority, Equals) {
EXPECT_EQ(WebTransportStreamPriority(),
- (WebTransportStreamPriority{
- WebTransportStreamPriority::StreamType::kData, 0}));
+ (WebTransportStreamPriority{0, 0, 0}));
EXPECT_NE(WebTransportStreamPriority(),
- (WebTransportStreamPriority{
- WebTransportStreamPriority::StreamType::kData, 1}));
+ (WebTransportStreamPriority{1, 2, 3}));
EXPECT_NE(WebTransportStreamPriority(),
- (WebTransportStreamPriority{
- WebTransportStreamPriority::StreamType::kHttp, 0}));
+ (WebTransportStreamPriority{0, 0, 1}));
}
TEST(QuicStreamPriority, Default) {
diff --git a/quiche/quic/core/quic_write_blocked_list.h b/quiche/quic/core/quic_write_blocked_list.h
index 3c3645f..1a72c82 100644
--- a/quiche/quic/core/quic_write_blocked_list.h
+++ b/quiche/quic/core/quic_write_blocked_list.h
@@ -13,6 +13,7 @@
#include "quiche/http2/core/priority_write_scheduler.h"
#include "quiche/quic/core/quic_packets.h"
#include "quiche/quic/core/quic_stream_priority.h"
+#include "quiche/quic/core/quic_types.h"
#include "quiche/quic/platform/api/quic_bug_tracker.h"
#include "quiche/quic/platform/api/quic_export.h"
#include "quiche/quic/platform/api/quic_flags.h"
diff --git a/quiche/quic/core/web_transport_write_blocked_list.cc b/quiche/quic/core/web_transport_write_blocked_list.cc
new file mode 100644
index 0000000..0e526d8
--- /dev/null
+++ b/quiche/quic/core/web_transport_write_blocked_list.cc
@@ -0,0 +1,297 @@
+// Copyright 2024 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "quiche/quic/core/web_transport_write_blocked_list.h"
+
+#include <cstddef>
+#include <optional>
+#include <string>
+
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/str_format.h"
+#include "quiche/quic/core/quic_stream_priority.h"
+#include "quiche/quic/core/quic_types.h"
+#include "quiche/common/platform/api/quiche_bug_tracker.h"
+#include "quiche/common/platform/api/quiche_logging.h"
+
+namespace quic {
+
+bool WebTransportWriteBlockedList::HasWriteBlockedDataStreams() const {
+ return main_schedule_.NumScheduledInPriorityRange(
+ std::nullopt, RemapUrgency(HttpStreamPriority::kMaximumUrgency,
+ /*is_http=*/true)) > 0;
+}
+
+size_t WebTransportWriteBlockedList::NumBlockedSpecialStreams() const {
+ return main_schedule_.NumScheduledInPriorityRange(
+ RemapUrgency(kStaticUrgency, /*is_http=*/false), std::nullopt);
+}
+
+size_t WebTransportWriteBlockedList::NumBlockedStreams() const {
+ size_t num_streams = main_schedule_.NumScheduled();
+ for (const auto& [key, scheduler] : web_transport_session_schedulers_) {
+ if (scheduler.HasScheduled()) {
+ num_streams += scheduler.NumScheduled();
+ // Account for the fact that the group itself has an entry in the main
+ // scheduler that does not correspond to any actual stream.
+ QUICHE_DCHECK(main_schedule_.IsScheduled(key));
+ --num_streams;
+ }
+ }
+ return num_streams;
+}
+
+void WebTransportWriteBlockedList::RegisterStream(
+ QuicStreamId stream_id, bool is_static_stream,
+ const QuicStreamPriority& raw_priority) {
+ QuicStreamPriority priority =
+ is_static_stream
+ ? QuicStreamPriority(HttpStreamPriority{kStaticUrgency, true})
+ : raw_priority;
+ auto [unused, success] = priorities_.emplace(stream_id, priority);
+ if (!success) {
+ QUICHE_BUG(WTWriteBlocked_RegisterStream_already_registered)
+ << "Tried to register stream " << stream_id
+ << " that is already registered";
+ return;
+ }
+
+ if (priority.type() == QuicPriorityType::kHttp) {
+ absl::Status status = main_schedule_.Register(
+ ScheduleKey::HttpStream(stream_id),
+ RemapUrgency(priority.http().urgency, /*is_http=*/true));
+ QUICHE_BUG_IF(WTWriteBlocked_RegisterStream_http_scheduler, !status.ok())
+ << status;
+ return;
+ }
+
+ QUICHE_DCHECK_EQ(priority.type(), QuicPriorityType::kWebTransport);
+ ScheduleKey group_key = ScheduleKey::WebTransportSession(priority);
+ auto [it, created_new] =
+ web_transport_session_schedulers_.try_emplace(group_key);
+ absl::Status status =
+ it->second.Register(stream_id, priority.web_transport().send_order);
+ QUICHE_BUG_IF(WTWriteBlocked_RegisterStream_data_scheduler, !status.ok())
+ << status;
+
+ // If the group is new, register it with the main scheduler.
+ if (created_new) {
+ // The IETF draft requires the priority of data streams associated with an
+ // individual session to be equivalent to the priority of the control
+ // stream.
+ auto session_priority_it =
+ priorities_.find(priority.web_transport().session_id);
+ // It is possible for a stream to be (re-)registered while the control
+ // stream is already gone.
+ QUICHE_DLOG_IF(WARNING, session_priority_it == priorities_.end())
+ << "Stream " << stream_id << " is associated with session ID "
+ << priority.web_transport().session_id
+ << ", but the session control stream is not registered; assuming "
+ "default urgency.";
+ QuicStreamPriority session_priority =
+ session_priority_it != priorities_.end()
+ ? session_priority_it->second
+ : QuicStreamPriority::Default(QuicPriorityType::kHttp);
+
+ status = main_schedule_.Register(
+ group_key,
+ RemapUrgency(session_priority.http().urgency, /*is_http=*/false));
+ QUICHE_BUG_IF(WTWriteBlocked_RegisterStream_main_scheduler, !status.ok())
+ << status;
+ }
+}
+
+void WebTransportWriteBlockedList::UnregisterStream(QuicStreamId stream_id) {
+ auto map_it = priorities_.find(stream_id);
+ if (map_it == priorities_.end()) {
+ QUICHE_BUG(WTWriteBlocked_UnregisterStream_not_found)
+ << "Stream " << stream_id << " not found";
+ return;
+ }
+ QuicStreamPriority priority = map_it->second;
+ priorities_.erase(map_it);
+
+ if (priority.type() != QuicPriorityType::kWebTransport) {
+ absl::Status status =
+ main_schedule_.Unregister(ScheduleKey::HttpStream(stream_id));
+ QUICHE_BUG_IF(WTWriteBlocked_UnregisterStream_http, !status.ok()) << status;
+ return;
+ }
+
+ ScheduleKey key = ScheduleKey::WebTransportSession(priority);
+ auto subscheduler_it = web_transport_session_schedulers_.find(key);
+ if (subscheduler_it == web_transport_session_schedulers_.end()) {
+ QUICHE_BUG(WTWriteBlocked_UnregisterStream_no_subscheduler)
+ << "Stream " << stream_id
+ << " is a WebTransport data stream, but has no scheduler for the "
+ "associated group";
+ return;
+ }
+ Subscheduler& subscheduler = subscheduler_it->second;
+ absl::Status status = subscheduler.Unregister(stream_id);
+ QUICHE_BUG_IF(WTWriteBlocked_UnregisterStream_subscheduler_stream_failed,
+ !status.ok())
+ << status;
+
+ // If this is the last stream associated with the group, remove the group.
+ if (!subscheduler.HasRegistered()) {
+ status = main_schedule_.Unregister(key);
+ QUICHE_BUG_IF(WTWriteBlocked_UnregisterStream_subscheduler_failed,
+ !status.ok())
+ << status;
+
+ web_transport_session_schedulers_.erase(subscheduler_it);
+ }
+}
+
+void WebTransportWriteBlockedList::UpdateStreamPriority(
+ QuicStreamId stream_id, const QuicStreamPriority& new_priority) {
+ UnregisterStream(stream_id);
+ RegisterStream(stream_id, /*is_static_stream=*/false, new_priority);
+
+ if (new_priority.type() == QuicPriorityType::kHttp) {
+ for (auto& [key, subscheduler] : web_transport_session_schedulers_) {
+ QUICHE_DCHECK(key.has_group());
+ if (key.stream() == stream_id) {
+ absl::Status status =
+ main_schedule_.UpdatePriority(key, new_priority.http().urgency);
+ QUICHE_BUG_IF(WTWriteBlocked_UpdateStreamPriority_subscheduler_failed,
+ !status.ok())
+ << status;
+ }
+ }
+ }
+}
+
+QuicStreamId WebTransportWriteBlockedList::PopFront() {
+ absl::StatusOr<ScheduleKey> main_key = main_schedule_.PopFront();
+ if (!main_key.ok()) {
+ QUICHE_BUG(WTWriteBlocked_PopFront_no_streams)
+ << "PopFront() called when no streams scheduled: " << main_key.status();
+ return 0;
+ }
+ if (!main_key->has_group()) {
+ return main_key->stream();
+ }
+
+ auto it = web_transport_session_schedulers_.find(*main_key);
+ if (it == web_transport_session_schedulers_.end()) {
+ QUICHE_BUG(WTWriteBlocked_PopFront_no_subscheduler)
+ << "Subscheduler for WebTransport group " << main_key->DebugString()
+ << " not found";
+ return 0;
+ }
+ Subscheduler& subscheduler = it->second;
+ absl::StatusOr<QuicStreamId> result = subscheduler.PopFront();
+ if (!result.ok()) {
+ QUICHE_BUG(WTWriteBlocked_PopFront_subscheduler_empty)
+ << "Subscheduler for group " << main_key->DebugString()
+ << " is empty while in the main schedule";
+ return 0;
+ }
+ if (subscheduler.HasScheduled()) {
+ absl::Status status = main_schedule_.Schedule(*main_key);
+ QUICHE_BUG_IF(WTWriteBlocked_PopFront_reschedule_group, !status.ok())
+ << status;
+ }
+ return *result;
+}
+
+void WebTransportWriteBlockedList::AddStream(QuicStreamId stream_id) {
+ QuicStreamPriority priority = GetPriorityOfStream(stream_id);
+ absl::Status status;
+ switch (priority.type()) {
+ case QuicPriorityType::kHttp:
+ status = main_schedule_.Schedule(ScheduleKey::HttpStream(stream_id));
+ QUICHE_BUG_IF(WTWriteBlocked_AddStream_http, !status.ok()) << status;
+ break;
+ case QuicPriorityType::kWebTransport:
+ status =
+ main_schedule_.Schedule(ScheduleKey::WebTransportSession(priority));
+ QUICHE_BUG_IF(WTWriteBlocked_AddStream_wt_main, !status.ok()) << status;
+
+ auto it = web_transport_session_schedulers_.find(
+ ScheduleKey::WebTransportSession(priority));
+ if (it == web_transport_session_schedulers_.end()) {
+ QUICHE_BUG(WTWriteBlocked_AddStream_no_subscheduler)
+ << ScheduleKey::WebTransportSession(priority);
+ return;
+ }
+ Subscheduler& subscheduler = it->second;
+ status = subscheduler.Schedule(stream_id);
+ QUICHE_BUG_IF(WTWriteBlocked_AddStream_wt_sub, !status.ok()) << status;
+ break;
+ }
+}
+
+bool WebTransportWriteBlockedList::IsStreamBlocked(
+ QuicStreamId stream_id) const {
+ QuicStreamPriority priority = GetPriorityOfStream(stream_id);
+ switch (priority.type()) {
+ case QuicPriorityType::kHttp:
+ return main_schedule_.IsScheduled(ScheduleKey::HttpStream(stream_id));
+ break;
+ case QuicPriorityType::kWebTransport:
+ auto it = web_transport_session_schedulers_.find(
+ ScheduleKey::WebTransportSession(priority));
+ if (it == web_transport_session_schedulers_.end()) {
+ QUICHE_BUG(WTWriteBlocked_IsStreamBlocked_no_subscheduler)
+ << ScheduleKey::WebTransportSession(priority);
+ return false;
+ }
+ const Subscheduler& subscheduler = it->second;
+ return subscheduler.IsScheduled(stream_id);
+ }
+}
+
+QuicStreamPriority WebTransportWriteBlockedList::GetPriorityOfStream(
+ QuicStreamId id) const {
+ auto it = priorities_.find(id);
+ if (it == priorities_.end()) {
+ QUICHE_BUG(WTWriteBlocked_GetPriorityOfStream_not_found)
+ << "Stream " << id << " not found";
+ return QuicStreamPriority::Default(QuicPriorityType::kHttp);
+ }
+ return it->second;
+}
+
+std::string WebTransportWriteBlockedList::ScheduleKey::DebugString() const {
+ return absl::StrFormat("(%d, %d)", stream_, group_);
+}
+
+bool WebTransportWriteBlockedList::ShouldYield(QuicStreamId id) const {
+ QuicStreamPriority priority = GetPriorityOfStream(id);
+ if (priority.type() == QuicPriorityType::kHttp) {
+ absl::StatusOr<bool> should_yield =
+ main_schedule_.ShouldYield(ScheduleKey::HttpStream(id));
+ QUICHE_BUG_IF(WTWriteBlocked_ShouldYield_http, !should_yield.ok())
+ << should_yield.status();
+ return *should_yield;
+ }
+ QUICHE_DCHECK_EQ(priority.type(), QuicPriorityType::kWebTransport);
+ absl::StatusOr<bool> should_yield =
+ main_schedule_.ShouldYield(ScheduleKey::WebTransportSession(priority));
+ QUICHE_BUG_IF(WTWriteBlocked_ShouldYield_wt_main, !should_yield.ok())
+ << should_yield.status();
+ if (*should_yield) {
+ return true;
+ }
+
+ auto it = web_transport_session_schedulers_.find(
+ ScheduleKey::WebTransportSession(priority));
+ if (it == web_transport_session_schedulers_.end()) {
+ QUICHE_BUG(WTWriteBlocked_ShouldYield_subscheduler_not_found)
+ << "Subscheduler not found for "
+ << ScheduleKey::WebTransportSession(priority);
+ return false;
+ }
+ const Subscheduler& subscheduler = it->second;
+
+ should_yield = subscheduler.ShouldYield(id);
+ QUICHE_BUG_IF(WTWriteBlocked_ShouldYield_wt_subscheduler, !should_yield.ok())
+ << should_yield.status();
+ return *should_yield;
+}
+} // namespace quic
diff --git a/quiche/quic/core/web_transport_write_blocked_list.h b/quiche/quic/core/web_transport_write_blocked_list.h
new file mode 100644
index 0000000..8cf294b
--- /dev/null
+++ b/quiche/quic/core/web_transport_write_blocked_list.h
@@ -0,0 +1,156 @@
+// Copyright 2024 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef QUICHE_QUIC_CORE_WEB_TRANSPORT_WRITE_BLOCKED_LIST_H_
+#define QUICHE_QUIC_CORE_WEB_TRANSPORT_WRITE_BLOCKED_LIST_H_
+
+#include <cstddef>
+#include <limits>
+#include <ostream>
+#include <string>
+
+#include "absl/container/flat_hash_map.h"
+#include "quiche/quic/core/quic_stream_priority.h"
+#include "quiche/quic/core/quic_types.h"
+#include "quiche/quic/core/quic_write_blocked_list.h"
+#include "quiche/common/btree_scheduler.h"
+#include "quiche/common/platform/api/quiche_export.h"
+#include "quiche/web_transport/web_transport.h"
+
+namespace quic {
+
+// Scheduler that is capable of handling both regular HTTP/3 priorities and
+// WebTransport priorities for multiple sessions at the same time.
+//
+// Here is a brief overview of the scheme:
+// - At the top, there are HTTP/3 streams that are ordered by urgency as
+// defined in RFC 9218.
+// - The HTTP/3 connection can be a host to multiple WebTransport sessions.
+// Those are identified by the ID of the HTTP/3 control stream that created
+// the session; they also inherit the priority from that stream.
+// - The sessions consist of send groups that all have equal priority.
+// - The send groups have individual WebTransport data streams; each data
+// stream has a send order, which is a strict priority expressed as int64.
+//
+// To simplify the implementation of an already excessively complex scheme, this
+// class makes a couple of affordances:
+// - Instead of first scheduling an individual session, then scheduling a
+// group within it, it schedules session-group pairs at the top level. This
+// is technically allowed by the spec, but it does mean that sessions with
+// more groups may get more bandwidth.
+// - Incremental priorities are not currently supported.
+class QUICHE_EXPORT WebTransportWriteBlockedList
+ : public QuicWriteBlockedListInterface {
+ public:
+ // Handle static streams by treating them as streams of priority MAX + 1.
+ static constexpr int kStaticUrgency = HttpStreamPriority::kMaximumUrgency + 1;
+
+ // QuicWriteBlockedListInterface implementation.
+ bool HasWriteBlockedDataStreams() const override;
+ size_t NumBlockedSpecialStreams() const override;
+ size_t NumBlockedStreams() const override;
+
+ void RegisterStream(QuicStreamId stream_id, bool is_static_stream,
+ const QuicStreamPriority& raw_priority) override;
+ void UnregisterStream(QuicStreamId stream_id) override;
+ void UpdateStreamPriority(QuicStreamId stream_id,
+ const QuicStreamPriority& new_priority) override;
+
+ bool ShouldYield(QuicStreamId id) const override;
+ QuicStreamPriority GetPriorityOfStream(QuicStreamId id) const override;
+ QuicStreamId PopFront() override;
+ void UpdateBytesForStream(QuicStreamId /*stream_id*/,
+ size_t /*bytes*/) override {}
+ void AddStream(QuicStreamId stream_id) override;
+ bool IsStreamBlocked(QuicStreamId stream_id) const override;
+
+ size_t NumRegisteredGroups() const {
+ return web_transport_session_schedulers_.size();
+ }
+ size_t NumRegisteredHttpStreams() const {
+ return main_schedule_.NumRegistered() - NumRegisteredGroups();
+ }
+
+ private:
+ // ScheduleKey represents anything that can be put into the main scheduler,
+ // which is either:
+ // - an HTTP/3 stream, or
+ // - an individual WebTransport session-send group pair.
+ class QUICHE_EXPORT ScheduleKey {
+ public:
+ static ScheduleKey HttpStream(QuicStreamId id) {
+ return ScheduleKey(id, kNoSendGroup);
+ }
+ static ScheduleKey WebTransportSession(QuicStreamId session_id,
+ webtransport::SendGroupId group_id) {
+ return ScheduleKey(session_id, group_id);
+ }
+ static ScheduleKey WebTransportSession(const QuicStreamPriority& priority) {
+ return ScheduleKey(priority.web_transport().session_id,
+ priority.web_transport().send_group_number);
+ }
+
+ bool operator==(const ScheduleKey& other) const {
+ return stream_ == other.stream_ && group_ == other.group_;
+ }
+ bool operator!=(const ScheduleKey& other) const {
+ return !(*this == other);
+ }
+
+ template <typename H>
+ friend H AbslHashValue(H h, const ScheduleKey& key) {
+ return H::combine(std::move(h), key.stream_, key.group_);
+ }
+
+ bool has_group() const { return group_ != kNoSendGroup; }
+ quic::QuicStreamId stream() const { return stream_; }
+
+ std::string DebugString() const;
+
+ friend inline std::ostream& operator<<(std::ostream& os,
+ const ScheduleKey& key) {
+ os << key.DebugString();
+ return os;
+ }
+
+ private:
+ static constexpr webtransport::SendGroupId kNoSendGroup =
+ std::numeric_limits<webtransport::SendGroupId>::max();
+
+ explicit ScheduleKey(quic::QuicStreamId stream,
+ webtransport::SendGroupId group)
+ : stream_(stream), group_(group) {}
+
+ quic::QuicStreamId stream_;
+ webtransport::SendGroupId group_;
+ };
+
+ // WebTransport requires individual sessions to have the same urgency as their
+ // control streams; in a naive implementation, that would mean that both would
+ // get the same urgency N, but we also want for the control streams to have
+ // higher priority than WebTransport user data. In order to achieve that, we
+ // enter control streams at urgency 2 * N + 1, and data streams at urgency
+ // 2 * N.
+ static constexpr int RemapUrgency(int urgency, bool is_http) {
+ return urgency * 2 + (is_http ? 1 : 0);
+ }
+
+ // Scheduler for individual WebTransport send groups.
+ using Subscheduler =
+ quiche::BTreeScheduler<QuicStreamId, webtransport::SendOrder>;
+
+ // Top-level scheduler used to multiplex WebTransport sessions and individual
+ // HTTP/3 streams.
+ quiche::BTreeScheduler<ScheduleKey, int> main_schedule_;
+ // Records of priority for every stream; used when looking up WebTransport
+ // session associated with an individual stream.
+ absl::flat_hash_map<QuicStreamId, QuicStreamPriority> priorities_;
+ // Schedulers for individual WebTransport send groups.
+ absl::flat_hash_map<ScheduleKey, Subscheduler>
+ web_transport_session_schedulers_;
+};
+
+} // namespace quic
+
+#endif // QUICHE_QUIC_CORE_WEB_TRANSPORT_WRITE_BLOCKED_LIST_H_
diff --git a/quiche/quic/core/web_transport_write_blocked_list_test.cc b/quiche/quic/core/web_transport_write_blocked_list_test.cc
new file mode 100644
index 0000000..f65450f
--- /dev/null
+++ b/quiche/quic/core/web_transport_write_blocked_list_test.cc
@@ -0,0 +1,515 @@
+// Copyright 2024 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "quiche/quic/core/web_transport_write_blocked_list.h"
+
+#include <algorithm>
+#include <array>
+#include <cstddef>
+#include <iterator>
+#include <vector>
+
+#include "absl/algorithm/container.h"
+#include "quiche/quic/core/quic_stream_priority.h"
+#include "quiche/quic/core/quic_types.h"
+#include "quiche/quic/test_tools/quic_test_utils.h"
+#include "quiche/common/platform/api/quiche_expect_bug.h"
+#include "quiche/common/platform/api/quiche_test.h"
+
+namespace quic::test {
+namespace {
+
+using ::testing::ElementsAre;
+using ::testing::ElementsAreArray;
+
+class WebTransportWriteBlockedListTest : public ::quiche::test::QuicheTest {
+ protected:
+ void RegisterStaticStream(QuicStreamId id) {
+ list_.RegisterStream(id, /*is_static_stream=*/true,
+ QuicStreamPriority::Default(QuicPriorityType::kHttp));
+ }
+ void RegisterHttpStream(QuicStreamId id,
+ int urgency = HttpStreamPriority::kDefaultUrgency) {
+ HttpStreamPriority priority;
+ priority.urgency = urgency;
+ list_.RegisterStream(id, /*is_static_stream=*/false,
+ QuicStreamPriority(priority));
+ }
+ void RegisterWebTransportDataStream(QuicStreamId id,
+ WebTransportStreamPriority priority) {
+ list_.RegisterStream(id, /*is_static_stream=*/false,
+ QuicStreamPriority(priority));
+ }
+
+ std::vector<QuicStreamId> PopAll() {
+ std::vector<QuicStreamId> result;
+ size_t expected_count = list_.NumBlockedStreams();
+ while (list_.NumBlockedStreams() > 0) {
+ EXPECT_TRUE(list_.HasWriteBlockedDataStreams() ||
+ list_.HasWriteBlockedSpecialStream());
+ result.push_back(list_.PopFront());
+ EXPECT_EQ(list_.NumBlockedStreams(), --expected_count);
+ }
+ return result;
+ }
+
+ WebTransportWriteBlockedList list_;
+};
+
+TEST_F(WebTransportWriteBlockedListTest, BasicHttpStreams) {
+ RegisterHttpStream(1);
+ RegisterHttpStream(2);
+ RegisterHttpStream(3, HttpStreamPriority::kDefaultUrgency + 1);
+ RegisterStaticStream(4);
+
+ EXPECT_EQ(list_.GetPriorityOfStream(1),
+ QuicStreamPriority::Default(QuicPriorityType::kHttp));
+ EXPECT_EQ(list_.GetPriorityOfStream(2),
+ QuicStreamPriority::Default(QuicPriorityType::kHttp));
+ EXPECT_EQ(list_.GetPriorityOfStream(3).http().urgency, 4);
+
+ EXPECT_EQ(list_.NumBlockedStreams(), 0);
+ EXPECT_EQ(list_.NumBlockedSpecialStreams(), 0);
+ list_.AddStream(1);
+ list_.AddStream(2);
+ list_.AddStream(3);
+ list_.AddStream(4);
+ EXPECT_EQ(list_.NumBlockedStreams(), 4);
+ EXPECT_EQ(list_.NumBlockedSpecialStreams(), 1);
+
+ EXPECT_THAT(PopAll(), ElementsAre(4, 3, 1, 2));
+ EXPECT_EQ(list_.NumBlockedStreams(), 0);
+ EXPECT_EQ(list_.NumBlockedSpecialStreams(), 0);
+
+ list_.AddStream(2);
+ list_.AddStream(3);
+ list_.AddStream(4);
+ list_.AddStream(1);
+ EXPECT_THAT(PopAll(), ElementsAre(4, 3, 2, 1));
+}
+
+TEST_F(WebTransportWriteBlockedListTest, RegisterDuplicateStream) {
+ RegisterHttpStream(1);
+ EXPECT_QUICHE_BUG(RegisterHttpStream(1), "already registered");
+}
+
+TEST_F(WebTransportWriteBlockedListTest, UnregisterMissingStream) {
+ EXPECT_QUICHE_BUG(list_.UnregisterStream(1), "not found");
+}
+
+TEST_F(WebTransportWriteBlockedListTest, GetPriorityMissingStream) {
+ EXPECT_QUICHE_BUG(list_.GetPriorityOfStream(1), "not found");
+}
+
+TEST_F(WebTransportWriteBlockedListTest, PopFrontMissing) {
+ RegisterHttpStream(1);
+ list_.AddStream(1);
+ EXPECT_EQ(list_.PopFront(), 1);
+ EXPECT_QUICHE_BUG(list_.PopFront(), "no streams scheduled");
+}
+
+TEST_F(WebTransportWriteBlockedListTest, HasWriteBlockedDataStreams) {
+ RegisterStaticStream(1);
+ RegisterHttpStream(2);
+
+ EXPECT_FALSE(list_.HasWriteBlockedDataStreams());
+ list_.AddStream(1);
+ EXPECT_FALSE(list_.HasWriteBlockedDataStreams());
+ list_.AddStream(2);
+ EXPECT_TRUE(list_.HasWriteBlockedDataStreams());
+ EXPECT_EQ(list_.PopFront(), 1);
+ EXPECT_TRUE(list_.HasWriteBlockedDataStreams());
+ EXPECT_EQ(list_.PopFront(), 2);
+ EXPECT_FALSE(list_.HasWriteBlockedDataStreams());
+}
+
+TEST_F(WebTransportWriteBlockedListTest, NestedStreams) {
+ RegisterHttpStream(1);
+ RegisterHttpStream(2);
+ RegisterWebTransportDataStream(3, WebTransportStreamPriority{1, 0, 0});
+ RegisterWebTransportDataStream(4, WebTransportStreamPriority{1, 0, 0});
+ RegisterWebTransportDataStream(5, WebTransportStreamPriority{2, 0, 0});
+ RegisterWebTransportDataStream(6, WebTransportStreamPriority{2, 0, 0});
+ EXPECT_EQ(list_.NumBlockedStreams(), 0);
+
+ list_.AddStream(3);
+ list_.AddStream(5);
+ list_.AddStream(4);
+ list_.AddStream(6);
+ EXPECT_EQ(list_.NumBlockedStreams(), 4);
+ EXPECT_THAT(PopAll(), ElementsAre(3, 5, 4, 6));
+ EXPECT_EQ(list_.NumBlockedStreams(), 0);
+
+ list_.AddStream(3);
+ list_.AddStream(4);
+ list_.AddStream(5);
+ EXPECT_EQ(list_.NumBlockedStreams(), 3);
+ EXPECT_THAT(PopAll(), ElementsAre(3, 5, 4));
+ EXPECT_EQ(list_.NumBlockedStreams(), 0);
+
+ list_.AddStream(4);
+ list_.AddStream(5);
+ list_.AddStream(6);
+ EXPECT_EQ(list_.NumBlockedStreams(), 3);
+ EXPECT_THAT(PopAll(), ElementsAre(4, 5, 6));
+ EXPECT_EQ(list_.NumBlockedStreams(), 0);
+
+ list_.AddStream(6);
+ list_.AddStream(3);
+ list_.AddStream(4);
+ list_.AddStream(5);
+ EXPECT_EQ(list_.NumBlockedStreams(), 4);
+ EXPECT_THAT(PopAll(), ElementsAre(6, 3, 5, 4));
+ EXPECT_EQ(list_.NumBlockedStreams(), 0);
+
+ list_.AddStream(6);
+ list_.AddStream(5);
+ list_.AddStream(4);
+ list_.AddStream(3);
+ EXPECT_EQ(list_.NumBlockedStreams(), 4);
+ EXPECT_THAT(PopAll(), ElementsAre(6, 4, 5, 3));
+ EXPECT_EQ(list_.NumBlockedStreams(), 0);
+}
+
+TEST_F(WebTransportWriteBlockedListTest, NestedStreamsWithHigherPriorityGroup) {
+ RegisterHttpStream(1, HttpStreamPriority::kDefaultUrgency + 1);
+ RegisterHttpStream(2);
+ RegisterWebTransportDataStream(3, WebTransportStreamPriority{1, 0, 0});
+ RegisterWebTransportDataStream(4, WebTransportStreamPriority{1, 0, 0});
+ RegisterWebTransportDataStream(5, WebTransportStreamPriority{2, 0, 0});
+ RegisterWebTransportDataStream(6, WebTransportStreamPriority{2, 0, 0});
+ EXPECT_EQ(list_.NumBlockedStreams(), 0);
+
+ list_.AddStream(3);
+ list_.AddStream(5);
+ list_.AddStream(4);
+ list_.AddStream(6);
+ EXPECT_EQ(list_.NumBlockedStreams(), 4);
+ EXPECT_THAT(PopAll(), ElementsAre(3, 4, 5, 6));
+ EXPECT_EQ(list_.NumBlockedStreams(), 0);
+
+ list_.AddStream(3);
+ list_.AddStream(4);
+ list_.AddStream(5);
+ EXPECT_EQ(list_.NumBlockedStreams(), 3);
+ EXPECT_THAT(PopAll(), ElementsAre(3, 4, 5));
+ EXPECT_EQ(list_.NumBlockedStreams(), 0);
+
+ list_.AddStream(4);
+ list_.AddStream(5);
+ list_.AddStream(6);
+ EXPECT_EQ(list_.NumBlockedStreams(), 3);
+ EXPECT_THAT(PopAll(), ElementsAre(4, 5, 6));
+ EXPECT_EQ(list_.NumBlockedStreams(), 0);
+
+ list_.AddStream(6);
+ list_.AddStream(3);
+ list_.AddStream(4);
+ list_.AddStream(5);
+ EXPECT_EQ(list_.NumBlockedStreams(), 4);
+ EXPECT_THAT(PopAll(), ElementsAre(3, 4, 6, 5));
+ EXPECT_EQ(list_.NumBlockedStreams(), 0);
+
+ list_.AddStream(6);
+ list_.AddStream(5);
+ list_.AddStream(4);
+ list_.AddStream(3);
+ EXPECT_EQ(list_.NumBlockedStreams(), 4);
+ EXPECT_THAT(PopAll(), ElementsAre(4, 3, 6, 5));
+ EXPECT_EQ(list_.NumBlockedStreams(), 0);
+}
+
+TEST_F(WebTransportWriteBlockedListTest, NestedStreamVsControlStream) {
+ RegisterHttpStream(1);
+ RegisterWebTransportDataStream(2, WebTransportStreamPriority{1, 0, 0});
+
+ list_.AddStream(2);
+ list_.AddStream(1);
+ EXPECT_THAT(PopAll(), ElementsAre(1, 2));
+
+ list_.AddStream(1);
+ list_.AddStream(2);
+ EXPECT_THAT(PopAll(), ElementsAre(1, 2));
+}
+
+TEST_F(WebTransportWriteBlockedListTest, NestedStreamsSendOrder) {
+ RegisterHttpStream(1);
+ RegisterWebTransportDataStream(2, WebTransportStreamPriority{1, 0, 0});
+ RegisterWebTransportDataStream(3, WebTransportStreamPriority{1, 0, 100});
+ RegisterWebTransportDataStream(4, WebTransportStreamPriority{1, 0, -100});
+
+ list_.AddStream(4);
+ list_.AddStream(3);
+ list_.AddStream(2);
+ list_.AddStream(1);
+ EXPECT_THAT(PopAll(), ElementsAre(1, 3, 2, 4));
+}
+
+TEST_F(WebTransportWriteBlockedListTest, NestedStreamsDifferentGroups) {
+ RegisterHttpStream(1);
+ RegisterWebTransportDataStream(2, WebTransportStreamPriority{1, 0, 0});
+ RegisterWebTransportDataStream(3, WebTransportStreamPriority{1, 1, 100});
+ RegisterWebTransportDataStream(4, WebTransportStreamPriority{1, 7, -100});
+
+ list_.AddStream(4);
+ list_.AddStream(3);
+ list_.AddStream(2);
+ list_.AddStream(1);
+ EXPECT_THAT(PopAll(), ElementsAre(1, 4, 3, 2));
+
+ list_.AddStream(1);
+ list_.AddStream(2);
+ list_.AddStream(3);
+ list_.AddStream(4);
+ EXPECT_THAT(PopAll(), ElementsAre(1, 2, 3, 4));
+}
+
+TEST_F(WebTransportWriteBlockedListTest, NestedStreamsDifferentSession) {
+ RegisterWebTransportDataStream(1, WebTransportStreamPriority{10, 0, 0});
+ RegisterWebTransportDataStream(2, WebTransportStreamPriority{11, 0, 100});
+ RegisterWebTransportDataStream(3, WebTransportStreamPriority{12, 0, -100});
+
+ list_.AddStream(3);
+ list_.AddStream(2);
+ list_.AddStream(1);
+ EXPECT_THAT(PopAll(), ElementsAre(3, 2, 1));
+
+ list_.AddStream(1);
+ list_.AddStream(2);
+ list_.AddStream(3);
+ EXPECT_THAT(PopAll(), ElementsAre(1, 2, 3));
+}
+
+TEST_F(WebTransportWriteBlockedListTest, UnregisterScheduledStreams) {
+ RegisterHttpStream(1);
+ RegisterHttpStream(2);
+ RegisterWebTransportDataStream(3, WebTransportStreamPriority{1, 0, 0});
+ RegisterWebTransportDataStream(4, WebTransportStreamPriority{1, 0, 0});
+ RegisterWebTransportDataStream(5, WebTransportStreamPriority{2, 0, 0});
+ RegisterWebTransportDataStream(6, WebTransportStreamPriority{2, 0, 0});
+
+ EXPECT_EQ(list_.NumBlockedStreams(), 0);
+ for (QuicStreamId id : {1, 2, 3, 4, 5, 6}) {
+ list_.AddStream(id);
+ }
+ EXPECT_EQ(list_.NumBlockedStreams(), 6);
+
+ list_.UnregisterStream(1);
+ EXPECT_EQ(list_.NumBlockedStreams(), 5);
+ list_.UnregisterStream(3);
+ EXPECT_EQ(list_.NumBlockedStreams(), 4);
+ list_.UnregisterStream(4);
+ EXPECT_EQ(list_.NumBlockedStreams(), 3);
+ list_.UnregisterStream(5);
+ EXPECT_EQ(list_.NumBlockedStreams(), 2);
+ list_.UnregisterStream(6);
+ EXPECT_EQ(list_.NumBlockedStreams(), 1);
+ list_.UnregisterStream(2);
+ EXPECT_EQ(list_.NumBlockedStreams(), 0);
+}
+
+TEST_F(WebTransportWriteBlockedListTest, UnregisterUnscheduledStreams) {
+ RegisterHttpStream(1);
+ RegisterHttpStream(2);
+ RegisterWebTransportDataStream(3, WebTransportStreamPriority{1, 0, 0});
+ RegisterWebTransportDataStream(4, WebTransportStreamPriority{1, 0, 0});
+ RegisterWebTransportDataStream(5, WebTransportStreamPriority{2, 0, 0});
+ RegisterWebTransportDataStream(6, WebTransportStreamPriority{2, 0, 0});
+
+ EXPECT_EQ(list_.NumRegisteredHttpStreams(), 2);
+ EXPECT_EQ(list_.NumRegisteredGroups(), 2);
+ list_.UnregisterStream(1);
+ EXPECT_EQ(list_.NumRegisteredHttpStreams(), 1);
+ EXPECT_EQ(list_.NumRegisteredGroups(), 2);
+ list_.UnregisterStream(3);
+ EXPECT_EQ(list_.NumRegisteredHttpStreams(), 1);
+ EXPECT_EQ(list_.NumRegisteredGroups(), 2);
+ list_.UnregisterStream(4);
+ EXPECT_EQ(list_.NumRegisteredHttpStreams(), 1);
+ EXPECT_EQ(list_.NumRegisteredGroups(), 1);
+
+ list_.UnregisterStream(5);
+ EXPECT_EQ(list_.NumRegisteredHttpStreams(), 1);
+ EXPECT_EQ(list_.NumRegisteredGroups(), 1);
+ list_.UnregisterStream(6);
+ EXPECT_EQ(list_.NumRegisteredHttpStreams(), 1);
+ EXPECT_EQ(list_.NumRegisteredGroups(), 0);
+ list_.UnregisterStream(2);
+ EXPECT_EQ(list_.NumRegisteredHttpStreams(), 0);
+ EXPECT_EQ(list_.NumRegisteredGroups(), 0);
+
+ RegisterHttpStream(1);
+ RegisterHttpStream(2);
+ RegisterWebTransportDataStream(3, WebTransportStreamPriority{1, 0, 0});
+ RegisterWebTransportDataStream(4, WebTransportStreamPriority{1, 0, 0});
+ RegisterWebTransportDataStream(5, WebTransportStreamPriority{2, 0, 0});
+ RegisterWebTransportDataStream(6, WebTransportStreamPriority{2, 0, 0});
+}
+
+TEST_F(WebTransportWriteBlockedListTest, IsStreamBlocked) {
+ RegisterHttpStream(1);
+ RegisterWebTransportDataStream(2, WebTransportStreamPriority{1, 0, 0});
+ RegisterWebTransportDataStream(3, WebTransportStreamPriority{9, 0, 0});
+
+ EXPECT_FALSE(list_.IsStreamBlocked(1));
+ EXPECT_FALSE(list_.IsStreamBlocked(2));
+ EXPECT_FALSE(list_.IsStreamBlocked(3));
+
+ list_.AddStream(3);
+ EXPECT_FALSE(list_.IsStreamBlocked(1));
+ EXPECT_FALSE(list_.IsStreamBlocked(2));
+ EXPECT_TRUE(list_.IsStreamBlocked(3));
+
+ list_.AddStream(1);
+ EXPECT_TRUE(list_.IsStreamBlocked(1));
+ EXPECT_FALSE(list_.IsStreamBlocked(2));
+ EXPECT_TRUE(list_.IsStreamBlocked(3));
+
+ ASSERT_EQ(list_.PopFront(), 1);
+ EXPECT_FALSE(list_.IsStreamBlocked(1));
+ EXPECT_FALSE(list_.IsStreamBlocked(2));
+ EXPECT_TRUE(list_.IsStreamBlocked(3));
+}
+
+TEST_F(WebTransportWriteBlockedListTest, UpdatePriorityHttp) {
+ RegisterHttpStream(1);
+ RegisterHttpStream(2);
+ RegisterHttpStream(3);
+
+ list_.AddStream(1);
+ list_.AddStream(2);
+ list_.AddStream(3);
+ EXPECT_THAT(PopAll(), ElementsAre(1, 2, 3));
+
+ list_.UpdateStreamPriority(
+ 2, QuicStreamPriority(
+ HttpStreamPriority{HttpStreamPriority::kMaximumUrgency, false}));
+
+ list_.AddStream(1);
+ list_.AddStream(2);
+ list_.AddStream(3);
+ EXPECT_THAT(PopAll(), ElementsAre(2, 1, 3));
+}
+
+TEST_F(WebTransportWriteBlockedListTest, UpdatePriorityWebTransport) {
+ RegisterWebTransportDataStream(1, WebTransportStreamPriority{0, 0, 0});
+ RegisterWebTransportDataStream(2, WebTransportStreamPriority{0, 0, 0});
+ RegisterWebTransportDataStream(3, WebTransportStreamPriority{0, 0, 0});
+
+ list_.AddStream(1);
+ list_.AddStream(2);
+ list_.AddStream(3);
+ EXPECT_THAT(PopAll(), ElementsAre(1, 2, 3));
+
+ list_.UpdateStreamPriority(
+ 2, QuicStreamPriority(WebTransportStreamPriority{0, 0, 1}));
+
+ list_.AddStream(1);
+ list_.AddStream(2);
+ list_.AddStream(3);
+ EXPECT_THAT(PopAll(), ElementsAre(2, 1, 3));
+}
+
+TEST_F(WebTransportWriteBlockedListTest, UpdatePriorityControlStream) {
+ RegisterHttpStream(1);
+ RegisterHttpStream(2);
+ RegisterWebTransportDataStream(3, WebTransportStreamPriority{1, 0, 0});
+ RegisterWebTransportDataStream(4, WebTransportStreamPriority{2, 0, 0});
+
+ list_.AddStream(3);
+ list_.AddStream(4);
+ EXPECT_THAT(PopAll(), ElementsAre(3, 4));
+ list_.AddStream(4);
+ list_.AddStream(3);
+ EXPECT_THAT(PopAll(), ElementsAre(4, 3));
+
+ list_.UpdateStreamPriority(
+ 2, QuicStreamPriority(
+ HttpStreamPriority{HttpStreamPriority::kMaximumUrgency, false}));
+
+ list_.AddStream(3);
+ list_.AddStream(4);
+ EXPECT_THAT(PopAll(), ElementsAre(4, 3));
+ list_.AddStream(4);
+ list_.AddStream(3);
+ EXPECT_THAT(PopAll(), ElementsAre(4, 3));
+}
+
+TEST_F(WebTransportWriteBlockedListTest, ShouldYield) {
+ RegisterHttpStream(1);
+ RegisterWebTransportDataStream(2, WebTransportStreamPriority{1, 0, 0});
+ RegisterWebTransportDataStream(3, WebTransportStreamPriority{1, 0, 0});
+ RegisterWebTransportDataStream(4, WebTransportStreamPriority{1, 0, 10});
+
+ EXPECT_FALSE(list_.ShouldYield(1));
+ EXPECT_FALSE(list_.ShouldYield(2));
+ EXPECT_FALSE(list_.ShouldYield(3));
+ EXPECT_FALSE(list_.ShouldYield(4));
+
+ list_.AddStream(1);
+ EXPECT_FALSE(list_.ShouldYield(1));
+ EXPECT_TRUE(list_.ShouldYield(2));
+ EXPECT_TRUE(list_.ShouldYield(3));
+ EXPECT_TRUE(list_.ShouldYield(4));
+ PopAll();
+
+ list_.AddStream(2);
+ EXPECT_FALSE(list_.ShouldYield(1));
+ EXPECT_FALSE(list_.ShouldYield(2));
+ EXPECT_TRUE(list_.ShouldYield(3));
+ EXPECT_FALSE(list_.ShouldYield(4));
+ PopAll();
+
+ list_.AddStream(4);
+ EXPECT_FALSE(list_.ShouldYield(1));
+ EXPECT_TRUE(list_.ShouldYield(2));
+ EXPECT_TRUE(list_.ShouldYield(3));
+ EXPECT_FALSE(list_.ShouldYield(4));
+ PopAll();
+}
+
+TEST_F(WebTransportWriteBlockedListTest, RandomizedTest) {
+ RegisterHttpStream(1);
+ RegisterHttpStream(2, HttpStreamPriority::kMinimumUrgency);
+ RegisterHttpStream(3, HttpStreamPriority::kMaximumUrgency);
+ RegisterWebTransportDataStream(4, WebTransportStreamPriority{1, 0, 0});
+ RegisterWebTransportDataStream(5, WebTransportStreamPriority{2, 0, +1});
+ RegisterWebTransportDataStream(6, WebTransportStreamPriority{2, 0, -1});
+ RegisterWebTransportDataStream(7, WebTransportStreamPriority{3, 8, 0});
+ RegisterWebTransportDataStream(8, WebTransportStreamPriority{3, 8, 100});
+ RegisterWebTransportDataStream(9, WebTransportStreamPriority{3, 8, 20000});
+ RegisterHttpStream(10, HttpStreamPriority::kDefaultUrgency + 1);
+ // The priorities of the streams above are arranged so that the priorities of
+ // all streams above are strictly ordered (i.e. there are no streams that
+ // would be round-robined).
+ constexpr std::array<QuicStreamId, 10> order = {3, 9, 8, 7, 10,
+ 1, 4, 2, 5, 6};
+
+ SimpleRandom random;
+ for (int i = 0; i < 1000; ++i) {
+ // Shuffle the streams.
+ std::vector<QuicStreamId> pushed_streams(order.begin(), order.end());
+ for (int j = pushed_streams.size() - 1; j > 0; --j) {
+ std::swap(pushed_streams[j],
+ pushed_streams[random.RandUint64() % (j + 1)]);
+ }
+
+ size_t stream_count = 1 + random.RandUint64() % order.size();
+ pushed_streams.resize(stream_count);
+
+ for (QuicStreamId id : pushed_streams) {
+ list_.AddStream(id);
+ }
+
+ std::vector<QuicStreamId> expected_streams;
+ absl::c_copy_if(
+ order, std::back_inserter(expected_streams), [&](QuicStreamId id) {
+ return absl::c_find(pushed_streams, id) != pushed_streams.end();
+ });
+ ASSERT_THAT(PopAll(), ElementsAreArray(expected_streams));
+ }
+}
+
+} // namespace
+} // namespace quic::test