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:
parent
4ac744082b
commit
7c19719946
5 changed files with 131 additions and 5 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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]]
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue