Skip to content

perf(oxc): conditionally rebuild semantic in compiler pipeline#23153

Merged
graphite-app[bot] merged 1 commit into
mainfrom
perf/compiler-conditional-semantic-rebuild
Jun 9, 2026
Merged

perf(oxc): conditionally rebuild semantic in compiler pipeline#23153
graphite-app[bot] merged 1 commit into
mainfrom
perf/compiler-conditional-semantic-rebuild

Conversation

@Boshen

@Boshen Boshen commented Jun 9, 2026

Copy link
Copy Markdown
Member

What

Track scoping/AST sync state with a single scoping_dirty flag in Compiler::compile so each downstream step (inject/define plugins, DCE, compress) only rebuilds semantic when it's actually needed.

Details

  • InjectGlobalVariables and ReplaceGlobalDefines already return a changed: bool; this was previously ignored. It's now OR-ed into scoping_dirty.
  • scoping_dirty starts true only when the transformer runs — the parser, React Compiler (rebuilds scoping on success, returns the untouched scoping on bail-out), and any fresh SemanticBuilder all leave scoping in sync.
  • The pre-plugin rebuild is now gated on scoping_dirty, skipping a needless rebuild when no transform ran.
  • DCE and Compress are now a single mutually-exclusive if/else, both rebuilding semantic only when scoping_dirty.
  • Compressor::build (which always rebuilds semantic internally) is replaced with build_with_scoping, so when scoping is already in sync the compressor skips its own SemanticBuilder pass.
  • The three rebuild sites are extracted into a rebuild_scoping helper, which enables enum eval when a transform is configured so const-enum resolution matches the original semantic build.

Note

The CompilerInterface::compress trait method gains a scoping: Scoping parameter — a breaking change for external implementors that override it.

@Boshen Boshen force-pushed the perf/compiler-conditional-semantic-rebuild branch from d2bc453 to 8befc4c Compare June 9, 2026 14:13
@Boshen Boshen marked this pull request as ready for review June 9, 2026 14:17
@codspeed-hq

codspeed-hq Bot commented Jun 9, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 8.91%

⚡ 4 improved benchmarks
✅ 48 untouched benchmarks
⏩ 19 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation pipeline[binder.ts] 15.1 ms 13.6 ms +11.41%
Simulation pipeline[App.tsx] 38.7 ms 35.2 ms +9.92%
Simulation pipeline[RadixUIAdoptionSection.jsx] 352.8 µs 329.1 µs +7.2%
Simulation pipeline[kitchen-sink.tsx] 130.9 ms 122.1 ms +7.15%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing perf/compiler-conditional-semantic-rebuild (fbdd1a4) with main (9e19b9b)

Open in CodSpeed

Footnotes

  1. 19 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.

@Boshen Boshen added the 0-merge Merge with Graphite Merge Queue label Jun 9, 2026

Boshen commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

Merge activity

## What

Track scoping/AST sync state with a single `scoping_dirty` flag in `Compiler::compile` so each downstream step (inject/define plugins, DCE, compress) only rebuilds semantic when it's actually needed.

## Details

- `InjectGlobalVariables` and `ReplaceGlobalDefines` already return a `changed: bool`; this was previously ignored. It's now OR-ed into `scoping_dirty`.
- `scoping_dirty` starts `true` only when the transformer runs — the parser, React Compiler (rebuilds scoping on success, returns the untouched scoping on bail-out), and any fresh `SemanticBuilder` all leave scoping in sync.
- The pre-plugin rebuild is now gated on `scoping_dirty`, skipping a needless rebuild when no transform ran.
- DCE and Compress are now a single mutually-exclusive `if/else`, both rebuilding semantic only when `scoping_dirty`.
- `Compressor::build` (which always rebuilds semantic internally) is replaced with `build_with_scoping`, so when scoping is already in sync the compressor skips its own `SemanticBuilder` pass.
- The three rebuild sites are extracted into a `rebuild_scoping` helper, which enables enum eval when a transform is configured so const-enum resolution matches the original semantic build.

## Note

