perf(es/transformer): remove O(n^2) statement mutation hotspots#11672
perf(es/transformer): remove O(n^2) statement mutation hotspots#11672
Conversation
|
Binary Sizes
Commit: fa5bb5e |
PR Review: perf(transformer): remove O(n^2) statement mutation hotspotsOverall this is a solid performance improvement PR. The changes are well-scoped, the algorithmic improvements are correct, and the new unit tests provide good coverage for the refactored logic. A few observations: Positive
Minor Issues / Suggestions
Correctness
No Issues Found
LGTM with the minor suggestions above. |
There was a problem hiding this comment.
Pull request overview
This PR targets known O(n^2)-prone AST mutation patterns in swc_ecma_transformer by replacing repeated Vec::insert/remove operations with single-pass rebuilds and faster membership checks in hot paths.
Changes:
- Replaces repeated front-insert statement prepends in object-rest lowering with a one-shot
prepend_stmts_to_frontmerge. - Rewrites object-rest
exit_module_itemsexport-var handling to rebuild module items in a single pass instead of repeated middle mutations. - Refactors
StmtInjectorto apply collected insertions via a single rebuild for bothVec<Stmt>andVec<ModuleItem>, and adds unit tests for ordering semantics (partially).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| crates/swc_ecma_transformer/src/es2022/private_property_in_object.rs | Switches private method/static tracking from Vec to FxHashSet to speed up membership checks. |
| crates/swc_ecma_transformer/src/es2018/object_rest_spread.rs | Removes repeated insert(0, ...) and rewrites export-var rewriting to a rebuild approach; adds a helper for prepend materialization. |
| crates/swc_ecma_transformer/src/common/statement_injector.rs | Replaces shift-heavy insertion loops with single-pass rebuilds and introduces new tests for insertion ordering (with some gaps). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| @@ -455,86 +445,55 @@ impl VisitMutHook<TraverseCtx> for ObjectRestSpreadPass { | |||
| } | |||
|
|
|||
| fn exit_module_items(&mut self, items: &mut Vec<ModuleItem>, _: &mut TraverseCtx) { | |||
There was a problem hiding this comment.
exit_module_items rebuilds the entire items vec unconditionally (items.take() + push into rewritten). When there are no export var declarations collected for rewriting (self.export_idents.is_empty()), this adds an avoidable allocation and full-module move/copy on the common fast path. Add an early return when export_idents is empty (and/or when no ExportDecl(Var) items are present) so modules that don't need export-var rewriting keep the cheaper no-op behavior.
| fn exit_module_items(&mut self, items: &mut Vec<ModuleItem>, _: &mut TraverseCtx) { | |
| fn exit_module_items(&mut self, items: &mut Vec<ModuleItem>, _: &mut TraverseCtx) { | |
| // Fast path: if no export var declarations were collected, there is nothing to rewrite. | |
| if self.export_idents.is_empty() { | |
| return; | |
| } |
| fn exit_module_items(&mut self, node: &mut Vec<ModuleItem>, ctx: &mut TraverseCtx) { | ||
| // First pass: collect all (index, adjacent_stmts) pairs while addresses are | ||
| // valid | ||
| // valid. | ||
| let mut insertions = Vec::new(); | ||
| let mut insertion_count = 0; | ||
| for (i, item) in node.iter().enumerate() { | ||
| // Only process ModuleItem::Stmt variants | ||
| if let ModuleItem::Stmt(stmt) = item { | ||
| let address = stmt as *const Stmt; | ||
| if let Some(adjacent_stmts) = ctx.statement_injector.take_stmts(address) { | ||
| insertion_count += adjacent_stmts.len(); | ||
| insertions.push((i, adjacent_stmts)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Second pass: process in reverse order to avoid index invalidation | ||
| for (i, adjacent_stmts) in insertions.into_iter().rev() { | ||
| let mut before_stmts = Vec::new(); | ||
| let mut after_stmts = Vec::new(); | ||
| if insertions.is_empty() { | ||
| return; | ||
| } | ||
|
|
||
| // Separate statements by direction | ||
| for adjacent in adjacent_stmts { | ||
| match adjacent.direction { | ||
| Direction::Before => before_stmts.push(adjacent.stmt), | ||
| Direction::After => after_stmts.push(adjacent.stmt), | ||
| } | ||
| } | ||
| let mut next_insertion = insertions.into_iter().peekable(); | ||
| let original = node.take(); | ||
| let mut rewritten = Vec::with_capacity(original.len() + insertion_count); | ||
|
|
||
| // Insert statements after (insert first since we're going backwards) | ||
| if !after_stmts.is_empty() { | ||
| // Insert all after statements at position i + 1 | ||
| for (offset, stmt) in after_stmts.into_iter().enumerate() { | ||
| node.insert(i + 1 + offset, ModuleItem::Stmt(stmt)); | ||
| } | ||
| } | ||
| for (i, item) in original.into_iter().enumerate() { | ||
| let mut after = Vec::new(); | ||
|
|
||
| if matches!( | ||
| next_insertion.peek(), | ||
| Some(&(insertion_idx, _)) if insertion_idx == i | ||
| ) { | ||
| let (_, adjacent_stmts) = next_insertion.next().unwrap(); | ||
|
|
||
| // Insert statements before | ||
| if !before_stmts.is_empty() { | ||
| // Insert all before statements at position i | ||
| for (offset, stmt) in before_stmts.into_iter().enumerate() { | ||
| node.insert(i + offset, ModuleItem::Stmt(stmt)); | ||
| for adjacent in adjacent_stmts { | ||
| match adjacent.direction { | ||
| Direction::Before => rewritten.push(ModuleItem::Stmt(adjacent.stmt)), | ||
| Direction::After => after.push(ModuleItem::Stmt(adjacent.stmt)), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| rewritten.push(item); | ||
| rewritten.extend(after); | ||
| } | ||
|
|
||
| *node = rewritten; | ||
| } |
There was a problem hiding this comment.
The new exit_module_items rebuild logic is only lightly covered by tests (current test asserts targeting behavior, but not before+after ordering for a single target nor ordering across multiple targets within Vec<ModuleItem>). Add a test analogous to exit_stmts_keeps_before_and_after_order_for_one_target, but for exit_module_items, to lock in the intended ordering semantics for both directions in the module-items path.
Summary
This PR addresses the highest-impact performance hotspots from #11667 in
swc_ecma_transformer(#1-#4scope):Vec::insert(0, ...)patterns in object-rest lowering by using one-shot prepend materialization.exit_module_itemsexport-var rewriting to a single-pass rebuild, avoiding repeated middleremove/insertmutations.Vec<Stmt>andVec<ModuleItem>to rebuild once while preserving insertion order and statement-address targeting semantics.methods,statics) fromVec<Atom>toFxHashSet<Atom>for faster membership checks in hot paths.Testing
git submodule update --init --recursivecargo test -p swc_ecma_transformercargo fmt --allcargo clippy --all --all-targets -- -D warningscargo test -p swc_ecma_transforms_compat(fails in this environment becausemochais unavailable for existing exec tests)cargo test -p swc_ecma_transforms_compat --test es2018_object_rest_spread -- --skip exec --skip issue_6029_1 --skip issue_6029_2 --skip issue_4631Notes
#1-#4; follow-up work (#5-#7) is not included.