Skip to content

Commit e79ac9c

Browse files
committed
perf(fmt): reuse parent context in batch ignore matching
1 parent 89ead58 commit e79ac9c

1 file changed

Lines changed: 135 additions & 57 deletions

File tree

  • crates/rstack-ignore/src

‎crates/rstack-ignore/src/lib.rs‎

Lines changed: 135 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -207,24 +207,14 @@ impl GitIgnoreMatcher {
207207
names: &[String],
208208
directory_flags: &[u8],
209209
) -> Vec<u8> {
210-
let relative_parent = normalize_relative_path(relative_parent);
211-
let separator = usize::from(!relative_parent.is_empty());
212-
let name_capacity = names.iter().map(String::len).max().unwrap_or(0);
213-
let mut relative_path =
214-
String::with_capacity(relative_parent.len() + separator + name_capacity);
215-
let mut ignored = Vec::with_capacity(names.len());
216-
217-
for (name, is_directory) in names.iter().zip(directory_flags) {
218-
relative_path.clear();
219-
if !relative_parent.is_empty() {
220-
relative_path.push_str(relative_parent);
221-
relative_path.push('/');
222-
}
223-
relative_path.push_str(name);
224-
ignored.push(u8::from(
225-
self.is_ignored(&relative_path, *is_directory != 0),
226-
));
227-
}
210+
debug_assert_eq!(names.len(), directory_flags.len());
211+
let mut ignored = vec![0; names.len()];
212+
self.match_batch(
213+
relative_parent,
214+
names,
215+
|index| directory_flags[index] != 0,
216+
&mut ignored,
217+
);
228218

229219
ignored
230220
}
@@ -257,26 +247,86 @@ impl GitIgnoreMatcher {
257247
) -> u32 {
258248
debug_assert!(names.len() <= u32::BITS as usize);
259249

250+
let mut ignored = [0; 32];
251+
self.match_batch(
252+
relative_parent,
253+
names,
254+
|index| directory_mask & (1 << index) != 0,
255+
&mut ignored[..names.len()],
256+
);
257+
ignored[..names.len()]
258+
.iter()
259+
.enumerate()
260+
.fold(0, |mask, (index, ignored)| {
261+
mask | (u32::from(*ignored) << index)
262+
})
263+
}
264+
265+
fn match_batch(
266+
&mut self,
267+
relative_parent: &str,
268+
names: &[String],
269+
is_directory: impl Fn(usize) -> bool,
270+
ignored: &mut [u8],
271+
) {
260272
let relative_parent = normalize_relative_path(relative_parent);
261-
let separator = usize::from(!relative_parent.is_empty());
262-
let name_capacity = names.iter().map(String::len).max().unwrap_or(0);
263-
let mut relative_path =
264-
String::with_capacity(relative_parent.len() + separator + name_capacity);
265-
let mut ignored_mask = 0;
273+
if names.is_empty() || self.matchers.is_empty() {
274+
return;
275+
}
276+
if self.is_directory_ignored(relative_parent) {
277+
ignored.fill(1);
278+
return;
279+
}
266280

267-
for (index, name) in names.iter().enumerate() {
281+
let name_capacity = names.iter().map(String::len).max().unwrap_or(0);
282+
let mut relative_path = String::with_capacity(relative_parent.len() + 1 + name_capacity);
283+
// Resolve applicable sources and each source's parent state once for the whole batch.
284+
let root_ends = std::iter::once(0)
285+
.chain(relative_parent.match_indices('/').map(|(index, _)| index))
286+
.chain((!relative_parent.is_empty()).then_some(relative_parent.len()));
287+
for root_end in root_ends {
288+
let Some(source) = self.matchers.get_mut(&relative_parent[..root_end]) else {
289+
continue;
290+
};
291+
let source_start = if root_end == 0 {
292+
0
293+
} else {
294+
(root_end + 1).min(relative_parent.len())
295+
};
296+
let source_parent = &relative_parent[source_start..];
297+
let parent_ignored =
298+
!source_parent.is_empty() && source.is_directory_ignored(source_parent);
268299
relative_path.clear();
269-
if !relative_parent.is_empty() {
270-
relative_path.push_str(relative_parent);
300+
relative_path.push_str(source_parent);
301+
if !source_parent.is_empty() {
271302
relative_path.push('/');
272303
}
273-
relative_path.push_str(name);
274-
if self.is_ignored(&relative_path, directory_mask & (1 << index) != 0) {
275-
ignored_mask |= 1 << index;
304+
let prefix_len = relative_path.len();
305+
for (index, name) in names.iter().enumerate() {
306+
relative_path.truncate(prefix_len);
307+
relative_path.push_str(name);
308+
if let Some(state) =
309+
source.match_path(&relative_path, is_directory(index), Some(parent_ignored))
310+
{
311+
ignored[index] = u8::from(state);
312+
}
276313
}
277314
}
278315

279-
ignored_mask
316+
relative_path.clear();
317+
relative_path.push_str(relative_parent);
318+
if !relative_parent.is_empty() {
319+
relative_path.push('/');
320+
}
321+
let prefix_len = relative_path.len();
322+
for (index, name) in names.iter().enumerate() {
323+
if is_directory(index) {
324+
relative_path.truncate(prefix_len);
325+
relative_path.push_str(name);
326+
self.ignored_directories
327+
.insert(relative_path.clone().into_boxed_str(), ignored[index] != 0);
328+
}
329+
}
280330
}
281331

282332
fn invalidate_directory_cache(&mut self, relative_root: &str) {
@@ -314,7 +364,7 @@ impl GitIgnoreMatcher {
314364
if self.matchers.len() == 1 {
315365
if let Some(root_matcher) = self.matchers.get_mut("") {
316366
return root_matcher
317-
.match_path(relative_path, is_directory)
367+
.match_path(relative_path, is_directory, None)
318368
.unwrap_or(false);
319369
}
320370
}
@@ -327,7 +377,7 @@ impl GitIgnoreMatcher {
327377
let matcher_root = &relative_path[..matcher_root_end];
328378
if let Some(matcher) = self.matchers.get_mut(matcher_root) {
329379
let path_from_matcher = &relative_path[path_from_matcher_start..];
330-
if let Some(state) = matcher.match_path(path_from_matcher, is_directory) {
380+
if let Some(state) = matcher.match_path(path_from_matcher, is_directory, None) {
331381
ignored = state;
332382
}
333383
}
@@ -355,39 +405,36 @@ impl GitIgnoreSourceMatcher {
355405
}
356406
}
357407

358-
fn match_path(&mut self, relative_path: &str, is_directory: bool) -> Option<bool> {
408+
fn match_path(
409+
&mut self,
410+
relative_path: &str,
411+
is_directory: bool,
412+
// Batch callers can reuse the shared parent's state.
413+
parent_ignored: Option<bool>,
414+
) -> Option<bool> {
359415
if is_directory {
360-
return self.match_directory(relative_path);
361-
}
362-
363-
match self.matcher.matched(relative_path, false) {
364-
Match::Ignore(_) => Some(true),
365-
Match::Whitelist(_) => Some(false),
366-
Match::None => parent_directory(relative_path)
367-
.is_some_and(|parent| self.is_directory_ignored(parent))
368-
.then_some(true),
369-
}
370-
}
371-
372-
fn match_directory(&mut self, relative_path: &str) -> Option<bool> {
373-
if let Some(state) = self.directory_states.get(relative_path) {
374-
return *state;
416+
if let Some(state) = self.directory_states.get(relative_path) {
417+
return *state;
418+
}
375419
}
376-
377-
let matched = self.matcher.matched(relative_path, true);
378-
let state = match matched {
420+
let state = match self.matcher.matched(relative_path, is_directory) {
379421
Match::Ignore(_) => Some(true),
380422
Match::Whitelist(_) => Some(false),
381-
Match::None => parent_directory(relative_path)
382-
.is_some_and(|parent| self.is_directory_ignored(parent))
423+
Match::None => parent_ignored
424+
.unwrap_or_else(|| {
425+
parent_directory(relative_path)
426+
.is_some_and(|parent| self.is_directory_ignored(parent))
427+
})
383428
.then_some(true),
384429
};
385-
self.directory_states.insert(relative_path.into(), state);
430+
if is_directory {
431+
self.directory_states.insert(relative_path.into(), state);
432+
}
386433
state
387434
}
388435

389436
fn is_directory_ignored(&mut self, relative_path: &str) -> bool {
390-
self.match_directory(relative_path) == Some(true)
437+
self.match_path(relative_path, true, None) == Some(true)
391438
}
392439
}
393440

@@ -421,6 +468,7 @@ impl SourceMatcher {
421468
ignored: &mut [u8],
422469
) -> usize {
423470
let relative_parent = self.relative_path(parent_path);
471+
let parent_ignored = self.is_directory_ignored(&to_posix_path(relative_parent.as_ref()));
424472
let name_capacity = names.iter().map(String::len).max().unwrap_or(0);
425473
let mut relative_path = PathBuf::with_capacity(
426474
relative_parent.as_os_str().len()
@@ -437,7 +485,8 @@ impl SourceMatcher {
437485
relative_path.clear();
438486
relative_path.push(relative_parent.as_ref());
439487
relative_path.push(name);
440-
if self.is_relative_path_ignored(&relative_path, directory_flags[index] != 0) {
488+
if parent_ignored || self.is_child_ignored(&relative_path, directory_flags[index] != 0)
489+
{
441490
ignored[index] = 1;
442491
matched += 1;
443492
}
@@ -456,6 +505,7 @@ impl SourceMatcher {
456505
debug_assert_ne!(candidate_mask, 0);
457506

458507
let relative_parent = self.relative_path(parent_path);
508+
let parent_ignored = self.is_directory_ignored(&to_posix_path(relative_parent.as_ref()));
459509
let name_capacity = names[candidate_mask.trailing_zeros() as usize].len();
460510
let mut relative_path = PathBuf::with_capacity(
461511
relative_parent.as_os_str().len()
@@ -473,14 +523,26 @@ impl SourceMatcher {
473523
relative_path.clear();
474524
relative_path.push(relative_parent.as_ref());
475525
relative_path.push(&names[index]);
476-
if self.is_relative_path_ignored(&relative_path, directory_mask & entry_mask != 0) {
526+
if parent_ignored
527+
|| self.is_child_ignored(&relative_path, directory_mask & entry_mask != 0)
528+
{
477529
ignored_mask |= entry_mask;
478530
}
479531
}
480532

481533
ignored_mask
482534
}
483535

536+
// Batch callers have already checked whether the shared parent is ignored.
537+
fn is_child_ignored(&mut self, relative_path: &Path, is_directory: bool) -> bool {
538+
let relative_path = to_posix_path(relative_path);
539+
if is_directory {
540+
self.is_directory_ignored(&relative_path)
541+
} else {
542+
is_ignore_match(self.matcher.matched(relative_path.as_ref(), false))
543+
}
544+
}
545+
484546
fn is_relative_path_ignored(&mut self, relative_path: &Path, is_directory: bool) -> bool {
485547
if relative_path.as_os_str().is_empty() {
486548
return false;
@@ -691,6 +753,22 @@ mod tests {
691753

692754
assert!(!matcher.is_ignored("scripts/debug", true));
693755
assert!(matcher.is_ignored("scripts/debug/launch.mjs", false));
756+
757+
let names = vec!["keep.js".into(), "drop.js".into(), "nested".into()];
758+
assert_eq!(
759+
matcher.is_ignored_batch("scripts/debug", &names, &[0, 0, 1]),
760+
vec![1, 1, 1]
761+
);
762+
assert!(matcher.is_ignored("scripts/debug/nested", true));
763+
764+
matcher
765+
.add_source("scripts/debug", "!keep.js\n!nested/\n")
766+
.unwrap();
767+
assert_eq!(
768+
matcher.is_ignored_batch_mask("scripts/debug", &names, 0b100),
769+
0b010
770+
);
771+
assert!(!matcher.is_ignored("scripts/debug/nested", true));
694772
}
695773

696774
#[test]

0 commit comments

Comments
 (0)