The `CompilerInterface::compress` trait method gains a `scoping: Scoping` parameter — a breaking change for external implementors that override it.
@graphite-app graphite-app Bot force-pushed the perf/compiler-conditional-semantic-rebuild branch from fbdd1a4 to 534f9c6 Compare June 9, 2026 14:25
@graphite-app graphite-app Bot merged commit 534f9c6 into main Jun 9, 2026
29 checks passed
@graphite-app graphite-app Bot removed the 0-merge Merge with Graphite Merge Queue label Jun 9, 2026
@graphite-app graphite-app Bot deleted the perf/compiler-conditional-semantic-rebuild branch June 9, 2026 14:30
graphite-app Bot pushed a commit that referenced this pull request Jun 15, 2026
… reuse stats in mangler builds (#23352)

## What

Fix three allocation problems in the semantic rebuilds performed by the minify/mangle paths, found while auditing the compiler pipeline's scoping rebuilds.

## Details

- **`Semantic::stats` reported `nodes: 0` after node-less builds.** It read `self.nodes.len()`, which is empty when the builder runs in ancestry mode (`with_build_nodes(false)` — the compiler pipeline default). `Semantic` now tracks `node_count` (from `AstNodeStore::node_count`, the source of truth in both modes) and `stats()` returns it. The mangler build inside `Minifier::build` previously received `nodes: 0` and grew the `AstNodes` store from zero capacity by repeated doubling; it now pre-allocates correctly.
- **`Minifier::build` with `compress: None` fed the mangler `Stats::default()`.** All-zero stats are worse than no stats: they skip the `Stats::count` fallback *and* reserve zero capacity for every table. Stats are now threaded as `Option<Stats>` and only passed when the compress phase produced them; the mangle-only path falls back to the counting pass and sizes correctly.
- **`Mangler::build` always re-counted the AST.** Added `Mangler::with_stats`, and `CompilerInterface::mangle` now receives the stats from the initial semantic build, so the mangler's internal semantic rebuild skips its full-AST counting traversal.

Output is unchanged — stats only affect pre-allocation.

## Note

`CompilerInterface::mangle` gains a `stats: Stats` parameter — a breaking change for external implementors that override it (same shape as the `compress` change in #23153).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Boshen added a commit that referenced this pull request Jun 15, 2026
### 💥 BREAKING CHANGES

- 7a24911 codegen: [**BREAKING**] Borrow sourcemaps from codegen
(#23422) (Boshen)
- bb0ed44 transformer: [**BREAKING**] Disable styled-components
transpileTemplateLiterals by default (#23171) (Boshen)

### 🚀 Features

- 1490a0a linter/react: Implement react-compiler rule (#23202) (Boshen)
- 6c0bdf0 transformer/react-refresh: Support `module.property.useHook()`
(#23190) (Dunqing)
- 47991bd semantic: Report TS1228 for invalid type predicates (#23174)
(camc314)
- 1d3af58 parser: Add TS2398 parameter property diagnostic (#23216)
(camc314)
- 44313da semantic: Add `scope_is_descendant_of` api (#22313) (camc314)
- e5050c0 parser: Improve diagnostic for rest initializer (#23205)
(camc314)
- ec266bb transformer: Run React Compiler as a feature-gated transform
pass (#23201) (Boshen)
- e7374fe parser: Report error for `const` modifier on interface type
parameter (#23173) (camc314)
- a7c1c9b parser: Report ambient definite variable assertions (#23165)
(camc314)
- d169fcd parser: Report invalid class definite assertions (#23164)
(camc314)
- 00244d8 parser: Report definite property initializer errors (#23160)
(camc314)

### 🐛 Bug Fixes

- 52d0c31 transformer: Replace ambient dot defines (#23231) (camc314)
- 2c28748 transformer/class: Parent generated constructors to class
scope (#23222) (camc314)
- 8edd234 parser: Report accessor definite assertion on token (#23203)
(camc314)
- de38a3f react_compiler: Keep imports referenced only by a local
re-export (#23176) (Boshen)
- f5721c2 codegen: Preserve parentheses around `intrinsic` type
reference (#23156) (Boshen)
- e89f81d parser: Don't emit TS1477 for parenthesized instantiation
expression (#23147) (Boshen)
- 8a04149 parser: Reject module-referencing imports/exports in a
namespace body (#22829) (Boshen)

### ⚡ Performance

- 2783295 parser: Table-driven operator precedence lookup (#23346)
(Boshen)
- 231d5de parser: Single-match member expression dispatch (#23347)
(Boshen)
- e89729b codegen: Accept one-shot wrap closures (#23265) (camc314)
- a6c11fa parser: Force-inline read_non_decimal to fold per-digit number
dispatch (#23157) (Boshen)
- d74964c parser: Store class definite assertion offset (#23170)
(camc314)
- f0fda4d parser: Shrink-wrap cold diagnostic tails out of hot frames
(#23159) (Boshen)
- a082180 parser: Store definite assertion offset (#23167) (camc314)
- 534f9c6 oxc: Conditionally rebuild semantic in compiler pipeline
(#23153) (Boshen)
- b435c6a parser: Skip checkpoint for `infer T extends U` constraint in
disallow context (#23128) (Boshen)
- 7464dce parser: Peek instead of checkpoint/rewind for `export default`
modifier (#23124) (Boshen)
- 80a9a32 parser: Fast-path single-keyword TS declarations (#23083)
(Boshen)
- da1a6c6 diagnostics: Migrate to allocation-optimized oxc-miette
(#23094) (Boshen)
- b7b08ce parser: Peek once for the static modifier disambiguation
(#23079) (Boshen)
- e7e07a3 parser: Fold unary dispatch into a single match (#23076)
(Boshen)

### 📚 Documentation

- d241add semantic: Add `AGENTS.md` test guidance for agents (#23441)
(camc314)
- 026f1ae parser: Add `AGENTS.md` test guidance for agents (#23440)
(camc314)
- 09755ac transformer: Add `AGENTS.md` test guidance for agents (#23439)
(camc314)
- e6bdfd4 lexer: Correct reference link for `byte_handlers!` (#23313)
(Dunqing)
- 65b6d7a allocator: Fix memory leaks in `Arena` examples (#23257)
(overlookmotel)

Co-authored-by: Boshen <1430279+Boshen@users.noreply.github.com>
Boshen added a commit that referenced this pull request Jun 16, 2026
… reuse stats in mangler builds (#23352)

Fix three allocation problems in the semantic rebuilds performed by the minify/mangle paths, found while auditing the compiler pipeline's scoping rebuilds.

- **`Semantic::stats` reported `nodes: 0` after node-less builds.** It read `self.nodes.len()`, which is empty when the builder runs in ancestry mode (`with_build_nodes(false)` — the compiler pipeline default). `Semantic` now tracks `node_count` (from `AstNodeStore::node_count`, the source of truth in both modes) and `stats()` returns it. The mangler build inside `Minifier::build` previously received `nodes: 0` and grew the `AstNodes` store from zero capacity by repeated doubling; it now pre-allocates correctly.
- **`Minifier::build` with `compress: None` fed the mangler `Stats::default()`.** All-zero stats are worse than no stats: they skip the `Stats::count` fallback *and* reserve zero capacity for every table. Stats are now threaded as `Option<Stats>` and only passed when the compress phase produced them; the mangle-only path falls back to the counting pass and sizes correctly.
- **`Mangler::build` always re-counted the AST.** Added `Mangler::with_stats`, and `CompilerInterface::mangle` now receives the stats from the initial semantic build, so the mangler's internal semantic rebuild skips its full-AST counting traversal.

Output is unchanged — stats only affect pre-allocation.

`CompilerInterface::mangle` gains a `stats: Stats` parameter — a breaking change for external implementors that override it (same shape as the `compress` change in #23153).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
camc314 pushed a commit that referenced this pull request Jul 3, 2026
## What

Track scoping/AST sync state with a single `scoping_dirty` flag in `Compiler::compile` so each downstream step (inject/define plugins, DCE, compress) only rebuilds semantic when it's actually needed.

## Details

- `InjectGlobalVariables` and `ReplaceGlobalDefines` already return a `changed: bool`; this was previously ignored. It's now OR-ed into `scoping_dirty`.
- `scoping_dirty` starts `true` only when the transformer runs — the parser, React Compiler (rebuilds scoping on success, returns the untouched scoping on bail-out), and any fresh `SemanticBuilder` all leave scoping in sync.
- The pre-plugin rebuild is now gated on `scoping_dirty`, skipping a needless rebuild when no transform ran.
- DCE and Compress are now a single mutually-exclusive `if/else`, both rebuilding semantic only when `scoping_dirty`.
- `Compressor::build` (which always rebuilds semantic internally) is replaced with `build_with_scoping`, so when scoping is already in sync the compressor skips its own `SemanticBuilder` pass.
- The three rebuild sites are extracted into a `rebuild_scoping` helper, which enables enum eval when a transform is configured so const-enum resolution matches the original semantic build.

## Note

The `CompilerInterface::compress` trait method gains a `scoping: Scoping` parameter — a breaking change for external implementors that override it.
camc314 pushed a commit that referenced this pull request Jul 3, 2026
### 💥 BREAKING CHANGES

- 7a24911 codegen: [**BREAKING**] Borrow sourcemaps from codegen
(#23422) (Boshen)
- bb0ed44 transformer: [**BREAKING**] Disable styled-components
transpileTemplateLiterals by default (#23171) (Boshen)

### 🚀 Features

- 1490a0a linter/react: Implement react-compiler rule (#23202) (Boshen)
- 6c0bdf0 transformer/react-refresh: Support `module.property.useHook()`
(#23190) (Dunqing)
- 47991bd semantic: Report TS1228 for invalid type predicates (#23174)
(camc314)
- 1d3af58 parser: Add TS2398 parameter property diagnostic (#23216)
(camc314)
- 44313da semantic: Add `scope_is_descendant_of` api (#22313) (camc314)
- e5050c0 parser: Improve diagnostic for rest initializer (#23205)
(camc314)
- ec266bb transformer: Run React Compiler as a feature-gated transform
pass (#23201) (Boshen)
- e7374fe parser: Report error for `const` modifier on interface type
parameter (#23173) (camc314)
- a7c1c9b parser: Report ambient definite variable assertions (#23165)
(camc314)
- d169fcd parser: Report invalid class definite assertions (#23164)
(camc314)
- 00244d8 parser: Report definite property initializer errors (#23160)
(camc314)

### 🐛 Bug Fixes

- 52d0c31 transformer: Replace ambient dot defines (#23231) (camc314)
- 2c28748 transformer/class: Parent generated constructors to class
scope (#23222) (camc314)
- 8edd234 parser: Report accessor definite assertion on token (#23203)
(camc314)
- de38a3f react_compiler: Keep imports referenced only by a local
re-export (#23176) (Boshen)
- f5721c2 codegen: Preserve parentheses around `intrinsic` type
reference (#23156) (Boshen)
- e89f81d parser: Don't emit TS1477 for parenthesized instantiation
expression (#23147) (Boshen)
- 8a04149 parser: Reject module-referencing imports/exports in a
namespace body (#22829) (Boshen)

### ⚡ Performance

- 2783295 parser: Table-driven operator precedence lookup (#23346)
(Boshen)
- 231d5de parser: Single-match member expression dispatch (#23347)
(Boshen)
- e89729b codegen: Accept one-shot wrap closures (#23265) (camc314)
- a6c11fa parser: Force-inline read_non_decimal to fold per-digit number
dispatch (#23157) (Boshen)
- d74964c parser: Store class definite assertion offset (#23170)
(camc314)
- f0fda4d parser: Shrink-wrap cold diagnostic tails out of hot frames
(#23159) (Boshen)
- a082180 parser: Store definite assertion offset (#23167) (camc314)
- 534f9c6 oxc: Conditionally rebuild semantic in compiler pipeline
(#23153) (Boshen)
- b435c6a parser: Skip checkpoint for `infer T extends U` constraint in
disallow context (#23128) (Boshen)
- 7464dce parser: Peek instead of checkpoint/rewind for `export default`
modifier (#23124) (Boshen)
- 80a9a32 parser: Fast-path single-keyword TS declarations (#23083)
(Boshen)
- da1a6c6 diagnostics: Migrate to allocation-optimized oxc-miette
(#23094) (Boshen)
- b7b08ce parser: Peek once for the static modifier disambiguation
(#23079) (Boshen)
- e7e07a3 parser: Fold unary dispatch into a single match (#23076)
(Boshen)

### 📚 Documentation

- d241add semantic: Add `AGENTS.md` test guidance for agents (#23441)
(camc314)
- 026f1ae parser: Add `AGENTS.md` test guidance for agents (#23440)
(camc314)
- 09755ac transformer: Add `AGENTS.md` test guidance for agents (#23439)
(camc314)
- e6bdfd4 lexer: Correct reference link for `byte_handlers!` (#23313)
(Dunqing)
- 65b6d7a allocator: Fix memory leaks in `Arena` examples (#23257)
(overlookmotel)

Co-authored-by: Boshen <1430279+Boshen@users.noreply.github.com>
camc314 pushed a commit that referenced this pull request Jul 3, 2026
… reuse stats in mangler builds (#23352)

Fix three allocation problems in the semantic rebuilds performed by the minify/mangle paths, found while auditing the compiler pipeline's scoping rebuilds.

- **`Semantic::stats` reported `nodes: 0` after node-less builds.** It read `self.nodes.len()`, which is empty when the builder runs in ancestry mode (`with_build_nodes(false)` — the compiler pipeline default). `Semantic` now tracks `node_count` (from `AstNodeStore::node_count`, the source of truth in both modes) and `stats()` returns it. The mangler build inside `Minifier::build` previously received `nodes: 0` and grew the `AstNodes` store from zero capacity by repeated doubling; it now pre-allocates correctly.
- **`Minifier::build` with `compress: None` fed the mangler `Stats::default()`.** All-zero stats are worse than no stats: they skip the `Stats::count` fallback *and* reserve zero capacity for every table. Stats are now threaded as `Option<Stats>` and only passed when the compress phase produced them; the mangle-only path falls back to the counting pass and sizes correctly.
- **`Mangler::build` always re-counted the AST.** Added `Mangler::with_stats`, and `CompilerInterface::mangle` now receives the stats from the initial semantic build, so the mangler's internal semantic rebuild skips its full-AST counting traversal.

Output is unchanged — stats only affect pre-allocation.

`CompilerInterface::mangle` gains a `stats: Stats` parameter — a breaking change for external implementors that override it (same shape as the `compress` change in #23153).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant