Skip to content

Commit 10e665c

Browse files
committed
tokio-quiche: lazily allocate H3 body_recv_buf
H3Driver eagerly allocated a 64 KiB body receive buffer per connection at construction. Make it Option<Limit<BytesMut>>, allocate it lazily on the first body read, and release it once no streams or flows remain, so idle connections hold no receive buffer.
1 parent 7afd499 commit 10e665c

3 files changed

Lines changed: 183 additions & 24 deletions

File tree

tokio-quiche/src/http3/driver/mod.rs

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -350,9 +350,11 @@ pub struct H3Driver<H: DriverHooks> {
350350
dgram_recv: OutboundFrameStream,
351351
/// Keeps the datagram channel open such that datagram flows can be created.
352352
dgram_send: OutboundFrameSender,
353-
/// A buffer to receive H3 body data from quiche. We initialize a large
354-
/// buffer and then `split()` off filled parts until we need to reallocate.
355-
body_recv_buf: bytes::buf::Limit<BytesMut>,
353+
/// A buffer to receive H3 body data from quiche. Lazily allocated on the
354+
/// first body read and released once no streams remain, so idle
355+
/// connections hold no receive buffer. We `split()` off filled parts until
356+
/// we need to reallocate.
357+
body_recv_buf: Option<bytes::buf::Limit<BytesMut>>,
356358

357359
/// The buffer used to interact with the underlying IoWorker.
358360
io_worker_buf: Vec<u8>,
@@ -393,8 +395,7 @@ impl<H: DriverHooks> H3Driver<H> {
393395
dgram_recv,
394396
dgram_send: PollSender::new(dgram_send),
395397
max_stream_seen: 0,
396-
body_recv_buf: BytesMut::with_capacity(BufFactory::MAX_BUF_SIZE)
397-
.limit(BufFactory::MAX_BUF_SIZE),
398+
body_recv_buf: None,
398399
io_worker_buf: vec![0u8; BufFactory::MAX_BUF_SIZE],
399400

400401
waiting_streams: FuturesUnordered::new(),
@@ -521,17 +522,23 @@ impl<H: DriverHooks> H3Driver<H> {
521522
};
522523
}
523524

524-
// NOTE: `self.body_recv_buf` is `Limit<BytesMut>` so
525+
// Lazily allocate the receive buffer on first use; idle
526+
// connections never receive body bytes and never allocate it.
527+
let body_recv_buf = self.body_recv_buf.get_or_insert_with(|| {
528+
BytesMut::with_capacity(BufFactory::MAX_BUF_SIZE)
529+
.limit(BufFactory::MAX_BUF_SIZE)
530+
});
531+
// NOTE: `body_recv_buf` is `Limit<BytesMut>` so
525532
// `has_remaining_mut()` will indicate if the buffer
526533
// has space available until the *limit* is
527534
// reached. (A plain `BytesMut` can reallocate and would always
528535
// return true)
529-
if !self.body_recv_buf.has_remaining_mut() {
530-
self.body_recv_buf =
536+
if !body_recv_buf.has_remaining_mut() {
537+
*body_recv_buf =
531538
BytesMut::with_capacity(BufFactory::MAX_BUF_SIZE)
532-
.limit(BufFactory::MAX_BUF_SIZE)
533-
};
534-
match conn.recv_body_buf(qconn, stream_id, &mut self.body_recv_buf) {
539+
.limit(BufFactory::MAX_BUF_SIZE);
540+
}
541+
match conn.recv_body_buf(qconn, stream_id, &mut *body_recv_buf) {
535542
Ok(n) => {
536543
ctx.audit_stats.add_downstream_bytes_recvd(n as u64);
537544
let event = H3Event::BodyBytesReceived {
@@ -541,12 +548,12 @@ impl<H: DriverHooks> H3Driver<H> {
541548
};
542549
let _ = self.h3_event_sender.send(event.into());
543550
// Take the filled part, leave the remaining capacity
544-
let filled_body = self.body_recv_buf.get_mut().split();
551+
let filled_body = body_recv_buf.get_mut().split();
545552
// Sanity check: the remaining spare capacity should equal
546553
// the limit.
547554
debug_assert_eq!(
548-
self.body_recv_buf.get_mut().spare_capacity_mut().len(),
549-
self.body_recv_buf.remaining_mut()
555+
body_recv_buf.get_mut().spare_capacity_mut().len(),
556+
body_recv_buf.remaining_mut()
550557
);
551558
permit.send(InboundFrame::Body(filled_body, false));
552559
},
@@ -1003,15 +1010,23 @@ impl<H: DriverHooks> H3Driver<H> {
10031010
Ok(())
10041011
}
10051012

1006-
/// Closes the connection with `NoError` if the H3 event receiver
1007-
/// has been dropped and there are no active streams or flows.
1008-
fn close_if_idle(&self, qconn: &mut QuicheConnection) {
1009-
if self.h3_event_receiver_dropped &&
1010-
self.stream_map.is_empty() &&
1011-
self.flow_map.is_empty()
1012-
{
1013-
let _ =
1014-
qconn.close(true, quiche::h3::WireErrorCode::NoError as u64, &[]);
1013+
/// Handles connection cleanup once no streams or flows remain.
1014+
///
1015+
/// Releases the body receive buffer (it is reallocated lazily on the next
1016+
/// body read; body bytes only flow on active streams, so an empty stream
1017+
/// map means it is unused) and closes the connection with `NoError` if the
1018+
/// H3 event receiver has been dropped.
1019+
fn close_if_idle(&mut self, qconn: &mut QuicheConnection) {
1020+
if self.stream_map.is_empty() && self.flow_map.is_empty() {
1021+
self.body_recv_buf = None;
1022+
1023+
if self.h3_event_receiver_dropped {
1024+
let _ = qconn.close(
1025+
true,
1026+
quiche::h3::WireErrorCode::NoError as u64,
1027+
&[],
1028+
);
1029+
}
10151030
}
10161031
}
10171032

tokio-quiche/src/http3/driver/test_utils.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,8 @@ impl<H: DriverHooks + GetConnectionForHook> DriverTestHelper<H> {
277277
}
278278

279279
pub fn driver_set_body_buf_size(&mut self, limit: usize) {
280-
self.driver.body_recv_buf = BytesMut::with_capacity(limit).limit(limit);
280+
self.driver.body_recv_buf =
281+
Some(BytesMut::with_capacity(limit).limit(limit));
281282
}
282283
}
283284

tokio-quiche/src/http3/driver/tests.rs

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,149 @@ mod client_side_driver {
243243
assert_eq!(helper.driver.stream_map.len(), 0);
244244
}
245245

246+
/// An idle connection (and a request/response that exchanges only
247+
/// headers) must never allocate the body receive buffer.
248+
#[test]
249+
fn client_body_recv_buf_not_allocated_when_idle() {
250+
let mut helper = DriverTestHelper::<ClientHooks>::new().unwrap();
251+
helper.complete_handshake().unwrap();
252+
helper.advance_and_run_loop().unwrap();
253+
254+
// Idle: never received body bytes, so the buffer is not allocated.
255+
assert!(helper.driver.body_recv_buf.is_none());
256+
257+
// client sends a request with fin (no request body)
258+
let stream_id = helper
259+
.driver_send_request(make_request_headers("GET"), true)
260+
.unwrap();
261+
262+
// server reads request and sends response headers (no body)
263+
helper.advance_and_run_loop().unwrap();
264+
assert_matches!(
265+
helper.peer_server_poll().unwrap(),
266+
(0, h3::Event::Headers { .. })
267+
);
268+
helper.peer_server_send_response(0, false).unwrap();
269+
helper.advance_and_run_loop().unwrap();
270+
271+
// Client receives response headers
272+
let resp = assert_matches!(
273+
helper.driver_recv_core_event().unwrap(),
274+
H3Event::IncomingHeaders(headers) => { headers }
275+
);
276+
assert_eq!(resp.stream_id, stream_id);
277+
278+
// Only headers have been exchanged; still no body buffer.
279+
assert!(helper.driver.body_recv_buf.is_none());
280+
}
281+
282+
/// The body receive buffer is lazily allocated on the first body read
283+
/// and released once the last stream is cleaned up.
284+
#[test]
285+
fn client_body_recv_buf_allocated_on_body_and_released_on_close() {
286+
let mut helper = DriverTestHelper::<ClientHooks>::new().unwrap();
287+
helper.complete_handshake().unwrap();
288+
helper.advance_and_run_loop().unwrap();
289+
290+
let stream_id = helper
291+
.driver_send_request(make_request_headers("GET"), true)
292+
.unwrap();
293+
294+
helper.advance_and_run_loop().unwrap();
295+
assert_matches!(
296+
helper.peer_server_poll().unwrap(),
297+
(0, h3::Event::Headers { .. })
298+
);
299+
helper.peer_server_send_response(0, false).unwrap();
300+
helper.advance_and_run_loop().unwrap();
301+
302+
let resp = assert_matches!(
303+
helper.driver_recv_core_event().unwrap(),
304+
H3Event::IncomingHeaders(headers) => { headers }
305+
);
306+
assert_eq!(resp.stream_id, stream_id);
307+
let mut from_server = resp.recv;
308+
309+
// No body yet: the buffer is still unallocated.
310+
assert!(helper.driver.body_recv_buf.is_none());
311+
312+
// Server sends a body chunk (not fin).
313+
helper.peer_server_send_body(0, &[7; 10], false).unwrap();
314+
helper.advance_and_run_loop().unwrap();
315+
assert_eq!(helper.driver_try_recv_body(&mut from_server).0, vec![7; 10]);
316+
317+
// The first body read lazily allocated the buffer.
318+
assert!(helper.driver.body_recv_buf.is_some());
319+
320+
// Server finishes the stream.
321+
helper.peer_server_send_body(0, &[8; 10], true).unwrap();
322+
helper.advance_and_run_loop().unwrap();
323+
let (body, fin, _) = helper.driver_try_recv_body(&mut from_server);
324+
assert_eq!(body, vec![8; 10]);
325+
assert!(fin);
326+
327+
// Stream cleaned up on both-directions-close, buffer released.
328+
assert_eq!(helper.driver.stream_map.len(), 0);
329+
assert!(helper.driver.body_recv_buf.is_none());
330+
}
331+
332+
/// A body larger than the receive buffer exercises the reallocation
333+
/// branch; the buffer stays allocated across reallocations and is
334+
/// released once the stream closes.
335+
#[test]
336+
fn client_body_recv_buf_reallocates_and_releases() {
337+
let mut helper = DriverTestHelper::<ClientHooks>::new().unwrap();
338+
helper.complete_handshake().unwrap();
339+
helper.advance_and_run_loop().unwrap();
340+
341+
let stream_id = helper
342+
.driver_send_request(make_request_headers("GET"), true)
343+
.unwrap();
344+
345+
helper.advance_and_run_loop().unwrap();
346+
assert_matches!(
347+
helper.peer_server_poll().unwrap(),
348+
(0, h3::Event::Headers { .. })
349+
);
350+
helper.peer_server_send_response(0, false).unwrap();
351+
helper.advance_and_run_loop().unwrap();
352+
353+
let resp = assert_matches!(
354+
helper.driver_recv_core_event().unwrap(),
355+
H3Event::IncomingHeaders(headers) => { headers }
356+
);
357+
assert_eq!(resp.stream_id, stream_id);
358+
let mut from_server = resp.recv;
359+
360+
// Force a small receive buffer so the body exhausts it and the
361+
// reallocation branch (`*body_recv_buf = ...`) runs.
362+
helper.driver_set_body_buf_size(20);
363+
364+
// Send 40 bytes across the 20-byte buffer, exhausting it repeatedly.
365+
helper.peer_server_send_body(0, &[1; 10], false).unwrap();
366+
helper.advance_and_run_loop().unwrap();
367+
assert_eq!(helper.driver_try_recv_body(&mut from_server).0, vec![1; 10]);
368+
helper.peer_server_send_body(0, &[2; 10], false).unwrap();
369+
helper.advance_and_run_loop().unwrap();
370+
assert_eq!(helper.driver_try_recv_body(&mut from_server).0, vec![2; 10]);
371+
helper.peer_server_send_body(0, &[3; 10], false).unwrap();
372+
helper.advance_and_run_loop().unwrap();
373+
assert_eq!(helper.driver_try_recv_body(&mut from_server).0, vec![3; 10]);
374+
// Buffer remains allocated across reallocation.
375+
assert!(helper.driver.body_recv_buf.is_some());
376+
377+
// Final chunk with fin.
378+
helper.peer_server_send_body(0, &[4; 10], true).unwrap();
379+
helper.advance_and_run_loop().unwrap();
380+
let (body, fin, _) = helper.driver_try_recv_body(&mut from_server);
381+
assert_eq!(body, vec![4; 10]);
382+
assert!(fin);
383+
384+
// Stream cleaned up, buffer released.
385+
assert_eq!(helper.driver.stream_map.len(), 0);
386+
assert!(helper.driver.body_recv_buf.is_none());
387+
}
388+
246389
/// Test that dropping the OutboundFrame channel causes the driver to
247390
/// send a RESET_STREAM frame to the peer.
248391
#[test]

0 commit comments

Comments
 (0)