Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions benches/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ version = "0.0.0"
edition = "2018"
publish = false

# `benches` is excluded from the root workspace, so declare an empty workspace
# table to let it build as a standalone package.
[workspace]

[dependencies]
bytes = "1"
fnv = "1.0.5"
Expand Down Expand Up @@ -41,3 +45,8 @@ path = "src/method.rs"
[[bench]]
name = "uri"
path = "src/uri.rs"

[[bench]]
name = "opt_paths"
path = "src/opt_paths.rs"
harness = false
68 changes: 68 additions & 0 deletions benches/src/opt_paths.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use http::header::*;
use http::{HeaderValue, Uri};

static SHORT: &[u8] = b"localhost";
static LONG: &[u8] = b"Mozilla/5.0 (X11; CrOS x86_64 9592.71.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.80 Safari/537.36";

const REL: &str = "/wp-content/uploads/2010/03/hello-kitty-darth-vader-pink.jpg";
const REL_QUERY: &str = "/wp-content/uploads/2010/03/hello-kitty-darth-vader-pink.jpg?foo=bar&baz=quux";
const ABS: &str = "https://www.example.com/wp-content/uploads/hello.jpg?foo=bar";

const STD: &[HeaderName] = &[
HOST, CONTENT_TYPE, CONTENT_LENGTH, ACCEPT, ACCEPT_ENCODING, USER_AGENT,
CONNECTION, CACHE_CONTROL, DATE, SERVER,
];

fn header_value(c: &mut Criterion) {
c.bench_function("hv_from_bytes_short", |b| {
b.iter(|| HeaderValue::from_bytes(black_box(SHORT)).unwrap())
});
c.bench_function("hv_from_bytes_long", |b| {
b.iter(|| HeaderValue::from_bytes(black_box(LONG)).unwrap())
});
let short = HeaderValue::from_bytes(SHORT).unwrap();
let long = HeaderValue::from_bytes(LONG).unwrap();
c.bench_function("hv_to_str_short", |b| {
b.iter(|| black_box(&short).to_str().unwrap())
});
c.bench_function("hv_to_str_long", |b| {
b.iter(|| black_box(&long).to_str().unwrap())
});
}

fn uri(c: &mut Criterion) {
c.bench_function("uri_parse_relative_medium", |b| {
b.iter(|| black_box(REL).parse::<Uri>().unwrap())
});
c.bench_function("uri_parse_relative_query", |b| {
b.iter(|| black_box(REL_QUERY).parse::<Uri>().unwrap())
});
let rel: Uri = REL.parse().unwrap();
let rel_query: Uri = REL_QUERY.parse().unwrap();
let abs: Uri = ABS.parse().unwrap();
c.bench_function("uri_to_string_relative", |b| {
b.iter(|| black_box(&rel).to_string())
});
c.bench_function("uri_to_string_relative_query", |b| {
b.iter(|| black_box(&rel_query).to_string())
});
c.bench_function("uri_to_string_absolute", |b| {
b.iter(|| black_box(&abs).to_string())
});
}

fn header_map(c: &mut Criterion) {
c.bench_function("hm_insert_10_std", |b| {
b.iter(|| {
let mut m = HeaderMap::default();
for hdr in STD {
m.insert(hdr.clone(), "foo");
}
black_box(m)
})
});
}

criterion_group!(benches, header_value, uri, header_map);
criterion_main!(benches);
1 change: 1 addition & 0 deletions src/header/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3721,6 +3721,7 @@ fn probe_distance(mask: Size, hash: HashValue, current: usize) -> usize {
current.wrapping_sub(desired_pos(mask, hash)) & mask as usize
}

#[inline]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems uncontroversial enough, and separate enough, that it could be a separate PR and we smash it through 🤓

