Skip to content

Commit ed1c6cc

Browse files
perf(ecmascript): shrink JsValue 64→32 bytes (vercel#93106)
Three changes that together shrink `JsValue` from **64 → 32 bytes** (−50%). JsValue is the analyzer's value type and is instantiated millions of times during large-app analysis, so this directly reduces peak memory and improves cache locality on the hot `link`/`replace_builtin` paths. ## Changes ### 1. Box `RequireContextValue` inside `WellKnownFunctionKind` `RequireContextValue` carries an `FxIndexMap` and is only used by three variants (`RequireContextRequire{,Keys,Resolve}`). Boxing it pulls those variants' payload down from 48B to 8B, dropping `WellKnownFunctionKind` from 64 → 16 bytes and `JsValue` from 64 → 48 bytes. ### 2. Unify `MemberCall`/`New`/`Call` payload into a single `Vec` `MemberCall` was `(u32, Box<JsValue>, Box<JsValue>, Vec<JsValue>)` (48B payload). Collapsed into `(u32, Vec<JsValue>)` (24B payload) with storage layout `[args..., prop, obj]`. The reversed order is the key trick: on the common fallthrough in `builtin::replace_builtin` (convert `obj.prop(args)` into a `Call(Member(obj, prop), args)`), we pop `obj` then `prop` off the tail of the Vec and reuse the remainder **as** the args Vec — zero extra allocations on the hot path. A similar optimization was applied to `New` and `Call` A tricky think about this layout is avoiding reallocations of 'arg' vecs when constructing, To make this easier I added parallel factory methods `JsValue::call_from_parts` and `JsValue::call_from_iter` to eliminate reallocations when constructing Drops JsValue from 48 → 40 bytes. ### 3. Replace `Unknown::reason` with `RcStr` The `reason` field of Unknown was a `Cow<&'static, str>` but nearly all uses were `&'static str` values so they were migrated to `rcstr!` which drops the reason field from 24 bytes to 8 bytes. Drops JsValue from 40 -> 32 bytes ## Test plan - [x] `cargo test -p turbopack-ecmascript --lib` — all 352 tests pass (snapshot fixtures byte-identical) ## Benchmark results Ran `cargo bench -p turbopack-ecmascript --bench analyzer` on this branch vs `canary` (59 fixtures × 2 benches each, criterion baselines). | Bench | Sum (canary → branch) | Δ | Geomean ratio | |---|---|---|---| | `link` | 72.26 ms → 66.41 ms | **−8.09%** | **−2.94%** | | `create_graph` | 8.15 ms → 7.93 ms | **−2.60%** | **−2.95%** | The biggest absolute wins are on `link`, the JsValue-heavy hot path (linker walks every node, clones, hashes via `similar_hash`, compares via `similar`). **Notable wins on `link`:** - `react-dom-production` **−5.70%** (38.40 ms → 36.21 ms — the largest fixture, ~2 ms saved) - `peg` **−11.39%** (25.65 ms → 22.72 ms) - `cycle-cache` **−9.04%** (3.94 ms → 3.58 ms) - `md5` **−19.19%** (1.09 ms → 879 µs) - `md5-reduced` **−12.75%** (386 µs → 337 µs) - `md5_2` **−11.33%** (783 µs → 694 µs) **Notable wins on `create_graph`:** - `webpack-target-node` **−8.30%**, `require-context` **−7.67%**, `peg` **−5.53%**, `cycle-cache` **−4.36%**, `mongoose-reduced` **−6.49%**, `process-and-os` **−6.11%** **Regressions:** zero in `create_graph`. Three nominal "+1.5% to +3.5%" outliers in `link` (`link/2`, `link/object`, `link/try`) all reproduce as **−4% to −7% improvements** when re-run individually against canary — they are run-to-run noise on small fixtures (criterion CI ≈ ±2-3%), not real regressions. Every fixture above ~1 ms shows a clear improvement consistently. <!-- NEXT_JS_LLM_PR --> --------- Co-authored-by: Luke Sandberg <lukesandberg@users.noreply.github.com>
1 parent d55787f commit ed1c6cc

6 files changed

Lines changed: 978 additions & 648 deletions

File tree

turbopack/crates/turbopack-ecmascript/src/analyzer/builtin.rs

Lines changed: 59 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use std::mem::take;
22

3+
use turbo_rcstr::rcstr;
4+
35
use super::{ConstantNumber, ConstantValue, JsValue, LogicalOperator, LogicalProperty, ObjectPart};
46
use crate::analyzer::JsValueUrlKind;
57

@@ -9,7 +11,8 @@ use crate::analyzer::JsValueUrlKind;
911
pub fn early_replace_builtin(value: &mut JsValue) -> bool {
1012
match value {
1113
// matching calls like `callee(arg1, arg2, ...)`
12-
JsValue::Call(_, box callee, args) => {
14+
JsValue::Call(_, call) => {
15+
let (args, callee) = call.as_parts_mut();
1316
let args_have_side_effects = || args.iter().any(|arg| arg.has_side_effects());
1417
match callee {
1518
// We don't know what the callee is, so we can early return
@@ -19,7 +22,7 @@ pub fn early_replace_builtin(value: &mut JsValue) -> bool {
1922
has_side_effects,
2023
} => {
2124
let has_side_effects = has_side_effects || args_have_side_effects();
22-
value.make_unknown(has_side_effects, "unknown callee");
25+
value.make_unknown(has_side_effects, rcstr!("unknown callee"));
2326
true
2427
}
2528
// We known that these callee will lead to an error at runtime, so we can skip
@@ -34,14 +37,15 @@ pub fn early_replace_builtin(value: &mut JsValue) -> bool {
3437
| JsValue::Add(_, _)
3538
| JsValue::Not(_, _) => {
3639
let has_side_effects = args_have_side_effects();
37-
value.make_unknown(has_side_effects, "non-function callee");
40+
value.make_unknown(has_side_effects, rcstr!("non-function callee"));
3841
true
3942
}
4043
_ => false,
4144
}
4245
}
4346
// matching calls with this context like `obj.prop(arg1, arg2, ...)`
44-
JsValue::MemberCall(_, box obj, box prop, args) => {
47+
JsValue::MemberCall(_, call) => {
48+
let (args, prop, obj) = call.as_parts_mut();
4549
let args_have_side_effects = || args.iter().any(|arg| arg.has_side_effects());
4650
match obj {
4751
// We don't know what the callee is, so we can early return
@@ -52,7 +56,7 @@ pub fn early_replace_builtin(value: &mut JsValue) -> bool {
5256
} => {
5357
let side_effects =
5458
has_side_effects || prop.has_side_effects() || args_have_side_effects();
55-
value.make_unknown(side_effects, "unknown callee object");
59+
value.make_unknown(side_effects, rcstr!("unknown callee object"));
5660
true
5761
}
5862
// otherwise we need to look at the property
@@ -64,7 +68,7 @@ pub fn early_replace_builtin(value: &mut JsValue) -> bool {
6468
has_side_effects,
6569
} => {
6670
let side_effects = has_side_effects || args_have_side_effects();
67-
value.make_unknown(side_effects, "unknown callee property");
71+
value.make_unknown(side_effects, rcstr!("unknown callee property"));
6872
true
6973
}
7074
_ => false,
@@ -83,7 +87,7 @@ pub fn early_replace_builtin(value: &mut JsValue) -> bool {
8387
box ref mut prop,
8488
) => {
8589
let side_effects = has_side_effects || prop.has_side_effects();
86-
value.make_unknown(side_effects, "unknown object");
90+
value.make_unknown(side_effects, rcstr!("unknown object"));
8791
true
8892
}
8993
_ => false,
@@ -137,7 +141,7 @@ pub fn replace_builtin(value: &mut JsValue) -> bool {
137141
items.push(JsValue::unknown(
138142
JsValue::member(Box::new(JsValue::array(Vec::new())), Box::new(take(prop))),
139143
false,
140-
"unknown array prototype methods or values",
144+
rcstr!("unknown array prototype methods or values"),
141145
));
142146
JsValue::alternatives(take(items))
143147
}
@@ -156,19 +160,19 @@ pub fn replace_builtin(value: &mut JsValue) -> bool {
156160
*value = JsValue::unknown(
157161
JsValue::member(Box::new(take(obj)), Box::new(take(prop))),
158162
false,
159-
"invalid index",
163+
rcstr!("invalid index"),
160164
);
161165
true
162166
}
163167
} else {
164-
value.make_unknown(false, "non-num constant property on array");
168+
value.make_unknown(false, rcstr!("non-num constant property on array"));
165169
true
166170
}
167171
}
168172
// accessing a non-numeric property on an array like `[1,2,3].length`
169173
// We don't know what happens here
170174
JsValue::Constant(_) => {
171-
value.make_unknown(false, "non-num constant property on array");
175+
value.make_unknown(false, rcstr!("non-num constant property on array"));
172176
true
173177
}
174178
// accessing multiple alternative properties on an array like `[1,2,3][(1 | 2 |
@@ -218,7 +222,7 @@ pub fn replace_builtin(value: &mut JsValue) -> bool {
218222
prop.clone(),
219223
),
220224
true,
221-
"spread object",
225+
rcstr!("spread object"),
222226
));
223227
}
224228
}
@@ -230,7 +234,7 @@ pub fn replace_builtin(value: &mut JsValue) -> bool {
230234
Box::new(take(prop)),
231235
),
232236
true,
233-
"unknown object prototype methods or values",
237+
rcstr!("unknown object prototype methods or values"),
234238
));
235239
}
236240
JsValue::alternatives(values)
@@ -295,7 +299,7 @@ pub fn replace_builtin(value: &mut JsValue) -> bool {
295299
}
296300
}
297301
ObjectPart::Spread(_) => {
298-
value.make_unknown(true, "spread object");
302+
value.make_unknown(true, rcstr!("spread object"));
299303
return true;
300304
}
301305
}
@@ -338,9 +342,12 @@ pub fn replace_builtin(value: &mut JsValue) -> bool {
338342
}
339343
_ => false,
340344
},
341-
// matching calls with this context like `obj.prop(arg1, arg2, ...)`
342-
JsValue::MemberCall(_, box obj, box prop, args) => {
343-
match obj {
345+
346+
JsValue::MemberCall(_, call) => {
347+
// `into_parts` pops obj + prop off the tail of the underlying `Vec`, and the
348+
// remaining `Vec` (owned, not reallocated) becomes `args`.
349+
let (mut obj, prop, args) = take(call).into_parts();
350+
match &mut obj {
344351
// matching calls on an array like `[1,2,3].concat([4,5,6])`
345352
JsValue::Array { items, mutable, .. } => {
346353
// matching cases where the property is a const string
@@ -368,25 +375,25 @@ pub fn replace_builtin(value: &mut JsValue) -> bool {
368375
mutable: inner_mutable,
369376
..
370377
} => {
371-
items.extend(take(inner));
372-
*mutable |= *inner_mutable;
378+
items.extend(inner);
379+
*mutable |= inner_mutable;
373380
}
374-
JsValue::Constant(_)
381+
other @ (JsValue::Constant(_)
375382
| JsValue::Url(_, JsValueUrlKind::Absolute)
376383
| JsValue::Concat(..)
377384
| JsValue::Add(..)
378385
| JsValue::WellKnownObject(_)
379386
| JsValue::WellKnownFunction(_)
380-
| JsValue::Function(..) => {
381-
items.push(take(arg));
387+
| JsValue::Function(..)) => {
388+
items.push(other);
382389
}
383390
_ => {
384391
unreachable!();
385392
}
386393
}
387394
}
388395
obj.update_total_nodes();
389-
*value = take(obj);
396+
*value = obj;
390397
return true;
391398
}
392399
// The Array.prototype.map method
@@ -397,9 +404,9 @@ pub fn replace_builtin(value: &mut JsValue) -> bool {
397404
.into_iter()
398405
.enumerate()
399406
.map(|(i, item)| {
400-
JsValue::call(
401-
Box::new(func.clone()),
402-
vec![
407+
JsValue::call_from_iter(
408+
func.clone(),
409+
[
403410
item,
404411
JsValue::Constant(ConstantValue::Num(
405412
(i as f64).into(),
@@ -427,10 +434,10 @@ pub fn replace_builtin(value: &mut JsValue) -> bool {
427434
take(values)
428435
.into_iter()
429436
.map(|alt| {
430-
JsValue::member_call(
431-
Box::new(alt),
432-
Box::new(prop.clone()),
433-
args.clone(),
437+
JsValue::member_call_from_iter(
438+
alt,
439+
prop.clone(),
440+
args.iter().cloned(),
434441
)
435442
})
436443
.collect(),
@@ -446,37 +453,42 @@ pub fn replace_builtin(value: &mut JsValue) -> bool {
446453
{
447454
// The String.prototype.concat method
448455
if str == "concat" {
449-
let mut values = vec![take(obj)];
450-
values.extend(take(args));
456+
let mut values = vec![obj];
457+
values.extend(args);
451458

452459
*value = JsValue::concat(values);
453460
return true;
454461
}
455462
}
456463

457464
// without special handling, we convert it into a normal call like
458-
// `(obj.prop)(arg1, arg2, ...)`
459-
*value = JsValue::call(
460-
Box::new(JsValue::member(Box::new(take(obj)), Box::new(take(prop)))),
461-
take(args),
465+
// `(obj.prop)(arg1, arg2, ...)`.
466+
//
467+
// Pass-through path: `args` came from `MemberCallList::into_parts` which yields
468+
// a `Vec` with `cap >= len + 2` (slack from the original layout). Re-wrapping it
469+
// into a `JsValue::Call` only needs `+1` slot, which fits in the existing slack —
470+
// no realloc. This is the original motivation for the `[args..., prop, obj]`
471+
// tail layout.
472+
*value = JsValue::call_from_parts(
473+
JsValue::member(Box::new(obj), Box::new(prop)),
474+
args,
462475
);
463476
true
464477
}
465478
// match calls when the callee are multiple alternative functions like `(func1 |
466479
// func2)(arg1, arg2, ...)`
467-
JsValue::Call(
468-
_,
469-
box JsValue::Alternatives {
470-
total_nodes: _,
471-
values,
472-
logical_property: _,
473-
},
474-
args,
475-
) => {
480+
JsValue::Call(_, call)
481+
if matches!(call.callee(), JsValue::Alternatives { .. }) =>
482+
{
483+
// Take ownership so we can move the alternatives `values` out of the callee.
484+
let (callee, args) = take(call).into_parts();
485+
let JsValue::Alternatives { values, .. } = callee else {
486+
unreachable!()
487+
};
476488
*value = JsValue::alternatives(
477-
take(values)
489+
values
478490
.into_iter()
479-
.map(|alt| JsValue::call(Box::new(alt), args.clone()))
491+
.map(|alt| JsValue::call_from_iter(alt, args.iter().cloned()))
480492
.collect(),
481493
);
482494
true

0 commit comments

Comments
 (0)