Skip to content

Commit 79e6c4a

Browse files
committed
perf: single-pass BFS with shared visited set for TLA extraction
Replace per-async-chunk independent BFS with a single BFS pass sharing one visited set across all async chunks. Pre-compute a reverse map (chunk → which async chunks consider it an ancestor) to enable O(1) ancestor lookups during traversal. Complexity: O(N + E + A × G) down from O(A × (N + E + G)) where N=modules, E=edges, A=async chunks, G=chunk groups.
1 parent 270df45 commit 79e6c4a

1 file changed

Lines changed: 56 additions & 45 deletions

File tree

crates/rspack_plugin_esm_library/src/optimize_chunks.rs

Lines changed: 56 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -48,77 +48,88 @@ pub(crate) fn extract_tla_shared_modules(compilation: &mut Compilation) -> bool
4848
return false;
4949
}
5050

51-
// 2. For each async chunk, find ancestor chunks and scan for cross-chunk deps.
52-
// Collect modules to extract keyed by module id → set of source chunks they
53-
// need to be removed from. This ensures each module is only extracted once
54-
// even if it appears in multiple ancestor chunks.
55-
let mut modules_to_extract: IdentifierMap<FxHashSet<ChunkUkey>> = IdentifierMap::default();
56-
57-
for async_chunk_ukey in &async_chunks {
58-
// Get all ancestor chunk group ukeys
51+
// 2. Pre-compute: for each async chunk, its ancestor chunks; also build a
52+
// reverse lookup from chunk → which async chunks consider it an ancestor.
53+
// Then do a single BFS across all async chunks sharing one visited set.
54+
let mut chunk_to_ancestor_of: FxHashMap<ChunkUkey, Vec<ChunkUkey>> = FxHashMap::default();
55+
let mut module_to_async_chunk: IdentifierMap<ChunkUkey> = IdentifierMap::default();
56+
let mut queue: VecDeque<ModuleIdentifier> = VecDeque::new();
57+
58+
for &async_chunk_ukey in &async_chunks {
5959
let chunk = compilation
6060
.build_chunk_graph_artifact
6161
.chunk_by_ukey
62-
.expect_get(async_chunk_ukey);
62+
.expect_get(&async_chunk_ukey);
6363
let mut ancestor_group_ukeys = FxHashSet::default();
6464
for group_ukey in chunk.groups() {
6565
let group = chunk_group_by_ukey.expect_get(group_ukey);
6666
ancestor_group_ukeys.extend(group.ancestors(chunk_group_by_ukey));
6767
}
68+
for g in &ancestor_group_ukeys {
69+
for &ancestor_chunk in &chunk_group_by_ukey.expect_get(g).chunks {
70+
chunk_to_ancestor_of
71+
.entry(ancestor_chunk)
72+
.or_default()
73+
.push(async_chunk_ukey);
74+
}
75+
}
6876

69-
// Collect ancestor chunk ukeys from ancestor groups
70-
let ancestor_chunks: FxHashSet<ChunkUkey> = ancestor_group_ukeys
71-
.iter()
72-
.flat_map(|g| chunk_group_by_ukey.expect_get(g).chunks.iter().copied())
73-
.collect();
77+
// Seed BFS with all modules in this async chunk
78+
for &m in chunk_graph.get_chunk_modules_identifier(&async_chunk_ukey) {
79+
module_to_async_chunk.insert(m, async_chunk_ukey);
80+
queue.push_back(m);
81+
}
82+
}
83+
84+
// Single BFS across all async chunks with a shared visited set — O(N + E).
85+
// Each module is visited at most once. When a target is in an ancestor chunk
86+
// of the originating async chunk, we mark it for extraction.
87+
let mut modules_to_extract: IdentifierMap<FxHashSet<ChunkUkey>> = IdentifierMap::default();
88+
let mut visited = IdentifierSet::default();
7489

75-
if ancestor_chunks.is_empty() {
90+
while let Some(module_id) = queue.pop_front() {
91+
if !visited.insert(module_id) {
7692
continue;
7793
}
7894

79-
let chunk_modules = chunk_graph.get_chunk_modules_identifier(async_chunk_ukey);
80-
81-
// BFS through all outgoing dependencies of modules in async chunk
82-
let mut visited = IdentifierSet::default();
83-
let mut queue: VecDeque<ModuleIdentifier> = chunk_modules.iter().copied().collect();
95+
// Determine which async chunk this module belongs to
96+
let Some(&origin_async_chunk) = module_to_async_chunk.get(&module_id) else {
97+
continue;
98+
};
8499

85-
while let Some(module_id) = queue.pop_front() {
86-
if !visited.insert(module_id) {
100+
for conn in module_graph.get_outgoing_connections(&module_id) {
101+
if !conn.is_target_active(
102+
module_graph,
103+
None,
104+
&compilation.module_graph_cache_artifact,
105+
&compilation.exports_info_artifact,
106+
) {
87107
continue;
88108
}
89109

90-
for conn in module_graph.get_outgoing_connections(&module_id) {
91-
// Skip inactive connections
92-
if !conn.is_target_active(
93-
module_graph,
94-
None,
95-
&compilation.module_graph_cache_artifact,
96-
&compilation.exports_info_artifact,
97-
) {
98-
continue;
99-
}
100-
101-
let Some(target) = module_graph.module_identifier_by_dependency_id(&conn.dependency_id)
102-
else {
103-
continue;
104-
};
110+
let Some(target) = module_graph.module_identifier_by_dependency_id(&conn.dependency_id)
111+
else {
112+
continue;
113+
};
105114

106-
let target_chunks = chunk_graph.get_module_chunks(*target);
115+
let target_chunks = chunk_graph.get_module_chunks(*target);
107116

108-
// Check if target is in an ancestor chunk → circular dep
109-
for &target_chunk in target_chunks {
110-
if ancestor_chunks.contains(&target_chunk) {
117+
// Check if target is in an ancestor chunk of origin_async_chunk → circular dep
118+
for &target_chunk in target_chunks {
119+
if let Some(ancestor_of) = chunk_to_ancestor_of.get(&target_chunk) {
120+
if ancestor_of.contains(&origin_async_chunk) {
111121
modules_to_extract
112122
.entry(*target)
113123
.or_default()
114124
.insert(target_chunk);
115125
}
116126
}
127+
}
117128

118-
// Continue BFS if target is in the same async chunk
119-
if target_chunks.contains(async_chunk_ukey) {
120-
queue.push_back(*target);
121-
}
129+
// Continue BFS if target is in the same async chunk
130+
if target_chunks.contains(&origin_async_chunk) {
131+
module_to_async_chunk.insert(*target, origin_async_chunk);
132+
queue.push_back(*target);
122133
}
123134
}
124135
}

0 commit comments

Comments
 (0)