LibJS: Avoid repeated lazy source decoding

Function.prototype.toString() can ask for the same cached function
source text repeatedly after a script was materialized from the
bytecode cache. In that path SourceCode still owns only the encoded
source bytes, so every request decoded the requested source range
again. Large ASCII bundles made that path expensive enough to stall
Speedometer 2.1's Ember debug test.

Keep the byte-backed SourceCode representation lazy, but let ASCII
UTF-8 source ranges slice the source bytes directly. For other ASCII
byte-backed encodings, first ask the decoder to prove that the source
bytes map to the same UTF-16 code units at the same positions.

Cache extracted function source text on shared function data after the
first request so repeated toString() calls do not keep going back to
SourceCode.

Add bytecode cache coverage for toString() on lazy UTF-8 source bytes,
keep malformed UTF-8 and UTF-16 edge cases on the decoder path, and
cover PDFDocEncoding bytes that are not identity-mapped.
This commit is contained in:
Andreas Kling 2026-05-19 13:59:58 +02:00 committed by Andreas Kling
parent 4ac744082b
commit 7c19719946
5 changed files with 131 additions and 5 deletions

View file

@ -77,7 +77,12 @@ Utf16String SharedFunctionInstanceData::source_text() const
if (!m_source_code)
return {};
return m_source_code->source_text_from_offsets(m_source_text_offset, m_source_text_length);
auto old_external_memory_size = utf16_string_external_memory_size(m_source_text_owner);
m_source_text_owner = m_source_code->source_text_from_offsets(m_source_text_offset, m_source_text_length);
auto new_external_memory_size = utf16_string_external_memory_size(m_source_text_owner);
if (new_external_memory_size > old_external_memory_size)
heap().did_allocate_external_memory(new_external_memory_size - old_external_memory_size);
return m_source_text_owner;
}
void SharedFunctionInstanceData::set_source_text(Utf16View source_text)

View file

@ -89,7 +89,7 @@ public:
// source text needs to be owned by the function data (e.g. for
// dynamically created functions via Function constructor).
RefPtr<SourceCode const> m_source_code;
Utf16String m_source_text_owner;
Utf16String mutable m_source_text_owner;
size_t m_source_text_offset { 0 };
size_t m_source_text_length { 0 }; // [[SourceText]]

View file

@ -4,7 +4,9 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/AllOf.h>
#include <AK/BinarySearch.h>
#include <AK/CharacterTypes.h>
#include <LibJS/SourceCode.h>
#include <LibJS/SourceRange.h>
#include <LibJS/Token.h>
@ -12,6 +14,30 @@
namespace JS {
static bool ascii_source_bytes_decode_to_same_code_units(StringView standardized_encoding, ReadonlyBytes bytes)
{
auto decoder = TextCodec::decoder_for_exact_name(standardized_encoding);
if (!decoder.has_value())
return false;
size_t byte_offset = 0;
bool bytes_are_identity_mapped = true;
auto result = decoder->process_code_points(StringView { bytes }, [&](auto code_point) -> ErrorOr<void> {
if (byte_offset >= bytes.size()) {
bytes_are_identity_mapped = false;
return {};
}
auto byte = bytes[byte_offset++];
if (code_point != byte)
bytes_are_identity_mapped = false;
return {};
});
result.release_value_but_fixme_should_propagate_errors();
return bytes_are_identity_mapped && byte_offset == bytes.size();
}
NonnullRefPtr<SourceCode const> SourceCode::create(String filename, Utf16String code)
{
return adopt_ref(*new SourceCode(move(filename), move(code)));
@ -84,13 +110,54 @@ Utf16String SourceCode::source_text_from_offsets(size_t start_offset, size_t len
if (m_code.has_value())
return Utf16String::from_utf16(m_code->utf16_view().substring_view(start_offset, length));
if (m_source_bytes.is_valid())
if (m_source_bytes.is_valid()) {
if (source_bytes_can_be_sliced_by_code_unit_offsets()) {
auto bytes = m_source_bytes.bytes();
VERIFY(m_length_in_code_units == bytes.size());
auto source_text_bytes = bytes.slice(start_offset, length);
if (all_of(source_text_bytes, AK::is_ascii))
return Utf16String::from_ascii_without_validation(source_text_bytes);
return Utf16String::from_utf8(StringView { source_text_bytes });
}
return decode_source_range(start_offset, length);
}
ensure_code();
return Utf16String::from_utf16(m_code->utf16_view().substring_view(start_offset, length));
}
bool SourceCode::source_bytes_can_be_sliced_by_code_unit_offsets() const
{
if (!m_source_bytes_can_be_sliced_by_code_unit_offsets.has_value()) {
auto standardized_encoding = TextCodec::get_standardized_encoding(m_source_encoding);
if (!standardized_encoding.has_value()) {
m_source_bytes_can_be_sliced_by_code_unit_offsets = false;
return *m_source_bytes_can_be_sliced_by_code_unit_offsets;
}
auto bytes = m_source_bytes.bytes();
if (m_length_in_code_units != bytes.size()) {
m_source_bytes_can_be_sliced_by_code_unit_offsets = false;
return *m_source_bytes_can_be_sliced_by_code_unit_offsets;
}
auto source_bytes_are_ascii = all_of(bytes, AK::is_ascii);
if (standardized_encoding->equals_ignoring_ascii_case("UTF-8"sv)) {
m_source_bytes_can_be_sliced_by_code_unit_offsets = source_bytes_are_ascii;
return *m_source_bytes_can_be_sliced_by_code_unit_offsets;
}
if (!source_bytes_are_ascii) {
m_source_bytes_can_be_sliced_by_code_unit_offsets = false;
return *m_source_bytes_can_be_sliced_by_code_unit_offsets;
}
m_source_bytes_can_be_sliced_by_code_unit_offsets = ascii_source_bytes_decode_to_same_code_units(*standardized_encoding, bytes);
}
return *m_source_bytes_can_be_sliced_by_code_unit_offsets;
}
Utf16String SourceCode::decode_source_range(size_t start_offset, size_t length) const
{
if (length == 0)

View file

@ -37,6 +37,7 @@ private:
SourceCode(String filename, size_t length_in_code_units, String source_encoding, Core::ImmutableBytes source_bytes);
void ensure_code() const;
Utf16String decode_source_range(size_t start_offset, size_t length) const;
bool source_bytes_can_be_sliced_by_code_unit_offsets() const;
String m_filename;
Optional<Utf16String> mutable m_code;
@ -58,6 +59,7 @@ private:
// Cached UTF-16 widening of ASCII source data, lazily populated by
// utf16_data() for use by the Rust compilation pipeline.
Vector<u16> mutable m_utf16_data_cache;
Optional<bool> mutable m_source_bytes_can_be_sliced_by_code_unit_offsets;
};
}

View file

@ -33,8 +33,8 @@ TEST_CASE(lazy_source_code_decoding_replaces_utf8_surrogates)
auto source_bytes = TRY_OR_FAIL(Core::ImmutableBytes::copy(source_data.span()));
auto source_code = JS::SourceCode::create("test.js"_string, 3, "UTF-8"_string, move(source_bytes));
EXPECT_EQ(source_code->code().to_utf8(), "\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd"sv);
EXPECT_EQ(source_code->source_text_from_offsets(0, 3).to_utf8(), "\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd"sv);
EXPECT_EQ(source_code->code().to_utf8(), "\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd"sv);
}
TEST_CASE(lazy_source_code_decoding_replaces_odd_trailing_utf16_byte)
@ -47,14 +47,34 @@ TEST_CASE(lazy_source_code_decoding_replaces_odd_trailing_utf16_byte)
EXPECT_EQ(source_code->source_text_from_offsets(0, 2).to_utf8(), "A\xef\xbf\xbd"sv);
}
TEST_CASE(lazy_source_code_decoding_replaces_single_trailing_utf16_byte)
{
auto source_data = Vector<u8> { 'A' };
auto source_bytes = TRY_OR_FAIL(Core::ImmutableBytes::copy(source_data.span()));
auto source_code = JS::SourceCode::create("test.js"_string, 1, "UTF-16LE"_string, move(source_bytes));
EXPECT_EQ(source_code->source_text_from_offsets(0, 1).to_utf8(), "\xef\xbf\xbd"sv);
}
TEST_CASE(lazy_source_code_decoding_uses_pdfdocencoding_mapping)
{
auto source_data = Vector<u8> { 0x18, 'A' };
auto source_bytes = TRY_OR_FAIL(Core::ImmutableBytes::copy(source_data.span()));
auto source_code = JS::SourceCode::create("test.js"_string, 2, "PDFDocEncoding"_string, move(source_bytes));
EXPECT_EQ(source_code->source_text_from_offsets(0, 1).to_utf8(), "\xcb\x98"sv);
EXPECT_EQ(source_code->code().to_utf8(), "\xcb\x98"
"A"sv);
}
TEST_CASE(lazy_source_code_decoding_replaces_overlong_utf8_sequences)
{
auto source_data = Vector<u8> { 0xc0, 0x80 };
auto source_bytes = TRY_OR_FAIL(Core::ImmutableBytes::copy(source_data.span()));
auto source_code = JS::SourceCode::create("test.js"_string, 2, "UTF-8"_string, move(source_bytes));
EXPECT_EQ(source_code->code().to_utf8(), "\xef\xbf\xbd\xef\xbf\xbd"sv);
EXPECT_EQ(source_code->source_text_from_offsets(0, 2).to_utf8(), "\xef\xbf\xbd\xef\xbf\xbd"sv);
EXPECT_EQ(source_code->code().to_utf8(), "\xef\xbf\xbd\xef\xbf\xbd"sv);
}
class BytecodeCacheBlobReader {
@ -438,6 +458,38 @@ TEST_CASE(bytecode_cache_materializes_function_executables_lazily)
EXPECT(!shared_data.m_cached_bytecode_executable);
}
TEST_CASE(bytecode_cache_to_string_caches_lazy_ascii_source_text)
{
auto vm = JS::VM::create();
auto root_execution_context = JS::create_simple_execution_context<JS::GlobalObject>(*vm);
auto& realm = *root_execution_context->realm;
auto source = "let f = function mapped() { return 'hello'; }; f.toString() + '|' + f.toString();"sv;
auto test_data = create_bytecode_cache_blob(source);
auto source_bytes = TRY_OR_FAIL(Core::ImmutableBytes::copy(source.bytes()));
auto source_code = JS::SourceCode::create("test.js"_string, test_data.source_code->length_in_code_units(), "UTF-8"_string, move(source_bytes));
auto* decoded_blob = JS::RustIntegration::decode_bytecode_cache_blob(test_data.blob.bytes(), JS::RustIntegration::ProgramType::Script, test_data.source_hash.bytes());
VERIFY(decoded_blob);
auto script_or_error = JS::Script::create_from_bytecode_cache(decoded_blob, source_code, realm);
VERIFY(!script_or_error.is_error());
auto script = script_or_error.release_value();
auto* executable = script->cached_executable();
VERIFY(executable);
VERIFY(!executable->shared_function_data.is_empty());
auto& shared_data = *executable->shared_function_data[0];
EXPECT(shared_data.m_source_text_owner.is_empty());
auto result = vm->run(script);
VERIFY(!result.is_throw_completion());
VERIFY(result.value().is_string());
EXPECT_EQ(result.value().as_string().utf8_string(), "function mapped() { return 'hello'; }|function mapped() { return 'hello'; }"_string);
EXPECT_EQ(shared_data.m_source_text_owner.to_utf8(), "function mapped() { return 'hello'; }"sv);
}
TEST_CASE(bytecode_cache_materializes_from_mapped_blob)
{
auto vm = JS::VM::create();