Skip to content

Commit 4c6f0e2

Browse files
authored
Merge pull request #3107 from spinframework/less-allocations-in-router
Less allocations in router
2 parents 1abed0c + 3bb5b8a commit 4c6f0e2

File tree

10 files changed

+168
-97
lines changed

10 files changed

+168
-97
lines changed

crates/http/src/routes.rs

Lines changed: 113 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use anyhow::{anyhow, Result};
66
use indexmap::IndexMap;
7-
use std::{collections::HashMap, fmt};
7+
use std::{borrow::Cow, collections::HashMap, fmt};
88

99
use crate::config::HttpTriggerRouteConfig;
1010

@@ -22,9 +22,9 @@ struct RouteHandler {
2222
/// The component ID that the route maps to.
2323
component_id: String,
2424
/// The route, including any application base.
25-
based_route: String,
25+
based_route: Cow<'static, str>,
2626
/// The route, not including any application base.
27-
raw_route: String,
27+
raw_route: Cow<'static, str>,
2828
/// The route, including any application base and capturing information about whether it has a trailing wildcard.
2929
/// (This avoids re-parsing the route string.)
3030
parsed_based_route: ParsedRoute,
@@ -111,8 +111,8 @@ impl Router {
111111

112112
let handler = RouteHandler {
113113
component_id: re.component_id.to_string(),
114-
based_route: re.based_route,
115-
raw_route: re.raw_route.to_string(),
114+
based_route: re.based_route.into(),
115+
raw_route: re.raw_route.to_string().into(),
116116
parsed_based_route: parsed,
117117
};
118118

@@ -163,37 +163,24 @@ impl Router {
163163
/// If multiple components could potentially handle the same request based on their
164164
/// defined routes, components with matching exact routes take precedence followed
165165
/// by matching wildcard patterns with the longest matching prefix.
166-
pub fn route(&self, p: &str) -> Result<RouteMatch> {
166+
pub fn route<'path, 'router: 'path>(
167+
&'router self,
168+
path: &'path str,
169+
) -> Result<RouteMatch<'router, 'path>> {
167170
let best_match = self
168171
.router
169-
.best_match(p)
170-
.ok_or_else(|| anyhow!("Cannot match route for path {p}"))?;
172+
.best_match(path)
173+
.ok_or_else(|| anyhow!("Cannot match route for path {path}"))?;
171174

172-
let route_handler = best_match.handler().clone();
173-
let named_wildcards = best_match
174-
.captures()
175-
.iter()
176-
.map(|(k, v)| (k.to_owned(), v.to_owned()))
177-
.collect();
178-
let trailing_wildcard = best_match.captures().wildcard().map(|s|
179-
// Backward compatibility considerations - Spin has traditionally
180-
// captured trailing slashes, but routefinder does not.
181-
match (s.is_empty(), p.ends_with('/')) {
182-
// route: /foo/..., path: /foo
183-
(true, false) => s.to_owned(),
184-
// route: /foo/..., path: /foo/
185-
(true, true) => "/".to_owned(),
186-
// route: /foo/..., path: /foo/bar
187-
(false, false) => format!("/{s}"),
188-
// route: /foo/..., path: /foo/bar/
189-
(false, true) => format!("/{s}/"),
190-
}
191-
);
175+
let route_handler = best_match.handler();
176+
let captures = best_match.captures();
192177

193178
Ok(RouteMatch {
194-
route_handler,
195-
named_wildcards,
196-
trailing_wildcard,
179+
inner: RouteMatchKind::Real {
180+
route_handler,
181+
captures,
182+
path,
183+
},
197184
})
198185
}
199186
}
@@ -235,69 +222,136 @@ impl fmt::Display for ParsedRoute {
235222
}
236223

237224
/// A routing match for a URL.
238-
pub struct RouteMatch {
239-
route_handler: RouteHandler,
240-
named_wildcards: HashMap<String, String>,
241-
trailing_wildcard: Option<String>,
225+
pub struct RouteMatch<'router, 'path> {
226+
inner: RouteMatchKind<'router, 'path>,
242227
}
243228

244-
impl RouteMatch {
229+
impl RouteMatch<'_, '_> {
245230
/// A synthetic match as if the given path was matched against the wildcard route.
246231
/// Used in service chaining.
247-
pub fn synthetic(component_id: &str, path: &str) -> Self {
232+
pub fn synthetic(component_id: String, path: String) -> Self {
248233
Self {
249-
route_handler: RouteHandler {
250-
component_id: component_id.to_string(),
251-
based_route: "/...".to_string(),
252-
raw_route: "/...".to_string(),
253-
parsed_based_route: ParsedRoute::TrailingWildcard(String::new()),
234+
inner: RouteMatchKind::Synthetic {
235+
route_handler: RouteHandler {
236+
component_id,
237+
based_route: "/...".into(),
238+
raw_route: "/...".into(),
239+
parsed_based_route: ParsedRoute::TrailingWildcard(String::new()),
240+
},
241+
trailing_wildcard: path,
254242
},
255-
named_wildcards: Default::default(),
256-
trailing_wildcard: Some(path.to_string()),
257243
}
258244
}
259245

260246
/// The matched component.
261247
pub fn component_id(&self) -> &str {
262-
&self.route_handler.component_id
248+
&self.inner.route_handler().component_id
263249
}
264250

265251
/// The matched route, as originally written in the manifest, combined with the base.
266252
pub fn based_route(&self) -> &str {
267-
&self.route_handler.based_route
253+
&self.inner.route_handler().based_route
268254
}
269255

270256
/// The matched route, excluding any trailing wildcard, combined with the base.
271-
pub fn based_route_or_prefix(&self) -> String {
272-
self.route_handler
257+
pub fn based_route_or_prefix(&self) -> &str {
258+
self.inner
259+
.route_handler()
273260
.based_route
274261
.strip_suffix("/...")
275-
.unwrap_or(&self.route_handler.based_route)
276-
.to_string()
262+
.unwrap_or(&self.inner.route_handler().based_route)
277263
}
278264

279265
/// The matched route, as originally written in the manifest.
280266
pub fn raw_route(&self) -> &str {
281-
&self.route_handler.raw_route
267+
&self.inner.route_handler().raw_route
282268
}
283269

284270
/// The matched route, excluding any trailing wildcard.
285-
pub fn raw_route_or_prefix(&self) -> String {
286-
self.route_handler
271+
pub fn raw_route_or_prefix(&self) -> &str {
272+
self.inner
273+
.route_handler()
287274
.raw_route
288275
.strip_suffix("/...")
289-
.unwrap_or(&self.route_handler.raw_route)
290-
.to_string()
276+
.unwrap_or(&self.inner.route_handler().raw_route)
277+
}
278+
279+
/// The named wildcards captured from the path, if any
280+
pub fn named_wildcards(&self) -> HashMap<&str, &str> {
281+
self.inner.named_wildcards()
282+
}
283+
284+
/// The trailing wildcard part of the path, if any
285+
pub fn trailing_wildcard(&self) -> Cow<'_, str> {
286+
self.inner.trailing_wildcard()
287+
}
288+
}
289+
290+
/// The kind of route match that was made.
291+
///
292+
/// Can either be real based on the routefinder or synthetic based on hardcoded results.
293+
enum RouteMatchKind<'router, 'path> {
294+
/// A synthetic match as if the given path was matched against the wildcard route.
295+
Synthetic {
296+
/// The route handler that matched the path.
297+
route_handler: RouteHandler,
298+
/// The trailing wildcard part of the path
299+
trailing_wildcard: String,
300+
},
301+
/// A real match.
302+
Real {
303+
/// The route handler that matched the path.
304+
route_handler: &'router RouteHandler,
305+
/// The best match for the path.
306+
captures: routefinder::Captures<'router, 'path>,
307+
/// The path that was matched.
308+
path: &'path str,
309+
},
310+
}
311+
312+
impl RouteMatchKind<'_, '_> {
313+
/// The route handler that matched the path.
314+
fn route_handler(&self) -> &RouteHandler {
315+
match self {
316+
RouteMatchKind::Synthetic { route_handler, .. } => route_handler,
317+
RouteMatchKind::Real { route_handler, .. } => route_handler,
318+
}
291319
}
292320

293321
/// The named wildcards captured from the path, if any
294-
pub fn named_wildcards(&self) -> &HashMap<String, String> {
295-
&self.named_wildcards
322+
pub fn named_wildcards(&self) -> HashMap<&str, &str> {
323+
let Self::Real { captures, .. } = &self else {
324+
return HashMap::new();
325+
};
326+
captures.iter().collect()
296327
}
297328

298329
/// The trailing wildcard part of the path, if any
299-
pub fn trailing_wildcard(&self) -> String {
300-
self.trailing_wildcard.clone().unwrap_or_default()
330+
pub fn trailing_wildcard(&self) -> Cow<'_, str> {
331+
let (captures, path) = match self {
332+
// If we have a synthetic match, we already have the trailing wildcard.
333+
Self::Synthetic {
334+
trailing_wildcard, ..
335+
} => return trailing_wildcard.into(),
336+
Self::Real { captures, path, .. } => (captures, path),
337+
};
338+
339+
captures
340+
.wildcard()
341+
.map(|s|
342+
// Backward compatibility considerations - Spin has traditionally
343+
// captured trailing slashes, but routefinder does not.
344+
match (s.is_empty(), path.ends_with('/')) {
345+
// route: /foo/..., path: /foo
346+
(true, false) => s.into(),
347+
// route: /foo/..., path: /foo/
348+
(true, true) => "/".into(),
349+
// route: /foo/..., path: /foo/bar
350+
(false, false) => format!("/{s}").into(),
351+
// route: /foo/..., path: /foo/bar/
352+
(false, true) => format!("/{s}/").into(),
353+
})
354+
.unwrap_or_default()
301355
}
302356
}
303357

crates/http/src/wagi/mod.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ pub fn build_headers(
105105
// https://datatracker.ietf.org/doc/html/rfc3875#section-4.1.13
106106
headers.insert(
107107
"SCRIPT_NAME".to_owned(),
108-
route_match.based_route_or_prefix(),
108+
route_match.based_route_or_prefix().to_owned(),
109109
);
110110
// PATH_INFO is any path information after SCRIPT_NAME
111111
//
@@ -117,7 +117,10 @@ pub fn build_headers(
117117
// https://datatracker.ietf.org/doc/html/rfc3875#section-4.1.5
118118
let pathsegment = path_info;
119119
let pathinfo = percent_encoding::percent_decode_str(&pathsegment).decode_utf8_lossy();
120-
headers.insert("X_RAW_PATH_INFO".to_owned(), pathsegment.clone());
120+
headers.insert(
121+
"X_RAW_PATH_INFO".to_owned(),
122+
pathsegment.as_ref().to_owned(),
123+
);
121124
headers.insert("PATH_INFO".to_owned(), pathinfo.to_string());
122125
// PATH_TRANSLATED is the url-decoded version of PATH_INFO
123126
// https://datatracker.ietf.org/doc/html/rfc3875#section-4.1.6

crates/oci/src/client.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -801,8 +801,8 @@ pub async fn unpack_archive_layer(
801801
fn digest_from_url(manifest_url: &str) -> Option<String> {
802802
// The URL is in the form "https://host/v2/refname/manifests/sha256:..."
803803
let manifest_url = Url::parse(manifest_url).ok()?;
804-
let segments = manifest_url.path_segments()?;
805-
let last = segments.last()?;
804+
let mut segments = manifest_url.path_segments()?;
805+
let last = segments.next_back()?;
806806
if last.contains(':') {
807807
Some(last.to_owned())
808808
} else {

crates/trigger-http/src/headers.rs

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
use std::{net::SocketAddr, str, str::FromStr};
1+
use std::{
2+
borrow::Cow,
3+
net::SocketAddr,
4+
str::{self, FromStr},
5+
};
26

37
use anyhow::Result;
48
use http::Uri;
@@ -21,23 +25,27 @@ pub const RAW_COMPONENT_ROUTE: [&str; 2] = ["SPIN_RAW_COMPONENT_ROUTE", "X_RAW_C
2125
pub const BASE_PATH: [&str; 2] = ["SPIN_BASE_PATH", "X_BASE_PATH"];
2226
pub const CLIENT_ADDR: [&str; 2] = ["SPIN_CLIENT_ADDR", "X_CLIENT_ADDR"];
2327

24-
pub fn compute_default_headers(
28+
// Header key/value pairs that use copy on write to avoid allocation
29+
pub type HeaderPair<'a> = ([Cow<'static, str>; 2], Cow<'a, str>);
30+
31+
/// Compute the default headers to be passed to the component.
32+
pub fn compute_default_headers<'a>(
2533
uri: &Uri,
2634
host: &str,
27-
route_match: &RouteMatch,
35+
route_match: &'a RouteMatch,
2836
client_addr: SocketAddr,
29-
) -> anyhow::Result<Vec<([String; 2], String)>> {
30-
fn owned(strs: &[&'static str; 2]) -> [String; 2] {
31-
[strs[0].to_owned(), strs[1].to_owned()]
37+
) -> anyhow::Result<Vec<HeaderPair<'a>>> {
38+
fn owned(strs: &[&'static str; 2]) -> [Cow<'static, str>; 2] {
39+
[strs[0].into(), strs[1].into()]
3240
}
3341

34-
let owned_full_url: [String; 2] = owned(&FULL_URL);
35-
let owned_path_info: [String; 2] = owned(&PATH_INFO);
36-
let owned_matched_route: [String; 2] = owned(&MATCHED_ROUTE);
37-
let owned_component_route: [String; 2] = owned(&COMPONENT_ROUTE);
38-
let owned_raw_component_route: [String; 2] = owned(&RAW_COMPONENT_ROUTE);
39-
let owned_base_path: [String; 2] = owned(&BASE_PATH);
40-
let owned_client_addr: [String; 2] = owned(&CLIENT_ADDR);
42+
let owned_full_url = owned(&FULL_URL);
43+
let owned_path_info = owned(&PATH_INFO);
44+
let owned_matched_route = owned(&MATCHED_ROUTE);
45+
let owned_component_route = owned(&COMPONENT_ROUTE);
46+
let owned_raw_component_route = owned(&RAW_COMPONENT_ROUTE);
47+
let owned_base_path = owned(&BASE_PATH);
48+
let owned_client_addr = owned(&CLIENT_ADDR);
4149

4250
let mut res = vec![];
4351
let abs_path = uri
@@ -52,21 +60,21 @@ pub fn compute_default_headers(
5260
let full_url = format!("{}://{}{}", scheme, host, abs_path);
5361

5462
res.push((owned_path_info, path_info));
55-
res.push((owned_full_url, full_url));
56-
res.push((owned_matched_route, route_match.based_route().to_string()));
63+
res.push((owned_full_url, full_url.into()));
64+
res.push((owned_matched_route, route_match.based_route().into()));
5765

58-
res.push((owned_base_path, "/".to_string()));
66+
res.push((owned_base_path, "/".into()));
67+
res.push((owned_raw_component_route, route_match.raw_route().into()));
5968
res.push((
60-
owned_raw_component_route,
61-
route_match.raw_route().to_string(),
69+
owned_component_route,
70+
route_match.raw_route_or_prefix().into(),
6271
));
63-
res.push((owned_component_route, route_match.raw_route_or_prefix()));
64-
res.push((owned_client_addr, client_addr.to_string()));
72+
res.push((owned_client_addr, client_addr.to_string().into()));
6573

6674
for (wild_name, wild_value) in route_match.named_wildcards() {
67-
let wild_header = format!("SPIN_PATH_MATCH_{}", wild_name.to_ascii_uppercase()); // TODO: safer
68-
let wild_wagi_header = format!("X_PATH_MATCH_{}", wild_name.to_ascii_uppercase()); // TODO: safer
69-
res.push(([wild_header, wild_wagi_header], wild_value.clone()));
75+
let wild_header = format!("SPIN_PATH_MATCH_{}", wild_name.to_ascii_uppercase()).into();
76+
let wild_wagi_header = format!("X_PATH_MATCH_{}", wild_name.to_ascii_uppercase()).into();
77+
res.push(([wild_header, wild_wagi_header], wild_value.into()));
7078
}
7179

7280
Ok(res)
@@ -110,7 +118,7 @@ pub fn prepare_request_headers(
110118
// In the future, we might want to have this information in a context
111119
// object as opposed to headers.
112120
for (keys, val) in compute_default_headers(req.uri(), host, route_match, client_addr)? {
113-
res.push((prepare_header_key(&keys[0]), val));
121+
res.push((prepare_header_key(&keys[0]), val.into_owned()));
114122
}
115123

116124
Ok(res)
@@ -138,6 +146,8 @@ fn prepare_header_key(key: &str) -> String {
138146

139147
#[cfg(test)]
140148
mod tests {
149+
use std::borrow::Cow;
150+
141151
use super::*;
142152
use anyhow::Result;
143153
use spin_http::routes::Router;
@@ -318,11 +328,14 @@ mod tests {
318328
assert!(req.headers().get("Host").is_some());
319329
}
320330

321-
fn search(keys: &[&str; 2], headers: &[([String; 2], String)]) -> Option<String> {
331+
fn search(
332+
keys: &[&str; 2],
333+
headers: &[([Cow<'static, str>; 2], Cow<'_, str>)],
334+
) -> Option<String> {
322335
let mut res: Option<String> = None;
323336
for (k, v) in headers {
324337
if k[0] == keys[0] && k[1] == keys[1] {
325-
res = Some(v.clone());
338+
res = Some(v.as_ref().to_owned());
326339
}
327340
}
328341

0 commit comments

Comments
 (0)