Skip to content

Commit 0332887

Browse files
committed
perf(http1): improve parsing of sequentially partial messages
If request headers are received in incremental partial chunks, hyper would restart parsing each time. This is because the HTTP/1 parser is stateless, since the most common case is a full message and stateless parses faster. However, if continuing to receive more partial chunks of the request, each subsequent full parse is slower and slower. Since partial parses is less common, we can store a little bit of state to improve performance in general. Now, if a partial request is received, hyper will check for the end of the message quickly, and if not found, simply save the length to allow the next partial chunk to start its search from there. Only once the end is found will a fill parse happen. Reported-by: Datong Sun <[email protected]>
1 parent c86a6bc commit 0332887

File tree

2 files changed

+63
-1
lines changed

2 files changed

+63
-1
lines changed

src/proto/h1/io.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const MAX_BUF_LIST_BUFFERS: usize = 16;
3232
pub(crate) struct Buffered<T, B> {
3333
flush_pipeline: bool,
3434
io: T,
35+
partial_len: Option<usize>,
3536
read_blocked: bool,
3637
read_buf: BytesMut,
3738
read_buf_strategy: ReadStrategy,
@@ -65,6 +66,7 @@ where
6566
Buffered {
6667
flush_pipeline: false,
6768
io,
69+
partial_len: None,
6870
read_blocked: false,
6971
read_buf: BytesMut::with_capacity(0),
7072
read_buf_strategy: ReadStrategy::default(),
@@ -176,6 +178,7 @@ where
176178
loop {
177179
match super::role::parse_headers::<S>(
178180
&mut self.read_buf,
181+
self.partial_len,
179182
ParseContext {
180183
cached_headers: parse_ctx.cached_headers,
181184
req_method: parse_ctx.req_method,
@@ -191,14 +194,17 @@ where
191194
)? {
192195
Some(msg) => {
193196
debug!("parsed {} headers", msg.head.headers.len());
197+
self.partial_len = None;
194198
return Poll::Ready(Ok(msg));
195199
}
196200
None => {
197201
let max = self.read_buf_strategy.max();
198-
if self.read_buf.len() >= max {
202+
let curr_len = self.read_buf.len();
203+
if curr_len >= max {
199204
debug!("max_buf_size ({}) reached, closing", max);
200205
return Poll::Ready(Err(crate::Error::new_too_large()));
201206
}
207+
self.partial_len = Some(curr_len);
202208
}
203209
}
204210
if ready!(self.poll_read_from_io(cx)).map_err(crate::Error::new_io)? == 0 {

src/proto/h1/role.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ macro_rules! maybe_panic {
6666

6767
pub(super) fn parse_headers<T>(
6868
bytes: &mut BytesMut,
69+
prev_len: Option<usize>,
6970
ctx: ParseContext<'_>,
7071
) -> ParseResult<T::Incoming>
7172
where
@@ -78,9 +79,42 @@ where
7879

7980
let _entered = trace_span!("parse_headers");
8081

82+
if let Some(prev_len) = prev_len {
83+
if !is_complete_fast(bytes, prev_len) {
84+
return Ok(None);
85+
}
86+
}
87+
8188
T::parse(bytes, ctx)
8289
}
8390

91+
/// A fast scan for the end of a message.
92+
/// Used when there was a partial read, to skip full parsing on a
93+
/// a slow connection.
94+
fn is_complete_fast(bytes: &[u8], prev_len: usize) -> bool {
95+
let start = if prev_len < 3 {
96+
0
97+
} else {
98+
prev_len - 3
99+
};
100+
let bytes = &bytes[start..];
101+
102+
for (i, b) in bytes.iter().copied().enumerate() {
103+
if b == b'\r' {
104+
if bytes[i+1..].chunks(3).next() == Some(&b"\n\r\n"[..]) {
105+
return true;
106+
}
107+
} else if b == b'\n' {
108+
if bytes.get(i + 1) == Some(&b'\n') {
109+
return true;
110+
}
111+
}
112+
113+
}
114+
115+
false
116+
}
117+
84118
pub(super) fn encode_headers<T>(
85119
enc: Encode<'_, T::Outgoing>,
86120
dst: &mut Vec<u8>,
@@ -2827,6 +2861,28 @@ mod tests {
28272861
parse(Some(200), 210, false);
28282862
}
28292863

2864+
#[test]
2865+
fn test_is_complete_fast() {
2866+
let s = b"GET / HTTP/1.1\r\na: b\r\n\r\n";
2867+
for n in 0..s.len() {
2868+
assert!(is_complete_fast(s, n), "{:?}; {}", s, n);
2869+
}
2870+
let s = b"GET / HTTP/1.1\na: b\n\n";
2871+
for n in 0..s.len() {
2872+
assert!(is_complete_fast(s, n));
2873+
}
2874+
2875+
// Not
2876+
let s = b"GET / HTTP/1.1\r\na: b\r\n\r";
2877+
for n in 0..s.len() {
2878+
assert!(!is_complete_fast(s, n));
2879+
}
2880+
let s = b"GET / HTTP/1.1\na: b\n";
2881+
for n in 0..s.len() {
2882+
assert!(!is_complete_fast(s, n));
2883+
}
2884+
}
2885+
28302886
#[test]
28312887
fn test_write_headers_orig_case_empty_value() {
28322888
let mut headers = HeaderMap::new();

0 commit comments

Comments
 (0)