diff --git a/actix-router/Cargo.toml b/actix-router/Cargo.toml index 02bd99ad..b0bcd9da 100644 --- a/actix-router/Cargo.toml +++ b/actix-router/Cargo.toml @@ -21,15 +21,17 @@ default = ["http"] [dependencies] bytestring = ">=0.1.5, <2" +firestorm = "0.4" http = { version = "0.2.3", optional = true } log = "0.4" regex = "1.5" serde = "1" [dev-dependencies] +criterion = { version = "0.3", features = ["html_reports"] } +firestorm = { version = "0.4", features = ["enable_system_time"] } http = "0.2.3" serde = { version = "1", features = ["derive"] } -criterion = { version = "0.3", features = ["html_reports"] } [[bench]] name = "router" diff --git a/actix-router/examples/flamegraph.rs b/actix-router/examples/flamegraph.rs new file mode 100644 index 00000000..0c69566b --- /dev/null +++ b/actix-router/examples/flamegraph.rs @@ -0,0 +1,172 @@ +macro_rules! register { + (brackets) => {{ + register!(finish => "{p1}", "{p2}", "{p3}", "{p4}") + }}; + (finish => $p1:literal, $p2:literal, $p3:literal, $p4:literal) => {{ + let arr = [ + concat!("/authorizations"), + concat!("/authorizations/", $p1), + concat!("/applications/", $p1, "/tokens/", $p2), + concat!("/events"), + concat!("/repos/", $p1, "/", $p2, "/events"), + concat!("/networks/", $p1, "/", $p2, "/events"), + concat!("/orgs/", $p1, "/events"), + concat!("/users/", $p1, "/received_events"), + concat!("/users/", $p1, "/received_events/public"), + concat!("/users/", $p1, "/events"), + concat!("/users/", $p1, "/events/public"), + concat!("/users/", $p1, "/events/orgs/", $p2), + concat!("/feeds"), + concat!("/notifications"), + concat!("/repos/", $p1, "/", $p2, "/notifications"), + concat!("/notifications/threads/", $p1), + concat!("/notifications/threads/", $p1, "/subscription"), + concat!("/repos/", $p1, "/", $p2, "/stargazers"), + concat!("/users/", $p1, "/starred"), + concat!("/user/starred"), + concat!("/user/starred/", $p1, "/", $p2), + concat!("/repos/", $p1, "/", $p2, "/subscribers"), + concat!("/users/", $p1, "/subscriptions"), + concat!("/user/subscriptions"), + concat!("/repos/", $p1, "/", $p2, "/subscription"), + concat!("/user/subscriptions/", $p1, "/", $p2), + concat!("/users/", $p1, "/gists"), + concat!("/gists"), + concat!("/gists/", $p1), + concat!("/gists/", $p1, "/star"), + concat!("/repos/", $p1, "/", $p2, "/git/blobs/", $p3), + concat!("/repos/", $p1, "/", $p2, "/git/commits/", $p3), + concat!("/repos/", $p1, "/", $p2, "/git/refs"), + concat!("/repos/", $p1, "/", $p2, "/git/tags/", $p3), + concat!("/repos/", $p1, "/", $p2, "/git/trees/", $p3), + concat!("/issues"), + concat!("/user/issues"), + concat!("/orgs/", $p1, "/issues"), + concat!("/repos/", $p1, "/", $p2, "/issues"), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3), + concat!("/repos/", $p1, "/", $p2, "/assignees"), + concat!("/repos/", $p1, "/", $p2, "/assignees/", $p3), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/comments"), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/events"), + concat!("/repos/", $p1, "/", $p2, "/labels"), + concat!("/repos/", $p1, "/", $p2, "/labels/", $p3), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/labels"), + concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3, "/labels"), + concat!("/repos/", $p1, "/", $p2, "/milestones/"), + concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3), + concat!("/emojis"), + concat!("/gitignore/templates"), + concat!("/gitignore/templates/", $p1), + concat!("/meta"), + concat!("/rate_limit"), + concat!("/users/", $p1, "/orgs"), + concat!("/user/orgs"), + concat!("/orgs/", $p1), + concat!("/orgs/", $p1, "/members"), + concat!("/orgs/", $p1, "/members", $p2), + concat!("/orgs/", $p1, "/public_members"), + concat!("/orgs/", $p1, "/public_members/", $p2), + concat!("/orgs/", $p1, "/teams"), + concat!("/teams/", $p1), + concat!("/teams/", $p1, "/members"), + concat!("/teams/", $p1, "/members", $p2), + concat!("/teams/", $p1, "/repos"), + concat!("/teams/", $p1, "/repos/", $p2, "/", $p3), + concat!("/user/teams"), + concat!("/repos/", $p1, "/", $p2, "/pulls"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/commits"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/files"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/merge"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/comments"), + concat!("/user/repos"), + concat!("/users/", $p1, "/repos"), + concat!("/orgs/", $p1, "/repos"), + concat!("/repositories"), + concat!("/repos/", $p1, "/", $p2), + concat!("/repos/", $p1, "/", $p2, "/contributors"), + concat!("/repos/", $p1, "/", $p2, "/languages"), + concat!("/repos/", $p1, "/", $p2, "/teams"), + concat!("/repos/", $p1, "/", $p2, "/tags"), + concat!("/repos/", $p1, "/", $p2, "/branches"), + concat!("/repos/", $p1, "/", $p2, "/branches/", $p3), + concat!("/repos/", $p1, "/", $p2, "/collaborators"), + concat!("/repos/", $p1, "/", $p2, "/collaborators/", $p3), + concat!("/repos/", $p1, "/", $p2, "/comments"), + concat!("/repos/", $p1, "/", $p2, "/commits/", $p3, "/comments"), + concat!("/repos/", $p1, "/", $p2, "/commits"), + concat!("/repos/", $p1, "/", $p2, "/commits/", $p3), + concat!("/repos/", $p1, "/", $p2, "/readme"), + concat!("/repos/", $p1, "/", $p2, "/keys"), + concat!("/repos/", $p1, "/", $p2, "/keys", $p3), + concat!("/repos/", $p1, "/", $p2, "/downloads"), + concat!("/repos/", $p1, "/", $p2, "/downloads", $p3), + concat!("/repos/", $p1, "/", $p2, "/forks"), + concat!("/repos/", $p1, "/", $p2, "/hooks"), + concat!("/repos/", $p1, "/", $p2, "/hooks", $p3), + concat!("/repos/", $p1, "/", $p2, "/releases"), + concat!("/repos/", $p1, "/", $p2, "/releases/", $p3), + concat!("/repos/", $p1, "/", $p2, "/releases/", $p3, "/assets"), + concat!("/repos/", $p1, "/", $p2, "/stats/contributors"), + concat!("/repos/", $p1, "/", $p2, "/stats/commit_activity"), + concat!("/repos/", $p1, "/", $p2, "/stats/code_frequency"), + concat!("/repos/", $p1, "/", $p2, "/stats/participation"), + concat!("/repos/", $p1, "/", $p2, "/stats/punch_card"), + concat!("/repos/", $p1, "/", $p2, "/statuses/", $p3), + concat!("/search/repositories"), + concat!("/search/code"), + concat!("/search/issues"), + concat!("/search/users"), + concat!("/legacy/issues/search/", $p1, "/", $p2, "/", $p3, "/", $p4), + concat!("/legacy/repos/search/", $p1), + concat!("/legacy/user/search/", $p1), + concat!("/legacy/user/email/", $p1), + concat!("/users/", $p1), + concat!("/user"), + concat!("/users"), + concat!("/user/emails"), + concat!("/users/", $p1, "/followers"), + concat!("/user/followers"), + concat!("/users/", $p1, "/following"), + concat!("/user/following"), + concat!("/user/following/", $p1), + concat!("/users/", $p1, "/following", $p2), + concat!("/users/", $p1, "/keys"), + concat!("/user/keys"), + concat!("/user/keys/", $p1), + ]; + std::array::IntoIter::new(arr) + }}; +} + +fn call() -> impl Iterator { + let arr = [ + "/authorizations", + "/user/repos", + "/repos/rust-lang/rust/stargazers", + "/orgs/rust-lang/public_members/nikomatsakis", + "/repos/rust-lang/rust/releases/1.51.0", + ]; + + std::array::IntoIter::new(arr) +} + +fn main() { + let mut router = actix_router::Router::::build(); + + for route in register!(brackets) { + router.path(route, true); + } + + let actix = router.finish(); + + if firestorm::enabled() { + firestorm::bench("target", || { + for route in call() { + let mut path = actix_router::Path::new(route); + actix.recognize(&mut path).unwrap(); + } + }) + .unwrap(); + } +} diff --git a/actix-router/src/path.rs b/actix-router/src/path.rs index f6ae6f2d..b937665c 100644 --- a/actix-router/src/path.rs +++ b/actix-router/src/path.rs @@ -1,6 +1,7 @@ use std::borrow::Cow; use std::ops::Index; +use firestorm::profile_method; use serde::de; use crate::de::PathDeserializer; @@ -37,21 +38,23 @@ impl Path { } } - /// Get reference to inner path instance + /// Get reference to inner path instance. #[inline] pub fn get_ref(&self) -> &T { &self.path } - /// Get mutable reference to inner path instance + /// Get mutable reference to inner path instance. #[inline] pub fn get_mut(&mut self) -> &mut T { &mut self.path } - /// Path + /// Path. #[inline] pub fn path(&self) -> &str { + profile_method!(path); + let skip = self.skip as usize; let path = self.path.path(); if skip <= path.len() { @@ -61,7 +64,7 @@ impl Path { } } - /// Set new path + /// Set new path. #[inline] pub fn set(&mut self, path: T) { self.skip = 0; @@ -69,20 +72,22 @@ impl Path { self.segments.clear(); } - /// Reset state + /// Reset state. #[inline] pub fn reset(&mut self) { self.skip = 0; self.segments.clear(); } - /// Skip first `n` chars in path + /// Skip first `n` chars in path. #[inline] pub fn skip(&mut self, n: u16) { self.skip += n; } pub(crate) fn add(&mut self, name: impl Into>, value: PathItem) { + profile_method!(add); + match value { PathItem::Static(s) => self.segments.push((name.into(), PathItem::Static(s))), PathItem::Segment(begin, end) => self.segments.push(( @@ -116,6 +121,8 @@ impl Path { /// Get matched parameter by name without type conversion pub fn get(&self, key: &str) -> Option<&str> { + profile_method!(get); + for item in self.segments.iter() { if key == item.0 { return match item.1 { @@ -140,9 +147,10 @@ impl Path { /// Get matched parameter by name. /// - /// If keyed parameter is not available empty string is used as default - /// value. + /// If keyed parameter is not available empty string is used as default value. pub fn query(&self, key: &str) -> &str { + profile_method!(query); + if let Some(s) = self.get(key) { s } else { @@ -150,7 +158,7 @@ impl Path { } } - /// Return iterator to items in parameter container + /// Return iterator to items in parameter container. pub fn iter(&self) -> PathIter<'_, T> { PathIter { idx: 0, @@ -160,6 +168,7 @@ impl Path { /// Try to deserialize matching parameters to a specified type `U` pub fn load<'de, U: serde::Deserialize<'de>>(&'de self) -> Result { + profile_method!(load); de::Deserialize::deserialize(PathDeserializer::new(self)) } } diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs index cb0264ae..8d838c97 100644 --- a/actix-router/src/resource.rs +++ b/actix-router/src/resource.rs @@ -6,6 +6,7 @@ use std::{ mem, }; +use firestorm::{profile_fn, profile_method, profile_section}; use regex::{escape, Regex, RegexSet}; use crate::{ @@ -78,6 +79,8 @@ impl ResourceDef { /// /// Panics if path pattern is malformed. pub fn new(path: T) -> Self { + profile_method!(new); + match path.patterns() { Patterns::Single(pattern) => ResourceDef::from_single_pattern(&pattern, false), @@ -124,6 +127,7 @@ impl ResourceDef { /// /// Panics if path regex pattern is malformed. pub fn prefix(path: &str) -> Self { + profile_method!(prefix); ResourceDef::from_single_pattern(path, true) } @@ -134,6 +138,7 @@ impl ResourceDef { /// /// Panics if path regex pattern is malformed. pub fn root_prefix(path: &str) -> Self { + profile_method!(root_prefix); ResourceDef::from_single_pattern(&insert_slash(path), true) } @@ -149,6 +154,8 @@ impl ResourceDef { /// Parse path pattern and create a new instance fn from_single_pattern(pattern: &str, for_prefix: bool) -> Self { + profile_method!(from_single_pattern); + let pattern = pattern.to_owned(); let (pat_type, elements) = ResourceDef::parse(&pattern, for_prefix, false); @@ -179,6 +186,8 @@ impl ResourceDef { /// Check if path matches this pattern. #[inline] pub fn is_match(&self, path: &str) -> bool { + profile_method!(is_match); + match self.pat_type { PatternType::Static(ref s) => s == path, PatternType::Prefix(ref s) => path.starts_with(s), @@ -189,6 +198,8 @@ impl ResourceDef { /// Is prefix path a match against this resource. pub fn is_prefix_match(&self, path: &str) -> Option { + profile_method!(is_prefix_match); + let path_len = path.len(); let path = if path.is_empty() { "/" } else { path }; @@ -245,6 +256,7 @@ impl ResourceDef { /// Is the given path and parameters a match against this pattern. pub fn match_path(&self, path: &mut Path) -> bool { + profile_method!(match_path); self.match_path_checked(path, &|_, _| true, &Some(())) } @@ -260,11 +272,15 @@ impl ResourceDef { R: Resource, F: Fn(&R, &Option) -> bool, { + profile_method!(match_path_checked); + let mut segments: [PathItem; MAX_DYNAMIC_SEGMENTS] = Default::default(); let path = res.resource_path(); let (matched_len, matched_vars) = match self.pat_type { PatternType::Static(ref segment) => { + profile_section!(pattern_static); + if segment != path.path() { return false; } @@ -273,6 +289,8 @@ impl ResourceDef { } PatternType::Prefix(ref prefix) => { + profile_section!(pattern_dynamic); + let path_str = path.path(); let path_len = path_str.len(); @@ -300,24 +318,39 @@ impl ResourceDef { } PatternType::Dynamic(ref re, ref names) => { - let captures = match re.captures(path.path()) { - Some(captures) => captures, - _ => return false, + profile_section!(pattern_dynamic); + + let captures = { + profile_section!(pattern_dynamic_regex_exec); + + match re.captures(path.path()) { + Some(captures) => captures, + _ => return false, + } }; - for (no, name) in names.iter().enumerate() { - if let Some(m) = captures.name(&name) { - segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16); - } else { - log::error!("Dynamic path match but not all segments found: {}", name); - return false; + { + profile_section!(pattern_dynamic_extract_captures); + + for (no, name) in names.iter().enumerate() { + if let Some(m) = captures.name(&name) { + segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16); + } else { + log::error!( + "Dynamic path match but not all segments found: {}", + name + ); + return false; + } } - } + }; (captures[0].len(), Some(names)) } PatternType::DynamicSet(ref re, ref params) => { + profile_section!(pattern_dynamic_set); + let path = path.path(); let (pattern, names) = match re.matches(path).into_iter().next() { Some(idx) => ¶ms[idx], @@ -394,6 +427,7 @@ impl ResourceDef { U: Iterator, I: AsRef, { + profile_method!(resource_path_from_iter); self.build_resource_path(path, |_| elements.next()) } @@ -404,6 +438,7 @@ impl ResourceDef { U: Iterator, I: AsRef, { + profile_method!(build_resource_path); self.resource_path_from_iter(path, elements) } @@ -420,6 +455,7 @@ impl ResourceDef { V: AsRef, S: BuildHasher, { + profile_method!(resource_path_from_map); self.build_resource_path(path, |name| { name.and_then(|name| elements.get(name).map(AsRef::::as_ref)) }) @@ -458,6 +494,7 @@ impl ResourceDef { S: BuildHasher, T: AsRef, { + profile_method!(resource_path_from_map_with_tail); self.build_resource_path(path, |name| match name { Some(name) => elements.get(name).map(AsRef::::as_ref), None => Some(tail.as_ref()), @@ -465,6 +502,8 @@ impl ResourceDef { } fn parse_param(pattern: &str) -> (PatternElement, String, &str) { + profile_method!(parse_param); + const DEFAULT_PATTERN: &str = "[^/]+"; const DEFAULT_PATTERN_TAIL: &str = ".*"; @@ -526,6 +565,8 @@ impl ResourceDef { for_prefix: bool, force_dynamic: bool, ) -> (PatternType, Vec) { + profile_method!(parse); + let mut unprocessed = pattern; if !force_dynamic && unprocessed.find('{').is_none() && !unprocessed.ends_with('*') { @@ -637,6 +678,8 @@ impl From for ResourceDef { } pub(crate) fn insert_slash(path: &str) -> Cow<'_, str> { + profile_fn!(insert_slash); + if !path.is_empty() && !path.starts_with('/') { let mut new_path = String::with_capacity(path.len() + 1); new_path.push('/'); diff --git a/actix-router/src/router.rs b/actix-router/src/router.rs index 007ef495..2f345ecf 100644 --- a/actix-router/src/router.rs +++ b/actix-router/src/router.rs @@ -1,3 +1,5 @@ +use firestorm::profile_method; + use crate::{IntoPatterns, Resource, ResourceDef, ResourcePath}; #[derive(Debug, Copy, Clone, PartialEq)] @@ -24,6 +26,8 @@ impl Router { R: Resource

, P: ResourcePath, { + profile_method!(recognize); + for item in self.0.iter() { if item.0.match_path(resource.resource_path()) { return Some((&item.1, ResourceId(item.0.id()))); @@ -37,6 +41,8 @@ impl Router { R: Resource

, P: ResourcePath, { + profile_method!(recognize_mut); + for item in self.0.iter_mut() { if item.0.match_path(resource.resource_path()) { return Some((&mut item.1, ResourceId(item.0.id()))); @@ -55,6 +61,8 @@ impl Router { R: Resource

, P: ResourcePath, { + profile_method!(recognize_checked); + for item in self.0.iter() { if item.0.match_path_checked(resource, &check, &item.2) { return Some((&item.1, ResourceId(item.0.id()))); @@ -73,6 +81,8 @@ impl Router { R: Resource

, P: ResourcePath, { + profile_method!(recognize_mut_checked); + for item in self.0.iter_mut() { if item.0.match_path_checked(resource, &check, &item.2) { return Some((&mut item.1, ResourceId(item.0.id()))); @@ -93,6 +103,8 @@ impl RouterBuilder { path: P, resource: T, ) -> &mut (ResourceDef, T, Option) { + profile_method!(path); + self.resources .push((ResourceDef::new(path), resource, None)); self.resources.last_mut().unwrap() @@ -100,6 +112,8 @@ impl RouterBuilder { /// Register resource for specified path prefix. pub fn prefix(&mut self, prefix: &str, resource: T) -> &mut (ResourceDef, T, Option) { + profile_method!(prefix); + self.resources .push((ResourceDef::prefix(prefix), resource, None)); self.resources.last_mut().unwrap() @@ -107,6 +121,8 @@ impl RouterBuilder { /// Register resource for ResourceDef pub fn rdef(&mut self, rdef: ResourceDef, resource: T) -> &mut (ResourceDef, T, Option) { + profile_method!(rdef); + self.resources.push((rdef, resource, None)); self.resources.last_mut().unwrap() }