perf(h1): optimize for when Body is only 1 chunk

- When the `Body` is created from a buffer of bytes (such as
  `Body::from("hello")`), we can skip some bookkeeping that is
  normally required for streaming bodies.
- Orthogonally, optimize encoding body chunks when the strategy
  is to flatten into the headers buf, by skipping the EncodedBuf
  enum.
This commit is contained in:
Sean McArthur
2018-05-31 17:42:55 -07:00
parent 89c5643713
commit 898e919504
7 changed files with 207 additions and 83 deletions

View File

@@ -26,8 +26,11 @@ use http::HeaderMap;
use common::Never;
pub use chunk::Chunk;
use self::internal::{FullDataArg, FullDataRet};
type BodySender = mpsc::Sender<Result<Chunk, ::Error>>;
/// This trait represents a streaming body of a `Request` or `Response`.
///
/// The built-in implementation of this trait is [`Body`](Body), in case you
@@ -80,6 +83,16 @@ pub trait Payload: Send + 'static {
fn content_length(&self) -> Option<u64> {
None
}
// This API is unstable, and is impossible to use outside of hyper. Some
// form of it may become stable in a later version.
//
// The only thing a user *could* do is reference the method, but DON'T
// DO THAT! :)
#[doc(hidden)]
fn __hyper_full_data(&mut self, FullDataArg) -> FullDataRet<Self::Data> {
FullDataRet(None)
}
}
impl<E: Payload> Payload for Box<E> {
@@ -343,6 +356,14 @@ impl Payload for Body {
Kind::Wrapped(..) => None,
}
}
// We can improve the performance of `Body` when we know it is a Once kind.
fn __hyper_full_data(&mut self, _: FullDataArg) -> FullDataRet<Self::Data> {
match self.kind {
Kind::Once(ref mut val) => FullDataRet(val.take()),
_ => FullDataRet(None),
}
}
}
impl Stream for Body {
@@ -469,6 +490,22 @@ impl From<Cow<'static, str>> for Body {
}
}
// The full_data API is not stable, so these types are to try to prevent
// users from being able to:
//
// - Implment `__hyper_full_data` on their own Payloads.
// - Call `__hyper_full_data` on any Payload.
//
// That's because to implement it, they need to name these types, and
// they can't because they aren't exported. And to call it, they would
// need to create one of these values, which they also can't.
pub(crate) mod internal {
#[allow(missing_debug_implementations)]
pub struct FullDataArg(pub(crate) ());
#[allow(missing_debug_implementations)]
pub struct FullDataRet<B>(pub(crate) Option<B>);
}
fn _assert_send_sync() {
fn _assert_send<T: Send>() {}
fn _assert_sync<T: Sync>() {}

View File

@@ -388,7 +388,35 @@ where I: AsyncRead + AsyncWrite,
self.io.can_buffer()
}
pub fn write_head(&mut self, mut head: MessageHead<T::Outgoing>, body: Option<BodyLength>) {
pub fn write_head(&mut self, head: MessageHead<T::Outgoing>, body: Option<BodyLength>) {
if let Some(encoder) = self.encode_head(head, body) {
self.state.writing = if !encoder.is_eof() {
Writing::Body(encoder)
} else if encoder.is_last() {
Writing::Closed
} else {
Writing::KeepAlive
};
}
}
pub fn write_full_msg(&mut self, head: MessageHead<T::Outgoing>, body: B) {
if let Some(encoder) = self.encode_head(head, Some(BodyLength::Known(body.remaining() as u64))) {
let is_last = encoder.is_last();
// Make sure we don't write a body if we weren't actually allowed
// to do so, like because its a HEAD request.
if !encoder.is_eof() {
encoder.danger_full_buf(body, self.io.write_buf());
}
self.state.writing = if is_last {
Writing::Closed
} else {
Writing::KeepAlive
}
}
}
fn encode_head(&mut self, mut head: MessageHead<T::Outgoing>, body: Option<BodyLength>) -> Option<Encoder> {
debug_assert!(self.can_write_head());
if !T::should_read_first() {
@@ -398,7 +426,7 @@ where I: AsyncRead + AsyncWrite,
self.enforce_version(&mut head);
let buf = self.io.headers_buf();
self.state.writing = match T::encode(Encode {
match T::encode(Encode {
head: &mut head,
body,
keep_alive: self.state.wants_keep_alive(),
@@ -409,19 +437,14 @@ where I: AsyncRead + AsyncWrite,
debug_assert!(self.state.cached_headers.is_none());
debug_assert!(head.headers.is_empty());
self.state.cached_headers = Some(head.headers);
if !encoder.is_eof() {
Writing::Body(encoder)
} else if encoder.is_last() {
Writing::Closed
} else {
Writing::KeepAlive
}
Some(encoder)
},
Err(err) => {
self.state.error = Some(err);
Writing::Closed
}
};
self.state.writing = Writing::Closed;
None
},
}
}
// If we know the remote speaks an older version, we try to fix up any messages
@@ -474,8 +497,7 @@ where I: AsyncRead + AsyncWrite,
let state = match self.state.writing {
Writing::Body(ref encoder) => {
let (encoded, can_keep_alive) = encoder.encode_and_end(chunk);
self.io.buffer(encoded);
let can_keep_alive = encoder.encode_and_end(chunk, self.io.write_buf());
if can_keep_alive {
Writing::KeepAlive
} else {

View File

@@ -4,6 +4,7 @@ use http::{Request, Response, StatusCode};
use tokio_io::{AsyncRead, AsyncWrite};
use body::{Body, Payload};
use body::internal::FullDataArg;
use proto::{BodyLength, Conn, MessageHead, RequestHead, RequestLine, ResponseHead};
use super::Http1Transaction;
use service::Service;
@@ -20,7 +21,7 @@ pub(crate) trait Dispatch {
type PollItem;
type PollBody;
type RecvItem;
fn poll_msg(&mut self) -> Poll<Option<(Self::PollItem, Option<Self::PollBody>)>, ::Error>;
fn poll_msg(&mut self) -> Poll<Option<(Self::PollItem, Self::PollBody)>, ::Error>;
fn recv_msg(&mut self, msg: ::Result<(Self::RecvItem, Body)>) -> ::Result<()>;
fn poll_ready(&mut self) -> Poll<(), ()>;
fn should_poll(&self) -> bool;
@@ -222,14 +223,26 @@ where
if self.is_closing {
return Ok(Async::Ready(()));
} else if self.body_rx.is_none() && self.conn.can_write_head() && self.dispatch.should_poll() {
if let Some((head, body)) = try_ready!(self.dispatch.poll_msg()) {
let body_type = body.as_ref().map(|body| {
body.content_length()
if let Some((head, mut body)) = try_ready!(self.dispatch.poll_msg()) {
// Check if the body knows its full data immediately.
//
// If so, we can skip a bit of bookkeeping that streaming
// bodies need to do.
if let Some(full) = body.__hyper_full_data(FullDataArg(())).0 {
self.conn.write_full_msg(head, full);
return Ok(Async::Ready(()));
}
let body_type = if body.is_end_stream() {
self.body_rx = None;
None
} else {
let btype = body.content_length()
.map(BodyLength::Known)
.unwrap_or(BodyLength::Unknown)
});
.or_else(|| Some(BodyLength::Unknown));
self.body_rx = Some(body);
btype
};
self.conn.write_head(head, body_type);
self.body_rx = body;
} else {
self.close();
return Ok(Async::Ready(()));
@@ -349,7 +362,7 @@ where
type PollBody = Bs;
type RecvItem = RequestHead;
fn poll_msg(&mut self) -> Poll<Option<(Self::PollItem, Option<Self::PollBody>)>, ::Error> {
fn poll_msg(&mut self) -> Poll<Option<(Self::PollItem, Self::PollBody)>, ::Error> {
if let Some(mut fut) = self.in_flight.take() {
let resp = match fut.poll().map_err(::Error::new_user_service)? {
Async::Ready(res) => res,
@@ -364,11 +377,6 @@ where
subject: parts.status,
headers: parts.headers,
};
let body = if body.is_end_stream() {
None
} else {
Some(body)
};
Ok(Async::Ready(Some((head, body))))
} else {
unreachable!("poll_msg shouldn't be called if no inflight");
@@ -419,7 +427,7 @@ where
type PollBody = B;
type RecvItem = ResponseHead;
fn poll_msg(&mut self) -> Poll<Option<(Self::PollItem, Option<Self::PollBody>)>, ::Error> {
fn poll_msg(&mut self) -> Poll<Option<(Self::PollItem, Self::PollBody)>, ::Error> {
match self.rx.poll() {
Ok(Async::Ready(Some((req, mut cb)))) => {
// check that future hasn't been canceled already
@@ -435,12 +443,6 @@ where
subject: RequestLine(parts.method, parts.uri),
headers: parts.headers,
};
let body = if body.is_end_stream() {
None
} else {
Some(body)
};
self.callback = Some(cb);
Ok(Async::Ready(Some((head, body))))
}

View File

@@ -5,6 +5,7 @@ use bytes::buf::{Chain, Take};
use iovec::IoVec;
use common::StaticBuf;
use super::io::WriteBuf;
/// Encoders to handle different Transfer-Encodings.
#[derive(Debug, Clone, PartialEq)]
@@ -126,7 +127,7 @@ impl Encoder {
}
}
pub fn encode_and_end<B>(&self, msg: B) -> (EncodedBuf<B::Buf>, bool)
pub(super) fn encode_and_end<B>(&self, msg: B, dst: &mut WriteBuf<EncodedBuf<B::Buf>>) -> bool
where
B: IntoBuf,
{
@@ -134,13 +135,14 @@ impl Encoder {
let len = msg.remaining();
debug_assert!(len > 0, "encode() called with empty buf");
let (kind, eof) = match self.kind {
match self.kind {
Kind::Chunked => {
trace!("encoding chunked {}B", len);
let buf = ChunkSize::new(len)
.chain(msg)
.chain(StaticBuf(b"\r\n0\r\n\r\n"));
(BufKind::Chunked(buf), !self.is_last)
dst.buffer(buf);
!self.is_last
},
Kind::Length(remaining) => {
use std::cmp::Ordering;
@@ -148,25 +150,56 @@ impl Encoder {
trace!("sized write, len = {}", len);
match (len as u64).cmp(&remaining) {
Ordering::Equal => {
(BufKind::Exact(msg), !self.is_last)
dst.buffer(msg);
!self.is_last
},
Ordering::Greater => {
(BufKind::Limited(msg.take(remaining as usize)), !self.is_last)
dst.buffer(msg.take(remaining as usize));
!self.is_last
},
Ordering::Less => {
(BufKind::Exact(msg), false)
dst.buffer(msg);
false
}
}
},
Kind::CloseDelimited => {
trace!("close delimited write {}B", len);
(BufKind::Exact(msg), false)
dst.buffer(msg);
false
}
};
}
}
(EncodedBuf {
kind,
}, eof)
/// Encodes the full body, without verifying the remaining length matches.
///
/// This is used in conjunction with Payload::__hyper_full_data(), which
/// means we can trust that the buf has the correct size (the buf itself
/// was checked to make the headers).
pub(super) fn danger_full_buf<B>(self, msg: B, dst: &mut WriteBuf<EncodedBuf<B::Buf>>)
where
B: IntoBuf,
{
let msg = msg.into_buf();
debug_assert!(msg.remaining() > 0, "encode() called with empty buf");
debug_assert!(match self.kind {
Kind::Length(len) => len == msg.remaining() as u64,
_ => true,
}, "danger_full_buf length mismatches");
match self.kind {
Kind::Chunked => {
let len = msg.remaining();
trace!("encoding chunked {}B", len);
let buf = ChunkSize::new(len)
.chain(msg)
.chain(StaticBuf(b"\r\n0\r\n\r\n"));
dst.buffer(buf);
},
_ => {
dst.buffer(msg);
},
}
}
}
@@ -283,6 +316,30 @@ impl fmt::Write for ChunkSize {
}
}
impl<B: Buf> From<B> for EncodedBuf<B> {
fn from(buf: B) -> Self {
EncodedBuf {
kind: BufKind::Exact(buf),
}
}
}
impl<B: Buf> From<Take<B>> for EncodedBuf<B> {
fn from(buf: Take<B>) -> Self {
EncodedBuf {
kind: BufKind::Limited(buf),
}
}
}
impl<B: Buf> From<Chain<Chain<ChunkSize, B>, StaticBuf>> for EncodedBuf<B> {
fn from(buf: Chain<Chain<ChunkSize, B>, StaticBuf>) -> Self {
EncodedBuf {
kind: BufKind::Chunked(buf),
}
}
}
#[cfg(test)]
mod tests {
use bytes::{BufMut};

View File

@@ -99,7 +99,11 @@ where
&mut buf.bytes
}
pub fn buffer(&mut self, buf: B) {
pub(super) fn write_buf(&mut self) -> &mut WriteBuf<B> {
&mut self.write_buf
}
pub fn buffer<BB: Buf + Into<B>>(&mut self, buf: BB) {
self.write_buf.buffer(buf)
}
@@ -300,7 +304,7 @@ impl<T: AsRef<[u8]>> Buf for Cursor<T> {
}
// an internal buffer to collect writes before flushes
struct WriteBuf<B> {
pub(super) struct WriteBuf<B> {
/// Re-usable buffer that holds message headers
headers: Cursor<Vec<u8>>,
max_buf_size: usize,
@@ -334,7 +338,7 @@ where
WriteBufAuto::new(self)
}
fn buffer(&mut self, buf: B) {
pub(super) fn buffer<BB: Buf + Into<B>>(&mut self, buf: BB) {
debug_assert!(buf.has_remaining());
match self.strategy {
Strategy::Flatten => {
@@ -342,7 +346,7 @@ where
head.bytes.put(buf);
},
Strategy::Auto | Strategy::Queue => {
self.queue.bufs.push_back(buf);
self.queue.bufs.push_back(buf.into());
},
}
}

View File

@@ -431,6 +431,11 @@ where
};
}
if !Server::can_have_body(msg.req_method, msg.head.subject) {
trace!("body not allowed for {:?} {:?}", msg.req_method, msg.head.subject);
encoder = Encoder::length(0);
}
// cached date is much faster than formatting every request
if !wrote_date {
dst.reserve(date::DATE_VALUE_LENGTH + 8);
@@ -479,41 +484,9 @@ where
}
impl Server<()> {
/*
fn set_length(head: &mut MessageHead<StatusCode>, body: Option<BodyLength>, method: Option<&Method>) -> Encoder {
// these are here thanks to borrowck
// `if method == Some(&Method::Get)` says the RHS doesn't live long enough
const HEAD: Option<&'static Method> = Some(&Method::HEAD);
const CONNECT: Option<&'static Method> = Some(&Method::CONNECT);
let can_have_body = {
if method == HEAD {
false
} else if method == CONNECT && head.subject.is_success() {
false
} else {
match head.subject {
// TODO: support for 1xx codes needs improvement everywhere
// would be 100...199 => false
StatusCode::SWITCHING_PROTOCOLS |
StatusCode::NO_CONTENT |
StatusCode::NOT_MODIFIED => false,
_ => true,
}
}
};
if let (Some(body), true) = (body, can_have_body) {
set_length(&mut head.headers, body, head.version == Version::HTTP_11)
} else {
head.headers.remove(header::TRANSFER_ENCODING);
if can_have_body {
headers::content_length_zero(&mut head.headers);
}
Encoder::length(0)
}
fn can_have_body(method: &Option<Method>, status: StatusCode) -> bool {
Server::can_chunked(method, status)
}
*/
fn can_chunked(method: &Option<Method>, status: StatusCode) -> bool {
if method == &Some(Method::HEAD) {