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:1326 → string_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
- 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.
- 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.
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_valueis 38.3 % of the sampled window andjs_boxed_string_new23.6 %. The mechanism isjs_boxed_string_new → install_string_wrapper_indices: it installs one own property per code unit — a shape transition, aset_builtin_property_attrsentry (intodescriptor_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
fn.call(200-char string)fn.apply(...)Object(str)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.prototypemethods are native thunks and do not box, which is why this hides until an application calls its own sloppy-mode helper with a stringthis.Why the work is unnecessary
The read path already computes index properties on demand (
get_field_get/get_field_by_name_tail.rs:1326→string_index_value). The eagerly installed ones exist only so reflection sees them (#3987), and that issue's own doc comment says "typicalnew Stringreceivers are short".Two fixes, each with a sibling already in-tree
USES_THISbody flag.perry-hiralready computesclosure_uses_this; a spare bit besideSTRICTwould letcall_primitive_closure_valueskip boxing entirely for a callee that never readsthis. This removes the whole path for the common case rather than making it cheaper.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), andinstall_string_wrapper_indicesmust 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.