Skip to content

Commit 97b595e

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 739d5e6 commit 97b595e

File tree

2 files changed

+60
-1
lines changed

2 files changed

+60
-1
lines changed

src/proto/h1/io.rs

+9-1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const MAX_BUF_LIST_BUFFERS: usize = 16;
4040
pub(crate) struct Buffered<T, B> {
4141
flush_pipeline: bool,
4242
io: T,
43+
partial_len: Option<usize>,
4344
read_blocked: bool,
4445
read_buf: BytesMut,
4546
read_buf_strategy: ReadStrategy,
@@ -73,6 +74,7 @@ where
7374
Buffered {
7475
flush_pipeline: false,
7576
io,
77+
partial_len: None,
7678
read_blocked: false,
7779
read_buf: BytesMut::with_capacity(0),
7880
read_buf_strategy: ReadStrategy::default(),
@@ -184,6 +186,7 @@ where
184186
loop {
185187
match super::role::parse_headers::<S>(
186188
&mut self.read_buf,
189+
self.partial_len,
187190
ParseContext {
188191
cached_headers: parse_ctx.cached_headers,
189192
req_method: parse_ctx.req_method,
@@ -220,11 +223,13 @@ where
220223
.reset(Instant::now() + Duration::from_secs(30 * 24 * 60 * 60));
221224
}
222225
}
226+
self.partial_len = None;
223227
return Poll::Ready(Ok(msg));
224228
}
225229
None => {
226230
let max = self.read_buf_strategy.max();
227-
if self.read_buf.len() >= max {
231+
let curr_len = self.read_buf.len();
232+
if curr_len >= max {
228233
debug!("max_buf_size ({}) reached, closing", max);
229234
return Poll::Ready(Err(crate::Error::new_too_large()));
230235
}
@@ -242,6 +247,9 @@ where
242247
}
243248
}
244249
}
250+
if curr_len > 0 {
251+
self.partial_len = Some(curr_len);
252+
}
245253
}
246254
}
247255
if ready!(self.poll_read_from_io(cx)).map_err(crate::Error::new_io)? == 0 {

src/proto/h1/role.rs

+51
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ macro_rules! maybe_panic {
6262

6363
pub(super) fn parse_headers<T>(
6464
bytes: &mut BytesMut,
65+
prev_len: Option<usize>,
6566
ctx: ParseContext<'_>,
6667
) -> ParseResult<T::Incoming>
6768
where
@@ -97,9 +98,37 @@ where
9798
let span = trace_span!("parse_headers");
9899
let _s = span.enter();
99100

101+
if let Some(prev_len) = prev_len {
102+
if !is_complete_fast(bytes, prev_len) {
103+
return Ok(None);
104+
}
105+
}
106+
100107
T::parse(bytes, ctx)
101108
}
102109

110+
/// A fast scan for the end of a message.
111+
/// Used when there was a partial read, to skip full parsing on a
112+
/// a slow connection.
113+
fn is_complete_fast(bytes: &[u8], prev_len: usize) -> bool {
114+
let start = if prev_len < 3 { 0 } else { prev_len - 3 };
115+
let bytes = &bytes[start..];
116+
117+
for (i, b) in bytes.iter().copied().enumerate() {
118+
if b == b'\r' {
119+
if bytes[i + 1..].chunks(3).next() == Some(&b"\n\r\n"[..]) {
120+
return true;
121+
}
122+
} else if b == b'\n' {
123+
if bytes.get(i + 1) == Some(&b'\n') {
124+
return true;
125+
}
126+
}
127+
}
128+
129+
false
130+
}
131+
103132
pub(super) fn encode_headers<T>(
104133
enc: Encode<'_, T::Outgoing>,
105134
dst: &mut Vec<u8>,
@@ -2635,6 +2664,28 @@ mod tests {
26352664
assert_eq!(parsed.head.headers["server"], "hello\tworld");
26362665
}
26372666

2667+
#[test]
2668+
fn test_is_complete_fast() {
2669+
let s = b"GET / HTTP/1.1\r\na: b\r\n\r\n";
2670+
for n in 0..s.len() {
2671+
assert!(is_complete_fast(s, n), "{:?}; {}", s, n);
2672+
}
2673+
let s = b"GET / HTTP/1.1\na: b\n\n";
2674+
for n in 0..s.len() {
2675+
assert!(is_complete_fast(s, n));
2676+
}
2677+
2678+
// Not
2679+
let s = b"GET / HTTP/1.1\r\na: b\r\n\r";
2680+
for n in 0..s.len() {
2681+
assert!(!is_complete_fast(s, n));
2682+
}
2683+
let s = b"GET / HTTP/1.1\na: b\n";
2684+
for n in 0..s.len() {
2685+
assert!(!is_complete_fast(s, n));
2686+
}
2687+
}
2688+
26382689
#[test]
26392690
fn test_write_headers_orig_case_empty_value() {
26402691
let mut headers = HeaderMap::new();

0 commit comments

Comments
 (0)