fn hash_elem_using<K>(danger: &Danger, k: &K) -> HashValue
where
K: Hash + ?Sized,
Expand Down
18 changes: 12 additions & 6 deletions src/header/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,13 @@ impl HeaderValue {
src: T,
into: F,
) -> Result<HeaderValue, InvalidHeaderValue> {
// Avoid an early return so the loop vectorizes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense! Looks much better in all the benchmarks.

I guess the only thing to consider is that it does mean a long value with a bad byte towards the beginning will need to do more work, now. I suppose that's not really any worse than a long value with a bad byte at the end.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The vectorization is really quite valuable for perf, so I think the edge case is worth it.

let mut bad = false;
for &b in src.as_ref() {
if !is_valid(b) {
return Err(InvalidHeaderValue { _priv: () });
}
bad |= !is_valid(b);
}
if bad {
return Err(InvalidHeaderValue { _priv: () });
}
Ok(HeaderValue {
inner: into(src),
Expand All @@ -240,10 +243,13 @@ impl HeaderValue {
pub fn to_str(&self) -> Result<&str, ToStrError> {
let bytes = self.as_ref();

// Avoid an early return so the loop vectorizes.
let mut bad = false;
for &b in bytes {
if !is_visible_ascii(b) {
return Err(ToStrError { _priv: () });
}
bad |= !is_visible_ascii(b);
}
if bad {
return Err(ToStrError { _priv: () });
}

unsafe { Ok(str::from_utf8_unchecked(bytes)) }
Expand Down
10 changes: 6 additions & 4 deletions src/uri/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1032,17 +1032,19 @@ impl Default for Uri {
impl fmt::Display for Uri {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(scheme) = self.scheme() {
write!(f, "{}://", scheme)?;
f.write_str(scheme.as_str())?;
f.write_str("://")?;
}

if let Some(authority) = self.authority() {
write!(f, "{}", authority)?;
f.write_str(authority.as_str())?;
}

write!(f, "{}", self.path())?;
f.write_str(self.path())?;

if let Some(query) = self.query() {
write!(f, "?{}", query)?;
f.write_str("?")?;
f.write_str(query)?;
}

Ok(())
Expand Down
123 changes: 71 additions & 52 deletions src/uri/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,11 +278,14 @@ impl fmt::Display for PathAndQuery {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
if !self.data.is_empty() {
match self.data.as_bytes()[0] {
b'/' | b'*' => write!(fmt, "{}", &self.data[..]),
_ => write!(fmt, "/{}", &self.data[..]),
b'/' | b'*' => fmt.write_str(&self.data),
_ => {
fmt.write_str("/")?;
fmt.write_str(&self.data)
}
}
} else {
write!(fmt, "/")
fmt.write_str("/")
}
}
}
Expand Down Expand Up @@ -405,6 +408,62 @@ struct Scanned {
is_maybe_not_utf8: bool,
}

// Per-byte character classes for the path and query scanners.
const CLASS_VALID: u8 = 0;
const CLASS_QUERY: u8 = 1;
const CLASS_FRAGMENT: u8 = 2;
const CLASS_HIGH: u8 = 3;
const CLASS_INVALID: u8 = 4;

const fn build_path_map() -> [u8; 256] {
let mut t = [CLASS_INVALID; 256];
let mut i = 0;
while i < 256 {
// See https://url.spec.whatwg.org/#path-state
t[i] = match i as u8 {
b'?' => CLASS_QUERY,
b'#' => CLASS_FRAGMENT,

// Bytes that don't need to be percent-encoded in the path.
0x21 | 0x24..=0x3B | 0x3D | 0x40..=0x5F | 0x61..=0x7A | 0x7C | 0x7E => CLASS_VALID,

// Potentially utf8, checked later.
0x80..=0xFF => CLASS_HIGH,

// Should be percent-encoded, but accepted for parity with clients
// that send them as-is (e.g. JSON embedded in the path).
b'"' | b'{' | b'}' => CLASS_VALID,

_ => CLASS_INVALID,
};
i += 1;
}
t
}

const fn build_query_map() -> [u8; 256] {
let mut t = [CLASS_INVALID; 256];
let mut i = 0;
while i < 256 {
// See https://url.spec.whatwg.org/#query-state
t[i] = match i as u8 {
b'#' => CLASS_FRAGMENT,

// Allowed: 0x21 / 0x24 - 0x3B / 0x3D / 0x3F - 0x7E
0x21 | 0x24..=0x3B | 0x3D | 0x3F..=0x7E => CLASS_VALID,

0x80..=0xFF => CLASS_HIGH,

_ => CLASS_INVALID,
};
i += 1;
}
t
}

const PATH_MAP: [u8; 256] = build_path_map();
const QUERY_MAP: [u8; 256] = build_query_map();

const fn scan_path_and_query(bytes: &[u8]) -> Result<Scanned, ErrorKind> {
let mut i = 0;
let mut query = NONE;
Expand All @@ -429,49 +488,21 @@ const fn scan_path_and_query(bytes: &[u8]) -> Result<Scanned, ErrorKind> {
}

while i < bytes.len() {
// See https://url.spec.whatwg.org/#path-state
match bytes[i] {
b'?' => {
match PATH_MAP[bytes[i] as usize] {
CLASS_VALID => {}
CLASS_QUERY => {
debug_assert!(query == NONE);
query = i as u16;
i += 1;
break;
}
b'#' => {
CLASS_FRAGMENT => {
fragment = Some(i as u16);
break;
}

// This is the range of bytes that don't need to be
// percent-encoded in the path. If it should have been
// percent-encoded, then error.
#[rustfmt::skip]
0x21 |
0x24..=0x3B |
0x3D |
0x40..=0x5F |
0x61..=0x7A |
0x7C |
0x7E => {}

// potentially utf8, might not, should check
0x80..=0xFF => {
CLASS_HIGH => {
is_maybe_not_utf8 = true;
}

// These are code points that are supposed to be
// percent-encoded in the path but there are clients
// out there sending them as is and httparse accepts
// to parse those requests, so they are allowed here
// for parity.
//
// For reference, those are code points that are used
// to send requests with JSON directly embedded in
// the URI path. Yes, those things happen for real.
#[rustfmt::skip]
b'"' |
b'{' | b'}' => {}

_ => return Err(ErrorKind::InvalidUriChar),
}
i += 1;
Expand All @@ -480,27 +511,15 @@ const fn scan_path_and_query(bytes: &[u8]) -> Result<Scanned, ErrorKind> {
// query ...
if query != NONE {
while i < bytes.len() {
match bytes[i] {
// While queries *should* be percent-encoded, most
// bytes are actually allowed...
// See https://url.spec.whatwg.org/#query-state
//
// Allowed: 0x21 / 0x24 - 0x3B / 0x3D / 0x3F - 0x7E
#[rustfmt::skip]
0x21 |
0x24..=0x3B |
0x3D |
0x3F..=0x7E => {}

0x80..=0xFF => {
match QUERY_MAP[bytes[i] as usize] {
CLASS_VALID => {}
CLASS_HIGH => {
is_maybe_not_utf8 = true;
}

b'#' => {
CLASS_FRAGMENT => {
fragment = Some(i as u16);
break;
}

_ => return Err(ErrorKind::InvalidUriChar),
}
i += 1;
Expand Down