Skip to content

perf(es/transformer): remove O(n^2) statement mutation hotspots#11672

Merged
kdy1 merged 1 commit intomainfrom
kdy1/fix-11667-transformer-hotspots
Mar 12, 2026
Merged

perf(es/transformer): remove O(n^2) statement mutation hotspots#11672
kdy1 merged 1 commit intomainfrom
kdy1/fix-11667-transformer-hotspots

Conversation

@kdy1
Copy link
Copy Markdown
Member

@kdy1 kdy1 commented Mar 12, 2026

Summary

This PR addresses the highest-impact performance hotspots from #11667 in swc_ecma_transformer (#1-#4 scope):

  • Remove repeated front Vec::insert(0, ...) patterns in object-rest lowering by using one-shot prepend materialization.
  • Rewrite object-rest exit_module_items export-var rewriting to a single-pass rebuild, avoiding repeated middle remove/insert mutations.
  • Rework statement injector insertion for both Vec<Stmt> and Vec<ModuleItem> to rebuild once while preserving insertion order and statement-address targeting semantics.
  • Switch private-in-object class membership tracking (methods, statics) from Vec<Atom> to FxHashSet<Atom> for faster membership checks in hot paths.

Testing

  • git submodule update --init --recursive
  • cargo test -p swc_ecma_transformer
  • cargo fmt --all
  • cargo clippy --all --all-targets -- -D warnings
  • cargo test -p swc_ecma_transforms_compat (fails in this environment because mocha is 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_4631

Notes

  • No public API changes.
  • Kept scope intentionally limited to findings #1-#4; follow-up work (#5-#7) is not included.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 12, 2026

⚠️ No Changeset found

Latest commit: 2f1f4eb

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor

Binary Sizes

File Size
swc.linux-x64-gnu.node 28M (28739528 bytes)

Commit: fa5bb5e

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Mar 12, 2026

Merging this PR will not alter performance

✅ 218 untouched benchmarks


Comparing kdy1/fix-11667-transformer-hotspots (2f1f4eb) with main (0f24ee1)

Open in CodSpeed

@kdy1 kdy1 marked this pull request as ready for review March 12, 2026 01:07
@kdy1 kdy1 requested a review from a team as a code owner March 12, 2026 01:07
Copilot AI review requested due to automatic review settings March 12, 2026 01:07
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 12, 2026

PR Review: perf(transformer): remove O(n^2) statement mutation hotspots

Overall 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

  • Correct algorithmic fix: Replacing reverse-iterate-and-insert with single-pass rebuild in StmtInjector is the right approach. The old code was O(n*k) due to repeated Vec::insert calls that shift elements; the new code is O(n+k).
  • Good test coverage: The three new unit tests in statement_injector.rs cover single-target before/after ordering, multi-target ordering, and module-item-specific behavior. These directly validate the invariants of the rewrite.
  • FxHashSet for membership checks: Switching methods and statics from Vec<Atom> to FxHashSet<Atom> in private_property_in_object.rs is a clear win — these are only used for .contains() lookups, never iterated in order.
  • Clean refactor of exit_module_items: The object-rest exit_module_items rewrite from index-mutation loop to for item in original is much more readable and avoids the fragile items.remove(i) / items.insert(i, ...) pattern.

Minor Issues / Suggestions

  1. prepend_stmts_to_front with vec![single_stmt]: There are three call sites that pass vec![stmt] (lines 434, 535, 574). For these single-element cases, the function allocates a temporary Vec, then mem::takes the original, allocates a new merged vec, and does two append calls. A simple stmts.insert(0, stmt) would be cheaper for a single element when stmts is small (which it typically is for a function/catch body). Consider either:

    • An overload like prepend_stmt_to_front(stmts: &mut Vec<Stmt>, stmt: Stmt) for the single-element case that just does stmts.insert(0, stmt).
    • Or accepting a single Stmt parameter alongside the Vec variant.

    That said, this is a minor nit — the hot paths (collector.into_stmts() cases) already pass multi-element vectors, and the single-element sites are unlikely to be hot.

  2. after vec allocation inside the loop: In both exit_module_items and exit_stmts, a let mut after = Vec::new() is allocated on every iteration of the loop, even for items that have no insertions (the common case). Consider hoisting the after vec outside the loop and using after.clear() to reuse the allocation:

    let mut after = Vec::new();
    for (i, item) in original.into_iter().enumerate() {
        after.clear();
        // ...
        rewritten.extend(after.drain(..));
    }

    This avoids repeated small allocations when multiple items have after-insertions.

  3. Code duplication between exit_module_items and exit_stmts: The two methods are nearly identical — they differ only in the wrapper type (ModuleItem::Stmt(...) vs bare Stmt). This isn't new to this PR, but the rewrite makes the parallel structure even more visible. A generic helper or macro could reduce the duplication, though this is a matter of taste and not a blocker.

Correctness

  • The pointer-based address matching relies on the original vec being built from node.take() / stmts.take(), which moves elements out. Since the first pass collects addresses before the take, and take() uses mem::take (which replaces with default, not realloc), the addresses collected in pass 1 remain valid for matching in pass 2. This is correct.
  • The insertions iterator is consumed in order, matching monotonically increasing indices from the first pass — the peek/next pattern is correct.

No Issues Found

  • No security concerns.
  • No public API changes.
  • Test coverage is appropriate for the changes.

LGTM with the minor suggestions above.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_front merge.
  • Rewrites object-rest exit_module_items export-var handling to rebuild module items in a single pass instead of repeated middle mutations.
  • Refactors StmtInjector to apply collected insertions via a single rebuild for both Vec<Stmt> and Vec<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) {
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;
}

Copilot uses AI. Check for mistakes.
Comment on lines 114 to 160
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;
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@kdy1 kdy1 changed the title perf(transformer): remove O(n^2) statement mutation hotspots perf(es/transformer): remove O(n^2) statement mutation hotspots Mar 12, 2026
@kdy1 kdy1 merged commit bdc24b7 into main Mar 12, 2026
208 checks passed
@kdy1 kdy1 deleted the kdy1/fix-11667-transformer-hotspots branch March 12, 2026 01:42
@github-actions github-actions Bot added this to the Planned milestone Mar 12, 2026
@github-actions github-actions Bot modified the milestones: Planned, 1.15.21 Mar 22, 2026
@swc-project swc-project locked as resolved and limited conversation to collaborators Apr 22, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants