Skip to content

Commit 45b60fe

Browse files
authored
Improve granularity of benchmarks (#122)
To provide better baselines for perf work, etc... It unfortunately requires exposing a few functions as public, though they are hidden from the docs and re-exported under a `_benchable` mod. They have warnings. Don't use them. We will change them. You will be sad. You've been warned.
1 parent b1633ae commit 45b60fe

File tree

3 files changed

+150
-21
lines changed

3 files changed

+150
-21
lines changed

benches/parse.rs

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,112 @@ fn resp_short(c: &mut Criterion) {
8383
}));
8484
}
8585

86+
fn uri(c: &mut Criterion) {
87+
fn _uri(c: &mut Criterion, name: &str, input: &'static [u8]) {
88+
c.benchmark_group("uri")
89+
.throughput(Throughput::Bytes(input.len() as u64))
90+
.bench_function(name, |b| b.iter(|| {
91+
black_box({
92+
let mut b = httparse::_benchable::Bytes::new(input);
93+
httparse::_benchable::parse_uri(&mut b).unwrap()
94+
});
95+
}));
96+
}
97+
98+
const S: &[u8] = b" ";
99+
const CHUNK64: &[u8] = b"/wp-content/uploads/2022/08/31/hello-kitty-darth-vader-pink.webp";
100+
let chunk_4k = CHUNK64.repeat(64);
101+
102+
// 1b to 4096b
103+
for p in 0..=12 {
104+
let n = 1 << p;
105+
_uri(c, &format!("uri_{}b", n), [chunk_4k[..n].to_vec(), S.into()].concat().leak());
106+
}
107+
}
108+
109+
fn header(c: &mut Criterion) {
110+
fn _header(c: &mut Criterion, name: &str, input: &'static [u8]) {
111+
let mut headers = [httparse::EMPTY_HEADER; 128];
112+
c.benchmark_group("header")
113+
.throughput(Throughput::Bytes(input.len() as u64))
114+
.bench_function(name, |b| b.iter(|| {
115+
black_box({
116+
let _ = httparse::parse_headers(input, &mut headers).unwrap();
117+
});
118+
}));
119+
}
120+
121+
const RN: &[u8] = b"\r\n";
122+
const RNRN: &[u8] = b"\r\n\r\n";
123+
const TINY_RN: &[u8] = b"a: b\r\n"; // minimal header line
124+
const XFOOBAR: &[u8] = b"X-Foobar";
125+
let xfoobar_4k = XFOOBAR.repeat(4096/XFOOBAR.len());
126+
127+
// header names 1b to 4096b
128+
for p in 0..=12 {
129+
let n = 1 << p;
130+
let payload = [&xfoobar_4k[..n], b": b", RNRN].concat().leak();
131+
_header(c, &format!("name_{}b", n), payload);
132+
}
133+
134+
// header values 1b to 4096b
135+
for p in 0..=12 {
136+
let n = 1 << p;
137+
let payload = [b"a: ", &xfoobar_4k[..n], RNRN].concat().leak();
138+
_header(c, &format!("value_{}b", n), payload);
139+
}
140+
141+
// 1 to 128
142+
for p in 0..=7 {
143+
let n = 1 << p;
144+
_header(c, &format!("count_{}", n), [TINY_RN.repeat(n), RN.into()].concat().leak());
145+
}
146+
}
147+
148+
fn version(c: &mut Criterion) {
149+
fn _version(c: &mut Criterion, name: &str, input: &'static [u8]) {
150+
c.benchmark_group("version")
151+
.throughput(Throughput::Bytes(input.len() as u64))
152+
.bench_function(name, |b| b.iter(|| {
153+
black_box({
154+
let mut b = httparse::_benchable::Bytes::new(input);
155+
httparse::_benchable::parse_version(&mut b).unwrap()
156+
});
157+
}));
158+
}
159+
160+
_version(c, "http10", b"HTTP/1.0\r\n");
161+
_version(c, "http11", b"HTTP/1.1\r\n");
162+
_version(c, "partial", b"HTTP/1.");
163+
}
164+
165+
fn method(c: &mut Criterion) {
166+
fn _method(c: &mut Criterion, name: &str, input: &'static [u8]) {
167+
c.benchmark_group("method")
168+
.throughput(Throughput::Bytes(input.len() as u64))
169+
.bench_function(name, |b| b.iter(|| {
170+
black_box({
171+
let mut b = httparse::_benchable::Bytes::new(input);
172+
httparse::_benchable::parse_method(&mut b).unwrap()
173+
});
174+
}));
175+
}
176+
177+
// Common methods should be fast-pathed
178+
const COMMON_METHODS: &[&str] = &["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"];
179+
for method in COMMON_METHODS {
180+
_method(c, &method.to_lowercase(), format!("{} / HTTP/1.1\r\n", method).into_bytes().leak());
181+
}
182+
// Custom methods should be infrequent and thus not worth optimizing
183+
_method(c, "custom", b"CUSTOM / HTTP/1.1\r\n");
184+
}
185+
186+
const WARMUP: Duration = Duration::from_millis(100);
187+
const MTIME: Duration = Duration::from_millis(100);
188+
const SAMPLES: usize = 200;
86189
criterion_group!{
87190
name = benches;
88-
config = Criterion::default().sample_size(100).measurement_time(Duration::from_secs(10));
89-
targets = req, req_short, resp, resp_short
191+
config = Criterion::default().sample_size(SAMPLES).warm_up_time(WARMUP).measurement_time(MTIME);
192+
targets = req, req_short, resp, resp_short, uri, header, version, method
90193
}
91194
criterion_main!(benches);

