Skip to content

Commit 4668df9

Browse files
committed
merge_nodes_with_same_starting_point
1 parent 2283eca commit 4668df9

File tree

1 file changed

+100
-44
lines changed

1 file changed

+100
-44
lines changed

turbopack/crates/turbopack-ecmascript/src/tree_shake/optimizations.rs

+100-44
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ impl GraphOptimizer<'_> {
125125
did_work
126126
}
127127

128+
/// This function merges nodes that can only be reached from a single starting point.
129+
/// Example:
130+
/// If we have a graph with edges: A->B, B->C, A->C, B->E, D->E
131+
/// Then B and C can only be reached from A, so they will be merged into A.
132+
/// The resulting graph would have edges like: (A,B,C)->E, D->E
128133
pub(super) fn merge_nodes_with_same_starting_point<N>(
129134
&self,
130135
g: &mut Graph<Vec<N>, Dependency>,
@@ -134,64 +139,115 @@ impl GraphOptimizer<'_> {
134139
Self: Index<N, Output = ItemId>,
135140
{
136141
let mut did_work = false;
137-
let mut nodes_to_merge = Vec::new();
142+
let mut reachability: FxHashMap<_, FxHashSet<_>> = FxHashMap::default();
138143

139-
for node in g.node_indices() {
140-
let items = g.node_weight(node).expect("Node should exist");
141-
for item in items {
142-
let item_id = &self[*item];
143-
if matches!(item_id, ItemId::Group(..)) {
144-
let incoming_edges: Vec<_> =
145-
g.edges_directed(node, Direction::Incoming).collect();
146-
if incoming_edges.len() == 1 && incoming_edges[0].source() == node {
147-
nodes_to_merge.push(node);
148-
break;
149-
}
144+
// Step 1: Build a reverse reachability map (which starting nodes can reach each node)
145+
// We consider a "starting node" as one with no incoming edges
146+
let starting_nodes: Vec<_> = g
147+
.node_indices()
148+
.filter(|&node| g.edges_directed(node, Direction::Incoming).count() == 0)
149+
.collect();
150+
151+
// For each starting node, find all nodes reachable from it
152+
for &start in &starting_nodes {
153+
let mut visited = FxHashSet::default();
154+
let mut queue = vec![start];
155+
156+
while let Some(node) = queue.pop() {
157+
if !visited.insert(node) {
158+
continue;
159+
}
160+
161+
// For each outgoing edge, add the target to queue
162+
for edge in g.edges_directed(node, Direction::Outgoing) {
163+
let target = edge.target();
164+
queue.push(target);
165+
166+
// Add this starting node to the set of starting nodes that can reach target
167+
reachability.entry(target).or_default().insert(start);
150168
}
151169
}
152170
}
153171

154-
for node in nodes_to_merge {
155-
let mut dependencies = g
156-
.edges_directed(node, Direction::Outgoing)
157-
.map(|e| (e.target(), *e.weight()))
158-
.collect::<Vec<_>>();
172+
// Step 2: Find nodes that are reachable from exactly one starting node
173+
// and group them by that starting node
174+
let mut merge_groups: FxHashMap<_, Vec<_>> = FxHashMap::default();
159175

160-
// Handle transitive dependencies
161-
let mut visited = FxHashSet::default();
162-
let mut stack = dependencies.clone();
163-
while let Some((dependency, _weight)) = stack.pop() {
164-
if visited.insert(dependency) {
165-
let transitive_deps = g
166-
.edges_directed(dependency, Direction::Outgoing)
167-
.map(|e| (e.target(), *e.weight()))
168-
.collect::<Vec<_>>();
169-
stack.extend(transitive_deps.clone());
170-
dependencies.extend(transitive_deps);
176+
for node in g.node_indices() {
177+
// Skip starting nodes
178+
if starting_nodes.contains(&node) {
179+
continue;
180+
}
181+
182+
// Skip nodes that should not be merged
183+
if self.should_not_merge_iter(g.node_weight(node).expect("Node should exist")) {
184+
continue;
185+
}
186+
187+
// If this node is reachable from exactly one starting node, add it to that group
188+
if let Some(reachable_from) = reachability.get(&node) {
189+
if reachable_from.len() == 1 {
190+
let start = *reachable_from.iter().next().unwrap();
191+
192+
// Don't merge if the starting node should not be merged
193+
if self.should_not_merge_iter(g.node_weight(start).expect("Node should exist"))
194+
{
195+
continue;
196+
}
197+
198+
merge_groups.entry(start).or_default().push(node);
171199
}
172200
}
201+
}
173202

174-
for (dependency, weight) in dependencies {
175-
let edge = g
176-
.find_edge(node, dependency)
177-
.and_then(|e| g.edge_weight_mut(e));
178-
match edge {
179-
Some(v) => {
180-
if matches!(v, Dependency::Weak) {
181-
*v = weight;
203+
// Step 3: Merge nodes into their starting points
204+
for (start, nodes_to_merge) in merge_groups {
205+
if nodes_to_merge.is_empty() {
206+
continue;
207+
}
208+
209+
let mut nodes_to_remove = Vec::new();
210+
211+
for node in nodes_to_merge {
212+
// Move outgoing edges from node to start
213+
let outgoing_edges: Vec<_> = g
214+
.edges_directed(node, Direction::Outgoing)
215+
.map(|e| (e.target(), *e.weight()))
216+
.collect();
217+
218+
for (target, weight) in outgoing_edges {
219+
// If there's already an edge from start to target, only update if necessary
220+
let existing_edge = g.find_edge(start, target);
221+
match existing_edge {
222+
Some(e) => {
223+
let edge_weight = g.edge_weight_mut(e).unwrap();
224+
// Only upgrade from weak to strong dependency
225+
if matches!(edge_weight, Dependency::Weak)
226+
&& !matches!(weight, Dependency::Weak)
227+
{
228+
*edge_weight = weight;
229+
}
230+
}
231+
None => {
232+
// Add a new edge
233+
g.add_edge(start, target, weight);
182234
}
183-
}
184-
None => {
185-
g.add_edge(node, dependency, weight);
186235
}
187236
}
188-
}
189237

190-
let items = g.node_weight(node).expect("Node should exist").clone();
191-
g.node_weight_mut(node).unwrap().extend(items);
238+
// Move items from this node to the starting node
239+
let items = g.node_weight(node).expect("Node should exist").clone();
240+
g.node_weight_mut(start).unwrap().extend(items);
192241

193-
g.remove_node(node).expect("Node should exist");
194-
did_work = true;
242+
nodes_to_remove.push(node);
243+
}
244+
245+
// Remove merged nodes (in reverse order to preserve indices)
246+
nodes_to_remove.sort();
247+
for node in nodes_to_remove.into_iter().rev() {
248+
g.remove_node(node);
249+
did_work = true;
250+
}
195251
}
196252

197253
did_work

0 commit comments

Comments
 (0)