Skip to content

Guarded builtin-named method dispatch is 3-4x Node on dayjs/decimal-shaped receivers (#10476 follow-up) #10594

Open

Description

@proggeramlug

Follow-up to #10476 / PR #10591, which fixes a real correctness bug: a method call whose name matched a
Date/Number/Array.prototype builtin was lowered straight to that builtin regardless of the receiver, so a
user's own toISOString / toFixed / getTime was silently skipped.

The fix guards that dispatch on the receiver's runtime kind. For a receiver the compiler has statically proven
to be a Date/number/array, nothing changes. For any other receiver, the receiver is evaluated once, rooted, and
branched on at runtime. That guarded path is 3–4x slower than Node.

Measurements (200k iterations, perf stat -e instructions, 3 runs, from PR #10591)

shape fix instructions fix wall (3 runs) Node wall (3 runs) fix vs Node
dayjs_shape ~4.80B 651 / 675 / 678 ms 187 / 204 / 240 ms ~3.1–3.3x slower
money_shape ~1.30B 363 / 417 / 418 ms 102 / 104 / 107 ms ~3.6–4.0x slower

Control shapes (date_typed, fixed_typed, fixed_any, ends_typed, sorted_typed, date_any) stay within
~2% of baseline, so the cost is confined to the guarded path.

Why there is no "regression vs baseline" number

The pre-fix baseline is not a valid comparator for these two shapes:

  • dayjs_shape baseline crashed (RangeError: Invalid time value) before completing the iterations.
  • money_shape baseline computed the wrong answer (600000 where Node and the fix both give 1000000),
    because it dispatched to a numeric builtin instead of the user's method.

The old speed was the bug. Only fix-vs-Node is meaningful here.

Why it matters

This is the shape of every decimal/money/date-wrapper library — dayjs's toISOString/toJSON,
decimal.js and bignumber.js's toFixed — i.e. exactly the packages the compile-the-real-package work targets.
The project's bar is to match Node with 20% below acceptable; 3–4x is well outside it.

Where to look

  • crates/perry-codegen/src/lower_call/property_get/builtin_kind_guard.rs — the guard itself.
  • The per-call receiver re-evaluation and rooting is the obvious first suspect; compare with perf(runtime): stop re-deriving the receiver on every array push (−74%) #10414, where
    Array.prototype.push was re-deriving its receiver 6x per call and fixing it was worth −74.6%.
  • A monomorphic inline cache on the receiver kind at the call site would likely collapse most of this: these
    loops call the same user method on the same shape every iteration.

Fixtures used for the numbers are the dayjs_shape / money_shape cases in PR #10591's perf section.

Activity

  1. added
    performanceRuntime, compile-time, build-size, or memory performance
    package-auditFound by the 2026 package audit: compiling real npm packages from source instead of native bindings
    on Sep 18, 2026
  2. proggeramlug commented on Sep 18, 2026

    @proggeramlug
    ContributorAuthor

    Accepted as a known cost (2026-09-18): the correctness fix ships, and this regression is handed to the
    in-flight performance work rather than blocking it. There is deliberately no "restore the old speed" option —
    the old speed came from the buggy path.

    Highest-value lead recorded above: the per-call receiver re-derivation. #10414 was the same shape of defect in
    Array.prototype.push (receiver re-derived 6x per call) and fixing it was worth −74.6%. A monomorphic inline
    cache on receiver kind at the call site is the obvious first thing to try, since these loops call the same
    method on the same shape every iteration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    package-auditFound by the 2026 package audit: compiling real npm packages from source instead of native bindingsperformanceRuntime, compile-time, build-size, or memory performance

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions