perf(semantic): make building AST nodes optional via parent-pointer storage#22859
Closed
Boshen wants to merge 1 commit into
Closed
perf(semantic): make building AST nodes optional via parent-pointer storage#22859Boshen wants to merge 1 commit into
Boshen wants to merge 1 commit into
Conversation
Merging this PR will degrade performance by 4.99%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | Simulation | pipeline[App.tsx] |
34.3 ms | 36.4 ms | -5.77% |
| ❌ | Simulation | semantic[App.tsx] |
5.3 ms | 5.7 ms | -5.69% |
| ❌ | Simulation | pipeline[binder.ts] |
13.3 ms | 14 ms | -5.18% |
| ❌ | Simulation | semantic[react.development.js] |
1 ms | 1.1 ms | -4.96% |
| ❌ | Simulation | semantic[binder.ts] |
2.5 ms | 2.6 ms | -4.93% |
| ❌ | Simulation | pipeline[react.development.js] |
6.4 ms | 6.7 ms | -4.34% |
| ❌ | Simulation | pipeline[RadixUIAdoptionSection.jsx] |
320.9 µs | 334.4 µs | -4.06% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing perf/semantic-optional-ast-nodes (3831e17) with main (17e7cf3)
Footnotes
-
9 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
2951dd3 to
ff121b4
Compare
This was referenced May 31, 2026
semantic: decouple redeclaration checks from random AST-node access (prerequisite for #22859)
#22866
Closed
d102710 to
5c6ccff
Compare
…torage `SemanticBuilder` now defaults to a lightweight ancestor-stack node storage that retains only the live `root..=current` chain instead of every AST node. Consumers that need random access to nodes after the build (linter, formatter, mangler) opt in with `with_ast_nodes(true)`; `with_cfg(true)` forces full storage too. Scoping is produced identically in both modes, so pipelines that discard nodes and keep only `Scoping` (transform, minify, define/inject) avoid the per-node allocations of full storage.
5c6ccff to
3831e17
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
SemanticBuildercan now build without retaining every AST node. It defaults to a lightweight ancestor-stack storage that keeps only the liveroot..=currentchain during traversal. Consumers that need random access to nodes after the build opt back in with.with_ast_nodes(true)(and.with_cfg(true)forces full storage automatically, since CFG construction needs per-node data).This makes building AST nodes optional for pipelines that only consume
Scoping: theoxccompiler pipeline, transform, minify, and define/inject all callinto_scoping()and discard the nodes today, so building them was wasted work.How
A new
NodeStorageenum inoxc_semantic(node/storage.rs) with two backends:Full(AstNodes)— today's flat random-access storage, retained after the build.Ancestors { stack, len }— aStackof the live ancestor chain, pushed on enter / popped on exit, serving the parent/ancestor queries the binder, class-table builder, and syntax checker need (they only ever look upward).Both allocate
NodeIds from the same monotonic counter, soScopingis identical regardless of mode.Semantic::nodes()is empty in ancestor-stack mode; a newnode_countfield keepsSemantic::stats()accurate sowith_statsreuse still pre-allocates correctly.Redeclaration checks (the only build-time reads of an already-popped node)
Two redeclaration checks used to dereference a previously declared (already-popped) function node. Neither touches
AstNodesany more, so both behave identically in either storage mode:check_function_redeclaration(Annex B.3.3) consults theasync_or_generator_function_node_idslist recorded at bind time — landed separately onmainin refactor(semantic): identify illegal function declarations without using AST nodes #22972 / perf(semantic): move cold function redeclaration handling into#[cold]function #22973, which this branch is now rebased on.check_redeclarationdistinguishes a named function expression from a previous function declaration by comparingNodeIds: a function expression binds its own name in the scope it creates, so the name symbol's declaration node is that scope's node (symbol_declaration(id) == get_node_id(symbol_scope_id(id))). A previous function declaration is bound in the enclosing scope, so it doesn't match. No node kind is read.Every other build-time node read resolves the current node, the enclosing function, an enclosing scope's node, or the enclosing class's node — all guaranteed to be on the live ancestor stack.
Consumers
detect_code_removal, playground, coverage driver (via a newCompilerInterface::build_ast_nodes()hook), and semantic's own tests/examples. The linter already builds withwith_cfg(true), so it gets full storage automatically.Validation
oxc_semantic, mangler, minifier, transformer, and codegen tests pass.cargo coverage -- semanticleaves the snapshot failure-counts byte-identical (and it exercisescheck_redeclarationin full-node mode, so unchanged snapshots prove theNodeIdcomparison matches the original node read).clippy,rustdoc -D warnings, andcargo fmtclean.Note
The win is build-local:
into_scoping()already freed nodes before transform, so this trims the per-nodeAstNodesallocations/writes during each pipeline semantic build, not transform/codegen themselves.🤖 This PR was authored with Claude Code.