Skip to content

Commit d241f21

Browse files
Improve performance (#474)
* benchmark: router.route * refactor(server): reduce allocations * one clone less * one less box * avoid allocations in StripPrefixEndpoint * middleware: lazy cookies * fix: do not set content type twice * cleanup * fix workflow * fix double block
1 parent 7d0b848 commit d241f21

File tree

12 files changed

+154
-62
lines changed

12 files changed

+154
-62
lines changed

.github/workflows/ci.yaml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,13 @@ jobs:
4545
uses: actions-rs/cargo@v1
4646
with:
4747
command: check
48-
args: --all --benches --bins --examples --tests --features unstable
48+
args: --all --bins --examples --tests --features unstable
49+
50+
- name: check benches
51+
uses: actions-rs/cargo@v1
52+
with:
53+
command: check
54+
args: --benches --features __internal__bench
4955

5056
- name: tests
5157
uses: actions-rs/cargo@v1

Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ default = ["h1-server"]
2828
h1-server = ["async-h1"]
2929
docs = ["unstable"]
3030
unstable = []
31+
# DO NOT USE. Only exists to expose internals so they can be benchmarked.
32+
__internal__bench = []
3133

3234
[dependencies]
3335
async-h1 = { version = "1.1.2", optional = true }
@@ -50,8 +52,14 @@ juniper = "0.14.1"
5052
portpicker = "0.1.0"
5153
serde = { version = "1.0.102", features = ["derive"] }
5254
surf = "2.0.0-alpha.1"
55+
criterion = "0.3.1"
5356

5457
[[test]]
5558
name = "nested"
5659
path = "tests/nested.rs"
5760
required-features = ["unstable"]
61+
62+
[[bench]]
63+
name = "router"
64+
harness = false
65+

benches/router.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use criterion::{black_box, criterion_group, criterion_main, Criterion};
2+
use http_types::Method;
3+
use tide::router::Router;
4+
5+
fn criterion_benchmark(c: &mut Criterion) {
6+
let mut router = Router::<()>::new();
7+
router.add(
8+
"hello",
9+
Method::Get,
10+
Box::new(|_| async move { Ok("hello world") }),
11+
);
12+
13+
c.bench_function("route-match", |b| {
14+
b.iter(|| black_box(router.route("/hello", Method::Get)))
15+
});
16+
17+
c.bench_function("route-root", |b| {
18+
b.iter(|| black_box(router.route("", Method::Get)))
19+
});
20+
}
21+
22+
criterion_group!(benches, criterion_benchmark);
23+
criterion_main!(benches);

src/cookies/middleware.rs

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::response::CookieEvent;
22
use crate::utils::BoxFuture;
33
use crate::{Middleware, Next, Request};
44

5-
use crate::http::cookies::{Cookie, CookieJar};
5+
use crate::http::cookies::{Cookie, CookieJar, Delta};
66
use crate::http::headers;
77

88
use std::sync::{Arc, RwLock};
@@ -41,27 +41,32 @@ impl<State: Send + Sync + 'static> Middleware<State> for CookiesMiddleware {
4141
let cookie_jar = if let Some(cookie_data) = ctx.local::<CookieData>() {
4242
cookie_data.content.clone()
4343
} else {
44-
// no cookie data in local context, so we need to create it
4544
let cookie_data = CookieData::from_request(&ctx);
45+
// no cookie data in local context, so we try to create it
4646
let content = cookie_data.content.clone();
4747
ctx = ctx.set_local(cookie_data);
4848
content
4949
};
5050

5151
let mut res = next.run(ctx).await?;
5252

53+
// Don't do anything if there are no cookies.
54+
if res.cookie_events.is_empty() {
55+
return Ok(res);
56+
}
57+
58+
let jar = &mut *cookie_jar.write().unwrap();
59+
5360
// add modifications from response to original
5461
for cookie in res.cookie_events.drain(..) {
5562
match cookie {
56-
CookieEvent::Added(cookie) => cookie_jar.write().unwrap().add(cookie.clone()),
57-
CookieEvent::Removed(cookie) => {
58-
cookie_jar.write().unwrap().remove(cookie.clone())
59-
}
63+
CookieEvent::Added(cookie) => jar.add(cookie.clone()),
64+
CookieEvent::Removed(cookie) => jar.remove(cookie.clone()),
6065
}
6166
}
6267

6368
// iterate over added and removed cookies
64-
for cookie in cookie_jar.read().unwrap().delta() {
69+
for cookie in jar.delta() {
6570
let encoded_cookie = cookie.encoded().to_string();
6671
res = res.append_header(headers::SET_COOKIE, encoded_cookie);
6772
}
@@ -70,16 +75,48 @@ impl<State: Send + Sync + 'static> Middleware<State> for CookiesMiddleware {
7075
}
7176
}
7277