src/iter.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ use core::slice;
22
use core::convert::TryInto;
33
use core::convert::TryFrom;
44

5+
#[allow(missing_docs)]
56
pub struct Bytes<'a> {
67
slice: &'a [u8],
78
pos: usize
89
}
910

11+
#[allow(missing_docs)]
1012
impl<'a> Bytes<'a> {
1113
#[inline]
1214
pub fn new(slice: &'a [u8]) -> Bytes<'a> {

src/lib.rs

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ mod iter;
3030
#[macro_use] mod macros;
3131
mod simd;
3232

33+
#[doc(hidden)]
34+
// Expose some internal functions so we can bench them individually
35+
// WARNING: Exported for internal benchmarks, not fit for public consumption
36+
pub mod _benchable {
37+
pub use super::parse_uri;
38+
pub use super::parse_version;
39+
pub use super::parse_method;
40+
pub use super::iter::Bytes;
41+
}
42+
3343
/// Determines if byte is a token char.
3444
///
3545
/// > ```notrust
@@ -476,23 +486,7 @@ impl<'h, 'b> Request<'h, 'b> {
476486
let orig_len = buf.len();
477487
let mut bytes = Bytes::new(buf);
478488
complete!(skip_empty_lines(&mut bytes));
479-
const GET: [u8; 4] = *b"GET ";
480-
const POST: [u8; 4] = *b"POST";
481-
let method = match bytes.peek_n::<[u8; 4]>(4) {
482-
Some(GET) => {
483-
unsafe {
484-
bytes.advance_and_commit(4);
485-
}
486-
"GET"
487-
}
488-
Some(POST) if bytes.peek_ahead(4) == Some(b' ') => {
489-
unsafe {
490-
bytes.advance_and_commit(5);
491-
}
492-
"POST"
493-
}
494-
_ => complete!(parse_token(&mut bytes)),
495-
};
489+
let method = complete!(parse_method(&mut bytes));
496490
self.method = Some(method);
497491
if config.allow_multiple_spaces_in_request_line_delimiters {
498492
complete!(skip_spaces(&mut bytes));
@@ -743,7 +737,10 @@ impl<'a> fmt::Debug for Header<'a> {
743737
pub const EMPTY_HEADER: Header<'static> = Header { name: "", value: b"" };
744738

745739
#[inline]
746-
fn parse_version(bytes: &mut Bytes<'_>) -> Result<u8> {
740+
#[doc(hidden)]
741+
#[allow(missing_docs)]
742+
// WARNING: Exported for internal benchmarks, not fit for public consumption
743+
pub fn parse_version(bytes: &mut Bytes) -> Result<u8> {
747744
if let Some(eight) = bytes.peek_n::<[u8; 8]>(8) {
748745
unsafe { bytes.advance(8); }
749746
return match &eight {
@@ -767,6 +764,30 @@ fn parse_version(bytes: &mut Bytes<'_>) -> Result<u8> {
767764
Ok(Status::Partial)
768765
}
769766

767+
#[inline]
768+
#[doc(hidden)]
769+
#[allow(missing_docs)]
770+
// WARNING: Exported for internal benchmarks, not fit for public consumption
771+
pub fn parse_method<'a>(bytes: &mut Bytes<'a>) -> Result<&'a str> {
772+
const GET: [u8; 4] = *b"GET ";
773+
const POST: [u8; 4] = *b"POST";
774+
match bytes.peek_n::<[u8; 4]>(4) {
775+
Some(GET) => {
776+
unsafe {
777+
bytes.advance_and_commit(4);
778+
}
779+
Ok(Status::Complete("GET"))
780+
}
781+
Some(POST) if bytes.peek_ahead(4) == Some(b' ') => {
782+
unsafe {
783+
bytes.advance_and_commit(5);
784+
}
785+
Ok(Status::Complete("POST"))
786+
}
787+
_ => parse_token(bytes),
788+
}
789+
}
790+
770791
/// From [RFC 7230](https://tools.ietf.org/html/rfc7230):
771792
///
772793
/// > ```notrust
@@ -838,7 +859,10 @@ fn parse_token<'a>(bytes: &mut Bytes<'a>) -> Result<&'a str> {
838859
}
839860

840861
#[inline]
841-
fn parse_uri<'a>(bytes: &mut Bytes<'a>) -> Result<&'a str> {
862+
#[doc(hidden)]
863+
#[allow(missing_docs)]
864+
// WARNING: Exported for internal benchmarks, not fit for public consumption
865+
pub fn parse_uri<'a>(bytes: &mut Bytes<'a>) -> Result<&'a str> {
842866
let b = next!(bytes);
843867
if !is_uri_token(b) {
844868
// First char must be a URI char, it can't be a space which would indicate an empty path.

0 commit comments

Comments
 (0)