diff --git a/changelog.d/8460-array-join-capacity.md b/changelog.d/8460-array-join-capacity.md new file mode 100644 index 0000000000..95519402b8 --- /dev/null +++ b/changelog.d/8460-array-join-capacity.md @@ -0,0 +1,5 @@ +### Performance + +- Preallocate `Array.prototype.join`'s exact separator-byte floor instead of + growing its Rust assembly buffer from zero capacity. Element coercion remains + single-pass, so user `toString` side effects are not duplicated. diff --git a/crates/perry-runtime/src/array/iter_methods.rs b/crates/perry-runtime/src/array/iter_methods.rs index db3e02f247..db5f5a6af8 100644 --- a/crates/perry-runtime/src/array/iter_methods.rs +++ b/crates/perry-runtime/src/array/iter_methods.rs @@ -1118,8 +1118,13 @@ pub extern "C" fn js_array_join( std::str::from_utf8_unchecked(std::slice::from_raw_parts(sep_data, sep_len)) }; - // Build result string - let mut result = String::new(); + // Separators are an exact lower bound for the result. Reserving them + // up front avoids the zero-capacity growth ladder without speculatively + // coercing elements (which could run user code or mutate the array). + let separator_bytes = sep_str + .len() + .saturating_mul(length.saturating_sub(1) as usize); + let mut result = String::with_capacity(separator_bytes); for i in 0..length as usize { if i > 0 { result.push_str(sep_str); diff --git a/test-files/test_issue_8434_array_join_capacity.ts b/test-files/test_issue_8434_array_join_capacity.ts new file mode 100644 index 0000000000..c6ba300174 --- /dev/null +++ b/test-files/test_issue_8434_array_join_capacity.ts @@ -0,0 +1,23 @@ +const show = (label: string, value: string): void => { + console.log(`${label}:${value}|${value.length}`); +}; + +show("holes", new Array(3).join("|")); +show("empty", ["", "", ""].join("")); +show("unicode", ["A", "😀", "é"].join("·")); + +let calls = 0; +const values: unknown[] = []; +const allocating = { + toString(): string { + calls++; + for (let i = 0; i < 128; i++) { + ("allocation-" + i).repeat(32); + } + values[1] = "after"; + return "before"; + }, +}; +values.push(allocating, "original", "tail"); +show("coerce", values.join("/")); +console.log(`calls:${calls}`);