73-
#[derive(Debug, Clone)]
78+
#[derive(Debug, Default, Clone)]
7479
pub(crate) struct CookieData {
75-
pub(crate) content: Arc<RwLock<CookieJar>>,
80+
pub(crate) content: Arc<RwLock<LazyJar>>,
81+
}
82+
83+
#[derive(Debug, Default, Clone)]
84+
/// Wrapper around `CookieJar`, that initializes only when actually used.
85+
pub(crate) struct LazyJar(Option<CookieJar>);
86+
87+
impl LazyJar {
88+
fn add(&mut self, cookie: Cookie<'static>) {
89+
self.get_jar().add(cookie)
90+
}
91+
92+
fn remove(&mut self, cookie: Cookie<'static>) {
93+
self.get_jar().remove(cookie)
94+
}
95+
96+
fn delta(&mut self) -> Delta<'_> {
97+
self.get_jar().delta()
98+
}
99+
100+
pub(crate) fn get(&self, name: &str) -> Option<&Cookie<'static>> {
101+
if let Some(jar) = &self.0 {
102+
return jar.get(name);
103+
}
104+
None
105+
}
106+
107+
fn get_jar(&mut self) -> &mut CookieJar {
108+
if self.0.is_none() {
109+
self.0 = Some(CookieJar::new());
110+
}
111+
112+
self.0.as_mut().unwrap()
113+
}
76114
}
77115

