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:
37
src/body.rs
37
src/body.rs
@@ -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>() {}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))))
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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());
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user