Skip to content

Commit 3900a23

Browse files
committedOct 15, 2024·
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 <datong.sun@konghq.com>
1 parent c86a6bc commit 3900a23

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
@@ -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,19 @@ 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+
if curr_len > 0 {
208+
self.partial_len = Some(curr_len);
209+
}
202210
}
203211
}
204212
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
@@ -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,37 @@ 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 { 0 } else { prev_len - 3 };
96+
let bytes = &bytes[start..];
97+
98+
for (i, b) in bytes.iter().copied().enumerate() {
99+
if b == b'\r' {
100+
if bytes[i + 1..].chunks(3).next() == Some(&b"\n\r\n"[..]) {
101+
return true;
102+
}
103+
} else if b == b'\n' {
104+
if bytes.get(i + 1) == Some(&b'\n') {
105+
return true;
106+
}
107+
}
108+
}
109+
110+
false
111+
}
112+
84113
pub(super) fn encode_headers<T>(
85114
enc: Encode<'_, T::Outgoing>,
86115
dst: &mut Vec<u8>,
@@ -2827,6 +2856,28 @@ mod tests {
28272856
parse(Some(200), 210, false);
28282857
}
28292858

2859+
#[test]
2860+
fn test_is_complete_fast() {
2861+
let s = b"GET / HTTP/1.1\r\na: b\r\n\r\n";
2862+
for n in 0..s.len() {
2863+
assert!(is_complete_fast(s, n), "{:?}; {}", s, n);
2864+
}
2865+
let s = b"GET / HTTP/1.1\na: b\n\n";
2866+
for n in 0..s.len() {
2867+
assert!(is_complete_fast(s, n));
2868+
}
2869+
2870+
// Not
2871+
let s = b"GET / HTTP/1.1\r\na: b\r\n\r";
2872+
for n in 0..s.len() {
2873+
assert!(!is_complete_fast(s, n));
2874+
}
2875+
let s = b"GET / HTTP/1.1\na: b\n";
2876+
for n in 0..s.len() {
2877+
assert!(!is_complete_fast(s, n));
2878+
}
2879+
}
2880+
28302881
#[test]
28312882
fn test_write_headers_orig_case_empty_value() {
28322883
let mut headers = HeaderMap::new();

0 commit comments

Comments
 (0)
Please sign in to comment.