Skip to content

Commit 7c6212c

Browse files
author
Ralph Küpper
committed
perf(closure): hoist per-element closure dispatch out of the array-callback loops (#8180)
`js_closure_callN` re-derived, on EVERY element of every fused array-callback loop, three answers that cannot change while one closure is being called: 1. `get_valid_func_ptr` — two address-band checks, a volatile `CLOSURE_MAGIC` probe through `*(closure + 12)` and a volatile `func_ptr` load; 2. `resolve_strategy` — a `perry_thread_local!` single-slot cache, which on Darwin is a `tlv_get_addr` CALL plus a load and a compare even on a hit; 3. the `DispatchStrategy` match before the indirect jump. The tree already contained the answer, applied to exactly one call site: `array/sort.rs`'s `ComparatorCall`, introduced to "skip ~50M HashMap lookups over a 1.25M-element sort". New `closure/dispatch/direct.rs` generalises it to arities 1–4 as `DirectCall{1,2,3,4}` — resolve once, call directly, fall back to `js_closure_callN` for a bound method/function, a rest parameter, a declared arity above the call arity, or an invalid closure pointer, so the proxy-callee/throw path, the rest bundling and the undefined-padding stay in one place. `resolve_call2_direct` is DELETED rather than left standing beside it; `ComparatorCall` now holds a `DirectCall2`. Hoisted at 31 call sites: `array/iter_methods.rs` (14), `array/reduce_right.rs` (1), `typedarray/iterate.rs` (9), `typedarray/transform.rs` (5 — including the BigInt lane comparator, which resolved once per COMPARISON) and the uint8 `%TypedArray%.prototype` dispatcher's `RootedCallback{2,3,4}`. Measured on a quiet M1 mini, instructions retired, best of 5, arms interleaved, per-arm `PERRY_RUNTIME_DIR` + `PERRY_CACHE_DIR`, `PERRY_NO_AUTO_OPTIMIZE=1`: bench main(A) +8179(B) +8179+8180(C) C vs A arr 5,027,207,909 5,027,015,176 3,970,592,563 -21.0 % u8 1,979,043,224 2,535,814,615 1,978,697,176 -0.02 % `arr` is 21M plain-`Array` callback invocations (forEach/map/filter/reduce/ findIndex/some/every); `u8` is 7.9M Buffer-backed `Uint8Array` ones. Peak RSS is flat: `arr` 46,628,864 B on both A and C, `u8` 14,794,752 -> 14,876,672 B (+0.55 %, 20 pages, the handle stack and the resolved sites). #8179's rooting costs +28 % on the `u8` path on its own; this change pays all of it back and the plain-`Array` path is 21 % cheaper than main. SOUNDNESS. `closure->func_ptr` is written once at `js_closure_alloc` and never mutated; `lookup_closure_rest` / `lookup_closure_arity` are keyed by it and are insert-only per key, registered at closure creation; the two sentinels are process constants. The only way to observe a different strategy mid-loop is to call a DIFFERENT closure, and an array method calls one. Callers that root their callback pass the CURRENT address to `call`; the resolved target is a static CODE address, which relocation does not change — the argument `ComparatorCall::compare_at` already documents. The unit tests in `direct.rs` assert the fast path is LIVE (`is_direct()`), not merely that nothing threw, and assert each decline (higher declared arity, rest parameter, invalid pointer). `array/generic.rs`'s `js_arraylike_*` engine is deliberately NOT converted. Its per-element cost is dominated by generic array-like property access (`al_has` + `al_get`, full prototype-chain lookups) rather than by dispatch, and the spec order it implements reads `LengthOfArrayLike` BEFORE `IsCallable` — so a hoisted resolve would have to be sequenced after the existing `callable()` call rather than inserted at the top of the function, which is not the same mechanical edit and risks moving an observable throw.
1 parent 7368abf commit 7c6212c

12 files changed

Lines changed: 510 additions & 94 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
### Fixed — GC rooting in the uint8 Buffer callback dispatcher (#8179)
2+
3+
`dispatch_uint8_buffer_method` — the shared uint8 `%TypedArray%.prototype`
4+
dispatcher that every Buffer-backed `Uint8Array` callback method funnels
5+
through, on all three of its entries — kept the callback closure, the receiver,
6+
`map`'s freshly allocated result buffer, `sort`/`toSorted`'s permuted output and
7+
`reduce`/`reduceRight`'s accumulator in bare Rust locals across
8+
`js_closure_call{2,3,4}`.
9+
10+
The closure is the live half. It is an ordinary nursery allocation
11+
(`GC_TYPE_CLOSURE`, with a `GcMoveHookKind::ClosureDynamicProps` move hook — it
12+
both moves and dies), and a callback handed in by a frameless caller is
13+
reachable only through that raw parameter plus the native stack, which an
14+
evacuating minor does not scan. `array::buffer_receiver_dispatch` rooted it at
15+
the boundary; the `%TypedArray%.prototype` thunk and `dispatch_buffer_method`'s
16+
catch-all did not, so both `Uint8Array.prototype.map.call(u, fn)` and a
17+
statically typed `u.map(fn)` were exposed.
18+
19+
`test-files/test_gap_gc_uint8_buffer_callback_rooting.ts` fails on the **shipped
20+
default** before the fix (`TypeError: value is not a function`, exit 1), and
21+
under `PERRY_GC_SCHEDULE_RATE=1 PERRY_GC_SCHEDULE_SEED=<n>
22+
PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_VERIFY_EVACUATION=1` it dies on the FIRST
23+
scheduled collection, for every seed tried (1, 7, 42):
24+
25+
```
26+
[gc-fromspace-protect] FAULT: signal 10 at 0x2454161058c
27+
last-known object: user_ptr=0x24541610580 obj_type=4 size=24
28+
[gc-schedule] FAILURE (signal 10) under seed=7
29+
[gc-schedule] safepoints=1 scheduled_collections=1
30+
```
31+
32+
`obj_type=4` is `GC_TYPE_CLOSURE` and the faulting address is `user_ptr + 12`
33+
`CLOSURE_TYPE_TAG_OFFSET`, i.e. `get_valid_func_ptr`'s `CLOSURE_MAGIC` probe
34+
reading a retired from-space closure header. After the fix the same seeds run to
35+
completion with the instrument's own liveness verdict showing the subject was
36+
live: `safepoints=5306 scheduled_collections=5306 copying_minors=5306
37+
moved_objects=25760`, zero faults, exit 0. The witness is registered in
38+
`test-parity/gc_repsel_corpus.txt`.
39+
40+
The receiver is the other half, and is treated differently on purpose. A Buffer
41+
is `arena_alloc_gc_old` + `GC_FLAG_TENURED` (`buffer/header.rs`) — the same
42+
old-arena space `typed_array_alloc` calls "non-movable space: raw data pointers
43+
are handed out" — and every `%TypedArray%` sibling already holds its receiver in
44+
a plain local across callbacks on that invariant. It is now rooted for
45+
*liveness* (the raw parameter is otherwise its only reference on two of three
46+
entries) with its address read from the root once per arm rather than once per
47+
element. The one collector arm that relocates an old-arena page is old-page
48+
defrag, which is opt-in and default-off (`PERRY_GC_OLD_DEFRAG=1`); making that
49+
safe is a tree-wide property of every holder of an old-arena raw address, and
50+
re-reading per element cost +28 % on the `Uint8Array` benchmark for a knob that
51+
is off.
52+
53+
Two sibling families get the same treatment: `js_typed_array_reduce` /
54+
`js_typed_array_reduce_right` now root their accumulator (as `js_array_reduce`
55+
has since the 2026-07-02 audit), and the two non-BigInt arms of
56+
`js_typed_array_sort_with_comparator` /
57+
`js_typed_array_to_sorted_with_comparator` now root the comparator closure —
58+
`sorted_bigint_lanes` beside them already did.
59+
60+
### Performance — hoisted per-element closure dispatch (#8180)
61+
62+
`js_closure_callN` re-derived, on every element of every fused array-callback
63+
loop, three answers that cannot change while one closure is being called:
64+
`get_valid_func_ptr` (two address-band checks, a volatile `CLOSURE_MAGIC` probe
65+
through `*(closure + 12)`, a volatile `func_ptr` load), `resolve_strategy` (a
66+
`perry_thread_local!` single-slot cache — on Darwin a `tlv_get_addr` CALL plus a
67+
load and a compare even on a hit), and the `DispatchStrategy` match before the
68+
indirect jump.
69+
70+
New `closure/dispatch/direct.rs` generalises `array/sort.rs`'s `ComparatorCall`
71+
trick — introduced to "skip ~50M HashMap lookups over a 1.25M-element sort", and
72+
until now its only consumer — into `DirectCall{1,2,3,4}`: resolve once, call
73+
directly, fall back to `js_closure_callN` for a bound method/function, a rest
74+
parameter, a declared arity above the call arity, or an invalid closure pointer.
75+
`resolve_call2_direct` is deleted rather than left standing beside it.
76+
77+
Hoisted at 31 call sites: `array/iter_methods.rs` (14), `array/reduce_right.rs`
78+
(1), `typedarray/iterate.rs` (9), `typedarray/transform.rs` (5 — including the
79+
BigInt lane comparator, which resolved once per *comparison*) and the uint8
80+
`%TypedArray%.prototype` dispatcher.
81+
82+
Instructions retired, quiet M1 mini, best of 5, arms interleaved:
83+
84+
| benchmark | main | +#8179 | +#8179+#8180 | vs main |
85+
|---|---|---|---|---|
86+
| 21M plain-`Array` callback invocations | 5,027,207,909 | 5,027,015,176 | 3,970,592,563 | **−21.0 %** |
87+
| 7.9M Buffer-`Uint8Array` callback invocations | 1,979,043,224 | 2,535,814,615 | 1,978,697,176 | **−0.02 %** |
88+
89+
Peak RSS is flat: 46,628,864 B on both arms of the first benchmark;
90+
14,794,752 → 14,876,672 B (+0.55 %, 20 pages) on the second.
91+
92+
`array/generic.rs`'s `js_arraylike_*` engine is deliberately not converted: its
93+
per-element cost is dominated by generic array-like property access (`al_has` +
94+
`al_get`), not dispatch, and the spec order it implements reads
95+
`LengthOfArrayLike` before `IsCallable`, so a hoisted resolve would have to be
96+
sequenced after the existing `callable()` call rather than inserted at the top.

crates/perry-runtime/src/array/iter_methods.rs

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Higher-order array methods.
22
use super::*;
3-
use crate::closure::{js_closure_call3, js_closure_call4, ClosureHeader};
3+
use crate::closure::ClosureHeader;
44
use std::ptr;
55

66
/// NaN-box an array header pointer as the JS `array` receiver value passed as
@@ -195,6 +195,10 @@ pub extern "C" fn js_array_forEach(arr: *const ArrayHeader, callback: *const Clo
195195
let length = (*arr).length;
196196
let scope = crate::gc::RuntimeHandleScope::new();
197197
let rooted = RootedIterArray::new(&scope, arr);
198+
// #8180: resolve the callback's dispatch ONCE. It is invariant for a
199+
// fixed closure (see closure/dispatch/direct.rs), and this loop calls
200+
// exactly one.
201+
let cb_site = crate::closure::DirectCall3::resolve(callback);
198202
// The override is a movable `ObjectHeader` held across user callbacks
199203
// that allocate — root it for the duration of the loop.
200204
let self_handle = self_override.map(|recv| scope.root_nanbox_f64(recv));
@@ -210,7 +214,7 @@ pub extern "C" fn js_array_forEach(arr: *const ArrayHeader, callback: *const Clo
210214
continue;
211215
}
212216
let element = crate::array::array_spec_get(arr, i as u32);
213-
js_closure_call3(callback, element, i as f64, self_value(&rooted));
217+
cb_site.call(callback, element, i as f64, self_value(&rooted));
214218
}
215219
return;
216220
}
@@ -222,7 +226,7 @@ pub extern "C" fn js_array_forEach(arr: *const ArrayHeader, callback: *const Clo
222226
// dispatch path supports call3 safely, so bound native
223227
// methods like `array.forEach(console.log)` can observe the
224228
// source array just like Node.
225-
js_closure_call3(callback, element, i as f64, self_value(&rooted));
229+
cb_site.call(callback, element, i as f64, self_value(&rooted));
226230
}
227231
}
228232
}
@@ -261,6 +265,10 @@ pub extern "C" fn js_array_map(
261265
let length = (*arr).length;
262266
let scope = crate::gc::RuntimeHandleScope::new();
263267
let rooted = RootedIterArray::new(&scope, arr);
268+
// #8180: resolve the callback's dispatch ONCE. It is invariant for a
269+
// fixed closure (see closure/dispatch/direct.rs), and this loop calls
270+
// exactly one.
271+
let cb_site = crate::closure::DirectCall3::resolve(callback);
264272
// Root the callback closure across the iteration. A callback allocated
265273
// by a frameless caller (arrow/method — #6081) is reachable ONLY via
266274
// this raw param + the native stack, which an evacuating minor does NOT
@@ -307,7 +315,7 @@ pub extern "C" fn js_array_map(
307315
};
308316
// JS .map() callback receives (element, index, array).
309317
let callback = cb_handle.get_raw_const_ptr::<ClosureHeader>();
310-
let mapped = js_closure_call3(callback, element, i as f64, rooted.receiver());
318+
let mapped = cb_site.call(callback, element, i as f64, rooted.receiver());
311319
if is_plain {
312320
let result = result_arr(&result_rooted);
313321
let result_elements =
@@ -363,6 +371,10 @@ pub extern "C" fn js_array_map_discard(arr: *const ArrayHeader, callback: *const
363371
let length = (*arr).length;
364372
let scope = crate::gc::RuntimeHandleScope::new();
365373
let rooted = RootedIterArray::new(&scope, arr);
374+
// #8180: resolve the callback's dispatch ONCE. It is invariant for a
375+
// fixed closure (see closure/dispatch/direct.rs), and this loop calls
376+
// exactly one.
377+
let cb_site = crate::closure::DirectCall3::resolve(callback);
366378
// The callback needs the same root its sibling `js_array_map` gives it
367379
// (#6081), and for the same reason: a callback allocated by a frameless
368380
// caller — the arrow in `xs.map(x => …)` — is reachable ONLY through this
@@ -396,15 +408,15 @@ pub extern "C" fn js_array_map_discard(arr: *const ArrayHeader, callback: *const
396408
continue;
397409
}
398410
let element = crate::array::array_spec_get(arr, i as u32);
399-
let _ = js_closure_call3(current_callback(), element, i as f64, rooted.receiver());
411+
let _ = cb_site.call(current_callback(), element, i as f64, rooted.receiver());
400412
}
401413
return;
402414
}
403415
for i in 0..length as usize {
404416
let Some(element) = rooted.present(i) else {
405417
continue;
406418
};
407-
let _ = js_closure_call3(current_callback(), element, i as f64, rooted.receiver());
419+
let _ = cb_site.call(current_callback(), element, i as f64, rooted.receiver());
408420
}
409421
}
410422
}
@@ -443,6 +455,10 @@ pub extern "C" fn js_array_filter(
443455
let length = (*arr).length;
444456
let scope = crate::gc::RuntimeHandleScope::new();
445457
let rooted = RootedIterArray::new(&scope, arr);
458+
// #8180: resolve the callback's dispatch ONCE. It is invariant for a
459+
// fixed closure (see closure/dispatch/direct.rs), and this loop calls
460+
// exactly one.
461+
let cb_site = crate::closure::DirectCall3::resolve(callback);
446462
// Root the callback across the loop — see js_array_map / gh #6206.
447463
let cb_handle = scope.root_raw_const_ptr(callback);
448464
let _tg = DenseThisGuard::bind_undefined();
@@ -473,7 +489,7 @@ pub extern "C" fn js_array_filter(
473489
}
474490
};
475491
let callback = cb_handle.get_raw_const_ptr::<ClosureHeader>();
476-
let keep = js_closure_call3(callback, element, i as f64, rooted.receiver());
492+
let keep = cb_site.call(callback, element, i as f64, rooted.receiver());
477493
// Proper truthy check: handles NaN-boxed booleans (TAG_FALSE != 0.0 but is falsy)
478494
if crate::value::js_is_truthy(keep) != 0 {
479495
if is_plain {
@@ -527,6 +543,10 @@ pub extern "C" fn js_array_find(arr: *const ArrayHeader, callback: *const Closur
527543
let length = (*arr).length;
528544
let scope = crate::gc::RuntimeHandleScope::new();
529545
let rooted = RootedIterArray::new(&scope, arr);
546+
// #8180: resolve the callback's dispatch ONCE. It is invariant for a
547+
// fixed closure (see closure/dispatch/direct.rs), and this loop calls
548+
// exactly one.
549+
let cb_site = crate::closure::DirectCall3::resolve(callback);
530550
let _tg = DenseThisGuard::bind_undefined();
531551
let exotic = crate::array::array_iteration_is_exotic(arr);
532552

@@ -536,7 +556,7 @@ pub extern "C" fn js_array_find(arr: *const ArrayHeader, callback: *const Closur
536556
} else {
537557
rooted.get_or_undefined(i)
538558
};
539-
let result = js_closure_call3(callback, element, i as f64, rooted.receiver());
559+
let result = cb_site.call(callback, element, i as f64, rooted.receiver());
540560
// Proper truthy check: handles NaN-boxed booleans
541561
if crate::value::js_is_truthy(result) != 0 {
542562
return element;
@@ -583,6 +603,10 @@ pub extern "C" fn js_array_findIndex(
583603
let length = (*arr).length;
584604
let scope = crate::gc::RuntimeHandleScope::new();
585605
let rooted = RootedIterArray::new(&scope, arr);
606+
// #8180: resolve the callback's dispatch ONCE. It is invariant for a
607+
// fixed closure (see closure/dispatch/direct.rs), and this loop calls
608+
// exactly one.
609+
let cb_site = crate::closure::DirectCall3::resolve(callback);
586610
let _tg = DenseThisGuard::bind_undefined();
587611
let exotic = crate::array::array_iteration_is_exotic(arr);
588612

@@ -592,7 +616,7 @@ pub extern "C" fn js_array_findIndex(
592616
} else {
593617
rooted.get_or_undefined(i)
594618
};
595-
let result = js_closure_call3(callback, element, i as f64, rooted.receiver());
619+
let result = cb_site.call(callback, element, i as f64, rooted.receiver());
596620
// Proper truthy check: handles NaN-boxed booleans
597621
if crate::value::js_is_truthy(result) != 0 {
598622
return i as i32;
@@ -624,6 +648,10 @@ pub extern "C" fn js_array_find_last(
624648
let length = (*arr).length as usize;
625649
let scope = crate::gc::RuntimeHandleScope::new();
626650
let rooted = RootedIterArray::new(&scope, arr);
651+
// #8180: resolve the callback's dispatch ONCE. It is invariant for a
652+
// fixed closure (see closure/dispatch/direct.rs), and this loop calls
653+
// exactly one.
654+
let cb_site = crate::closure::DirectCall3::resolve(callback);
627655
let _tg = DenseThisGuard::bind_undefined();
628656
let exotic = crate::array::array_iteration_is_exotic(arr);
629657
for i in (0..length).rev() {
@@ -632,7 +660,7 @@ pub extern "C" fn js_array_find_last(
632660
} else {
633661
rooted.get_or_undefined(i)
634662
};
635-
let result = js_closure_call3(callback, element, i as f64, rooted.receiver());
663+
let result = cb_site.call(callback, element, i as f64, rooted.receiver());
636664
if crate::value::js_is_truthy(result) != 0 {
637665
return element;
638666
}
@@ -662,6 +690,10 @@ pub extern "C" fn js_array_find_last_index(
662690
let length = (*arr).length as usize;
663691
let scope = crate::gc::RuntimeHandleScope::new();
664692
let rooted = RootedIterArray::new(&scope, arr);
693+
// #8180: resolve the callback's dispatch ONCE. It is invariant for a
694+
// fixed closure (see closure/dispatch/direct.rs), and this loop calls
695+
// exactly one.
696+
let cb_site = crate::closure::DirectCall3::resolve(callback);
665697
let _tg = DenseThisGuard::bind_undefined();
666698
let exotic = crate::array::array_iteration_is_exotic(arr);
667699
for i in (0..length).rev() {
@@ -670,7 +702,7 @@ pub extern "C" fn js_array_find_last_index(
670702
} else {
671703
rooted.get_or_undefined(i)
672704
};
673-
let result = js_closure_call3(callback, element, i as f64, rooted.receiver());
705+
let result = cb_site.call(callback, element, i as f64, rooted.receiver());
674706
if crate::value::js_is_truthy(result) != 0 {
675707
return i as i32;
676708
}
@@ -757,6 +789,10 @@ pub extern "C" fn js_array_some(arr: *const ArrayHeader, callback: *const Closur
757789
let length = (*arr).length;
758790
let scope = crate::gc::RuntimeHandleScope::new();
759791
let rooted = RootedIterArray::new(&scope, arr);
792+
// #8180: resolve the callback's dispatch ONCE. It is invariant for a
793+
// fixed closure (see closure/dispatch/direct.rs), and this loop calls
794+
// exactly one.
795+
let cb_site = crate::closure::DirectCall3::resolve(callback);
760796
let _tg = DenseThisGuard::bind_undefined();
761797
let exotic = crate::array::array_iteration_is_exotic(arr);
762798

@@ -773,7 +809,7 @@ pub extern "C" fn js_array_some(arr: *const ArrayHeader, callback: *const Closur
773809
None => continue,
774810
}
775811
};
776-
let result = js_closure_call3(callback, element, i as f64, rooted.receiver());
812+
let result = cb_site.call(callback, element, i as f64, rooted.receiver());
777813
if crate::value::js_is_truthy(result) != 0 {
778814
return f64::from_bits(TAG_TRUE);
779815
}
@@ -816,6 +852,10 @@ pub extern "C" fn js_array_every(arr: *const ArrayHeader, callback: *const Closu
816852
let length = (*arr).length;
817853
let scope = crate::gc::RuntimeHandleScope::new();
818854
let rooted = RootedIterArray::new(&scope, arr);
855+
// #8180: resolve the callback's dispatch ONCE. It is invariant for a
856+
// fixed closure (see closure/dispatch/direct.rs), and this loop calls
857+
// exactly one.
858+
let cb_site = crate::closure::DirectCall3::resolve(callback);
819859
let _tg = DenseThisGuard::bind_undefined();
820860
let exotic = crate::array::array_iteration_is_exotic(arr);
821861

@@ -832,7 +872,7 @@ pub extern "C" fn js_array_every(arr: *const ArrayHeader, callback: *const Closu
832872
None => continue,
833873
}
834874
};
835-
let result = js_closure_call3(callback, element, i as f64, rooted.receiver());
875+
let result = cb_site.call(callback, element, i as f64, rooted.receiver());
836876
if crate::value::js_is_truthy(result) == 0 {
837877
return f64::from_bits(TAG_FALSE);
838878
}
@@ -857,6 +897,10 @@ pub extern "C" fn js_array_flatMap(
857897
let length = (*arr).length;
858898
let scope = crate::gc::RuntimeHandleScope::new();
859899
let rooted = RootedIterArray::new(&scope, arr);
900+
// #8180: resolve the callback's dispatch ONCE. It is invariant for a
901+
// fixed closure (see closure/dispatch/direct.rs), and this loop calls
902+
// exactly one.
903+
let cb_site = crate::closure::DirectCall3::resolve(callback);
860904
// Root the result across callbacks and pushes (a push both allocates
861905
// — possibly triggering a moving GC — and may reallocate the array).
862906
let result_rooted = scope.root_nanbox_f64(f64::from_bits(
@@ -879,7 +923,7 @@ pub extern "C" fn js_array_flatMap(
879923
let Some(element) = rooted.present(i) else {
880924
continue;
881925
};
882-
let mapped = js_closure_call3(callback, element, i as f64, rooted.receiver());
926+
let mapped = cb_site.call(callback, element, i as f64, rooted.receiver());
883927
// Root first: detecting a lazy array may materialize it, and a
884928
// push in the inner loop can move the callback result's target.
885929
sub_rooted.set_nanbox_f64(mapped);
@@ -958,6 +1002,10 @@ pub extern "C" fn js_array_reduce(
9581002
let length = (*arr).length as usize;
9591003
let scope = crate::gc::RuntimeHandleScope::new();
9601004
let rooted = RootedIterArray::new(&scope, arr);
1005+
// #8180: resolve the callback's dispatch ONCE. It is invariant for a
1006+
// fixed closure (see closure/dispatch/direct.rs), and this loop calls
1007+
// exactly one.
1008+
let cb_site = crate::closure::DirectCall4::resolve(callback);
9611009

9621010
if length == 0 {
9631011
if has_initial != 0 {
@@ -1005,7 +1053,7 @@ pub extern "C" fn js_array_reduce(
10051053
continue;
10061054
};
10071055
// Spec callback is `(accumulator, currentValue, currentIndex, array)`.
1008-
let next = js_closure_call4(
1056+
let next = cb_site.call(
10091057
callback,
10101058
acc_rooted.get_nanbox_f64(),
10111059
element,

0 commit comments

Comments
 (0)