Short string concatenations are a common allocation churn pattern in JS
execution. Many of these results are immediately observed as flat
strings, so the intended win is to avoid spending GC and flattening work
on an intermediate representation that does not carry its weight.
Microbenchmark:
const n = 10_000_000;
function bench(a, b) {
let total = 0;
for (let i = 0; i < n; ++i)
total += (a + b).length;
if (total !== n * 2)
throw new Error(String(total));
}
bench("a", "b");
Measured with hyperfine against the same build with the fast path
disabled:
baseline: 822.7 ms +/- 25.7 ms
optimized: 385.5 ms +/- 21.5 ms
speedup: 2.13 +/- 0.14 times faster
202 lines
6.6 KiB
C++
202 lines
6.6 KiB
C++
/*
|
|
* Copyright (c) 2026-present, the Ladybird developers.
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <AK/StringView.h>
|
|
#include <AK/Try.h>
|
|
#include <LibGC/Root.h>
|
|
#include <LibJS/Runtime/PrimitiveString.h>
|
|
#include <LibJS/Runtime/Realm.h>
|
|
#include <LibJS/Runtime/VM.h>
|
|
#include <LibTest/TestCase.h>
|
|
|
|
using namespace JS;
|
|
|
|
namespace {
|
|
|
|
struct TestVM {
|
|
TestVM()
|
|
: vm(VM::create())
|
|
, execution_context(MUST(Realm::initialize_host_defined_realm(*vm, nullptr, nullptr)))
|
|
{
|
|
}
|
|
|
|
~TestVM()
|
|
{
|
|
vm->pop_execution_context();
|
|
}
|
|
|
|
NonnullRefPtr<VM> vm;
|
|
NonnullOwnPtr<ExecutionContext> execution_context;
|
|
};
|
|
|
|
NEVER_INLINE static void materialize_temporary_rope(VM& vm)
|
|
{
|
|
auto rope = PrimitiveString::create(vm,
|
|
*PrimitiveString::create(vm, "hello"_string),
|
|
*PrimitiveString::create(vm, "world"_string));
|
|
EXPECT(rope->utf8_string_view() == "helloworld"sv);
|
|
}
|
|
|
|
NEVER_INLINE static void materialize_temporary_substring(VM& vm)
|
|
{
|
|
auto source = PrimitiveString::create(vm, "foobar"_string);
|
|
auto substring = PrimitiveString::create(vm, *source, 0, 3);
|
|
EXPECT(substring->utf8_string_view() == "foo"sv);
|
|
}
|
|
|
|
NEVER_INLINE static void clobber_stack()
|
|
{
|
|
char volatile stack_bytes[4096] {};
|
|
for (size_t i = 0; i < sizeof(stack_bytes); ++i)
|
|
stack_bytes[i] = static_cast<char>(i);
|
|
}
|
|
|
|
}
|
|
|
|
TEST_CASE(primitive_string_substring_supports_nested_ranges)
|
|
{
|
|
TestVM test_vm;
|
|
|
|
auto string = PrimitiveString::create(*test_vm.vm, "abcdef"_string);
|
|
auto substring = PrimitiveString::create(*test_vm.vm, *string, 1, 4);
|
|
auto nested_substring = PrimitiveString::create(*test_vm.vm, *substring, 1, 2);
|
|
|
|
EXPECT_EQ(substring->length_in_utf16_code_units(), 4u);
|
|
EXPECT(substring->utf8_string_view() == "bcde"sv);
|
|
EXPECT_EQ(nested_substring->length_in_utf16_code_units(), 2u);
|
|
EXPECT(nested_substring->utf8_string_view() == "cd"sv);
|
|
}
|
|
|
|
TEST_CASE(primitive_string_substring_materializes_rope_ranges)
|
|
{
|
|
TestVM test_vm;
|
|
|
|
auto rope = PrimitiveString::create(*test_vm.vm,
|
|
*PrimitiveString::create(*test_vm.vm, "abcd"_string),
|
|
*PrimitiveString::create(*test_vm.vm, "efgh"_string));
|
|
auto substring = PrimitiveString::create(*test_vm.vm, *rope, 3, 3);
|
|
|
|
EXPECT_EQ(substring->length_in_utf16_code_units(), 3u);
|
|
EXPECT(substring->utf8_string_view() == "def"sv);
|
|
}
|
|
|
|
TEST_CASE(primitive_string_concat_short_flat_strings_creates_flat_string)
|
|
{
|
|
TestVM test_vm;
|
|
|
|
auto concatenated = PrimitiveString::create(*test_vm.vm,
|
|
*PrimitiveString::create(*test_vm.vm, "foo"_string),
|
|
*PrimitiveString::create(*test_vm.vm, "bar"_string));
|
|
|
|
EXPECT(concatenated->has_utf8_string());
|
|
EXPECT(concatenated->utf8_string_view() == "foobar"sv);
|
|
}
|
|
|
|
TEST_CASE(primitive_string_concat_longer_strings_stays_deferred)
|
|
{
|
|
TestVM test_vm;
|
|
|
|
auto concatenated = PrimitiveString::create(*test_vm.vm,
|
|
*PrimitiveString::create(*test_vm.vm, "abcd"_string),
|
|
*PrimitiveString::create(*test_vm.vm, "efgh"_string));
|
|
|
|
EXPECT(!concatenated->has_utf8_string());
|
|
EXPECT(!concatenated->has_utf16_string());
|
|
EXPECT(concatenated->utf8_string_view() == "abcdefgh"sv);
|
|
}
|
|
|
|
TEST_CASE(primitive_string_substring_reuses_cached_single_ascii_strings)
|
|
{
|
|
TestVM test_vm;
|
|
|
|
GC::Root<PrimitiveString> cached_b = PrimitiveString::create(*test_vm.vm, "b"_string);
|
|
auto string = PrimitiveString::create(*test_vm.vm, "abcd"_string);
|
|
auto substring = PrimitiveString::create(*test_vm.vm, *string, 1, 1);
|
|
|
|
EXPECT_EQ(substring.ptr(), cached_b.ptr());
|
|
}
|
|
|
|
TEST_CASE(primitive_string_substring_handles_surrogate_boundaries)
|
|
{
|
|
TestVM test_vm;
|
|
|
|
auto string = PrimitiveString::create(*test_vm.vm, "😀x"_string);
|
|
auto leading_surrogate = PrimitiveString::create(*test_vm.vm, *string, 0, 1);
|
|
auto trailing_surrogate = PrimitiveString::create(*test_vm.vm, *string, 1, 1);
|
|
auto full_code_point = PrimitiveString::create(*test_vm.vm, *string, 0, 2);
|
|
|
|
EXPECT_EQ(leading_surrogate->length_in_utf16_code_units(), 1u);
|
|
EXPECT_EQ(leading_surrogate->utf16_string_view().code_unit_at(0), static_cast<u16>(0xd83d));
|
|
EXPECT_EQ(trailing_surrogate->length_in_utf16_code_units(), 1u);
|
|
EXPECT_EQ(trailing_surrogate->utf16_string_view().code_unit_at(0), static_cast<u16>(0xde00));
|
|
EXPECT(full_code_point->utf8_string_view() == "😀"sv);
|
|
}
|
|
|
|
TEST_CASE(primitive_string_concat_short_strings_handles_surrogate_boundaries)
|
|
{
|
|
TestVM test_vm;
|
|
|
|
auto string = PrimitiveString::create(*test_vm.vm, "😀"_string);
|
|
auto leading_surrogate = PrimitiveString::create(*test_vm.vm, *string, 0, 1);
|
|
auto trailing_surrogate = PrimitiveString::create(*test_vm.vm, *string, 1, 1);
|
|
|
|
(void)leading_surrogate->utf8_string_view();
|
|
(void)trailing_surrogate->utf8_string_view();
|
|
|
|
auto concatenated = PrimitiveString::create(*test_vm.vm, *leading_surrogate, *trailing_surrogate);
|
|
EXPECT(concatenated->utf8_string_view() == "😀"sv);
|
|
}
|
|
|
|
TEST_CASE(primitive_string_substring_utf16_views_stay_deferred)
|
|
{
|
|
TestVM test_vm;
|
|
|
|
auto string = PrimitiveString::create(*test_vm.vm, Utf16View { u"abcd", 4 });
|
|
auto substring = PrimitiveString::create(*test_vm.vm, *string, 1, 2);
|
|
|
|
EXPECT(string->has_utf16_string());
|
|
EXPECT(!substring->has_utf16_string());
|
|
|
|
auto view = substring->utf16_string_view();
|
|
|
|
EXPECT_EQ(view.length_in_code_units(), 2u);
|
|
EXPECT_EQ(view.code_unit_at(0), 'b');
|
|
EXPECT_EQ(view.code_unit_at(1), 'c');
|
|
EXPECT(!substring->has_utf16_string());
|
|
}
|
|
|
|
TEST_CASE(primitive_string_substring_equality_uses_utf16_code_units)
|
|
{
|
|
TestVM test_vm;
|
|
|
|
char16_t const source_code_units[] = { u'x', u'x', u'x', 0xd800, 0xdc00, 0xdc00, u'x', u'x' };
|
|
char16_t const substring_code_units[] = { 0xd800, 0xdc00, 0xdc00 };
|
|
|
|
auto source = PrimitiveString::create(*test_vm.vm, Utf16View { source_code_units, 8 });
|
|
auto substring = PrimitiveString::create(*test_vm.vm, *source, 3, 3);
|
|
auto expected = PrimitiveString::create(*test_vm.vm, Utf16View { substring_code_units, 3 });
|
|
|
|
EXPECT(*substring == *expected);
|
|
}
|
|
|
|
TEST_CASE(deferred_primitive_strings_do_not_evict_cached_strings)
|
|
{
|
|
TestVM test_vm;
|
|
|
|
GC::Root<PrimitiveString> cached_foo = PrimitiveString::create(*test_vm.vm, "foo"_string);
|
|
|
|
materialize_temporary_rope(*test_vm.vm);
|
|
clobber_stack();
|
|
|
|
test_vm.vm->heap().collect_garbage();
|
|
EXPECT_EQ(PrimitiveString::create(*test_vm.vm, "foo"_string).ptr(), cached_foo.ptr());
|
|
|
|
materialize_temporary_substring(*test_vm.vm);
|
|
clobber_stack();
|
|
|
|
test_vm.vm->heap().collect_garbage();
|
|
EXPECT_EQ(PrimitiveString::create(*test_vm.vm, "foo"_string).ptr(), cached_foo.ptr());
|
|
}
|