78116
impl CookieData {
79117
pub(crate) fn from_request<S>(req: &Request<S>) -> Self {
80-
let mut jar = CookieJar::new();
81-
82-
if let Some(cookie_headers) = req.header(&headers::COOKIE) {
118+
let jar = if let Some(cookie_headers) = req.header(&headers::COOKIE) {
119+
let mut jar = CookieJar::new();
83120
for cookie_header in cookie_headers {
84121
// spec says there should be only one, so this is permissive
85122
for pair in cookie_header.as_str().split(';') {
@@ -88,9 +125,13 @@ impl CookieData {
88125
}
89126
}
90127
}
91-
}
92128

93-
Self {
129+
LazyJar(Some(jar))
130+
} else {
131+
LazyJar::default()
132+
};
133+
134+
CookieData {
94135
content: Arc::new(RwLock::new(jar)),
95136
}
96137
}

src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,11 @@ mod redirect;
193193
mod request;
194194
mod response;
195195
mod route;
196+
197+
#[cfg(not(feature = "__internal__bench"))]
196198
mod router;
199+
#[cfg(feature = "__internal__bench")]
200+
pub mod router;
197201
mod server;
198202
mod utils;
199203

src/request.rs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,6 @@ impl<State> Request<State> {
182182
.parse()
183183
}
184184

185-
pub(crate) fn rest(&self) -> Option<&str> {
186-
self.route_params
187-
.last()
188-
.and_then(|params| params.find("--tide-path-rest"))
189-
}
190-
191185
/// Reads the entire request body into a byte buffer.
192186
///
193187
/// This method can be called after the body has already been read, but will
@@ -300,12 +294,10 @@ impl<State> Request<State> {
300294
/// returns a `Cookie` by name of the cookie.
301295
#[must_use]
302296
pub fn cookie(&self, name: &str) -> Option<Cookie<'static>> {
303-
let cookie_data = self
304-
.local::<CookieData>()
305-
.expect("should always be set by the cookies middleware");
306-
307-
let locked_jar = cookie_data.content.read().unwrap();
308-
locked_jar.get(name).cloned()
297+
if let Some(cookie_data) = self.local::<CookieData>() {
298+
return cookie_data.content.read().unwrap().get(name).cloned();
299+
}
300+
None
309301
}
310302

311303
/// Get the length of the body.
@@ -386,3 +378,9 @@ impl<'a, State> IntoIterator for &'a mut Request<State> {
386378
self.request.iter_mut()
387379
}
388380
}
381+
382+
pub(crate) fn rest(route_params: &[Params]) -> Option<&str> {
383+
route_params
384+
.last()
385+
.and_then(|params| params.find("--tide-path-rest"))
386+
}

src/response.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,6 @@ impl From<http::Response> for Response {
289289
impl From<String> for Response {
290290
fn from(s: String) -> Self {
291291
let mut res = http_types::Response::new(StatusCode::Ok);
292-
res.set_content_type(http_types::mime::PLAIN);
293292
res.set_body(s);
294293
Self {
295294
res,
@@ -301,7 +300,6 @@ impl From<String> for Response {
301300
impl<'a> From<&'a str> for Response {
302301
fn from(s: &'a str) -> Self {
303302
let mut res = http_types::Response::new(StatusCode::Ok);
304-
res.set_content_type(http_types::mime::PLAIN);
305303
res.set_body(String::from(s));
306304
Self {
307305
res,

src/route.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -275,14 +275,20 @@ impl<E> Clone for StripPrefixEndpoint<E> {
275275
}
276276

277277
impl<State, E: Endpoint<State>> Endpoint<State> for StripPrefixEndpoint<E> {
278-
fn call<'a>(&'a self, mut req: crate::Request<State>) -> BoxFuture<'a, crate::Result> {
279-
let rest = req.rest().unwrap_or("");
280-
let uri = req.uri();
281-
let mut new_uri = uri.clone();
282-
new_uri.set_path(rest);
278+
fn call<'a>(&'a self, req: crate::Request<State>) -> BoxFuture<'a, crate::Result> {
279+
let crate::Request {
280+
state,
281+
mut request,
282+
route_params,
283+
} = req;
283284

284-
*req.request.url_mut() = new_uri;
285+
let rest = crate::request::rest(&route_params).unwrap_or_else(|| "");
286+
request.url_mut().set_path(&rest);
285287

286-
self.0.call(req)
288+
self.0.call(crate::Request {
289+
state,
290+
request,
291+
route_params,
292+
})
287293
}
288294
}

src/router.rs

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,42 +10,38 @@ use crate::{Request, Response, StatusCode};
1010
/// Internally, we have a separate state machine per http method; indexing
1111
/// by the method first allows the table itself to be more efficient.
1212
#[allow(missing_debug_implementations)]
13-
pub(crate) struct Router<State> {
13+
pub struct Router<State> {
1414
method_map: HashMap<http_types::Method, MethodRouter<Box<DynEndpoint<State>>>>,
1515
all_method_router: MethodRouter<Box<DynEndpoint<State>>>,
1616
}
1717

1818
/// The result of routing a URL
19-
pub(crate) struct Selection<'a, State> {
19+
#[allow(missing_debug_implementations)]
20+
pub struct Selection<'a, State> {
2021
pub(crate) endpoint: &'a DynEndpoint<State>,
2122
pub(crate) params: Params,
2223
}
2324

2425
impl<State: 'static> Router<State> {
25-
pub(crate) fn new() -> Self {
26-
Self {
26+
pub fn new() -> Self {
27+
Router {
2728
method_map: HashMap::default(),
2829
all_method_router: MethodRouter::new(),
2930
}
3031
}
3132

32-
pub(crate) fn add(
33-
&mut self,
34-
path: &str,
35-
method: http_types::Method,
36-
ep: Box<DynEndpoint<State>>,
37-
) {
33+
pub fn add(&mut self, path: &str, method: http_types::Method, ep: Box<DynEndpoint<State>>) {
3834
self.method_map
3935
.entry(method)
4036
.or_insert_with(MethodRouter::new)
4137
.add(path, ep)
4238
}
4339

44-
pub(crate) fn add_all(&mut self, path: &str, ep: Box<DynEndpoint<State>>) {
40+
pub fn add_all(&mut self, path: &str, ep: Box<DynEndpoint<State>>) {
4541
self.all_method_router.add(path, ep)
4642
}
4743

48-
pub(crate) fn route(&self, path: &str, method: http_types::Method) -> Selection<'_, State> {
44+
pub fn route(&self, path: &str, method: http_types::Method) -> Selection<'_, State> {
4945
if let Some(Match { handler, params }) = self
5046
.method_map
5147
.get(&method)

src/server.rs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -340,10 +340,26 @@ impl<State: Send + Sync + 'static> Server<State> {
340340
where
341341
R: From<http_types::Response>,
342342
{
343-
let req = Request::new(self.state.clone(), req.into(), Vec::new());
344-
match self.call(req).await {
345-
Ok(res) => {
346-
let res: http_types::Response = res.into();
343+
let req = req.into();
344+
let Self {
345+
router,
346+
state,
347+
middleware,
348+
} = self.clone();
349+
350+
let method = req.method().to_owned();
351+
let Selection { endpoint, params } = router.route(&req.url().path(), method);
352+
let route_params = vec![params];
353+
let req = Request::new(state, req, route_params);
354+
355+
let next = Next {
356+
endpoint,
357+
next_middleware: &middleware,
358+
};
359+
360+
match next.run(req).await {
361+
Ok(value) => {
362+
let res: http_types::Response = value.into();
347363
// We assume that if an error was manually cast to a
348364
// Response that we actually want to send the body to the
349365
// client. At this point we don't scrub the message.

0 commit comments

Comments
 (0)