noq_proto/connection/transmit_buf.rs
1use std::num::NonZeroUsize;
2
3use bytes::BufMut;
4use tracing::trace;
5
6use crate::packet::BufLen;
7
8/// The buffer in which to write datagrams for [`Connection::poll_transmit`]
9///
10/// The `poll_transmit` function writes zero or more datagrams to a buffer. Multiple
11/// datagrams are possible in case GSO (Generic Segmentation Offload) is supported.
12///
13/// This buffer tracks datagrams being written to it. There is always a "current" datagram,
14/// which is started by calling [`TransmitBuf::start_new_datagram`]. Writing to the buffer
15/// is done through the [`BufMut`] interface.
16///
17/// Usually a datagram contains one QUIC packet, though QUIC-TRANSPORT 12.2 Coalescing
18/// Packets allows for placing multiple packets into a single datagram provided all but the
19/// last packet uses long headers. This is normally used during connection setup where often
20/// the initial, handshake and sometimes even a 1-RTT packet can be coalesced into a single
21/// datagram.
22///
23/// Inside a single packet multiple QUIC frames are written.
24///
25/// The buffer managed here is passed straight to the OS' `sendmsg` call (or variant) once
26/// `poll_transmit` returns. So needs to contain the datagrams as they are sent on the
27/// wire.
28///
29/// [`Connection::poll_transmit`]: super::Connection::poll_transmit
30#[derive(Debug)]
31pub(super) struct TransmitBuf<'a> {
32 /// The buffer itself, packets are written to this buffer
33 buf: &'a mut Vec<u8>,
34 /// Offset into the buffer at which the current datagram starts
35 ///
36 /// Note that when coalescing packets this might be before the start of the current
37 /// packet.
38 datagram_start: usize,
39 /// The maximum offset allowed to be used for the current datagram in the buffer
40 ///
41 /// The first and last datagram in a batch are allowed to be smaller then the maximum
42 /// size. All datagrams in between need to be exactly this size.
43 buf_capacity: usize,
44 /// The maximum number of datagrams allowed to write into [`TransmitBuf::buf`]
45 max_datagrams: NonZeroUsize,
46 /// The number of datagrams already (partially) written into the buffer
47 ///
48 /// Incremented by a call to [`TransmitBuf::start_new_datagram`].
49 pub(super) num_datagrams: usize,
50 /// The segment size of this GSO batch
51 ///
52 /// The segment size is the size of each datagram in the GSO batch, only the last
53 /// datagram in the batch may be smaller.
54 ///
55 /// For the first datagram this is set to the maximum size a datagram is allowed to be:
56 /// the current path MTU. After the first datagram is finished this is reduced to the
57 /// size of the first datagram and can no longer change.
58 segment_size: usize,
59}
60
61impl<'a> TransmitBuf<'a> {
62 pub(super) fn new(buf: &'a mut Vec<u8>, max_datagrams: NonZeroUsize, mtu: usize) -> Self {
63 buf.clear();
64 Self {
65 buf,
66 datagram_start: 0,
67 buf_capacity: 0,
68 max_datagrams,
69 num_datagrams: 0,
70 segment_size: mtu,
71 }
72 }
73
74 /// Starts a datagram with a custom datagram size
75 ///
76 /// This is a specialized version of [`TransmitBuf::start_new_datagram`] which sets the
77 /// datagram size. Useful for e.g. PATH_CHALLENGE, tail-loss probes or MTU probes.
78 ///
79 /// After the first datagram you can never increase the segment size. If you decrease
80 /// the size of a datagram in a batch, it must be the last datagram of the batch.
81 pub(super) fn start_new_datagram_with_size(&mut self, datagram_size: usize) {
82 // Only reserve space for this datagram, usually it is the last one in the batch.
83 let max_capacity_hint = datagram_size;
84 self.new_datagram_inner(datagram_size, max_capacity_hint)
85 }
86
87 /// Starts a new datagram in the transmit buffer
88 ///
89 /// If this starts the second datagram the segment size will be set to the size of the
90 /// first datagram.
91 ///
92 /// If the underlying buffer does not have enough capacity yet this will allocate enough
93 /// capacity for all the datagrams allowed in a single batch. Use
94 /// [`TransmitBuf::start_new_datagram_with_size`] if you know you will need less.
95 pub(super) fn start_new_datagram(&mut self) {
96 // We reserve the maximum space for sending `max_datagrams` upfront to avoid any
97 // reallocations if more datagrams have to be appended later on. Benchmarks have
98 // shown a 5-10% throughput improvement compared to continuously resizing the
99 // datagram buffer. While this will lead to over-allocation for small transmits
100 // (e.g. purely containing ACKs), modern memory allocators (e.g. mimalloc and
101 // jemalloc) will pool certain allocation sizes and therefore this is still rather
102 // efficient.
103 let max_capacity_hint = self.max_datagrams.get() * self.segment_size;
104 self.new_datagram_inner(self.segment_size, max_capacity_hint)
105 }
106
107 fn new_datagram_inner(&mut self, datagram_size: usize, max_capacity_hint: usize) {
108 debug_assert!(self.num_datagrams < self.max_datagrams.into());
109 if self.num_datagrams == 1 {
110 // Set the segment size to the size of the first datagram.
111 self.segment_size = self.buf.len();
112 }
113 if self.num_datagrams >= 1 {
114 debug_assert!(datagram_size <= self.segment_size);
115 if datagram_size < self.segment_size {
116 // If this is a GSO batch and this datagram is smaller than the segment
117 // size, this must be the last datagram in the batch.
118 self.max_datagrams = NonZeroUsize::MIN.saturating_add(self.num_datagrams);
119 }
120 }
121 self.datagram_start = self.buf.len();
122 debug_assert_eq!(
123 self.datagram_start % self.segment_size,
124 0,
125 "datagrams in a GSO batch must be aligned to the segment size"
126 );
127 self.buf_capacity = self.datagram_start + datagram_size;
128 if self.buf_capacity > self.buf.capacity() {
129 self.buf
130 .reserve_exact(max_capacity_hint.saturating_sub(self.buf.capacity()));
131 }
132 self.num_datagrams += 1;
133 }
134
135 /// Clips the segment size to the current size
136 ///
137 /// Only valid for the first datagram, when the datagram might be smaller than the
138 /// segment size. Needed before estimating the available space in the next datagram
139 /// based on [`TransmitBuf::segment_size`].
140 ///
141 /// Use [`TransmitBuf::start_new_datagram_with_size`] if you need to reduce the size of
142 /// the last datagram in a batch.
143 pub(super) fn clip_segment_size(&mut self) {
144 debug_assert_eq!(self.num_datagrams, 1);
145 if self.buf.len() < self.segment_size {
146 trace!(
147 segment_size = self.buf.len(),
148 prev_segment_size = self.segment_size,
149 "clipped datagram size"
150 );
151 }
152 self.segment_size = self.buf.len();
153 self.buf_capacity = self.buf.len();
154 }
155
156 /// Returns the GSO segment size
157 ///
158 /// This is also the maximum size datagrams are allowed to be. The first and last
159 /// datagram in a batch are allowed to be smaller however. After the first datagram the
160 /// segment size is clipped to the size of the first datagram.
161 ///
162 /// If the last datagram was created using [`TransmitBuf::start_new_datagram_with_size`]
163 /// the the segment size will be greater than the current datagram is allowed to be.
164 /// Thus [`TransmitBuf::datagram_remaining_mut`] should be used if you need to know the
165 /// amount of data that can be written into the datagram.
166 pub(super) fn segment_size(&self) -> usize {
167 self.segment_size
168 }
169
170 /// Returns the number of datagrams written into the buffer
171 ///
172 /// The last datagram is not necessarily finished yet.
173 pub(super) fn num_datagrams(&self) -> usize {
174 self.num_datagrams
175 }
176
177 /// Returns the maximum number of datagrams allowed to be written into the buffer
178 pub(super) fn max_datagrams(&self) -> NonZeroUsize {
179 self.max_datagrams
180 }
181
182 /// Returns the start offset of the current datagram in the buffer
183 ///
184 /// In other words, this offset contains the first byte of the current datagram.
185 pub(super) fn datagram_start_offset(&self) -> usize {
186 self.datagram_start
187 }
188
189 /// Returns the maximum offset in the buffer allowed for the current datagram
190 ///
191 /// The first and last datagram in a batch are allowed to be smaller then the maximum
192 /// size. All datagrams in between need to be exactly this size.
193 pub(super) fn datagram_max_offset(&self) -> usize {
194 self.buf_capacity
195 }
196
197 /// Returns the number of bytes that may still be written into this datagram
198 pub(super) fn datagram_remaining_mut(&self) -> usize {
199 self.buf_capacity.saturating_sub(self.buf.len())
200 }
201
202 /// Returns `true` if the buffer did not have anything written into it
203 pub(super) fn is_empty(&self) -> bool {
204 self.len() == 0
205 }
206
207 /// The number of bytes written into the buffer so far
208 pub(super) fn len(&self) -> usize {
209 self.buf.len()
210 }
211
212 /// Returns the already written bytes in the buffer
213 pub(super) fn as_mut_slice(&mut self) -> &mut [u8] {
214 self.buf.as_mut_slice()
215 }
216}
217
218unsafe impl BufMut for TransmitBuf<'_> {
219 fn remaining_mut(&self) -> usize {
220 self.buf.remaining_mut()
221 }
222
223 unsafe fn advance_mut(&mut self, cnt: usize) {
224 unsafe { self.buf.advance_mut(cnt) };
225 }
226
227 fn chunk_mut(&mut self) -> &mut bytes::buf::UninitSlice {
228 self.buf.chunk_mut()
229 }
230}
231
232impl BufLen for TransmitBuf<'_> {
233 fn len(&self) -> usize {
234 self.len()
235 }
236}