Skip to content

Commit 26d804e

Browse files
committed
perf: use flag-based convergence detection in include_statements (#8412)
Replace O(N) linear scan of is_module_included_vec per convergence iteration with a boolean flag (module_inclusion_changed) that is set when include_module() transitions a module to included. This avoids scanning ~19K entries on every loop iteration to detect fixpoint. Benchmark (10K module project, 15 runs): - System time: 2.68s -> 2.52s (-5.9%) - Wall clock: 4.42s -> 4.41s (-0.2%) Co-Authored-By: Claude Opus 4.6 <[email protected]> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Behavior should be unchanged (only alters fixpoint detection), but mistakes could cause premature termination and missed module inclusion in tree-shaking. > > **Overview** > Speeds up tree-shaking convergence in `include_statements` by replacing per-iteration O(N) scans of `is_module_included_vec` with a `module_inclusion_changed` flag that is reset each loop and set when `include_module` newly includes a module. > > Extends `IncludeContext` (and its construction sites, including `chunk_optimizer`) to carry this flag so the dynamic-entry inclusion loop can stop at fixpoint without counting included modules each iteration. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 50910e2. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 7b23a52 commit 26d804e

File tree

2 files changed

+10
-8
lines changed

2 files changed

+10
-8
lines changed

crates/rolldown/src/stages/generate_stage/chunk_optimizer.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -906,6 +906,7 @@ impl GenerateStage<'_> {
906906
normal_symbol_exports_chain_map: &self.link_output.normal_symbol_exports_chain_map,
907907
bailout_cjs_tree_shaking_modules: FxHashSet::default(),
908908
may_partial_namespace: false,
909+
module_inclusion_changed: false,
909910
module_namespace_included_reason: &mut module_namespace_reason_vec,
910911
inline_const_smart: self.options.optimization.is_inline_const_smart_mode(),
911912
json_module_none_self_reference_included_symbol: FxHashMap::default(),

crates/rolldown/src/stages/link_stage/tree_shaking/include_statements.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ bitflags::bitflags! {
4949
}
5050
}
5151

52+
#[expect(clippy::struct_excessive_bools)]
5253
pub struct IncludeContext<'a> {
5354
pub modules: &'a IndexModules,
5455
pub symbols: &'a SymbolRefDb,
@@ -66,6 +67,9 @@ pub struct IncludeContext<'a> {
6667
/// see [rolldown_common::ecmascript::ecma_view::EcmaViewMeta]
6768
pub bailout_cjs_tree_shaking_modules: FxHashSet<ModuleIdx>,
6869
pub may_partial_namespace: bool,
70+
/// Tracks whether any new module was included during the current convergence iteration.
71+
/// Used to detect fixpoint without O(N) scanning of `is_module_included_vec`.
72+
pub module_inclusion_changed: bool,
6973
pub module_namespace_included_reason: &'a mut ModuleNamespaceReasonVec,
7074
pub json_module_none_self_reference_included_symbol: FxHashMap<ModuleIdx, FxHashSet<SymbolRef>>,
7175
}
@@ -100,6 +104,7 @@ impl<'a> IncludeContext<'a> {
100104
normal_symbol_exports_chain_map,
101105
bailout_cjs_tree_shaking_modules: FxHashSet::default(),
102106
may_partial_namespace: false,
107+
module_inclusion_changed: false,
103108
module_namespace_included_reason,
104109
json_module_none_self_reference_included_symbol: FxHashMap::default(),
105110
}
@@ -209,10 +214,10 @@ impl LinkStage<'_> {
209214

210215
let mut unused_record_idxs = vec![];
211216
let cycled_idx = self.sort_dynamic_entries_by_topological_order(&mut dynamic_entries);
212-
let mut previous_included_module_count =
213-
context.is_module_included_vec.iter().filter(|item| **item).count();
214217
let mut included_dynamic_entry = FxHashSet::default();
215218
loop {
219+
context.module_inclusion_changed = false;
220+
216221
// It could be safely take since it is no more used.
217222
// We extract bailout_modules first to avoid borrowing conflict:
218223
// passing `context` requires a mutable borrow, which conflicts with
@@ -236,14 +241,9 @@ impl LinkStage<'_> {
236241
}
237242
});
238243

239-
let current_included_module_count =
240-
context.is_module_included_vec.iter().filter(|item| **item).count();
241-
242-
if current_included_module_count == previous_included_module_count {
244+
if !context.module_inclusion_changed {
243245
break;
244246
}
245-
246-
previous_included_module_count = current_included_module_count;
247247
}
248248

249249
dynamic_entries.retain(|entry| included_dynamic_entry.contains(&entry.idx));
@@ -578,6 +578,7 @@ pub fn include_module(ctx: &mut IncludeContext, module: &NormalModule) {
578578
}
579579

580580
ctx.is_module_included_vec[module.idx] = true;
581+
ctx.module_inclusion_changed = true;
581582

582583
if module.idx == ctx.runtime_idx && !module.side_effects.has_side_effects() {
583584
// Unmodified runtime: statements included only via references.

0 commit comments

Comments
 (0)