Skip to content

Commit d6aadb8

Browse files
authored
perf(lib): re-enable writev support (#2338)
Tokio's `AsyncWrite` trait once again has support for vectored writes in Tokio 0.3.4 (see tokio-rs/tokio#3149). This branch re-enables vectored writes in Hyper for HTTP/1. Using vectored writes in HTTP/2 will require an upstream change in the `h2` crate as well. I've removed the adaptive write buffer implementation that attempts to detect whether vectored IO is or is not available, since the Tokio 0.3.4 `AsyncWrite` trait exposes this directly via the `is_write_vectored` method. Now, we just ask the IO whether or not it supports vectored writes, and configure the buffer accordingly. This makes the implementation somewhat simpler. This also removes `http1_writev()` methods from the builders. These are no longer necessary, as Hyper can now determine whether or not to use vectored writes based on `is_write_vectored`, rather than trying to auto-detect it. Closes #2320 BREAKING CHANGE: Removed `http1_writev` methods from `client::Builder`, `client::conn::Builder`, `server::Builder`, and `server::conn::Builder`. Vectored writes are now enabled based on whether the `AsyncWrite` implementation in use supports them, rather than though adaptive detection. To explicitly disable vectored writes, users may wrap the IO in a newtype that implements `AsyncRead` and `AsyncWrite` and returns `false` from its `AsyncWrite::is_write_vectored` method.
1 parent 121c331 commit d6aadb8

File tree

10 files changed

+95
-217
lines changed

10 files changed

+95
-217
lines changed

src/client/conn.rs

-14
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ where
8282
#[derive(Clone, Debug)]
8383
pub struct Builder {
8484
pub(super) exec: Exec,
85-
h1_writev: Option<bool>,
8685
h1_title_case_headers: bool,
8786
h1_read_buf_exact_size: Option<usize>,
8887
h1_max_buf_size: Option<usize>,
@@ -453,7 +452,6 @@ impl Builder {
453452
pub fn new() -> Builder {
454453
Builder {
455454
exec: Exec::Default,
456-
h1_writev: None,
457455
h1_read_buf_exact_size: None,
458456
h1_title_case_headers: false,
459457
h1_max_buf_size: None,
@@ -475,11 +473,6 @@ impl Builder {
475473
self
476474
}
477475

478-
pub(super) fn h1_writev(&mut self, enabled: bool) -> &mut Builder {
479-
self.h1_writev = Some(enabled);
480-
self
481-
}
482-
483476
pub(super) fn h1_title_case_headers(&mut self, enabled: bool) -> &mut Builder {
484477
self.h1_title_case_headers = enabled;
485478
self
@@ -663,13 +656,6 @@ impl Builder {
663656
#[cfg(feature = "http1")]
664657
Proto::Http1 => {
665658
let mut conn = proto::Conn::new(io);
666-
if let Some(writev) = opts.h1_writev {
667-
if writev {
668-
conn.set_write_strategy_queue();
669-
} else {
670-
conn.set_write_strategy_flatten();
671-
}
672-
}
673659
if opts.h1_title_case_headers {
674660
conn.set_title_case_headers();
675661
}

src/client/mod.rs

+1-18
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ use http::{Method, Request, Response, Uri, Version};
6262
use self::connect::{sealed::Connect, Alpn, Connected, Connection};
6363
use self::pool::{Key as PoolKey, Pool, Poolable, Pooled, Reservation};
6464
use crate::body::{Body, HttpBody};
65-
use crate::common::{lazy as hyper_lazy, task, exec::BoxSendFuture, Future, Lazy, Pin, Poll};
65+
use crate::common::{exec::BoxSendFuture, lazy as hyper_lazy, task, Future, Lazy, Pin, Poll};
6666
use crate::rt::Executor;
6767

6868
#[cfg(feature = "tcp")]
@@ -987,23 +987,6 @@ impl Builder {
987987

988988
// HTTP/1 options
989989

990-
/// Set whether HTTP/1 connections should try to use vectored writes,
991-
/// or always flatten into a single buffer.
992-
///
993-
/// Note that setting this to false may mean more copies of body data,
994-
/// but may also improve performance when an IO transport doesn't
995-
/// support vectored writes well, such as most TLS implementations.
996-
///
997-
/// Setting this to true will force hyper to use queued strategy
998-
/// which may eliminate unnecessary cloning on some TLS backends
999-
///
1000-
/// Default is `auto`. In this mode hyper will try to guess which
1001-
/// mode to use
1002-
pub fn http1_writev(&mut self, val: bool) -> &mut Self {
1003-
self.conn_builder.h1_writev(val);
1004-
self
1005-
}
1006-
1007990
/// Sets the exact size of the read buffer to *always* use.
1008991
///
1009992
/// Note that setting this option unsets the `http1_max_buf_size` option.

src/common/io/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
mod rewind;
22

33
pub(crate) use self::rewind::Rewind;
4+
pub(crate) const MAX_WRITEV_BUFS: usize = 64;

src/common/io/rewind.rs

+12
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,25 @@ where
8484
Pin::new(&mut self.inner).poll_write(cx, buf)
8585
}
8686

87+
fn poll_write_vectored(
88+
mut self: Pin<&mut Self>,
89+
cx: &mut task::Context<'_>,
90+
bufs: &[io::IoSlice<'_>],
91+
) -> Poll<io::Result<usize>> {
92+
Pin::new(&mut self.inner).poll_write_vectored(cx, bufs)
93+
}
94+
8795
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<io::Result<()>> {
8896
Pin::new(&mut self.inner).poll_flush(cx)
8997
}
9098

9199
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<io::Result<()>> {
92100
Pin::new(&mut self.inner).poll_shutdown(cx)
93101
}
102+
103+
fn is_write_vectored(&self) -> bool {
104+
self.inner.is_write_vectored()
105+
}
94106
}
95107

96108
#[cfg(test)]

src/proto/h1/conn.rs

-8
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,6 @@ where
7171
self.io.set_read_buf_exact_size(sz);
7272
}
7373

74-
pub fn set_write_strategy_flatten(&mut self) {
75-
self.io.set_write_strategy_flatten();
76-
}
77-
78-
pub fn set_write_strategy_queue(&mut self) {
79-
self.io.set_write_strategy_queue();
80-
}
81-
8274
#[cfg(feature = "client")]
8375
pub fn set_title_case_headers(&mut self) {
8476
self.state.title_case_headers = true;

src/proto/h1/io.rs

+34-126
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use std::cell::Cell;
21
use std::cmp;
32
use std::fmt;
43
use std::io::{self, IoSlice};
@@ -57,13 +56,14 @@ where
5756
B: Buf,
5857
{
5958
pub fn new(io: T) -> Buffered<T, B> {
59+
let write_buf = WriteBuf::new(&io);
6060
Buffered {
6161
flush_pipeline: false,
6262
io,
6363
read_blocked: false,
6464
read_buf: BytesMut::with_capacity(0),
6565
read_buf_strategy: ReadStrategy::default(),
66-
write_buf: WriteBuf::new(),
66+
write_buf,
6767
}
6868
}
6969

@@ -98,13 +98,6 @@ where
9898
self.write_buf.set_strategy(WriteStrategy::Flatten);
9999
}
100100

101-
pub fn set_write_strategy_queue(&mut self) {
102-
// this should always be called only at construction time,
103-
// so this assert is here to catch myself
104-
debug_assert!(self.write_buf.queue.bufs_cnt() == 0);
105-
self.write_buf.set_strategy(WriteStrategy::Queue);
106-
}
107-
108101
pub fn read_buf(&self) -> &[u8] {
109102
self.read_buf.as_ref()
110103
}
@@ -237,13 +230,13 @@ where
237230
if let WriteStrategy::Flatten = self.write_buf.strategy {
238231
return self.poll_flush_flattened(cx);
239232
}
233+
240234
loop {
241-
// TODO(eliza): this basically ignores all of `WriteBuf`...put
242-
// back vectored IO and `poll_write_buf` when the appropriate Tokio
243-
// changes land...
244-
let n = ready!(Pin::new(&mut self.io)
245-
// .poll_write_buf(cx, &mut self.write_buf.auto()))?;
246-
.poll_write(cx, self.write_buf.auto().bytes()))?;
235+
let n = {
236+
let mut iovs = [IoSlice::new(&[]); crate::common::io::MAX_WRITEV_BUFS];
237+
let len = self.write_buf.bytes_vectored(&mut iovs);
238+
ready!(Pin::new(&mut self.io).poll_write_vectored(cx, &iovs[..len]))?
239+
};
247240
// TODO(eliza): we have to do this manually because
248241
// `poll_write_buf` doesn't exist in Tokio 0.3 yet...when
249242
// `poll_write_buf` comes back, the manual advance will need to leave!
@@ -462,12 +455,17 @@ pub(super) struct WriteBuf<B> {
462455
}
463456

464457
impl<B: Buf> WriteBuf<B> {
465-
fn new() -> WriteBuf<B> {
458+
fn new(io: &impl AsyncWrite) -> WriteBuf<B> {
459+
let strategy = if io.is_write_vectored() {
460+
WriteStrategy::Queue
461+
} else {
462+
WriteStrategy::Flatten
463+
};
466464
WriteBuf {
467465
headers: Cursor::new(Vec::with_capacity(INIT_BUFFER_SIZE)),
468466
max_buf_size: DEFAULT_MAX_BUFFER_SIZE,
469467
queue: BufList::new(),
470-
strategy: WriteStrategy::Auto,
468+
strategy,
471469
}
472470
}
473471
}
@@ -480,12 +478,6 @@ where
480478
self.strategy = strategy;
481479
}
482480

483-
// TODO(eliza): put back writev!
484-
#[inline]
485-
fn auto(&mut self) -> WriteBufAuto<'_, B> {
486-
WriteBufAuto::new(self)
487-
}
488-
489481
pub(super) fn buffer<BB: Buf + Into<B>>(&mut self, mut buf: BB) {
490482
debug_assert!(buf.has_remaining());
491483
match self.strategy {
@@ -505,7 +497,7 @@ where
505497
buf.advance(adv);
506498
}
507499
}
508-
WriteStrategy::Auto | WriteStrategy::Queue => {
500+
WriteStrategy::Queue => {
509501
self.queue.push(buf.into());
510502
}
511503
}
@@ -514,7 +506,7 @@ where
514506
fn can_buffer(&self) -> bool {
515507
match self.strategy {
516508
WriteStrategy::Flatten => self.remaining() < self.max_buf_size,
517-
WriteStrategy::Auto | WriteStrategy::Queue => {
509+
WriteStrategy::Queue => {
518510
self.queue.bufs_cnt() < MAX_BUF_LIST_BUFFERS && self.remaining() < self.max_buf_size
519511
}
520512
}
@@ -573,65 +565,8 @@ impl<B: Buf> Buf for WriteBuf<B> {
573565
}
574566
}
575567

576-
/// Detects when wrapped `WriteBuf` is used for vectored IO, and
577-
/// adjusts the `WriteBuf` strategy if not.
578-
struct WriteBufAuto<'a, B: Buf> {
579-
bytes_called: Cell<bool>,
580-
bytes_vec_called: Cell<bool>,
581-
inner: &'a mut WriteBuf<B>,
582-
}
583-
584-
impl<'a, B: Buf> WriteBufAuto<'a, B> {
585-
fn new(inner: &'a mut WriteBuf<B>) -> WriteBufAuto<'a, B> {
586-
WriteBufAuto {
587-
bytes_called: Cell::new(false),
588-
bytes_vec_called: Cell::new(false),
589-
inner,
590-
}
591-
}
592-
}
593-
594-
impl<'a, B: Buf> Buf for WriteBufAuto<'a, B> {
595-
#[inline]
596-
fn remaining(&self) -> usize {
597-
self.inner.remaining()
598-
}
599-
600-
#[inline]
601-
fn bytes(&self) -> &[u8] {
602-
self.bytes_called.set(true);
603-
self.inner.bytes()
604-
}
605-
606-
#[inline]
607-
fn advance(&mut self, cnt: usize) {
608-
self.inner.advance(cnt)
609-
}
610-
611-
#[inline]
612-
fn bytes_vectored<'t>(&'t self, dst: &mut [IoSlice<'t>]) -> usize {
613-
self.bytes_vec_called.set(true);
614-
self.inner.bytes_vectored(dst)
615-
}
616-
}
617-
618-
impl<'a, B: Buf + 'a> Drop for WriteBufAuto<'a, B> {
619-
fn drop(&mut self) {
620-
if let WriteStrategy::Auto = self.inner.strategy {
621-
if self.bytes_vec_called.get() {
622-
self.inner.strategy = WriteStrategy::Queue;
623-
} else if self.bytes_called.get() {
624-
trace!("detected no usage of vectored write, flattening");
625-
self.inner.strategy = WriteStrategy::Flatten;
626-
self.inner.headers.bytes.put(&mut self.inner.queue);
627-
}
628-
}
629-
}
630-
}
631-
632568
#[derive(Debug)]
633569
enum WriteStrategy {
634-
Auto,
635570
Flatten,
636571
Queue,
637572
}
@@ -643,8 +578,8 @@ mod tests {
643578

644579
use tokio_test::io::Builder as Mock;
645580

646-
#[cfg(feature = "nightly")]
647-
use test::Bencher;
581+
// #[cfg(feature = "nightly")]
582+
// use test::Bencher;
648583

649584
/*
650585
impl<T: Read> MemRead for AsyncIo<T> {
@@ -873,33 +808,6 @@ mod tests {
873808
buffered.flush().await.expect("flush");
874809
}
875810

876-
#[tokio::test]
877-
async fn write_buf_auto_flatten() {
878-
let _ = pretty_env_logger::try_init();
879-
880-
let mock = Mock::new()
881-
// Expects write_buf to only consume first buffer
882-
.write(b"hello ")
883-
// And then the Auto strategy will have flattened
884-
.write(b"world, it's hyper!")
885-
.build();
886-
887-
let mut buffered = Buffered::<_, Cursor<Vec<u8>>>::new(mock);
888-
889-
// we have 4 buffers, but hope to detect that vectored IO isn't
890-
// being used, and switch to flattening automatically,
891-
// resulting in only 2 writes
892-
buffered.headers_buf().extend(b"hello ");
893-
buffered.buffer(Cursor::new(b"world, ".to_vec()));
894-
buffered.buffer(Cursor::new(b"it's ".to_vec()));
895-
buffered.buffer(Cursor::new(b"hyper!".to_vec()));
896-
assert_eq!(buffered.write_buf.queue.bufs_cnt(), 3);
897-
898-
buffered.flush().await.expect("flush");
899-
900-
assert_eq!(buffered.write_buf.queue.bufs_cnt(), 0);
901-
}
902-
903811
#[tokio::test]
904812
async fn write_buf_queue_disable_auto() {
905813
let _ = pretty_env_logger::try_init();
@@ -928,19 +836,19 @@ mod tests {
928836
assert_eq!(buffered.write_buf.queue.bufs_cnt(), 0);
929837
}
930838

931-
#[cfg(feature = "nightly")]
932-
#[bench]
933-
fn bench_write_buf_flatten_buffer_chunk(b: &mut Bencher) {
934-
let s = "Hello, World!";
935-
b.bytes = s.len() as u64;
936-
937-
let mut write_buf = WriteBuf::<bytes::Bytes>::new();
938-
write_buf.set_strategy(WriteStrategy::Flatten);
939-
b.iter(|| {
940-
let chunk = bytes::Bytes::from(s);
941-
write_buf.buffer(chunk);
942-
::test::black_box(&write_buf);
943-
write_buf.headers.bytes.clear();
944-
})
945-
}
839+
// #[cfg(feature = "nightly")]
840+
// #[bench]
841+
// fn bench_write_buf_flatten_buffer_chunk(b: &mut Bencher) {
842+
// let s = "Hello, World!";
843+
// b.bytes = s.len() as u64;
844+
845+
// let mut write_buf = WriteBuf::<bytes::Bytes>::new();
846+
// write_buf.set_strategy(WriteStrategy::Flatten);
847+
// b.iter(|| {
848+
// let chunk = bytes::Bytes::from(s);
849+
// write_buf.buffer(chunk);
850+
// ::test::black_box(&write_buf);
851+
// write_buf.headers.bytes.clear();
852+
// })
853+
// }
946854
}

0 commit comments

Comments
 (0)