perf(linter/import/no-cycle): reuse traversal buffers between imports (#27481)

Each time we do another module graph traversal, we throw away any record
of files that might've been visited already from a previous traversal.
This means we also allocate a new list of visited records each time,
resulting in an initially empty array that must grow repeatedly which
causes reallocations.

With this PR, the space for storing the traversed module record paths is
reused between traversals so it only uses as much space as the single
largest traversal, instead of being based on the total number of
traversals. So, we have much fewer reallocations.

Running on `vscode`:


Metric | Before | After | Reduction
-- | -- | -- | --
Allocation calls | 16,807,784 | 16,744,378 | -0.4%
Reallocation calls | 92,751 | 66,763 | -28.0%
Allocated bytes | 1,506,926,944 B | 1,360,327,228 B | -9.7%

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Cam McHenryandCopilot authored and GitHub committed 2026-10-28 23:07:03 -04:00
1 parent fd5edb71b5
commit 20022bb3a2
2 files changed
+57 -8

No files matched your search

+51 -7
View File
@@ -20,6 +20,7 @@ type LeaveFn<'a> = dyn FnMut(ModulePair, &ModuleRecord) + 'a;
/// A builder for creating visitors that walk through the module graph
pub struct ModuleGraphVisitorBuilder<'a, T> {
max_depth: u32,
traversed: FxHashSet<PathBuf>,
filter: Option<Box<FilterFn<'a>>>,
event: Option<Box<EventFn<'a>>>,
enter: Option<Box<EnterFn<'a>>>,
@@ -60,6 +61,14 @@ impl<'a, T> ModuleGraphVisitorBuilder<'a, T> {
self
}
/// Reuses a previous traversal's set, clearing its contents but retaining its capacity.
#[must_use]
pub fn reuse_traversed(mut self, mut traversed: FxHashSet<PathBuf>) -> Self {
traversed.clear();
self.traversed = traversed;
self
}
/// Sets the filter closure.
#[must_use]
pub fn filter<F: (Fn(ModulePair, &ModuleRecord) -> bool) + 'a>(mut self, filter: F) -> Self {
@@ -84,11 +93,8 @@ impl<'a, T> ModuleGraphVisitorBuilder<'a, T> {
module: &ModuleRecord,
visit: V,
) -> ModuleGraphVisitResult<T> {
let mut visitor = ModuleGraphVisitor {
traversed: FxHashSet::default(),
depth: 0,
max_depth: self.max_depth,
};
let mut visitor =
ModuleGraphVisitor { traversed: self.traversed, depth: 0, max_depth: self.max_depth };
let filter = self.filter.unwrap_or_else(|| Box::new(|_, _| true));
let event = self.event.unwrap_or_else(|| Box::new(|_, _, _| {}));
let enter = self.enter.unwrap_or_else(|| Box::new(|_, _| {}));
@@ -104,6 +110,7 @@ impl<T> Default for ModuleGraphVisitorBuilder<'_, T> {
fn default() -> Self {
Self {
max_depth: u32::MAX,
traversed: FxHashSet::default(),
filter: None,
event: None,
enter: None,
@@ -115,13 +122,13 @@ impl<T> Default for ModuleGraphVisitorBuilder<'_, T> {
pub struct ModuleGraphVisitResult<T> {
pub result: T,
pub _traversed: FxHashSet<PathBuf>,
pub traversed: FxHashSet<PathBuf>,
pub _max_depth: u32,
}
impl<T> ModuleGraphVisitResult<T> {
fn with_result(result: T, visitor: ModuleGraphVisitor) -> Self {
Self { result, _traversed: visitor.traversed, _max_depth: visitor.max_depth }
Self { result, traversed: visitor.traversed, _max_depth: visitor.max_depth }
}
}
@@ -235,3 +242,40 @@ impl ModuleGraphVisitor {
accumulator
}
}
#[test]
fn test_reuse_traversed_clears_contents_and_retains_capacity() {
let mut root = ModuleRecord::default();
root.resolved_absolute_path = PathBuf::from("root.js");
let root = Arc::new(root);
let mut dependency = ModuleRecord::default();
dependency.resolved_absolute_path = PathBuf::from("dependency.js");
let dependency = Arc::new(dependency);
root.write_loaded_modules()
.insert(CompactStr::from("./dependency.js"), Arc::downgrade(&dependency));
dependency.write_loaded_modules().insert(CompactStr::from("./root.js"), Arc::downgrade(&root));
let mut traversed = FxHashSet::default();
traversed.reserve(16);
traversed.insert(dependency.resolved_absolute_path.clone());
let capacity = traversed.capacity();
for stop_early in [true, false, true, false] {
let builder = ModuleGraphVisitorBuilder::default().reuse_traversed(traversed);
assert!(builder.traversed.is_empty());
assert_eq!(builder.traversed.capacity(), capacity);
let result = builder.visit_fold(0, &root, |count, _, _| {
if stop_early {
VisitFoldWhile::Stop(count + 1)
} else {
VisitFoldWhile::Next(count + 1)
}
});
let expected = if stop_early { 1 } else { 2 };
assert_eq!(result.result, expected);
assert_eq!(result.traversed.len(), expected);
assert_eq!(result.traversed.capacity(), capacity);
traversed = result.traversed;
}
}
@@ -167,6 +167,8 @@ impl Rule for NoCycle {
.collect::<Vec<_>>();
direct_imports.sort_unstable_by(|a, b| a.0.cmp(&b.0));
let mut stack = Vec::new();
let mut traversed = FxHashSet::default();
for (key, loaded_module_record) in direct_imports {
if !self.should_traverse_module(&key, &loaded_module_record, module_record) {
continue;
@@ -174,7 +176,8 @@ impl Rule for NoCycle {
let requested_module = module_record.requested_modules[&key][0];
let span = requested_module.span;
let mut stack = vec![(key.clone(), Arc::clone(&loaded_module_record))];
stack.clear();
stack.push((key.clone(), Arc::clone(&loaded_module_record)));
if loaded_module_record.resolved_absolute_path == *needle {
ctx.diagnostic(self_referencing_cycle_diagnostic(span, requested_module.is_import));
@@ -183,6 +186,7 @@ impl Rule for NoCycle {
let visitor_result = ModuleGraphVisitorBuilder::default()
.max_depth(self.max_depth.saturating_sub(1))
.reuse_traversed(traversed)
.filter(|(key, val), parent| self.should_traverse_module(key, val, parent))
.event(|event, (key, val), _| match event {
ModuleGraphVisitorEvent::Enter => {
@@ -200,6 +204,7 @@ impl Rule for NoCycle {
}
});
traversed = visitor_result.traversed;
if visitor_result.result {
ctx.diagnostic(no_cycle_diagnostic(
span,