mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
perf(react_compiler): keep small hot-path collections inline (#24514)
## What Two kinds of small, hot collections in the react-compiler pass get heap-allocated on every use: - The HIR operand/lvalue visitors (`each_instruction_value_operand`, `each_terminal_operand`, and friends) build a fresh `Vec<Place>` for every instruction they're called on, and nearly every pass calls them in a loop. The lists almost always hold one to four entries. - The aliasing fixpoint allocates a successor list per block visit (`terminal_successors`) and an args/captures list per interned effect (`EffectKey`). ## Change - The visitors now return `PlaceList = SmallVec<[Place; 4]>`; element order and contents are unchanged. - `terminal_successors` returns a `SmallVec<[BlockId; 2]>`, and `EffectKey::Apply` args / `CreateFunction` captures are inline small vectors, so building a typical key no longer allocates. - Adds `smallvec` to the crate (already a workspace dependency). Numbers from the stock release profile (fat LTO), linting cal.com with `react/react-compiler` as the only enabled rule, `--threads=1`: | | before | after | delta | | ------------ | -------------: | -------------: | ------------------: | | instructions | 18,085,644,607 | 17,200,052,223 | −4.9% | | wall time | 2.787 s | 2.754 s | −1.2% | ## Verification - Lint output is byte-identical to main on cal.com and excalidraw, with `reportAllBailouts` both on and off (full stdout/stderr and exit codes, single-threaded). - Transform output is identical on all 1,438 tsx/jsx files across the two corpora, with no exit-code changes. - `cargo test -p oxc_react_compiler` (including the fixture snapshot suite), clippy, and rustfmt are all clean. This PR was assisted by Claude Code.
This commit is contained in:
1 parent
e6f7174029
commit
b6850625af
6 files changed
+52
-40
No files matched your search
Generated
+1
@@ -2447,6 +2447,7 @@ dependencies = [
|
||||
"oxc_str",
|
||||
"oxc_syntax",
|
||||
"rustc-hash",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# This crate owns the oxc <-> react_compiler_ast conversion layer, plus the React
|
||||
# Compiler *core* itself, vendored in-tree under `src/react_compiler*` (one module
|
||||
# per upstream crate, from the MIT fork at oxc-project/forked-react-compiler). The
|
||||
# core is frontend-agnostic (serde/indexmap only, never oxc); the AST/scope
|
||||
# core is frontend-agnostic (small utility crates only, never oxc); the AST/scope
|
||||
# conversion on our side is written against the live workspace oxc AST.
|
||||
|
||||
[package]
|
||||
@@ -40,10 +40,11 @@ oxc_str = { workspace = true }
|
||||
oxc_syntax = { workspace = true, features = ["to_js_string"] }
|
||||
|
||||
# External crates the vendored React Compiler modules (`src/react_compiler*`) depend
|
||||
# on. The compiler core is frontend-agnostic — serde/indexmap/hashing only, no oxc.
|
||||
# on. The compiler core is frontend-agnostic — small utility crates only, no oxc.
|
||||
indexmap = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
cow-utils = { workspace = true }
|
||||
smallvec = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
# The example and integration tests parse source and print the compiled program; the
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
use rustc_hash::FxHashMap;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use oxc_index::IndexSlice;
|
||||
|
||||
@@ -19,10 +20,14 @@ use crate::react_compiler_hir::{
|
||||
// Iterator functions (return Vec instead of generators)
|
||||
// =============================================================================
|
||||
|
||||
/// Operand/lvalue lists are almost always 1-4 entries; keep them inline instead
|
||||
/// of heap-allocating a `Vec` for every instruction visited.
|
||||
pub type PlaceList = SmallVec<[Place; 4]>;
|
||||
|
||||
/// Yields `instr.lvalue` plus the value's lvalues.
|
||||
/// Equivalent to TS `eachInstructionLValue`.
|
||||
pub fn each_instruction_lvalue(instr: &Instruction) -> Vec<Place> {
|
||||
let mut result = Vec::new();
|
||||
pub fn each_instruction_lvalue(instr: &Instruction) -> PlaceList {
|
||||
let mut result = PlaceList::new();
|
||||
result.push(instr.lvalue.clone());
|
||||
result.extend(each_instruction_value_lvalue(&instr.value));
|
||||
result
|
||||
@@ -30,8 +35,8 @@ pub fn each_instruction_lvalue(instr: &Instruction) -> Vec<Place> {
|
||||
|
||||
/// Yields lvalues from DeclareLocal/StoreLocal/DeclareContext/StoreContext/Destructure/PostfixUpdate/PrefixUpdate.
|
||||
/// Equivalent to TS `eachInstructionValueLValue`.
|
||||
pub fn each_instruction_value_lvalue(value: &InstructionValue) -> Vec<Place> {
|
||||
let mut result = Vec::new();
|
||||
pub fn each_instruction_value_lvalue(value: &InstructionValue) -> PlaceList {
|
||||
let mut result = PlaceList::new();
|
||||
match value {
|
||||
InstructionValue::DeclareContext { lvalue, .. }
|
||||
| InstructionValue::StoreContext { lvalue, .. }
|
||||
@@ -88,13 +93,13 @@ pub fn each_instruction_value_lvalue(value: &InstructionValue) -> Vec<Place> {
|
||||
|
||||
/// Delegates to each_instruction_value_operand.
|
||||
/// Equivalent to TS `eachInstructionOperand`.
|
||||
pub fn each_instruction_operand(instr: &Instruction, env: &Environment) -> Vec<Place> {
|
||||
pub fn each_instruction_operand(instr: &Instruction, env: &Environment) -> PlaceList {
|
||||
each_instruction_value_operand(&instr.value, env)
|
||||
}
|
||||
|
||||
/// Yields operand places from an InstructionValue.
|
||||
/// Equivalent to TS `eachInstructionValueOperand`.
|
||||
pub fn each_instruction_value_operand(value: &InstructionValue, env: &Environment) -> Vec<Place> {
|
||||
pub fn each_instruction_value_operand(value: &InstructionValue, env: &Environment) -> PlaceList {
|
||||
each_instruction_value_operand_with_functions(value, &env.functions)
|
||||
}
|
||||
|
||||
@@ -103,8 +108,8 @@ pub fn each_instruction_value_operand(value: &InstructionValue, env: &Environmen
|
||||
pub fn each_instruction_value_operand_with_functions(
|
||||
value: &InstructionValue,
|
||||
functions: &IndexSlice<FunctionId, [HirFunction]>,
|
||||
) -> Vec<Place> {
|
||||
let mut result = Vec::new();
|
||||
) -> PlaceList {
|
||||
let mut result = PlaceList::new();
|
||||
match value {
|
||||
InstructionValue::NewExpression { callee, args, .. }
|
||||
| InstructionValue::CallExpression { callee, args, .. } => {
|
||||
@@ -282,8 +287,8 @@ pub fn each_instruction_value_operand_with_functions(
|
||||
|
||||
/// Yields each arg's place.
|
||||
/// Equivalent to TS `eachCallArgument`.
|
||||
pub fn each_call_argument(args: &[PlaceOrSpread]) -> Vec<Place> {
|
||||
let mut result = Vec::new();
|
||||
pub fn each_call_argument(args: &[PlaceOrSpread]) -> PlaceList {
|
||||
let mut result = PlaceList::new();
|
||||
for arg in args {
|
||||
match arg {
|
||||
PlaceOrSpread::Place(place) => {
|
||||
@@ -299,8 +304,8 @@ pub fn each_call_argument(args: &[PlaceOrSpread]) -> Vec<Place> {
|
||||
|
||||
/// Yields places from array/object patterns.
|
||||
/// Equivalent to TS `eachPatternOperand`.
|
||||
pub fn each_pattern_operand(pattern: &Pattern) -> Vec<Place> {
|
||||
let mut result = Vec::new();
|
||||
pub fn each_pattern_operand(pattern: &Pattern) -> PlaceList {
|
||||
let mut result = PlaceList::new();
|
||||
match pattern {
|
||||
Pattern::Array(arr) => {
|
||||
for item in &arr.items {
|
||||
@@ -421,8 +426,8 @@ pub fn each_terminal_successor(terminal: &Terminal) -> Vec<BlockId> {
|
||||
|
||||
/// Yields places used by terminal.
|
||||
/// Equivalent to TS `eachTerminalOperand`.
|
||||
pub fn each_terminal_operand(terminal: &Terminal) -> Vec<Place> {
|
||||
let mut result = Vec::new();
|
||||
pub fn each_terminal_operand(terminal: &Terminal) -> PlaceList {
|
||||
let mut result = PlaceList::new();
|
||||
match terminal {
|
||||
Terminal::If { test, .. } => {
|
||||
result.push(test.clone());
|
||||
|
||||
+22
-18
@@ -63,6 +63,8 @@ use crate::react_compiler_hir::type_config::ValueKind;
|
||||
use crate::react_compiler_hir::type_config::ValueReason;
|
||||
use crate::react_compiler_hir::visitors;
|
||||
use oxc_span::Span;
|
||||
use smallvec::SmallVec;
|
||||
use smallvec::smallvec;
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
|
||||
@@ -734,7 +736,7 @@ enum EffectKey {
|
||||
receiver: IdentifierId,
|
||||
function: IdentifierId,
|
||||
mutates_function: bool,
|
||||
args: Vec<ArgKey>,
|
||||
args: SmallVec<[ArgKey; 4]>,
|
||||
into: IdentifierId,
|
||||
},
|
||||
CreateFrom {
|
||||
@@ -801,7 +803,7 @@ enum EffectKey {
|
||||
CreateFunction {
|
||||
into: IdentifierId,
|
||||
function_id: FunctionId,
|
||||
captures: Vec<IdentifierId>,
|
||||
captures: SmallVec<[IdentifierId; 4]>,
|
||||
},
|
||||
/// Synthetic keys used by the `Assign` handler for kind-preserving copies;
|
||||
/// they share the ValueId cache but never collide with real-effect keys.
|
||||
@@ -826,7 +828,7 @@ enum ArgKey {
|
||||
fn effect_key(effect: &AliasingEffect) -> EffectKey {
|
||||
match effect {
|
||||
AliasingEffect::Apply { receiver, function, mutates_function, args, into, .. } => {
|
||||
let mut key_args: Vec<ArgKey> = args
|
||||
let mut key_args: SmallVec<[ArgKey; 4]> = args
|
||||
.iter()
|
||||
.map(|a| match a {
|
||||
PlaceOrSpreadOrHole::Hole => ArgKey::Hole,
|
||||
@@ -3275,29 +3277,31 @@ fn create_temp_place(env: &mut Environment, span: Option<Span>) -> Place {
|
||||
/// successors but NOT pseudo-successors (fallthroughs). Fallthroughs for
|
||||
/// Logical/Ternary/Optional and Try/Scope/PrunedScope are reached naturally
|
||||
/// via the block iteration order (blocks are stored in topological order).
|
||||
fn terminal_successors(terminal: &Terminal) -> Vec<BlockId> {
|
||||
fn terminal_successors(terminal: &Terminal) -> SmallVec<[BlockId; 2]> {
|
||||
match terminal {
|
||||
Terminal::Goto { block, .. } => vec![*block],
|
||||
Terminal::If { consequent, alternate, .. } => vec![*consequent, *alternate],
|
||||
Terminal::Branch { consequent, alternate, .. } => vec![*consequent, *alternate],
|
||||
Terminal::Goto { block, .. } => smallvec![*block],
|
||||
Terminal::If { consequent, alternate, .. } => smallvec![*consequent, *alternate],
|
||||
Terminal::Branch { consequent, alternate, .. } => smallvec![*consequent, *alternate],
|
||||
Terminal::Switch { cases, .. } => cases.iter().map(|c| c.block).collect(),
|
||||
Terminal::For { init, .. } => vec![*init],
|
||||
Terminal::ForOf { init, .. } | Terminal::ForIn { init, .. } => vec![*init],
|
||||
Terminal::DoWhile { loop_block, .. } => vec![*loop_block],
|
||||
Terminal::While { test, .. } => vec![*test],
|
||||
Terminal::Return { .. } | Terminal::Throw { .. } | Terminal::Unreachable { .. } => vec![],
|
||||
Terminal::Try { block, .. } => vec![*block],
|
||||
Terminal::For { init, .. } => smallvec![*init],
|
||||
Terminal::ForOf { init, .. } | Terminal::ForIn { init, .. } => smallvec![*init],
|
||||
Terminal::DoWhile { loop_block, .. } => smallvec![*loop_block],
|
||||
Terminal::While { test, .. } => smallvec![*test],
|
||||
Terminal::Return { .. } | Terminal::Throw { .. } | Terminal::Unreachable { .. } => {
|
||||
smallvec![]
|
||||
}
|
||||
Terminal::Try { block, .. } => smallvec![*block],
|
||||
Terminal::MaybeThrow { continuation, handler, .. } => {
|
||||
let mut v = vec![*continuation];
|
||||
let mut v = smallvec![*continuation];
|
||||
if let Some(h) = handler {
|
||||
v.push(*h);
|
||||
}
|
||||
v
|
||||
}
|
||||
Terminal::Label { block, .. } | Terminal::Sequence { block, .. } => vec![*block],
|
||||
Terminal::Logical { test, .. } | Terminal::Ternary { test, .. } => vec![*test],
|
||||
Terminal::Optional { test, .. } => vec![*test],
|
||||
Terminal::Scope { block, .. } | Terminal::PrunedScope { block, .. } => vec![*block],
|
||||
Terminal::Label { block, .. } | Terminal::Sequence { block, .. } => smallvec![*block],
|
||||
Terminal::Logical { test, .. } | Terminal::Ternary { test, .. } => smallvec![*test],
|
||||
Terminal::Optional { test, .. } => smallvec![*test],
|
||||
Terminal::Scope { block, .. } | Terminal::PrunedScope { block, .. } => smallvec![*block],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ pub trait ReactiveFunctionVisitor<'a> {
|
||||
let inner_func = &self.env().functions[func_id];
|
||||
let block = &inner_func.body.blocks[&block_id];
|
||||
let instr_ids: Vec<_> = block.instructions.clone();
|
||||
let terminal_operands: Vec<Place> = each_terminal_operand(&block.terminal);
|
||||
let terminal_operands = each_terminal_operand(&block.terminal);
|
||||
let terminal_id = block.terminal.evaluation_order();
|
||||
|
||||
for instr_id in &instr_ids {
|
||||
|
||||
+6
-5
@@ -9,11 +9,12 @@ use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_index::IndexSlice;
|
||||
use smallvec::smallvec;
|
||||
|
||||
use crate::diagnostics::ErrorCategory;
|
||||
use crate::react_compiler_hir::environment::Environment;
|
||||
use crate::react_compiler_hir::visitors::{
|
||||
each_instruction_lvalue_ids, each_instruction_value_operand, each_terminal_operand,
|
||||
PlaceList, each_instruction_lvalue_ids, each_instruction_value_operand, each_terminal_operand,
|
||||
};
|
||||
use crate::react_compiler_hir::{
|
||||
Effect, FunctionId, HirFunction, Identifier, IdentifierId, IdentifierName, InstructionValue,
|
||||
@@ -204,24 +205,24 @@ fn get_context_reassignment(
|
||||
// For calls with noAlias signatures, only check the callee/receiver
|
||||
// (not args) to avoid false positives from callbacks that reassign
|
||||
// context variables.
|
||||
let operands: Vec<Place> = match &instr.value {
|
||||
let operands: PlaceList = match &instr.value {
|
||||
InstructionValue::CallExpression { callee, .. } => {
|
||||
if env.has_no_alias_signature(callee.identifier) {
|
||||
vec![callee.clone()]
|
||||
smallvec![callee.clone()]
|
||||
} else {
|
||||
each_instruction_value_operand(&instr.value, env)
|
||||
}
|
||||
}
|
||||
InstructionValue::MethodCall { receiver, property, .. } => {
|
||||
if env.has_no_alias_signature(property.identifier) {
|
||||
vec![receiver.clone(), property.clone()]
|
||||
smallvec![receiver.clone(), property.clone()]
|
||||
} else {
|
||||
each_instruction_value_operand(&instr.value, env)
|
||||
}
|
||||
}
|
||||
InstructionValue::TaggedTemplateExpression { tag, .. } => {
|
||||
if env.has_no_alias_signature(tag.identifier) {
|
||||
vec![tag.clone()]
|
||||
smallvec![tag.clone()]
|
||||
} else {
|
||||
each_instruction_value_operand(&instr.value, env)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user