Skip to content

A non-strict function called with a string receiver installs one own property per character — 38% of a claude-code render window, 16,000x node #9810

Description

@proggeramlug

Measured during the cc performance campaign on the combined candidate, and it is now the largest single item in the profile — larger than the collector, with 0.0 % of it inside a GC frame.

The cost

call_primitive_closure_value is 38.3 % of the sampled window and js_boxed_string_new 23.6 %. The mechanism is js_boxed_string_new → install_string_wrapper_indices: it installs one own property per code unit — a shape transition, a set_builtin_property_attrs entry (into descriptor_state, the table an allocation census independently measured at 284 MB/turn), and roughly three allocations, per character, per call.

Offline controls, 20,000 calls, perry vs node

case perry node
sloppy method, 200-char receiver 15,963–29,342 ms 0 ms
fn.call(200-char string) 16,886 ms 1 ms
fn.apply(...) 22,524 ms 4 ms
strict callee 10 ms, flat in length
Number receiver 21 ms
bound function 1 ms
Object(str) 7,575 ms 1 ms

So the cost is specific to a non-strict callee receiving a string receiver, and it scales with the length of the string. Built-in String.prototype methods are native thunks and do not box, which is why this hides until an application calls its own sloppy-mode helper with a string this.

Why the work is unnecessary

The read path already computes index properties on demand (get_field_get/get_field_by_name_tail.rs:1326string_index_value). The eagerly installed ones exist only so reflection sees them (#3987), and that issue's own doc comment says "typical new String receivers are short".

Two fixes, each with a sibling already in-tree

  1. A USES_THIS body flag. perry-hir already computes closure_uses_this; a spare bit beside STRICT would let call_primitive_closure_value skip boxing entirely for a callee that never reads this. This removes the whole path for the common case rather than making it cheaper.
  2. Lazy index materialisation on the wrapper, so the properties exist when reflected upon and cost nothing otherwise — the read path already has the machinery.

Both are runtime/HIR changes; neither needs a codegen ABI change.

Suggested falsifier for whoever takes it

The offline control above is the direct one: fn.call(long string) must fall from ~17 s to the strict-callee figure (~10 ms, flat in length), and install_string_wrapper_indices must disappear from the sampled profile of a claude-code render window. If a fix moves the microbenchmark but not the window, the attribution above is wrong and should be re-derived before anything lands.

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions