Skip to content

Commit 133ce09

Browse files
committed
perf: Do not build glob matchers repeatedly when include-exclude feature is enabled
I've noticed in my profiles that if include-exclude feature is enabled, the call to ::iter() function is dominated by path filtering. This commit fixes that by explicitly passing in a premade Globset for a given object. Note that we're using std::sync::OnceLock to lazily store away premade Globsets, which bumps MSRV to 1.70. An alternative would be to use once_cell as an explicit dependency. For my use case, in debug builds this takes down the matching time from 650ms to 6ms. I know you shouldn't benchmark debug builds, but rust-embed::iter is on my app's initialization path and that's how I noticed it in the first place. I'd expect release timings to be somewhat similar
1 parent 1d17cae commit 133ce09

File tree

2 files changed

+65
-53
lines changed

2 files changed

+65
-53
lines changed

impl/src/lib.rs

+16-11
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ extern crate proc_macro;
66

77
use proc_macro::TokenStream;
88
use proc_macro2::TokenStream as TokenStream2;
9+
use rust_embed_utils::PathMatcher;
910
use std::{
1011
collections::BTreeMap,
1112
env,
@@ -25,7 +26,8 @@ fn embedded(
2526

2627
let includes: Vec<&str> = includes.iter().map(AsRef::as_ref).collect();
2728
let excludes: Vec<&str> = excludes.iter().map(AsRef::as_ref).collect();
28-
for rust_embed_utils::FileEntry { rel_path, full_canonical_path } in rust_embed_utils::get_files(absolute_folder_path.clone(), &includes, &excludes) {
29+
let matcher = PathMatcher::new(&includes, &excludes);
30+
for rust_embed_utils::FileEntry { rel_path, full_canonical_path } in rust_embed_utils::get_files(absolute_folder_path.clone(), matcher) {
2931
match_values.insert(
3032
rel_path.clone(),
3133
embed_file(relative_folder_path, ident, &rel_path, &full_canonical_path, metadata_only)?,
@@ -125,8 +127,8 @@ fn dynamic(ident: &syn::Ident, folder_path: String, prefix: Option<&str>, includ
125127
const EXCLUDES: &[&str] = &[#(#excludes),*];
126128
};
127129

128-
// In metadata_only mode, we still need to read file contents to generate the file hash, but
129-
// then we drop the file data.
130+
// In metadata_only mode, we still need to read file contents to generate the
131+
// file hash, but then we drop the file data.
130132
let strip_contents = metadata_only.then_some(quote! {
131133
.map(|mut file| { file.data = ::std::default::Default::default(); file })
132134
});
@@ -137,13 +139,18 @@ fn dynamic(ident: &syn::Ident, folder_path: String, prefix: Option<&str>, includ
137139
quote! {
138140
#[cfg(debug_assertions)]
139141
impl #ident {
142+
143+
144+
fn matcher() -> ::rust_embed::utils::PathMatcher {
145+
#declare_includes
146+
#declare_excludes
147+
static PATH_MATCHER: ::std::sync::OnceLock<::rust_embed::utils::PathMatcher> = ::std::sync::OnceLock::new();
148+
PATH_MATCHER.get_or_init(|| rust_embed::utils::PathMatcher::new(INCLUDES, EXCLUDES)).clone()
149+
}
140150
/// Get an embedded file and its metadata.
141151
pub fn get(file_path: &str) -> ::std::option::Option<rust_embed::EmbeddedFile> {
142152
#handle_prefix
143153

144-
#declare_includes
145-
#declare_excludes
146-
147154
let rel_file_path = file_path.replace("\\", "/");
148155
let file_path = ::std::path::Path::new(#folder_path).join(&rel_file_path);
149156

@@ -162,8 +169,8 @@ fn dynamic(ident: &syn::Ident, folder_path: String, prefix: Option<&str>, includ
162169
return ::std::option::Option::None;
163170
}
164171
}
165-
166-
if rust_embed::utils::is_path_included(&rel_file_path, INCLUDES, EXCLUDES) {
172+
let path_matcher = Self::matcher();
173+
if path_matcher.is_path_included(&rel_file_path) {
167174
rust_embed::utils::read_file_from_fs(&canonical_file_path).ok() #strip_contents
168175
} else {
169176
::std::option::Option::None
@@ -174,10 +181,8 @@ fn dynamic(ident: &syn::Ident, folder_path: String, prefix: Option<&str>, includ
174181
pub fn iter() -> impl ::std::iter::Iterator<Item = ::std::borrow::Cow<'static, str>> {
175182
use ::std::path::Path;
176183

177-
#declare_includes
178-
#declare_excludes
179184

180-
rust_embed::utils::get_files(::std::string::String::from(#folder_path), INCLUDES, EXCLUDES)
185+
rust_embed::utils::get_files(::std::string::String::from(#folder_path), Self::matcher())
181186
.map(|e| #map_iter)
182187
}
183188
}

utils/src/lib.rs

+49-42
Original file line numberDiff line numberDiff line change
@@ -12,47 +12,8 @@ pub struct FileEntry {
1212
pub full_canonical_path: String,
1313
}
1414

15-
#[cfg(not(feature = "include-exclude"))]
16-
pub fn is_path_included(_path: &str, _includes: &[&str], _excludes: &[&str]) -> bool {
17-
true
18-
}
19-
20-
#[cfg(feature = "include-exclude")]
21-
pub fn is_path_included(rel_path: &str, includes: &[&str], excludes: &[&str]) -> bool {
22-
use globset::Glob;
23-
24-
// ignore path matched by exclusion pattern
25-
for exclude in excludes {
26-
let pattern = Glob::new(exclude)
27-
.unwrap_or_else(|_| panic!("invalid exclude pattern '{}'", exclude))
28-
.compile_matcher();
29-
30-
if pattern.is_match(rel_path) {
31-
return false;
32-
}
33-
}
34-
35-
// accept path if no includes provided
36-
if includes.is_empty() {
37-
return true;
38-
}
39-
40-
// accept path if matched by inclusion pattern
41-
for include in includes {
42-
let pattern = Glob::new(include)
43-
.unwrap_or_else(|_| panic!("invalid include pattern '{}'", include))
44-
.compile_matcher();
45-
46-
if pattern.is_match(rel_path) {
47-
return true;
48-
}
49-
}
50-
51-
false
52-
}
53-
5415
#[cfg_attr(all(debug_assertions, not(feature = "debug-embed")), allow(unused))]
55-
pub fn get_files<'patterns>(folder_path: String, includes: &'patterns [&str], excludes: &'patterns [&str]) -> impl Iterator<Item = FileEntry> + 'patterns {
16+
pub fn get_files(folder_path: String, matcher: PathMatcher) -> impl Iterator<Item = FileEntry> {
5617
walkdir::WalkDir::new(&folder_path)
5718
.follow_links(true)
5819
.sort_by_file_name()
@@ -68,8 +29,7 @@ pub fn get_files<'patterns>(folder_path: String, includes: &'patterns [&str], ex
6829
} else {
6930
rel_path
7031
};
71-
72-
if is_path_included(&rel_path, includes, excludes) {
32+
if matcher.is_path_included(&rel_path) {
7333
Some(FileEntry { rel_path, full_canonical_path })
7434
} else {
7535
None
@@ -176,3 +136,50 @@ pub fn read_file_from_fs(file_path: &Path) -> io::Result<EmbeddedFile> {
176136
fn path_to_str<P: AsRef<std::path::Path>>(p: P) -> String {
177137
p.as_ref().to_str().expect("Path does not have a string representation").to_owned()
178138
}
139+
140+
#[derive(Clone)]
141+
pub struct PathMatcher {
142+
#[cfg(feature = "include-exclude")]
143+
include_matcher: globset::GlobSet,
144+
#[cfg(feature = "include-exclude")]
145+
exclude_matcher: globset::GlobSet,
146+
}
147+
148+
#[cfg(feature = "include-exclude")]
149+
impl PathMatcher {
150+
pub fn new(includes: &[&str], excludes: &[&str]) -> Self {
151+
let mut include_matcher = globset::GlobSetBuilder::new();
152+
for include in includes {
153+
include_matcher.add(globset::Glob::new(include).unwrap_or_else(|_| panic!("invalid include pattern '{}'", include)));
154+
}
155+
let include_matcher = include_matcher
156+
.build()
157+
.unwrap_or_else(|_| panic!("Could not compile included patterns matcher"));
158+
159+
let mut exclude_matcher = globset::GlobSetBuilder::new();
160+
for exclude in excludes {
161+
exclude_matcher.add(globset::Glob::new(exclude).unwrap_or_else(|_| panic!("invalid exclude pattern '{}'", exclude)));
162+
}
163+
let exclude_matcher = exclude_matcher
164+
.build()
165+
.unwrap_or_else(|_| panic!("Could not compile excluded patterns matcher"));
166+
167+
Self {
168+
include_matcher,
169+
exclude_matcher,
170+
}
171+
}
172+
pub fn is_path_included(&self, path: &str) -> bool {
173+
!self.exclude_matcher.is_match(path) && (self.include_matcher.is_empty() || self.include_matcher.is_match(path))
174+
}
175+
}
176+
177+
#[cfg(not(feature = "include-exclude"))]
178+
impl PathMatcher {
179+
pub fn new(_includes: &[&str], _excludes: &[&str]) -> Self {
180+
Self {}
181+
}
182+
pub fn is_path_included(&self, _path: &str) -> bool {
183+
true
184+
}
185+
}

0 commit comments

Comments
 (0)