Skip to content

perf(ecmascript): shrink Call/New args from Vec to Box<[T]>#93111

Closed
mmastrac wants to merge 1 commit intommastrac/jsvalue-perf-experimentfrom
mmastrac/jsvalue-shrink-to-32b
Closed

perf(ecmascript): shrink Call/New args from Vec to Box<[T]>#93111
mmastrac wants to merge 1 commit intommastrac/jsvalue-perf-experimentfrom
mmastrac/jsvalue-shrink-to-32b

Conversation

@mmastrac
Copy link
Copy Markdown
Contributor

@mmastrac mmastrac commented Apr 22, 2026

Stacked on #93106. Swaps Vec<JsValue> for Box<[JsValue]> on the args field of JsValue::Call and JsValue::New. Shrinks each variant's payload from 40 B to 32 B (16 B Box vs 24 B Vec) while the overall enum stays at 40 B (driven by Unknown).

Why Box<[T]>

Analysis never pushes to args after construction. The Vec's capacity field is dead weight — 8 B wasted on every Call/New. Box<[JsValue]> stores only (ptr, len). into_boxed_slice() at construction is a no-op when capacity already equals length (true for size_hint-exact iterators like call_expr.args.iter().map(...).collect()), and consumer sites that need an owned Vec convert via into_vec() — an O(1) pointer-level conversion that reuses the backing allocation.

Measured perf (criterion, references bench)

Benchmark Parent This PR Δ
packages-bundle.js / full 139.6 ms 135.6 ms −2.9%
packages-bundle.js / tracing 119.7 ms 116.1 ms −3.0%
app-page-turbo / full 95.2 ms 92.8 ms −2.5%
app-page-turbo / tracing 81.5 ms 80.0 ms −1.8%
react-dom-client / full 63.0 ms 61.5 ms −2.3%
react-dom-client / tracing 55.9 ms 54.7 ms −2.1%
jsonwebtoken.js / full 44.4 ms 41.9 ms −5.5%
jsonwebtoken.js / tracing 39.6 ms 37.3 ms −5.9%

Net perf win. Vec::capacity isn't checked in the hot paths any more, and element-wise access / iteration is identical.

Investigation note — why this PR doesn't also shrink JsValue to 32 B

An earlier revision of this PR also changed Unknown.reason from Cow<'static, str> (24 B) to &'static str (16 B), which would have brought Unknown to 32 B and therefore JsValue to 32 B overall. But that combined change regressed perf by 2–5% even though each change was individually perf-neutral or perf-positive. Ran isolation experiments:

  • Unknown.reason shrink alone (JsValue stays 40 B): ~0 ± 1% (noise).
  • Box<[T]> for Call/New alone (JsValue stays 40 B): −2 to −6 % (this PR).
  • Both (JsValue = 32 B): +2 to +5 %.
  • Both + a padding variant forcing JsValue back to 40 B: back to parity with parent.

The regression appears to be triggered specifically by JsValue shrinking from 40 B to 32 B — likely a subtle allocator or codegen effect that I couldn't pinpoint. Ruled out TurboMalloc (same with system allocator). Boxing the whole Unknown payload as an alternative path to 32 B was even worse (+10–16 %). Filed for follow-up; shipping the unambiguous win for now.

Test plan

  • cargo test -p turbopack-ecmascript --lib — 351 passing
  • No snapshot changes
  • Codspeed report (this PR)

Copy link
Copy Markdown
Contributor Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@mmastrac mmastrac changed the title perf(ecmascript): shrink JsValue from 40 → 32 bytes perf(ecmascript): shrink JsValue 40 → 32 bytes Apr 22, 2026
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 22, 2026

Merging this PR will degrade performance by 4.73%

❌ 1 regressed benchmark
✅ 16 untouched benchmarks
⏩ 3 skipped benchmarks1

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation app-page-turbo.runtime.prod.js[full] 591.2 ms 620.5 ms -4.73%

Comparing mmastrac/jsvalue-shrink-to-32b (e45dc0f) with mmastrac/jsvalue-perf-experiment (9df34bc)

Open in CodSpeed

Footnotes

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

Swap `Vec<JsValue>` (24 B) for `Box<[JsValue]>` (16 B) on `JsValue::Call`
and `JsValue::New` args. Analysis never pushes to args after construction
— the `Vec`'s capacity field is dead weight. A few consumer sites that
want an owned `Vec` convert via `into_vec()` (O(1), reuses the backing
allocation).

Shrinks `Call`/`New` variant payloads from 40 B to 32 B. `JsValue`'s
overall enum size stays at 40 B (driven by `Unknown`, unchanged).

Measured net perf win of 2–6 % on the `references` criterion bench:
`Vec::capacity` is checked / decremented across the analyzer's hot
paths, and dropping those accesses pays for the O(1) conversion cost.
Snapshots stable. All 351 lib tests pass.
@mmastrac mmastrac force-pushed the mmastrac/jsvalue-shrink-to-32b branch from 3e73dca to e45dc0f Compare April 22, 2026 01:25
@mmastrac mmastrac changed the title perf(ecmascript): shrink JsValue 40 → 32 bytes perf(ecmascript): shrink Call/New args from Vec to Box<[T]> Apr 22, 2026
@mmastrac mmastrac closed this Apr 22, 2026
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