// Copyright 2021 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "include/v8-initialization.h" #include "src/api/api-inl.h" #include "src/api/api.h" #include "src/base/strings.h" #include "src/common/assert-scope.h" #include "src/common/globals.h" #include "src/flags/flags.h" #include "src/heap/factory.h" #include "src/heap/heap-inl.h" #include "src/heap/heap-layout-inl.h" #include "src/heap/heap.h" #include "src/heap/memory-chunk-layout.h" #include "src/heap/mutable-page-metadata.h" #include "src/heap/parked-scope-inl.h" #include "src/heap/remembered-set.h" #include "src/heap/safepoint.h" #include "src/objects/fixed-array.h" #include "src/objects/heap-object.h" #include "src/objects/js-weak-refs.h" #include "src/objects/objects-inl.h" #include "src/objects/string-forwarding-table-inl.h" #include "test/cctest/cctest.h" #include "test/cctest/heap/heap-utils.h" // In multi-cage mode we create one cage per isolate // and we don't share objects between cages. #if V8_CAN_CREATE_SHARED_HEAP_BOOL && !COMPRESS_POINTERS_IN_MULTIPLE_CAGES_BOOL namespace v8 { namespace internal { namespace test_shared_strings { struct V8_NODISCARD IsolateWrapper { explicit IsolateWrapper(v8::Isolate* isolate) : isolate(isolate) {} ~IsolateWrapper() { isolate->Dispose(); } v8::Isolate* const isolate; }; // Some tests in this file allocate two Isolates in the same thread to directly // test shared string behavior. Because both are considered running, when // disposing these Isolates, one must be parked to not cause a deadlock in the // shared heap verification that happens on client Isolate disposal. struct V8_NODISCARD IsolateParkOnDisposeWrapper { IsolateParkOnDisposeWrapper(v8::Isolate* isolate, v8::Isolate* isolate_to_park) : isolate(isolate), isolate_to_park(isolate_to_park) {} ~IsolateParkOnDisposeWrapper() { auto main_isolate = reinterpret_cast(isolate_to_park) ->main_thread_local_isolate(); main_isolate->ExecuteMainThreadWhileParked( [this]() { isolate->Dispose(); }); } v8::Isolate* const isolate; v8::Isolate* const isolate_to_park; }; class MultiClientIsolateTest { public: MultiClientIsolateTest() { std::unique_ptr allocator( v8::ArrayBuffer::Allocator::NewDefaultAllocator()); v8::Isolate::CreateParams create_params; create_params.array_buffer_allocator = allocator.get(); main_isolate_ = v8::Isolate::New(create_params); i_main_isolate()->Enter(); } ~MultiClientIsolateTest() { i_main_isolate()->Exit(); main_isolate_->Dispose(); } v8::Isolate* main_isolate() const { return main_isolate_; } Isolate* i_main_isolate() const { return reinterpret_cast(main_isolate_); } int& main_isolate_wakeup_counter() { return main_isolate_wakeup_counter_; } v8::Isolate* NewClientIsolate() { CHECK_NOT_NULL(main_isolate_); std::unique_ptr allocator( v8::ArrayBuffer::Allocator::NewDefaultAllocator()); v8::Isolate::CreateParams create_params; create_params.array_buffer_allocator = allocator.get(); return v8::Isolate::New(create_params); } private: v8::Isolate* main_isolate_; int main_isolate_wakeup_counter_ = 0; }; UNINITIALIZED_TEST(InPlaceInternalizableStringsAreShared) { if (v8_flags.single_generation) return; v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); MultiClientIsolateTest test; Isolate* i_isolate1 = test.i_main_isolate(); Factory* factory1 = i_isolate1->factory(); HandleScope handle_scope(i_isolate1); const char raw_one_byte[] = "foo"; base::uc16 raw_two_byte[] = {2001, 2002, 2003}; base::Vector two_byte(raw_two_byte, 3); // Old generation 1- and 2-byte seq strings are in-place internalizable. DirectHandle old_one_byte_seq = factory1->NewStringFromAsciiChecked(raw_one_byte, AllocationType::kOld); CHECK(HeapLayout::InAnySharedSpace(*old_one_byte_seq)); DirectHandle old_two_byte_seq = factory1->NewStringFromTwoByte(two_byte, AllocationType::kOld) .ToHandleChecked(); CHECK(HeapLayout::InAnySharedSpace(*old_two_byte_seq)); // Young generation are not internalizable and not shared when sharing the // string table. DirectHandle young_one_byte_seq = factory1->NewStringFromAsciiChecked(raw_one_byte, AllocationType::kYoung); CHECK(!HeapLayout::InAnySharedSpace(*young_one_byte_seq)); DirectHandle young_two_byte_seq = factory1->NewStringFromTwoByte(two_byte, AllocationType::kYoung) .ToHandleChecked(); CHECK(!HeapLayout::InAnySharedSpace(*young_two_byte_seq)); // Internalized strings are shared. uint64_t seed = HashSeed(i_isolate1); DirectHandle one_byte_intern = factory1->NewOneByteInternalizedString( base::OneByteVector(raw_one_byte), StringHasher::HashSequentialString(raw_one_byte, 3, seed)); CHECK(HeapLayout::InAnySharedSpace(*one_byte_intern)); DirectHandle two_byte_intern = factory1->NewTwoByteInternalizedString( two_byte, StringHasher::HashSequentialString(raw_two_byte, 3, seed)); CHECK(HeapLayout::InAnySharedSpace(*two_byte_intern)); } UNINITIALIZED_TEST(InPlaceInternalization) { v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); MultiClientIsolateTest test; ManualGCScope manual_gc_scope(test.i_main_isolate()); IsolateParkOnDisposeWrapper isolate_wrapper(test.NewClientIsolate(), test.main_isolate()); Isolate* i_isolate1 = test.i_main_isolate(); Factory* factory1 = i_isolate1->factory(); HandleScope scope1(i_isolate1); const char raw_one_byte[] = "foo"; base::uc16 raw_two_byte[] = {2001, 2002, 2003}; base::Vector two_byte(raw_two_byte, 3); // Allocate two in-place internalizable strings in isolate1 then intern // them. DirectHandle old_one_byte_seq1 = factory1->NewStringFromAsciiChecked(raw_one_byte, AllocationType::kOld); DirectHandle old_two_byte_seq1 = factory1->NewStringFromTwoByte(two_byte, AllocationType::kOld) .ToHandleChecked(); DirectHandle one_byte_intern1 = factory1->InternalizeString(old_one_byte_seq1); DirectHandle two_byte_intern1 = factory1->InternalizeString(old_two_byte_seq1); CHECK(HeapLayout::InAnySharedSpace(*old_one_byte_seq1)); CHECK(HeapLayout::InAnySharedSpace(*old_two_byte_seq1)); CHECK(HeapLayout::InAnySharedSpace(*one_byte_intern1)); CHECK(HeapLayout::InAnySharedSpace(*two_byte_intern1)); CHECK(old_one_byte_seq1.equals(one_byte_intern1)); CHECK(old_two_byte_seq1.equals(two_byte_intern1)); CHECK_EQ(*old_one_byte_seq1, *one_byte_intern1); CHECK_EQ(*old_two_byte_seq1, *two_byte_intern1); // Allocate two in-place internalizable strings with the same contents in // isolate2 then intern them. They should be the same as the interned strings // from isolate1. v8::Isolate::Scope isolate2_scope(isolate_wrapper.isolate); Isolate* i_isolate2 = reinterpret_cast(isolate_wrapper.isolate); Factory* factory2 = i_isolate2->factory(); HandleScope scope2(i_isolate2); DirectHandle old_one_byte_seq2 = factory2->NewStringFromAsciiChecked(raw_one_byte, AllocationType::kOld); DirectHandle old_two_byte_seq2 = factory2->NewStringFromTwoByte(two_byte, AllocationType::kOld) .ToHandleChecked(); DirectHandle one_byte_intern2 = factory2->InternalizeString(old_one_byte_seq2); DirectHandle two_byte_intern2 = factory2->InternalizeString(old_two_byte_seq2); CHECK(HeapLayout::InAnySharedSpace(*old_one_byte_seq2)); CHECK(HeapLayout::InAnySharedSpace(*old_two_byte_seq2)); CHECK(HeapLayout::InAnySharedSpace(*one_byte_intern2)); CHECK(HeapLayout::InAnySharedSpace(*two_byte_intern2)); CHECK(!old_one_byte_seq2.equals(one_byte_intern2)); CHECK(!old_two_byte_seq2.equals(two_byte_intern2)); CHECK_NE(*old_one_byte_seq2, *one_byte_intern2); CHECK_NE(*old_two_byte_seq2, *two_byte_intern2); CHECK_EQ(*one_byte_intern1, *one_byte_intern2); CHECK_EQ(*two_byte_intern1, *two_byte_intern2); } UNINITIALIZED_TEST(YoungInternalization) { if (v8_flags.single_generation) return; v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); MultiClientIsolateTest test; IsolateParkOnDisposeWrapper isolate_wrapper(test.NewClientIsolate(), test.main_isolate()); Isolate* i_isolate1 = test.i_main_isolate(); Factory* factory1 = i_isolate1->factory(); Isolate* i_isolate2 = reinterpret_cast(isolate_wrapper.isolate); Factory* factory2 = i_isolate2->factory(); HandleScope scope1(i_isolate1); HandleScope scope2(i_isolate2); const char raw_one_byte[] = "foo"; base::uc16 raw_two_byte[] = {2001, 2002, 2003}; base::Vector two_byte(raw_two_byte, 3); // Allocate two young strings in isolate1 then intern them. Young strings // aren't in-place internalizable and are copied when internalized. Handle young_one_byte_seq1; Handle young_two_byte_seq1; Handle one_byte_intern1; Handle two_byte_intern1; i_isolate2->main_thread_local_isolate()->ExecuteMainThreadWhileParked([&]() { young_one_byte_seq1 = factory1->NewStringFromAsciiChecked( raw_one_byte, AllocationType::kYoung); young_two_byte_seq1 = factory1->NewStringFromTwoByte(two_byte, AllocationType::kYoung) .ToHandleChecked(); one_byte_intern1 = factory1->InternalizeString(young_one_byte_seq1); two_byte_intern1 = factory1->InternalizeString(young_two_byte_seq1); CHECK(!HeapLayout::InAnySharedSpace(*young_one_byte_seq1)); CHECK(!HeapLayout::InAnySharedSpace(*young_two_byte_seq1)); CHECK(HeapLayout::InAnySharedSpace(*one_byte_intern1)); CHECK(HeapLayout::InAnySharedSpace(*two_byte_intern1)); CHECK(!young_one_byte_seq1.equals(one_byte_intern1)); CHECK(!young_two_byte_seq1.equals(two_byte_intern1)); CHECK_NE(*young_one_byte_seq1, *one_byte_intern1); CHECK_NE(*young_two_byte_seq1, *two_byte_intern1); }); // Allocate two young strings with the same contents in isolate2 then intern // them. They should be the same as the interned strings from isolate1. Handle young_one_byte_seq2; Handle young_two_byte_seq2; Handle one_byte_intern2; Handle two_byte_intern2; { v8::Isolate::Scope isolate_scope(isolate_wrapper.isolate); young_one_byte_seq2 = factory2->NewStringFromAsciiChecked( raw_one_byte, AllocationType::kYoung); young_two_byte_seq2 = factory2->NewStringFromTwoByte(two_byte, AllocationType::kYoung) .ToHandleChecked(); one_byte_intern2 = factory2->InternalizeString(young_one_byte_seq2); two_byte_intern2 = factory2->InternalizeString(young_two_byte_seq2); CHECK(!young_one_byte_seq2.equals(one_byte_intern2)); CHECK(!young_two_byte_seq2.equals(two_byte_intern2)); CHECK_NE(*young_one_byte_seq2, *one_byte_intern2); CHECK_NE(*young_two_byte_seq2, *two_byte_intern2); CHECK_EQ(*one_byte_intern1, *one_byte_intern2); CHECK_EQ(*two_byte_intern1, *two_byte_intern2); } } class ConcurrentStringThreadBase : public ParkingThread { public: ConcurrentStringThreadBase(const char* name, MultiClientIsolateTest* test, IndirectHandle shared_strings, ParkingSemaphore* sema_ready, ParkingSemaphore* sema_execute_start, ParkingSemaphore* sema_execute_complete) : ParkingThread(base::Thread::Options(name)), test_(test), shared_strings_(shared_strings), sema_ready_(sema_ready), sema_execute_start_(sema_execute_start), sema_execute_complete_(sema_execute_complete) {} virtual void Setup() {} virtual void RunForString(Handle string, int counter) = 0; virtual void Teardown() {} void Run() override { IsolateWrapper isolate_wrapper(test_->NewClientIsolate()); i_isolate = reinterpret_cast(isolate_wrapper.isolate); Setup(); sema_ready_->Signal(); sema_execute_start_->ParkedWait(i_isolate->main_thread_local_isolate()); { v8::Isolate::Scope isolate_scope(isolate_wrapper.isolate); HandleScope scope(i_isolate); for (int i = 0; i < shared_strings_->length(); i++) { Handle input_string(Cast(shared_strings_->get(i)), i_isolate); RunForString(input_string, i); } } sema_execute_complete_->Signal(); Teardown(); i_isolate = nullptr; } protected: Isolate* i_isolate; MultiClientIsolateTest* test_; IndirectHandle shared_strings_; ParkingSemaphore* sema_ready_; ParkingSemaphore* sema_execute_start_; ParkingSemaphore* sema_execute_complete_; }; enum TestHitOrMiss { kTestMiss, kTestHit }; class ConcurrentInternalizationThread final : public ConcurrentStringThreadBase { public: ConcurrentInternalizationThread(MultiClientIsolateTest* test, IndirectHandle shared_strings, TestHitOrMiss hit_or_miss, ParkingSemaphore* sema_ready, ParkingSemaphore* sema_execute_start, ParkingSemaphore* sema_execute_complete) : ConcurrentStringThreadBase("ConcurrentInternalizationThread", test, shared_strings, sema_ready, sema_execute_start, sema_execute_complete), hit_or_miss_(hit_or_miss) {} void Setup() override { factory = i_isolate->factory(); } void RunForString(Handle input_string, int counter) override { CHECK(input_string->IsShared()); DirectHandle interned = factory->InternalizeString(input_string); CHECK(interned->IsShared()); CHECK(IsInternalizedString(*interned)); if (hit_or_miss_ == kTestMiss) { CHECK_EQ(*input_string, *interned); } else { CHECK(input_string->HasForwardingIndex(kAcquireLoad)); CHECK(String::Equals(i_isolate, input_string, interned)); } } private: TestHitOrMiss hit_or_miss_; Factory* factory; }; namespace { std::pair, MaybeDirectHandle> CreateSharedOneByteString(Isolate* isolate, Factory* factory, int length, bool internalize) { char* ascii = new char[length + 1]; // Don't make single character strings, which will end up deduplicating to // an RO string and mess up the string table hit test. CHECK_GT(length, 1); for (int j = 0; j < length; j++) ascii[j] = 'a'; ascii[length] = '\0'; MaybeHandle internalized; if (internalize) { // When testing concurrent string table hits, pre-internalize a string // of the same contents so all subsequent internalizations are hits. internalized = factory->InternalizeString(factory->NewStringFromAsciiChecked(ascii)); CHECK(IsInternalizedString(*internalized.ToHandleChecked())); } Handle string = String::Share( isolate, factory->NewStringFromAsciiChecked(ascii, AllocationType::kOld)); delete[] ascii; CHECK(string->IsShared()); string->EnsureHash(); return std::make_pair(string, internalized); } IndirectHandle CreateSharedOneByteStrings( Isolate* isolate, Factory* factory, int count, int lo_count, int min_length = 2, bool internalize = false) { IndirectHandle shared_strings = factory->NewFixedArray(count + lo_count, AllocationType::kSharedOld); // Buffer to keep internalized strings alive in the current scope. DirectHandle internalized_handles; if (internalize) { internalized_handles = factory->NewFixedArray(count + lo_count, AllocationType::kOld); } { // Create strings in their own scope to be able to delete and GC them. HandleScope scope(isolate); for (int i = 0; i < count; i++) { int length = i + min_length + 1; auto strings = CreateSharedOneByteString(isolate, factory, length, internalize); shared_strings->set(i, *strings.first); if (internalize) { internalized_handles->set(i, *strings.second.ToHandleChecked()); } } int min_lo_length = isolate->heap()->MaxRegularHeapObjectSize(AllocationType::kOld) + 1; for (int i = 0; i < lo_count; i++) { int length = i + min_lo_length + 1; auto strings = CreateSharedOneByteString(isolate, factory, length, internalize); shared_strings->set(count + i, *strings.first); if (internalize) { internalized_handles->set(count + i, *strings.second.ToHandleChecked()); } } } return shared_strings; } void TestConcurrentInternalization(TestHitOrMiss hit_or_miss) { v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); constexpr int kThreads = 4; constexpr int kStrings = 4096; constexpr int kLOStrings = 16; MultiClientIsolateTest test; Isolate* i_isolate = test.i_main_isolate(); Factory* factory = i_isolate->factory(); HandleScope scope(i_isolate); IndirectHandle shared_strings = CreateSharedOneByteStrings(i_isolate, factory, kStrings - kLOStrings, kLOStrings, 2, hit_or_miss == kTestHit); ParkingSemaphore sema_ready(0); ParkingSemaphore sema_execute_start(0); ParkingSemaphore sema_execute_complete(0); std::vector> threads; for (int i = 0; i < kThreads; i++) { auto thread = std::make_unique( &test, shared_strings, hit_or_miss, &sema_ready, &sema_execute_start, &sema_execute_complete); CHECK(thread->Start()); threads.push_back(std::move(thread)); } LocalIsolate* local_isolate = i_isolate->main_thread_local_isolate(); for (int i = 0; i < kThreads; i++) { sema_ready.ParkedWait(local_isolate); } for (int i = 0; i < kThreads; i++) { sema_execute_start.Signal(); } for (int i = 0; i < kThreads; i++) { sema_execute_complete.ParkedWait(local_isolate); } ParkingThread::ParkedJoinAll(local_isolate, threads); } } // namespace UNINITIALIZED_TEST(ConcurrentInternalizationMiss) { TestConcurrentInternalization(kTestMiss); } UNINITIALIZED_TEST(ConcurrentInternalizationHit) { TestConcurrentInternalization(kTestHit); } class ConcurrentStringTableLookupThread final : public ConcurrentStringThreadBase { public: ConcurrentStringTableLookupThread(MultiClientIsolateTest* test, IndirectHandle shared_strings, ParkingSemaphore* sema_ready, ParkingSemaphore* sema_execute_start, ParkingSemaphore* sema_execute_complete) : ConcurrentStringThreadBase("ConcurrentStringTableLookup", test, shared_strings, sema_ready, sema_execute_start, sema_execute_complete) {} void RunForString(Handle input_string, int counter) override { CHECK(input_string->IsShared()); Tagged result = Tagged(StringTable::TryStringToIndexOrLookupExisting( i_isolate, input_string->ptr())); if (IsString(result)) { Tagged internalized = Cast(result); CHECK(IsInternalizedString(internalized)); CHECK_IMPLIES(IsInternalizedString(*input_string), *input_string == internalized); } else { CHECK_EQ(Cast(result).value(), ResultSentinel::kNotFound); } } }; UNINITIALIZED_TEST(ConcurrentStringTableLookup) { v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); constexpr int kTotalThreads = 4; constexpr int kInternalizationThreads = 1; constexpr int kStrings = 4096; constexpr int kLOStrings = 16; MultiClientIsolateTest test; Isolate* i_isolate = test.i_main_isolate(); Factory* factory = i_isolate->factory(); HandleScope scope(i_isolate); IndirectHandle shared_strings = CreateSharedOneByteStrings( i_isolate, factory, kStrings - kLOStrings, kLOStrings, 2, false); ParkingSemaphore sema_ready(0); ParkingSemaphore sema_execute_start(0); ParkingSemaphore sema_execute_complete(0); std::vector> threads; for (int i = 0; i < kInternalizationThreads; i++) { auto thread = std::make_unique( &test, shared_strings, kTestMiss, &sema_ready, &sema_execute_start, &sema_execute_complete); CHECK(thread->Start()); threads.push_back(std::move(thread)); } for (int i = 0; i < kTotalThreads - kInternalizationThreads; i++) { auto thread = std::make_unique( &test, shared_strings, &sema_ready, &sema_execute_start, &sema_execute_complete); CHECK(thread->Start()); threads.push_back(std::move(thread)); } LocalIsolate* local_isolate = i_isolate->main_thread_local_isolate(); for (int i = 0; i < kTotalThreads; i++) { sema_ready.ParkedWait(local_isolate); } for (int i = 0; i < kTotalThreads; i++) { sema_execute_start.Signal(); } for (int i = 0; i < kTotalThreads; i++) { sema_execute_complete.ParkedWait(local_isolate); } ParkingThread::ParkedJoinAll(local_isolate, threads); } namespace { void CheckSharedStringIsEqualCopy(DirectHandle shared, DirectHandle original) { CHECK(shared->IsShared()); CHECK(shared->Equals(*original)); CHECK_NE(*shared, *original); } Handle ShareAndVerify(Isolate* isolate, Handle string) { Handle shared = String::Share(isolate, string); CHECK(shared->IsShared()); #ifdef VERIFY_HEAP Object::ObjectVerify(*shared, isolate); Object::ObjectVerify(*string, isolate); #endif // VERIFY_HEAP return shared; } class OneByteResource : public v8::String::ExternalOneByteStringResource { public: OneByteResource(const char* data, size_t length) : data_(data), length_(length) {} const char* data() const override { return data_; } size_t length() const override { return length_; } void Dispose() override { CHECK(!IsDisposed()); i::DeleteArray(data_); data_ = nullptr; } bool IsDisposed() const { return data_ == nullptr; } private: const char* data_; size_t length_; }; class TwoByteResource : public v8::String::ExternalStringResource { public: TwoByteResource(const uint16_t* data, size_t length) : data_(data), length_(length) {} const uint16_t* data() const override { return data_; } size_t length() const override { return length_; } void Dispose() override { i::DeleteArray(data_); data_ = nullptr; } bool IsDisposed() const { return data_ == nullptr; } private: const uint16_t* data_; size_t length_; }; class ExternalResourceFactory { public: ~ExternalResourceFactory() { for (auto* res : one_byte_resources_) { CHECK(res->IsDisposed()); delete res; } for (auto* res : two_byte_resources_) { CHECK(res->IsDisposed()); delete res; } } OneByteResource* CreateOneByte(const char* data, size_t length, bool copy = true) { OneByteResource* res = new OneByteResource(copy ? i::StrDup(data) : data, length); Register(res); return res; } OneByteResource* CreateOneByte(const char* data, bool copy = true) { return CreateOneByte(data, strlen(data), copy); } TwoByteResource* CreateTwoByte(const uint16_t* data, size_t length, bool copy = true) { TwoByteResource* res = new TwoByteResource(data, length); Register(res); return res; } TwoByteResource* CreateTwoByte(base::Vector vector, bool copy = true) { auto vec = copy ? vector.Clone() : vector; return CreateTwoByte(vec.begin(), vec.size(), copy); } void Register(OneByteResource* res) { one_byte_resources_.push_back(res); } void Register(TwoByteResource* res) { two_byte_resources_.push_back(res); } private: std::vector one_byte_resources_; std::vector two_byte_resources_; }; } // namespace UNINITIALIZED_TEST(StringShare) { v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); ManualGCScope manual_gc_scope; ExternalResourceFactory resource_factory; MultiClientIsolateTest test; Isolate* i_isolate = test.i_main_isolate(); Factory* factory = i_isolate->factory(); HandleScope scope(i_isolate); // A longer string so that concatenated to itself, the result is > // ConsString::kMinLength. const char raw_one_byte[] = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"; base::uc16 raw_two_byte[] = {2001, 2002, 2003}; base::Vector two_byte(raw_two_byte, 3); { // Old-generation sequential strings are shared in-place. Handle one_byte_seq = factory->NewStringFromAsciiChecked(raw_one_byte, AllocationType::kOld); Handle two_byte_seq = factory->NewStringFromTwoByte(two_byte, AllocationType::kOld) .ToHandleChecked(); CHECK(!one_byte_seq->IsShared()); CHECK(!two_byte_seq->IsShared()); DirectHandle shared_one_byte = ShareAndVerify(i_isolate, one_byte_seq); DirectHandle shared_two_byte = ShareAndVerify(i_isolate, two_byte_seq); CHECK_EQ(*one_byte_seq, *shared_one_byte); CHECK_EQ(*two_byte_seq, *shared_two_byte); } { // Internalized strings are always shared. Handle one_byte_seq = factory->NewStringFromAsciiChecked(raw_one_byte, AllocationType::kOld); Handle two_byte_seq = factory->NewStringFromTwoByte(two_byte, AllocationType::kOld) .ToHandleChecked(); CHECK(!one_byte_seq->IsShared()); CHECK(!two_byte_seq->IsShared()); Handle one_byte_intern = factory->InternalizeString(one_byte_seq); Handle two_byte_intern = factory->InternalizeString(two_byte_seq); CHECK(one_byte_intern->IsShared()); CHECK(two_byte_intern->IsShared()); DirectHandle shared_one_byte_intern = ShareAndVerify(i_isolate, one_byte_intern); DirectHandle shared_two_byte_intern = ShareAndVerify(i_isolate, two_byte_intern); CHECK_EQ(*one_byte_intern, *shared_one_byte_intern); CHECK_EQ(*two_byte_intern, *shared_two_byte_intern); } { // Old-generation external strings are shared in-place. Handle one_byte_ext = factory->NewStringFromAsciiChecked(raw_one_byte, AllocationType::kOld); Handle two_byte_ext = factory->NewStringFromTwoByte(two_byte, AllocationType::kOld) .ToHandleChecked(); OneByteResource* one_byte_res = resource_factory.CreateOneByte(raw_one_byte); TwoByteResource* two_byte_res = resource_factory.CreateTwoByte(two_byte); CHECK(one_byte_ext->MakeExternal(i_isolate, one_byte_res)); CHECK(two_byte_ext->MakeExternal(i_isolate, two_byte_res)); if (v8_flags.always_use_string_forwarding_table) { i_isolate->heap()->CollectGarbageShared( i_isolate->main_thread_local_heap(), GarbageCollectionReason::kTesting); } CHECK(IsExternalString(*one_byte_ext)); CHECK(IsExternalString(*two_byte_ext)); CHECK(!one_byte_ext->IsShared()); CHECK(!two_byte_ext->IsShared()); DirectHandle shared_one_byte = ShareAndVerify(i_isolate, one_byte_ext); DirectHandle shared_two_byte = ShareAndVerify(i_isolate, two_byte_ext); CHECK_EQ(*one_byte_ext, *shared_one_byte); CHECK_EQ(*two_byte_ext, *shared_two_byte); } // All other strings are flattened then copied if the flatten didn't already // create a new copy. if (!v8_flags.single_generation) { // Young strings Handle young_one_byte_seq = factory->NewStringFromAsciiChecked( raw_one_byte, AllocationType::kYoung); Handle young_two_byte_seq = factory->NewStringFromTwoByte(two_byte, AllocationType::kYoung) .ToHandleChecked(); CHECK(HeapLayout::InYoungGeneration(*young_one_byte_seq)); CHECK(HeapLayout::InYoungGeneration(*young_two_byte_seq)); CHECK(!young_one_byte_seq->IsShared()); CHECK(!young_two_byte_seq->IsShared()); DirectHandle shared_one_byte = ShareAndVerify(i_isolate, young_one_byte_seq); DirectHandle shared_two_byte = ShareAndVerify(i_isolate, young_two_byte_seq); CheckSharedStringIsEqualCopy(shared_one_byte, young_one_byte_seq); CheckSharedStringIsEqualCopy(shared_two_byte, young_two_byte_seq); } if (!v8_flags.always_use_string_forwarding_table) { // Thin strings Handle one_byte_seq1 = factory->NewStringFromAsciiChecked(raw_one_byte); Handle one_byte_seq2 = factory->NewStringFromAsciiChecked(raw_one_byte); CHECK(!one_byte_seq1->IsShared()); CHECK(!one_byte_seq2->IsShared()); factory->InternalizeString(one_byte_seq1); factory->InternalizeString(one_byte_seq2); CHECK(StringShape(*one_byte_seq2).IsThin()); DirectHandle shared = ShareAndVerify(i_isolate, one_byte_seq2); CheckSharedStringIsEqualCopy(shared, one_byte_seq2); } { // Cons strings Handle one_byte_seq1 = factory->NewStringFromAsciiChecked(raw_one_byte); Handle one_byte_seq2 = factory->NewStringFromAsciiChecked(raw_one_byte); CHECK(!one_byte_seq1->IsShared()); CHECK(!one_byte_seq2->IsShared()); Handle cons = factory->NewConsString(one_byte_seq1, one_byte_seq2).ToHandleChecked(); CHECK(!cons->IsShared()); CHECK(IsConsString(*cons)); DirectHandle shared = ShareAndVerify(i_isolate, cons); CheckSharedStringIsEqualCopy(shared, cons); } { // Sliced strings Handle one_byte_seq = factory->NewStringFromAsciiChecked(raw_one_byte); CHECK(!one_byte_seq->IsShared()); Handle sliced = factory->NewSubString(one_byte_seq, 1, one_byte_seq->length()); CHECK(!sliced->IsShared()); CHECK(IsSlicedString(*sliced)); DirectHandle shared = ShareAndVerify(i_isolate, sliced); CheckSharedStringIsEqualCopy(shared, sliced); } } UNINITIALIZED_TEST(PromotionMarkCompact) { if (v8_flags.single_generation) return; v8_flags.stress_concurrent_allocation = false; // For SealCurrentObjects. v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); ManualGCScope manual_gc_scope; heap::ManualEvacuationCandidatesSelectionScope manual_evacuation_candidate_selection_scope(manual_gc_scope); MultiClientIsolateTest test; v8::Isolate* isolate = test.main_isolate(); Isolate* i_isolate = test.i_main_isolate(); Factory* factory = i_isolate->factory(); Heap* heap = i_isolate->heap(); // Heap* shared_heap = test.i_shared_isolate()->heap(); const char raw_one_byte[] = "foo"; { Global one_byte_seq_global; ObjectSlot slot; { HandleScope scope(i_isolate); IndirectHandle one_byte_seq = factory->NewStringFromAsciiChecked( raw_one_byte, AllocationType::kYoung); CHECK(String::IsInPlaceInternalizable(*one_byte_seq)); CHECK(heap->InSpace(*one_byte_seq, NEW_SPACE)); // 1st GC moves `one_byte_seq` to old space and 2nd GC evacuates it within // old space. heap::InvokeMajorGC(heap); heap::ForceEvacuationCandidate( i::PageMetadata::FromHeapObject(*one_byte_seq)); one_byte_seq_global.Reset(isolate, v8::Utils::ToLocal(one_byte_seq)); } { // We need to invoke GC without stack, otherwise no compaction is // performed. DisableConservativeStackScanningScopeForTesting no_stack_scanning(heap); heap::InvokeMajorGC(heap); } { v8::HandleScope nested_scope(isolate); IndirectHandle one_byte_seq = v8::Utils::OpenHandle(*one_byte_seq_global.Get(isolate)); // In-place-internalizable strings are promoted into the shared heap when // sharing. CHECK(heap->SharedHeapContains(*one_byte_seq)); } } } UNINITIALIZED_TEST(PromotionScavenge) { if (v8_flags.minor_ms) return; if (v8_flags.single_generation) return; v8_flags.stress_concurrent_allocation = false; // For SealCurrentObjects. v8_flags.shared_string_table = true; v8_flags.scavenger_precise_object_pinning = false; v8_flags.precise_object_pinning = false; i::FlagList::EnforceFlagImplications(); MultiClientIsolateTest test; Isolate* i_isolate = test.i_main_isolate(); Factory* factory = i_isolate->factory(); Heap* heap = i_isolate->heap(); // Heap* shared_heap = test.i_shared_isolate()->heap(); const char raw_one_byte[] = "foo"; { HandleScope scope(i_isolate); // heap::SealCurrentObjects(heap); // heap::SealCurrentObjects(shared_heap); IndirectHandle one_byte_seq = factory->NewStringFromAsciiChecked( raw_one_byte, AllocationType::kYoung); CHECK(String::IsInPlaceInternalizable(*one_byte_seq)); CHECK(heap->InSpace(*one_byte_seq, NEW_SPACE)); { // CSS prevents moving the string to shared space. DisableConservativeStackScanningScopeForTesting no_stack_scanning(heap); for (int i = 0; i < 2; i++) { heap::InvokeMinorGC(heap); } } // In-place-internalizable strings are promoted into the shared heap when // sharing. CHECK(heap->SharedHeapContains(*one_byte_seq)); } } UNINITIALIZED_TEST(PromotionScavengeOldToShared) { if (v8_flags.minor_ms) { // Promoting from new space directly to shared heap is not implemented in // MinorMS. return; } if (v8_flags.single_generation) return; if (v8_flags.stress_concurrent_allocation) return; v8_flags.shared_string_table = true; v8_flags.scavenger_precise_object_pinning = false; v8_flags.precise_object_pinning = false; i::FlagList::EnforceFlagImplications(); MultiClientIsolateTest test; Isolate* i_isolate = test.i_main_isolate(); Factory* factory = i_isolate->factory(); Heap* heap = i_isolate->heap(); ManualGCScope manual_gc(i_isolate); const char raw_one_byte[] = "foo"; { HandleScope scope(i_isolate); IndirectHandle old_object = factory->NewFixedArray(1, AllocationType::kOld); MemoryChunk* old_object_chunk = MemoryChunk::FromHeapObject(*old_object); CHECK(!old_object_chunk->InYoungGeneration()); IndirectHandle one_byte_seq = factory->NewStringFromAsciiChecked( raw_one_byte, AllocationType::kYoung); CHECK(String::IsInPlaceInternalizable(*one_byte_seq)); CHECK(MemoryChunk::FromHeapObject(*one_byte_seq)->InYoungGeneration()); old_object->set(0, *one_byte_seq); ObjectSlot slot = old_object->RawFieldOfFirstElement(); CHECK(RememberedSet::Contains( MutablePageMetadata::cast( MutablePageMetadata::cast(old_object_chunk->Metadata())), slot.address())); { // CSS prevents moving the string to shared space. DisableConservativeStackScanningScopeForTesting no_stack_scanning(heap); for (int i = 0; i < 2; i++) { heap::InvokeMinorGC(heap); } } // In-place-internalizable strings are promoted into the shared heap when // sharing. CHECK(heap->SharedHeapContains(*one_byte_seq)); // Since the GC promoted that string into shared heap, it also needs to // create an OLD_TO_SHARED slot. CHECK(RememberedSet::Contains( MutablePageMetadata::cast(old_object_chunk->Metadata()), slot.address())); } } UNINITIALIZED_TEST(PromotionMarkCompactNewToShared) { if (v8_flags.single_generation) return; if (v8_flags.stress_concurrent_allocation) return; v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); ManualGCScope manual_gc_scope; heap::ManualEvacuationCandidatesSelectionScope manual_evacuation_candidate_selection_scope(manual_gc_scope); v8_flags.page_promotion = false; MultiClientIsolateTest test; v8::Isolate* isolate = test.main_isolate(); Isolate* i_isolate = test.i_main_isolate(); Factory* factory = i_isolate->factory(); Heap* heap = i_isolate->heap(); const char raw_one_byte[] = "foo"; { HandleScope scope(i_isolate); IndirectHandle old_object = factory->NewFixedArray(1, AllocationType::kOld); MemoryChunk* old_object_chunk = MemoryChunk::FromHeapObject(*old_object); CHECK(!old_object_chunk->InYoungGeneration()); Global one_byte_seq_global; { HandleScope nested_scope(i_isolate); IndirectHandle one_byte_seq = factory->NewStringFromAsciiChecked( raw_one_byte, AllocationType::kYoung); CHECK(String::IsInPlaceInternalizable(*one_byte_seq)); CHECK(MemoryChunk::FromHeapObject(*one_byte_seq)->InYoungGeneration()); old_object->set(0, *one_byte_seq); one_byte_seq_global.Reset(isolate, v8::Utils::ToLocal(one_byte_seq)); } ObjectSlot slot = old_object->RawFieldOfFirstElement(); CHECK(RememberedSet::Contains( MutablePageMetadata::cast(old_object_chunk->Metadata()), slot.address())); { // We need to invoke GC without stack, otherwise no compaction is // performed. DisableConservativeStackScanningScopeForTesting no_stack_scanning(heap); heap::InvokeMajorGC(heap); } { v8::HandleScope nested_scope(isolate); IndirectHandle one_byte_seq = v8::Utils::OpenHandle(*one_byte_seq_global.Get(isolate)); // In-place-internalizable strings are promoted into the shared heap when // sharing. CHECK(heap->SharedHeapContains(*one_byte_seq)); } // Since the GC promoted that string into shared heap, it also needs to // create an OLD_TO_SHARED slot. CHECK(RememberedSet::Contains( MutablePageMetadata::cast(old_object_chunk->Metadata()), slot.address())); } } UNINITIALIZED_TEST(PromotionMarkCompactOldToShared) { if (v8_flags.stress_concurrent_allocation) return; if (!v8_flags.page_promotion) return; if (v8_flags.single_generation) { // String allocated in old space may be "pretenured" to the shared heap. return; } v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); ManualGCScope manual_gc_scope; heap::ManualEvacuationCandidatesSelectionScope manual_evacuation_candidate_selection_scope(manual_gc_scope); MultiClientIsolateTest test; v8::Isolate* isolate = test.main_isolate(); Isolate* i_isolate = test.i_main_isolate(); Factory* factory = i_isolate->factory(); Heap* heap = i_isolate->heap(); const char raw_one_byte[] = "foo"; { HandleScope scope(i_isolate); IndirectHandle old_object = factory->NewFixedArray(1, AllocationType::kOld); MemoryChunk* old_object_chunk = MemoryChunk::FromHeapObject(*old_object); CHECK(!old_object_chunk->InYoungGeneration()); Global one_byte_seq_global; ObjectSlot slot; { HandleScope nested_scope(i_isolate); IndirectHandle one_byte_seq = factory->NewStringFromAsciiChecked( raw_one_byte, AllocationType::kYoung); CHECK(String::IsInPlaceInternalizable(*one_byte_seq)); CHECK(MemoryChunk::FromHeapObject(*one_byte_seq)->InYoungGeneration()); DirectHandleVector handles(i_isolate); // Fill the page and do a full GC. Page promotion should kick in and // promote the page as is to old space. heap::FillCurrentPage(heap->new_space(), &handles); heap::InvokeMajorGC(heap); // Make sure 'one_byte_seq' is in old space. CHECK(!MemoryChunk::FromHeapObject(*one_byte_seq)->InYoungGeneration()); CHECK(heap->Contains(*one_byte_seq)); old_object->set(0, *one_byte_seq); slot = old_object->RawFieldOfFirstElement(); CHECK(!RememberedSet::Contains( MutablePageMetadata::cast(old_object_chunk->Metadata()), slot.address())); heap::ForceEvacuationCandidate( PageMetadata::FromHeapObject(*one_byte_seq)); one_byte_seq_global.Reset(isolate, v8::Utils::ToLocal(one_byte_seq)); } { // We need to invoke GC without stack, otherwise no compaction is // performed. DisableConservativeStackScanningScopeForTesting no_stack_scanning(heap); heap::InvokeMajorGC(heap); } { v8::HandleScope nested_scope(isolate); IndirectHandle one_byte_seq = v8::Utils::OpenHandle(*one_byte_seq_global.Get(isolate)); // In-place-internalizable strings are promoted into the shared heap when // sharing. CHECK(heap->SharedHeapContains(*one_byte_seq)); } // Since the GC promoted that string into shared heap, it also needs to // create an OLD_TO_SHARED slot. CHECK(RememberedSet::Contains( MutablePageMetadata::cast(old_object_chunk->Metadata()), slot.address())); } } UNINITIALIZED_TEST(PagePromotionRecordingOldToShared) { if (v8_flags.single_generation) return; if (v8_flags.stress_concurrent_allocation) return; v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); ManualGCScope manual_gc_scope; heap::ManualEvacuationCandidatesSelectionScope manual_evacuation_candidate_selection_scope(manual_gc_scope); MultiClientIsolateTest test; Isolate* i_isolate = test.i_main_isolate(); Factory* factory = i_isolate->factory(); Heap* heap = i_isolate->heap(); const char raw_one_byte[] = "foo"; { HandleScope scope(i_isolate); DirectHandle young_object = factory->NewFixedArray(1, AllocationType::kYoung); CHECK(HeapLayout::InYoungGeneration(*young_object)); Address young_object_address = young_object->address(); DirectHandleVector handles(i_isolate); // Make the whole page transition from new->old, getting the buffers // processed in the sweeper (relying on marking information) instead of // processing during newspace evacuation. heap::FillCurrentPage(heap->new_space(), &handles); DirectHandle shared_string = factory->NewStringFromAsciiChecked( raw_one_byte, AllocationType::kSharedOld); CHECK(HeapLayout::InWritableSharedSpace(*shared_string)); young_object->set(0, *shared_string); heap::EmptyNewSpaceUsingGC(heap); // Object should get promoted using page promotion, so address should remain // the same. CHECK(!HeapLayout::InYoungGeneration(*shared_string)); CHECK_EQ(young_object_address, young_object->address()); // Since the GC promoted that string into shared heap, it also needs to // create an OLD_TO_SHARED slot. ObjectSlot slot = young_object->RawFieldOfFirstElement(); CHECK(RememberedSet::Contains( MutablePageMetadata::FromHeapObject(*young_object), slot.address())); } } namespace { void TriggerGCWithTransitions(Heap* heap) { v8_flags.transition_strings_during_gc_with_stack = true; heap::CollectSharedGarbage(heap); v8_flags.transition_strings_during_gc_with_stack = false; } } // namespace UNINITIALIZED_TEST(InternalizedSharedStringsTransitionDuringGC) { v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); constexpr int kStrings = 4096; constexpr int kLOStrings = 16; MultiClientIsolateTest test; Isolate* i_isolate = test.i_main_isolate(); Factory* factory = i_isolate->factory(); HandleScope scope(i_isolate); // Run two times to test that everything is reset correctly during GC. for (int run = 0; run < 2; run++) { DirectHandle shared_strings = CreateSharedOneByteStrings( i_isolate, factory, kStrings - kLOStrings, kLOStrings, 2, run == 0); // Check strings are in the forwarding table after internalization. for (int i = 0; i < shared_strings->length(); i++) { Handle input_string(Cast(shared_strings->get(i)), i_isolate); DirectHandle interned = factory->InternalizeString(input_string); CHECK(input_string->IsShared()); CHECK(!IsThinString(*input_string)); CHECK(input_string->HasForwardingIndex(kAcquireLoad)); CHECK(String::Equals(i_isolate, input_string, interned)); } // Trigger garbage collection on the shared isolate. TriggerGCWithTransitions(i_isolate->heap()); // Check that GC cleared the forwarding table. CHECK_EQ(i_isolate->string_forwarding_table()->size(), 0); // Check all strings are transitioned to ThinStrings for (int i = 0; i < shared_strings->length(); i++) { DirectHandle input_string(Cast(shared_strings->get(i)), i_isolate); CHECK(IsThinString(*input_string)); } } } UNINITIALIZED_TEST(ShareExternalString) { if (v8_flags.single_generation) return; v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); ExternalResourceFactory resource_factory; MultiClientIsolateTest test; Isolate* i_isolate1 = test.i_main_isolate(); Factory* factory1 = i_isolate1->factory(); HandleScope handle_scope(i_isolate1); const char raw_one_byte[] = "external string"; // External strings in old space can be shared in-place. Handle one_byte = factory1->NewStringFromAsciiChecked(raw_one_byte, AllocationType::kOld); CHECK(!one_byte->IsShared()); OneByteResource* resource = resource_factory.CreateOneByte(raw_one_byte); one_byte->MakeExternal(i_isolate1, resource); if (v8_flags.always_use_string_forwarding_table) { i_isolate1->heap()->CollectGarbageShared( i_isolate1->main_thread_local_heap(), GarbageCollectionReason::kTesting); } CHECK(IsExternalString(*one_byte)); Handle one_byte_external = Cast(one_byte); DirectHandle shared_one_byte = ShareAndVerify(i_isolate1, one_byte_external); CHECK_EQ(*shared_one_byte, *one_byte); } namespace { void CheckExternalStringResource( DirectHandle string, v8::String::ExternalStringResourceBase* resource) { const bool is_one_byte = string->IsOneByteRepresentation(); Local api_string = Utils::ToLocal(string); v8::String::Encoding encoding; CHECK_EQ(resource, api_string->GetExternalStringResourceBase(&encoding)); if (is_one_byte) { CHECK_EQ(encoding, v8::String::Encoding::ONE_BYTE_ENCODING); CHECK_EQ(resource, api_string->GetExternalOneByteStringResource()); } else { CHECK(string->IsTwoByteRepresentation()); CHECK_EQ(encoding, v8::String::Encoding::TWO_BYTE_ENCODING); CHECK_EQ(resource, api_string->GetExternalStringResource()); } } } // namespace UNINITIALIZED_TEST(ExternalizeSharedString) { if (v8_flags.single_generation) return; v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); ExternalResourceFactory resource_factory; MultiClientIsolateTest test; Isolate* i_isolate1 = test.i_main_isolate(); Factory* factory1 = i_isolate1->factory(); HandleScope handle_scope(i_isolate1); const char raw_one_byte[] = "external string"; base::uc16 raw_two_byte[] = {2001, 2002, 2003}; base::Vector two_byte_vec(raw_two_byte, 3); Handle one_byte = factory1->NewStringFromAsciiChecked(raw_one_byte, AllocationType::kOld); Handle two_byte = factory1->NewStringFromTwoByte(two_byte_vec, AllocationType::kOld) .ToHandleChecked(); CHECK(one_byte->IsOneByteRepresentation()); CHECK(two_byte->IsTwoByteRepresentation()); CHECK(!one_byte->IsShared()); CHECK(!two_byte->IsShared()); DirectHandle shared_one_byte = ShareAndVerify(i_isolate1, one_byte); DirectHandle shared_two_byte = ShareAndVerify(i_isolate1, two_byte); OneByteResource* one_byte_res = resource_factory.CreateOneByte(raw_one_byte); TwoByteResource* two_byte_res = resource_factory.CreateTwoByte(two_byte_vec); shared_one_byte->MakeExternal(i_isolate1, one_byte_res); shared_two_byte->MakeExternal(i_isolate1, two_byte_res); CHECK(!IsExternalString(*shared_one_byte)); CHECK(!IsExternalString(*shared_two_byte)); CHECK(shared_one_byte->HasExternalForwardingIndex(kAcquireLoad)); CHECK(shared_two_byte->HasExternalForwardingIndex(kAcquireLoad)); // Check that API calls return the resource from the forwarding table. CheckExternalStringResource(shared_one_byte, one_byte_res); CheckExternalStringResource(shared_two_byte, two_byte_res); } UNINITIALIZED_TEST(ExternalizedSharedStringsTransitionDuringGC) { v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); ExternalResourceFactory resource_factory; MultiClientIsolateTest test; constexpr int kStrings = 4096; constexpr int kLOStrings = 16; Isolate* i_isolate = test.i_main_isolate(); Factory* factory = i_isolate->factory(); HandleScope scope(i_isolate); // Run two times to test that everything is reset correctly during GC. for (int run = 0; run < 2; run++) { DirectHandle shared_strings = CreateSharedOneByteStrings( i_isolate, factory, kStrings - kLOStrings, kLOStrings, sizeof(UncachedExternalString), run == 0); // Check strings are in the forwarding table after internalization. for (int i = 0; i < shared_strings->length(); i++) { DirectHandle input_string(Cast(shared_strings->get(i)), i_isolate); const int length = input_string->length(); char* buffer = new char[length + 1]; String::WriteToFlat(*input_string, reinterpret_cast(buffer), 0, length); OneByteResource* resource = resource_factory.CreateOneByte(buffer, length, false); CHECK(input_string->MakeExternal(i_isolate, resource)); CHECK(input_string->IsShared()); CHECK(!IsExternalString(*input_string)); CHECK(input_string->HasExternalForwardingIndex(kAcquireLoad)); } // Trigger garbage collection on the shared isolate. TriggerGCWithTransitions(i_isolate->heap()); // Check that GC cleared the forwarding table. CHECK_EQ(i_isolate->string_forwarding_table()->size(), 0); // Check all strings are transitioned to ExternalStrings for (int i = 0; i < shared_strings->length(); i++) { DirectHandle input_string(Cast(shared_strings->get(i)), i_isolate); CHECK(IsExternalString(*input_string)); } } } UNINITIALIZED_TEST(ExternalizeInternalizedString) { if (v8_flags.single_generation) return; v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); ExternalResourceFactory resource_factory; MultiClientIsolateTest test; Isolate* i_isolate1 = test.i_main_isolate(); Factory* factory1 = i_isolate1->factory(); HandleScope handle_scope(i_isolate1); const char raw_one_byte[] = "external string"; base::uc16 raw_two_byte[] = {2001, 2002, 2003}; base::Vector two_byte_vec(raw_two_byte, 3); Handle one_byte = factory1->NewStringFromAsciiChecked(raw_one_byte, AllocationType::kOld); Handle two_byte = factory1->NewStringFromTwoByte(two_byte_vec, AllocationType::kOld) .ToHandleChecked(); // Internalize copies, s.t. internalizing the original strings creates a // forwarding entry. factory1->InternalizeString( factory1->NewStringFromAsciiChecked(raw_one_byte)); factory1->InternalizeString( factory1->NewStringFromTwoByte(two_byte_vec).ToHandleChecked()); DirectHandle one_byte_intern = factory1->InternalizeString(one_byte); DirectHandle two_byte_intern = factory1->InternalizeString(two_byte); if (v8_flags.always_use_string_forwarding_table) { i_isolate1->heap()->CollectGarbageShared( i_isolate1->main_thread_local_heap(), GarbageCollectionReason::kTesting); } CHECK(IsThinString(*one_byte)); CHECK(IsThinString(*two_byte)); CHECK(one_byte_intern->IsOneByteRepresentation()); CHECK(two_byte_intern->IsTwoByteRepresentation()); CHECK(one_byte_intern->IsShared()); CHECK(two_byte_intern->IsShared()); uint32_t one_byte_hash = one_byte_intern->hash(); uint32_t two_byte_hash = two_byte_intern->hash(); OneByteResource* one_byte_res = resource_factory.CreateOneByte(raw_one_byte); TwoByteResource* two_byte_res = resource_factory.CreateTwoByte(two_byte_vec); CHECK(one_byte_intern->MakeExternal(i_isolate1, one_byte_res)); CHECK(two_byte_intern->MakeExternal(i_isolate1, two_byte_res)); CHECK(!IsExternalString(*one_byte_intern)); CHECK(!IsExternalString(*two_byte_intern)); CHECK(one_byte_intern->HasExternalForwardingIndex(kAcquireLoad)); CHECK(two_byte_intern->HasExternalForwardingIndex(kAcquireLoad)); // The hash of internalized strings is stored in the forwarding table. CHECK_EQ(one_byte_intern->hash(), one_byte_hash); CHECK_EQ(two_byte_intern->hash(), two_byte_hash); // Check that API calls return the resource from the forwarding table. CheckExternalStringResource(one_byte_intern, one_byte_res); CheckExternalStringResource(two_byte_intern, two_byte_res); } UNINITIALIZED_TEST(InternalizeSharedExternalString) { if (v8_flags.single_generation) return; v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); ExternalResourceFactory resource_factory; MultiClientIsolateTest test; Isolate* i_isolate1 = test.i_main_isolate(); Factory* factory1 = i_isolate1->factory(); HandleScope handle_scope(i_isolate1); const char raw_one_byte[] = "external string"; base::uc16 raw_two_byte[] = {2001, 2002, 2003}; base::Vector two_byte_vec(raw_two_byte, 3); Handle one_byte = factory1->NewStringFromAsciiChecked(raw_one_byte, AllocationType::kOld); Handle two_byte = factory1->NewStringFromTwoByte(two_byte_vec, AllocationType::kOld) .ToHandleChecked(); Handle shared_one_byte = ShareAndVerify(i_isolate1, one_byte); DirectHandle shared_two_byte = ShareAndVerify(i_isolate1, two_byte); OneByteResource* one_byte_res = resource_factory.CreateOneByte(raw_one_byte); TwoByteResource* two_byte_res = resource_factory.CreateTwoByte(two_byte_vec); CHECK(shared_one_byte->MakeExternal(i_isolate1, one_byte_res)); CHECK(shared_two_byte->MakeExternal(i_isolate1, two_byte_res)); CHECK(shared_one_byte->HasExternalForwardingIndex(kAcquireLoad)); CHECK(shared_two_byte->HasExternalForwardingIndex(kAcquireLoad)); // Trigger GC to externalize the shared string. TriggerGCWithTransitions(i_isolate1->heap()); CHECK(shared_one_byte->IsShared()); CHECK(IsExternalString(*shared_one_byte)); CHECK(shared_two_byte->IsShared()); CHECK(IsExternalString(*shared_two_byte)); // Shared cached external strings are in-place internalizable. DirectHandle one_byte_intern = factory1->InternalizeString(shared_one_byte); CHECK_EQ(*one_byte_intern, *shared_one_byte); CHECK(IsExternalString(*shared_one_byte)); CHECK(IsInternalizedString(*shared_one_byte)); // Depending on the architecture/build options the two byte string might be // cached or uncached. const bool is_uncached = two_byte->Size() < static_cast(sizeof(ExternalString)); if (is_uncached) { // Shared uncached external strings are not internalizable. A new internal // copy will be created. DirectHandle two_byte_intern = factory1->InternalizeString(two_byte); CHECK_NE(*two_byte_intern, *shared_two_byte); CHECK(shared_two_byte->HasInternalizedForwardingIndex(kAcquireLoad)); CHECK(IsInternalizedString(*two_byte_intern)); CHECK(!IsExternalString(*two_byte_intern)); } else { DirectHandle two_byte_intern = factory1->InternalizeString(two_byte); CHECK_EQ(*two_byte_intern, *shared_two_byte); CHECK(IsExternalString(*shared_two_byte)); CHECK(IsInternalizedString(*shared_two_byte)); } // Another GC should create an externalized internalized string of the cached // (one byte) string and turn the uncached (two byte) string into a // ThinString, disposing the external resource. TriggerGCWithTransitions(i_isolate1->heap()); CHECK_EQ(shared_one_byte->map()->instance_type(), InstanceType::EXTERNAL_INTERNALIZED_ONE_BYTE_STRING_TYPE); if (is_uncached) { CHECK(IsThinString(*shared_two_byte)); CHECK(two_byte_res->IsDisposed()); } else { CHECK_EQ(shared_two_byte->map()->instance_type(), InstanceType::EXTERNAL_INTERNALIZED_TWO_BYTE_STRING_TYPE); } } UNINITIALIZED_TEST(ExternalizeAndInternalizeMissSharedString) { if (v8_flags.single_generation) return; v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); ExternalResourceFactory resource_factory; MultiClientIsolateTest test; Isolate* i_isolate1 = test.i_main_isolate(); Factory* factory1 = i_isolate1->factory(); HandleScope handle_scope(i_isolate1); const char raw_one_byte[] = "external string"; Handle one_byte = factory1->NewStringFromAsciiChecked(raw_one_byte, AllocationType::kOld); uint32_t one_byte_hash = one_byte->EnsureHash(); Handle shared_one_byte = ShareAndVerify(i_isolate1, one_byte); OneByteResource* one_byte_res = resource_factory.CreateOneByte(raw_one_byte); CHECK(shared_one_byte->MakeExternal(i_isolate1, one_byte_res)); CHECK(shared_one_byte->HasExternalForwardingIndex(kAcquireLoad)); DirectHandle one_byte_intern = factory1->InternalizeString(shared_one_byte); CHECK_EQ(*one_byte_intern, *shared_one_byte); CHECK(IsInternalizedString(*shared_one_byte)); // Check that we have both, a forwarding index and an accessible hash. CHECK(shared_one_byte->HasExternalForwardingIndex(kAcquireLoad)); CHECK(shared_one_byte->HasHashCode()); CHECK_EQ(shared_one_byte->hash(), one_byte_hash); } UNINITIALIZED_TEST(InternalizeHitAndExternalizeSharedString) { if (v8_flags.single_generation) return; v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); ExternalResourceFactory resource_factory; MultiClientIsolateTest test; Isolate* i_isolate1 = test.i_main_isolate(); Factory* factory1 = i_isolate1->factory(); HandleScope handle_scope(i_isolate1); const char raw_one_byte[] = "external string"; base::uc16 raw_two_byte[] = {2001, 2002, 2003}; base::Vector two_byte_vec(raw_two_byte, 3); Handle one_byte = factory1->NewStringFromAsciiChecked(raw_one_byte, AllocationType::kOld); Handle two_byte = factory1->NewStringFromTwoByte(two_byte_vec, AllocationType::kOld) .ToHandleChecked(); Handle shared_one_byte = ShareAndVerify(i_isolate1, one_byte); Handle shared_two_byte = ShareAndVerify(i_isolate1, two_byte); // Internalize copies, s.t. internalizing the original strings creates a // forwarding entry. factory1->InternalizeString( factory1->NewStringFromAsciiChecked(raw_one_byte)); factory1->InternalizeString( factory1->NewStringFromTwoByte(two_byte_vec).ToHandleChecked()); DirectHandle one_byte_intern = factory1->InternalizeString(shared_one_byte); DirectHandle two_byte_intern = factory1->InternalizeString(shared_two_byte); CHECK_NE(*one_byte_intern, *shared_one_byte); CHECK_NE(*two_byte_intern, *shared_two_byte); CHECK(String::IsHashFieldComputed(one_byte_intern->raw_hash_field())); CHECK(String::IsHashFieldComputed(two_byte_intern->raw_hash_field())); CHECK(shared_one_byte->HasInternalizedForwardingIndex(kAcquireLoad)); CHECK(shared_two_byte->HasInternalizedForwardingIndex(kAcquireLoad)); OneByteResource* one_byte_res = resource_factory.CreateOneByte(raw_one_byte); TwoByteResource* two_byte_res = resource_factory.CreateTwoByte(two_byte_vec); CHECK(shared_one_byte->MakeExternal(i_isolate1, one_byte_res)); CHECK(shared_two_byte->MakeExternal(i_isolate1, two_byte_res)); CHECK(shared_one_byte->HasExternalForwardingIndex(kAcquireLoad)); CHECK(shared_two_byte->HasExternalForwardingIndex(kAcquireLoad)); CHECK(shared_one_byte->HasInternalizedForwardingIndex(kAcquireLoad)); CHECK(shared_two_byte->HasInternalizedForwardingIndex(kAcquireLoad)); // Check that API calls return the resource from the forwarding table. CheckExternalStringResource(shared_one_byte, one_byte_res); CheckExternalStringResource(shared_two_byte, two_byte_res); } UNINITIALIZED_TEST(InternalizeMissAndExternalizeSharedString) { if (v8_flags.single_generation) return; v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); ExternalResourceFactory resource_factory; MultiClientIsolateTest test; Isolate* i_isolate1 = test.i_main_isolate(); Factory* factory1 = i_isolate1->factory(); HandleScope handle_scope(i_isolate1); const char raw_one_byte[] = "external string"; base::uc16 raw_two_byte[] = {2001, 2002, 2003}; base::Vector two_byte_vec(raw_two_byte, 3); Handle one_byte = factory1->NewStringFromAsciiChecked(raw_one_byte, AllocationType::kOld); Handle two_byte = factory1->NewStringFromTwoByte(two_byte_vec, AllocationType::kOld) .ToHandleChecked(); Handle shared_one_byte = ShareAndVerify(i_isolate1, one_byte); Handle shared_two_byte = ShareAndVerify(i_isolate1, two_byte); DirectHandle one_byte_intern = factory1->InternalizeString(shared_one_byte); DirectHandle two_byte_intern = factory1->InternalizeString(shared_two_byte); CHECK_EQ(*one_byte_intern, *shared_one_byte); CHECK_EQ(*two_byte_intern, *shared_two_byte); CHECK(!shared_one_byte->HasInternalizedForwardingIndex(kAcquireLoad)); CHECK(!shared_two_byte->HasInternalizedForwardingIndex(kAcquireLoad)); OneByteResource* one_byte_res = resource_factory.CreateOneByte(raw_one_byte); TwoByteResource* two_byte_res = resource_factory.CreateTwoByte(two_byte_vec); CHECK(shared_one_byte->MakeExternal(i_isolate1, one_byte_res)); CHECK(shared_two_byte->MakeExternal(i_isolate1, two_byte_res)); CHECK(shared_one_byte->HasExternalForwardingIndex(kAcquireLoad)); CHECK(shared_two_byte->HasExternalForwardingIndex(kAcquireLoad)); CHECK(one_byte_intern->HasExternalForwardingIndex(kAcquireLoad)); CHECK(two_byte_intern->HasExternalForwardingIndex(kAcquireLoad)); // Check that API calls return the resource from the forwarding table. CheckExternalStringResource(shared_one_byte, one_byte_res); CheckExternalStringResource(shared_two_byte, two_byte_res); } class ConcurrentExternalizationThread final : public ConcurrentStringThreadBase { public: ConcurrentExternalizationThread(MultiClientIsolateTest* test, IndirectHandle shared_strings, std::vector resources, bool share_resources, ParkingSemaphore* sema_ready, ParkingSemaphore* sema_execute_start, ParkingSemaphore* sema_execute_complete) : ConcurrentStringThreadBase("ConcurrentExternalizationThread", test, shared_strings, sema_ready, sema_execute_start, sema_execute_complete), resources_(resources), share_resources_(share_resources) {} void RunForString(Handle input_string, int counter) override { CHECK(input_string->IsShared()); OneByteResource* resource = Resource(counter); if (!input_string->MakeExternal(i_isolate, resource)) { if (!share_resources_) { resource->Unaccount(reinterpret_cast(i_isolate)); resource->Dispose(); } } CHECK(input_string->HasForwardingIndex(kAcquireLoad)); } OneByteResource* Resource(int index) const { return resources_[index]; } private: std::vector resources_; const bool share_resources_; }; namespace { void CreateExternalResources(Isolate* i_isolate, DirectHandle strings, std::vector& resources, ExternalResourceFactory& resource_factory) { HandleScope scope(i_isolate); resources.reserve(strings->length()); for (int i = 0; i < strings->length(); i++) { DirectHandle input_string(Cast(strings->get(i)), i_isolate); CHECK(Utils::ToLocal(input_string) ->CanMakeExternal(v8::String::Encoding::ONE_BYTE_ENCODING)); const int length = input_string->length(); char* buffer = new char[length + 1]; String::WriteToFlat(*input_string, reinterpret_cast(buffer), 0, length); resources.push_back(resource_factory.CreateOneByte(buffer, length, false)); } } void CheckStringAndResource( Tagged string, int index, bool should_be_alive, Tagged deleted_string, bool check_transition, bool shared_resources, const std::vector>& threads) { if (check_transition) { if (should_be_alive) { CHECK(IsExternalString(string)); } else { CHECK_EQ(string, deleted_string); } } int alive_resources = 0; for (size_t t = 0; t < threads.size(); t++) { ConcurrentExternalizationThread* thread = threads[t].get(); if (!thread->Resource(index)->IsDisposed()) { alive_resources++; } } // Check exact alive resources only if the string has transitioned, otherwise // there can still be multiple resource instances in the forwarding table. // Only check no resource is alive if the string is dead. const bool check_alive = check_transition || !should_be_alive; if (check_alive) { size_t expected_alive; if (should_be_alive) { if (shared_resources) { // Since we share the same resource for all threads, we accounted for it // in every thread. expected_alive = threads.size(); } else { // Check that exactly one resource is alive. expected_alive = 1; } } else { expected_alive = 0; } CHECK_EQ(alive_resources, expected_alive); } } } // namespace void TestConcurrentExternalization(bool share_resources) { v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); ExternalResourceFactory resource_factory; MultiClientIsolateTest test; constexpr int kThreads = 4; constexpr int kStrings = 4096; constexpr int kLOStrings = 16; Isolate* i_isolate = test.i_main_isolate(); Factory* factory = i_isolate->factory(); HandleScope scope(i_isolate); IndirectHandle shared_strings = CreateSharedOneByteStrings( i_isolate, factory, kStrings - kLOStrings, kLOStrings, sizeof(UncachedExternalString), false); ParkingSemaphore sema_ready(0); ParkingSemaphore sema_execute_start(0); ParkingSemaphore sema_execute_complete(0); std::vector> threads; std::vector shared_resources; if (share_resources) { CreateExternalResources(i_isolate, shared_strings, shared_resources, resource_factory); } for (int i = 0; i < kThreads; i++) { std::vector local_resources; if (share_resources) { local_resources = shared_resources; } else { CreateExternalResources(i_isolate, shared_strings, local_resources, resource_factory); } auto thread = std::make_unique( &test, shared_strings, local_resources, share_resources, &sema_ready, &sema_execute_start, &sema_execute_complete); CHECK(thread->Start()); threads.push_back(std::move(thread)); } LocalIsolate* local_isolate = i_isolate->main_thread_local_isolate(); for (int i = 0; i < kThreads; i++) { sema_ready.ParkedWait(local_isolate); } for (int i = 0; i < kThreads; i++) { sema_execute_start.Signal(); } for (int i = 0; i < kThreads; i++) { sema_execute_complete.ParkedWait(local_isolate); } TriggerGCWithTransitions(i_isolate->heap()); for (int i = 0; i < shared_strings->length(); i++) { DirectHandle input_string(Cast(shared_strings->get(i)), i_isolate); Tagged string = *input_string; CheckStringAndResource(string, i, true, {}, true, share_resources, threads); } ParkingThread::ParkedJoinAll(local_isolate, threads); } UNINITIALIZED_TEST(ConcurrentExternalizationWithUniqueResources) { TestConcurrentExternalization(false); } UNINITIALIZED_TEST(ConcurrentExternalizationWithSharedResources) { TestConcurrentExternalization(true); } void TestConcurrentExternalizationWithDeadStrings(bool share_resources, bool transition_with_stack) { v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); ExternalResourceFactory resource_factory; MultiClientIsolateTest test; constexpr int kThreads = 4; constexpr int kStrings = 12; constexpr int kLOStrings = 2; Isolate* i_isolate = test.i_main_isolate(); Factory* factory = i_isolate->factory(); ManualGCScope manual_gc_scope(i_isolate); HandleScope scope(i_isolate); IndirectHandle shared_strings = CreateSharedOneByteStrings( i_isolate, factory, kStrings - kLOStrings, kLOStrings, sizeof(UncachedExternalString), false); ParkingSemaphore sema_ready(0); ParkingSemaphore sema_execute_start(0); ParkingSemaphore sema_execute_complete(0); std::vector> threads; std::vector shared_resources; if (share_resources) { CreateExternalResources(i_isolate, shared_strings, shared_resources, resource_factory); } for (int i = 0; i < kThreads; i++) { std::vector local_resources; if (share_resources) { local_resources = shared_resources; } else { CreateExternalResources(i_isolate, shared_strings, local_resources, resource_factory); } auto thread = std::make_unique( &test, shared_strings, local_resources, share_resources, &sema_ready, &sema_execute_start, &sema_execute_complete); CHECK(thread->Start()); threads.push_back(std::move(thread)); } LocalIsolate* local_isolate = i_isolate->main_thread_local_isolate(); for (int i = 0; i < kThreads; i++) { sema_ready.ParkedWait(local_isolate); } for (int i = 0; i < kThreads; i++) { sema_execute_start.Signal(); } for (int i = 0; i < kThreads; i++) { sema_execute_complete.ParkedWait(local_isolate); } DirectHandle empty_string( ReadOnlyRoots(i_isolate->heap()).empty_string(), i_isolate); for (int i = 0; i < shared_strings->length(); i++) { DirectHandle input_string(Cast(shared_strings->get(i)), i_isolate); // Patch every third string to empty. The next GC will dispose the external // resources. if (i % 3 == 0) { input_string.SetValue(*empty_string); shared_strings->set(i, *input_string); } } v8_flags.transition_strings_during_gc_with_stack = transition_with_stack; i_isolate->heap()->CollectGarbageShared(i_isolate->main_thread_local_heap(), GarbageCollectionReason::kTesting); for (int i = 0; i < shared_strings->length(); i++) { DirectHandle input_string(Cast(shared_strings->get(i)), i_isolate); const bool should_be_alive = i % 3 != 0; Tagged string = *input_string; CheckStringAndResource(string, i, should_be_alive, *empty_string, transition_with_stack, share_resources, threads); } // If we didn't test transitions during GC with stack, trigger another GC // (allowing transitions with stack) to ensure everything is handled // correctly. if (!transition_with_stack) { v8_flags.transition_strings_during_gc_with_stack = true; i_isolate->heap()->CollectGarbageShared(i_isolate->main_thread_local_heap(), GarbageCollectionReason::kTesting); for (int i = 0; i < shared_strings->length(); i++) { DirectHandle input_string(Cast(shared_strings->get(i)), i_isolate); const bool should_be_alive = i % 3 != 0; Tagged string = *input_string; CheckStringAndResource(string, i, should_be_alive, *empty_string, true, share_resources, threads); } } ParkingThread::ParkedJoinAll(local_isolate, threads); } UNINITIALIZED_TEST( ExternalizationWithDeadStringsAndUniqueResourcesTransitionWithStack) { TestConcurrentExternalizationWithDeadStrings(false, true); } UNINITIALIZED_TEST( ExternalizationWithDeadStringsAndSharedResourcesTransitionWithStack) { TestConcurrentExternalizationWithDeadStrings(true, true); } UNINITIALIZED_TEST(ExternalizationWithDeadStringsAndUniqueResources) { TestConcurrentExternalizationWithDeadStrings(false, false); } UNINITIALIZED_TEST(ExternalizationWithDeadStringsAndSharedResources) { TestConcurrentExternalizationWithDeadStrings(true, false); } void TestConcurrentExternalizationAndInternalization( TestHitOrMiss hit_or_miss) { v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); ExternalResourceFactory resource_factory; MultiClientIsolateTest test; constexpr int kInternalizationThreads = 4; constexpr int kExternalizationThreads = 4; constexpr int kTotalThreads = kInternalizationThreads + kExternalizationThreads; constexpr int kStrings = 4096; constexpr int kLOStrings = 16; Isolate* i_isolate = test.i_main_isolate(); Factory* factory = i_isolate->factory(); HandleScope scope(i_isolate); IndirectHandle shared_strings = CreateSharedOneByteStrings( i_isolate, factory, kStrings - kLOStrings, kLOStrings, sizeof(UncachedExternalString), hit_or_miss == kTestHit); ParkingSemaphore sema_ready(0); ParkingSemaphore sema_execute_start(0); ParkingSemaphore sema_execute_complete(0); std::vector> threads; for (int i = 0; i < kInternalizationThreads; i++) { auto thread = std::make_unique( &test, shared_strings, hit_or_miss, &sema_ready, &sema_execute_start, &sema_execute_complete); CHECK(thread->Start()); threads.push_back(std::move(thread)); } for (int i = 0; i < kExternalizationThreads; i++) { std::vector resources; CreateExternalResources(i_isolate, shared_strings, resources, resource_factory); auto thread = std::make_unique( &test, shared_strings, resources, false, &sema_ready, &sema_execute_start, &sema_execute_complete); CHECK(thread->Start()); threads.push_back(std::move(thread)); } LocalIsolate* local_isolate = i_isolate->main_thread_local_isolate(); for (int i = 0; i < kTotalThreads; i++) { sema_ready.ParkedWait(local_isolate); } for (int i = 0; i < kTotalThreads; i++) { sema_execute_start.Signal(); } for (int i = 0; i < kTotalThreads; i++) { sema_execute_complete.ParkedWait(local_isolate); } TriggerGCWithTransitions(i_isolate->heap()); for (int i = 0; i < shared_strings->length(); i++) { DirectHandle input_string(Cast(shared_strings->get(i)), i_isolate); Tagged string = *input_string; if (hit_or_miss == kTestHit) { CHECK(IsThinString(string)); string = Cast(string)->actual(); } int alive_resources = 0; for (int t = kInternalizationThreads; t < kTotalThreads; t++) { ConcurrentExternalizationThread* thread = reinterpret_cast(threads[t].get()); if (!thread->Resource(i)->IsDisposed()) { alive_resources++; } } StringShape shape(string); CHECK(shape.IsInternalized()); // Check at most one external resource is alive. // If internalization happens on an external string and we already have an // internalized string with the same content, we turn it into a ThinString // and dispose the resource. CHECK_LE(alive_resources, 1); CHECK_EQ(shape.IsExternal(), alive_resources); CHECK(string->HasHashCode()); } ParkingThread::ParkedJoinAll(local_isolate, threads); } UNINITIALIZED_TEST(ConcurrentExternalizationAndInternalizationMiss) { TestConcurrentExternalizationAndInternalization(kTestMiss); } UNINITIALIZED_TEST(ConcurrentExternalizationAndInternalizationHit) { TestConcurrentExternalizationAndInternalization(kTestHit); } UNINITIALIZED_TEST(SharedStringInGlobalHandle) { v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); MultiClientIsolateTest test; Isolate* i_isolate = test.i_main_isolate(); Factory* factory = i_isolate->factory(); HandleScope handle_scope(i_isolate); DirectHandle shared_string = factory->NewStringFromAsciiChecked("foobar", AllocationType::kSharedOld); CHECK(HeapLayout::InWritableSharedSpace(*shared_string)); v8::Local lh_shared_string = Utils::ToLocal(shared_string); v8::Global gh_shared_string(test.main_isolate(), lh_shared_string); gh_shared_string.SetWeak(); heap::InvokeMajorGC(i_isolate->heap()); CHECK(!gh_shared_string.IsEmpty()); } class WakeupTask : public CancelableTask { public: explicit WakeupTask(Isolate* isolate, int& wakeup_counter) : CancelableTask(isolate), wakeup_counter_(wakeup_counter) {} private: // v8::internal::CancelableTask overrides. void RunInternal() override { (wakeup_counter_)++; } int& wakeup_counter_; }; class WorkerIsolateThread : public v8::base::Thread { public: WorkerIsolateThread(const char* name, MultiClientIsolateTest* test) : v8::base::Thread(base::Thread::Options(name)), test_(test) {} void Run() override { v8::Isolate* client = test_->NewClientIsolate(); Isolate* i_client = reinterpret_cast(client); Factory* factory = i_client->factory(); v8::Global gh_shared_string; { v8::Isolate::Scope isolate_scope(client); HandleScope handle_scope(i_client); DirectHandle shared_string = factory->NewStringFromAsciiChecked( "foobar", AllocationType::kSharedOld); CHECK(HeapLayout::InWritableSharedSpace(*shared_string)); v8::Local lh_shared_string = Utils::ToLocal(shared_string); gh_shared_string.Reset(test_->main_isolate(), lh_shared_string); gh_shared_string.SetWeak(); } { // We need to invoke GC without stack, otherwise some objects may survive. DisableConservativeStackScanningScopeForTesting no_stack_scanning( i_client->heap()); i_client->heap()->CollectGarbageShared(i_client->main_thread_local_heap(), GarbageCollectionReason::kTesting); } CHECK(gh_shared_string.IsEmpty()); client->Dispose(); V8::GetCurrentPlatform() ->GetForegroundTaskRunner(test_->main_isolate()) ->PostTask(std::make_unique( test_->i_main_isolate(), test_->main_isolate_wakeup_counter())); } private: MultiClientIsolateTest* test_; }; UNINITIALIZED_TEST(SharedStringInClientGlobalHandle) { v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); MultiClientIsolateTest test; ManualGCScope manual_gc_scope(test.i_main_isolate()); WorkerIsolateThread thread("worker", &test); CHECK(thread.Start()); while (test.main_isolate_wakeup_counter() < 1) { v8::platform::PumpMessageLoop( i::V8::GetCurrentPlatform(), test.main_isolate(), v8::platform::MessageLoopBehavior::kWaitForWork); } thread.Join(); } class ClientIsolateThreadForPagePromotions : public v8::base::Thread { public: // Expects a ManualGCScope to be in scope while `Run()` is executed. ClientIsolateThreadForPagePromotions(const char* name, MultiClientIsolateTest* test, Handle* shared_string, const ManualGCScope& witness) : v8::base::Thread(base::Thread::Options(name)), test_(test), shared_string_(shared_string) {} void Run() override { CHECK(v8_flags.minor_ms); v8::Isolate* client = test_->NewClientIsolate(); Isolate* i_client = reinterpret_cast(client); Factory* factory = i_client->factory(); Heap* heap = i_client->heap(); { v8::Isolate::Scope isolate_scope(client); HandleScope handle_scope(i_client); DirectHandle young_object = factory->NewFixedArray(1, AllocationType::kYoung); CHECK(HeapLayout::InYoungGeneration(*young_object)); Address young_object_address = young_object->address(); DirectHandleVector handles(i_client); // Make the whole page transition from new->old, getting the buffers // processed in the sweeper (relying on marking information) instead of // processing during newspace evacuation. heap::FillCurrentPage(heap->new_space(), &handles); CHECK(!heap->Contains(**shared_string_)); CHECK(heap->SharedHeapContains(**shared_string_)); young_object->set(0, **shared_string_); heap::EmptyNewSpaceUsingGC(heap); heap->CompleteSweepingFull(); // Object should get promoted using page promotion, so address should // remain the same. CHECK(!HeapLayout::InYoungGeneration(*young_object)); CHECK(heap->Contains(*young_object)); CHECK_EQ(young_object_address, young_object->address()); // Since the GC promoted that string into shared heap, it also needs to // create an OLD_TO_SHARED slot. ObjectSlot slot = young_object->RawFieldOfFirstElement(); CHECK(RememberedSet::Contains( MutablePageMetadata::FromHeapObject(*young_object), slot.address())); } client->Dispose(); V8::GetCurrentPlatform() ->GetForegroundTaskRunner(test_->main_isolate()) ->PostTask(std::make_unique( test_->i_main_isolate(), test_->main_isolate_wakeup_counter())); } private: MultiClientIsolateTest* test_; Handle* shared_string_; }; UNINITIALIZED_TEST(RegisterOldToSharedForPromotedPageFromClient) { if (v8_flags.single_generation) return; if (!v8_flags.minor_ms) return; v8_flags.stress_concurrent_allocation = false; // For SealCurrentObjects. v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); ManualGCScope manual_gc_scope; heap::ManualEvacuationCandidatesSelectionScope manual_evacuation_candidate_selection_scope(manual_gc_scope); MultiClientIsolateTest test; Isolate* i_isolate = test.i_main_isolate(); Isolate* shared_isolate = i_isolate->shared_space_isolate(); Heap* shared_heap = shared_isolate->heap(); HandleScope scope(i_isolate); const char raw_one_byte[] = "foo"; Handle shared_string = i_isolate->factory()->NewStringFromAsciiChecked( raw_one_byte, AllocationType::kSharedOld); CHECK(shared_heap->Contains(*shared_string)); ClientIsolateThreadForPagePromotions thread("worker", &test, &shared_string, manual_gc_scope); CHECK(thread.Start()); while (test.main_isolate_wakeup_counter() < 1) { v8::platform::PumpMessageLoop( i::V8::GetCurrentPlatform(), test.main_isolate(), v8::platform::MessageLoopBehavior::kWaitForWork); } thread.Join(); } UNINITIALIZED_TEST( RegisterOldToSharedForPromotedPageFromClientDuringIncrementalMarking) { if (v8_flags.single_generation) return; if (!v8_flags.minor_ms) return; v8_flags.stress_concurrent_allocation = false; // For SealCurrentObjects. v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); ManualGCScope manual_gc_scope; heap::ManualEvacuationCandidatesSelectionScope manual_evacuation_candidate_selection_scope(manual_gc_scope); v8_flags.incremental_marking_task = false; // Prevent the incremental GC from finishing and finalizing in a // task. MultiClientIsolateTest test; Isolate* i_isolate = test.i_main_isolate(); Isolate* shared_isolate = i_isolate->shared_space_isolate(); Heap* shared_heap = shared_isolate->heap(); HandleScope scope(i_isolate); const char raw_one_byte[] = "foo"; Handle shared_string = i_isolate->factory()->NewStringFromAsciiChecked( raw_one_byte, AllocationType::kSharedOld); CHECK(shared_heap->Contains(*shared_string)); // Start an incremental shared GC such that shared_string resides on an // evacuation candidate. heap::ForceEvacuationCandidate(PageMetadata::FromHeapObject(*shared_string)); i::IncrementalMarking* marking = shared_heap->incremental_marking(); CHECK(marking->IsStopped()); { SafepointScope safepoint_scope(shared_isolate, kGlobalSafepointForSharedSpaceIsolate); shared_heap->tracer()->StartCycle( GarbageCollector::MARK_COMPACTOR, GarbageCollectionReason::kTesting, "collector cctest", GCTracer::MarkingType::kIncremental); marking->Start(GarbageCollector::MARK_COMPACTOR, i::GarbageCollectionReason::kTesting); } ClientIsolateThreadForPagePromotions thread("worker", &test, &shared_string, manual_gc_scope); CHECK(thread.Start()); while (test.main_isolate_wakeup_counter() < 1) { v8::platform::PumpMessageLoop( i::V8::GetCurrentPlatform(), test.main_isolate(), v8::platform::MessageLoopBehavior::kWaitForWork); } thread.Join(); } class ClientIsolateThreadForRetainingByRememberedSet : public v8::base::Thread { public: // Expects a ManualGCScope to be in scope while `Run()` is executed. ClientIsolateThreadForRetainingByRememberedSet( const char* name, MultiClientIsolateTest* test, Persistent* weak_ref, const ManualGCScope& witness) : v8::base::Thread(base::Thread::Options(name)), test_(test), weak_ref_(weak_ref) {} void Run() override { CHECK(v8_flags.minor_ms); client_isolate_ = test_->NewClientIsolate(); Isolate* i_client = reinterpret_cast(client_isolate_); Factory* factory = i_client->factory(); Heap* heap = i_client->heap(); { v8::Isolate::Scope isolate_scope(client_isolate_); HandleScope scope(i_client); IndirectHandle young_object = factory->NewFixedArray(1, AllocationType::kYoung); CHECK(HeapLayout::InYoungGeneration(*young_object)); Address young_object_address = young_object->address(); DirectHandleVector handles(i_client); // Make the whole page transition from new->old, getting the buffers // processed in the sweeper (relying on marking information) instead of // processing during newspace evacuation. heap::FillCurrentPage(heap->new_space(), &handles); // Create a new to shared reference. CHECK(!weak_ref_->IsEmpty()); IndirectHandle shared_string = Utils::OpenHandle( weak_ref_->Get(client_isolate_)); CHECK(!heap->Contains(*shared_string)); CHECK(heap->SharedHeapContains(*shared_string)); young_object->set(0, *shared_string); heap::EmptyNewSpaceUsingGC(heap); // Object should get promoted using page promotion, so address should // remain the same. CHECK(!HeapLayout::InYoungGeneration(*young_object)); CHECK(heap->Contains(*young_object)); CHECK_EQ(young_object_address, young_object->address()); // GC should still be in progress (unless heap verification is enabled). CHECK_IMPLIES(!v8_flags.verify_heap, heap->sweeping_in_progress()); // Inform main thread that the client is set up and is doing a GC. V8::GetCurrentPlatform() ->GetForegroundTaskRunner(test_->main_isolate()) ->PostTask(std::make_unique( test_->i_main_isolate(), test_->main_isolate_wakeup_counter())); // We need to ensure that the shared GC does not scan the stack for this // client, otherwise some objects may survive. DisableConservativeStackScanningScopeForTesting no_stack_scanning(heap); // Wait for main thread to do a shared GC. while (wakeup_counter_ < 1) { v8::platform::PumpMessageLoop( i::V8::GetCurrentPlatform(), isolate(), v8::platform::MessageLoopBehavior::kWaitForWork); } // Since the GC promoted that string into shared heap, it also needs to // create an OLD_TO_SHARED slot. ObjectSlot slot = young_object->RawFieldOfFirstElement(); CHECK(RememberedSet::Contains( MutablePageMetadata::FromHeapObject(*young_object), slot.address())); } client_isolate_->Dispose(); // Inform main thread that client is finished. V8::GetCurrentPlatform() ->GetForegroundTaskRunner(test_->main_isolate()) ->PostTask(std::make_unique( test_->i_main_isolate(), test_->main_isolate_wakeup_counter())); } v8::Isolate* isolate() const { DCHECK_NOT_NULL(client_isolate_); return client_isolate_; } int& wakeup_counter() { return wakeup_counter_; } private: MultiClientIsolateTest* test_; Persistent* weak_ref_; v8::Isolate* client_isolate_; int wakeup_counter_ = 0; }; UNINITIALIZED_TEST(SharedObjectRetainedByClientRememberedSet) { if (v8_flags.single_generation) return; if (!v8_flags.minor_ms) return; v8_flags.stress_concurrent_allocation = false; // For SealCurrentObjects. v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); ManualGCScope manual_gc_scope; heap::ManualEvacuationCandidatesSelectionScope manual_evacuation_candidate_selection_scope(manual_gc_scope); MultiClientIsolateTest test; v8::Isolate* isolate = test.main_isolate(); Isolate* i_isolate = test.i_main_isolate(); Isolate* shared_isolate = i_isolate->shared_space_isolate(); Heap* shared_heap = shared_isolate->heap(); // We need to invoke GC without stack, otherwise some objects may survive. DisableConservativeStackScanningScopeForTesting no_stack_scanning( shared_heap); // Create two weak references to Strings. One should die, the other should be // kept alive by the client isolate. Persistent live_weak_ref; Persistent dead_weak_ref; { HandleScope scope(i_isolate); const char raw_one_byte[] = "foo"; DirectHandle live_shared_string = i_isolate->factory()->NewStringFromAsciiChecked( raw_one_byte, AllocationType::kSharedOld); CHECK(shared_heap->Contains(*live_shared_string)); live_weak_ref.Reset(isolate, Utils::ToLocal(live_shared_string)); live_weak_ref.SetWeak(); DirectHandle dead_shared_string = i_isolate->factory()->NewStringFromAsciiChecked( raw_one_byte, AllocationType::kSharedOld); CHECK(shared_heap->Contains(*dead_shared_string)); dead_weak_ref.Reset(isolate, Utils::ToLocal(dead_shared_string)); dead_weak_ref.SetWeak(); } ClientIsolateThreadForRetainingByRememberedSet thread( "worker", &test, &live_weak_ref, manual_gc_scope); CHECK(thread.Start()); // Wait for client isolate to allocate objects and start a GC. while (test.main_isolate_wakeup_counter() < 1) { v8::platform::PumpMessageLoop( i::V8::GetCurrentPlatform(), test.main_isolate(), v8::platform::MessageLoopBehavior::kWaitForWork); } // Do shared GC. The live weak ref should be kept alive via an OLD_TO_SHARED // slot in the client isolate. CHECK(!live_weak_ref.IsEmpty()); CHECK(!dead_weak_ref.IsEmpty()); heap::CollectSharedGarbage(i_isolate->heap()); CHECK(!live_weak_ref.IsEmpty()); CHECK(dead_weak_ref.IsEmpty()); // Inform client that shared GC is finished. auto thread_wakeup_task = std::make_unique( reinterpret_cast(thread.isolate()), thread.wakeup_counter()); V8::GetCurrentPlatform() ->GetForegroundTaskRunner(thread.isolate()) ->PostTask(std::move(thread_wakeup_task)); while (test.main_isolate_wakeup_counter() < 2) { v8::platform::PumpMessageLoop( i::V8::GetCurrentPlatform(), test.main_isolate(), v8::platform::MessageLoopBehavior::kWaitForWork); } thread.Join(); } class Regress1424955ClientIsolateThread : public v8::base::Thread { public: Regress1424955ClientIsolateThread(const char* name, MultiClientIsolateTest* test) : v8::base::Thread(base::Thread::Options(name)), test_(test) {} void Run() override { client_isolate_ = test_->NewClientIsolate(); Isolate* i_client = reinterpret_cast(client_isolate_); Heap* i_client_heap = i_client->heap(); Factory* factory = i_client->factory(); { // Allocate an object so that there is work for the sweeper. Otherwise, // starting a minor GC after a full GC may finalize sweeping since it is // out of work. v8::Isolate::Scope isolate_scope(client_isolate_); HandleScope handle_scope(i_client); Handle array = factory->NewFixedArray(64, AllocationType::kOld); USE(array); // Start sweeping. heap::InvokeMajorGC(i_client_heap); CHECK(i_client_heap->sweeping_in_progress()); // Inform the initiator thread it's time to request a global safepoint. V8::GetCurrentPlatform() ->GetForegroundTaskRunner(test_->main_isolate()) ->PostTask(std::make_unique( test_->i_main_isolate(), test_->main_isolate_wakeup_counter())); // Wait for the initiator thread to request a global safepoint. while (!i_client->shared_space_isolate() ->global_safepoint() ->IsRequestedForTesting()) { v8::base::OS::Sleep(v8::base::TimeDelta::FromMilliseconds(1)); } // Start a minor GC. This will cause this client isolate to join the // global safepoint. At which point, the initiator isolate will try to // finalize sweeping on behalf of this client isolate. heap::InvokeMinorGC(i_client_heap); } // Wait for the initiator isolate to finish the shared GC. while (wakeup_counter_ < 1) { v8::platform::PumpMessageLoop( i::V8::GetCurrentPlatform(), client_isolate_, v8::platform::MessageLoopBehavior::kWaitForWork); } client_isolate_->Dispose(); V8::GetCurrentPlatform() ->GetForegroundTaskRunner(test_->main_isolate()) ->PostTask(std::make_unique( test_->i_main_isolate(), test_->main_isolate_wakeup_counter())); } v8::Isolate* isolate() const { DCHECK_NOT_NULL(client_isolate_); return client_isolate_; } int& wakeup_counter() { return wakeup_counter_; } private: MultiClientIsolateTest* test_; v8::Isolate* client_isolate_; int wakeup_counter_ = 0; }; UNINITIALIZED_TEST(Regress1424955) { if (v8_flags.single_generation) return; // When heap verification is enabled, sweeping is finalized in the atomic // pause. This issue requires that sweeping is still in progress after the // atomic pause is finished. if (v8_flags.verify_heap) return; v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); ManualGCScope manual_gc_scope; MultiClientIsolateTest test; Regress1424955ClientIsolateThread thread("worker", &test); CHECK(thread.Start()); // Wait for client thread to start sweeping. while (test.main_isolate_wakeup_counter() < 1) { v8::platform::PumpMessageLoop( i::V8::GetCurrentPlatform(), test.main_isolate(), v8::platform::MessageLoopBehavior::kWaitForWork); } // Client isolate waits for this isolate to request a global safepoint and // then triggers a minor GC. heap::CollectSharedGarbage(test.i_main_isolate()->heap()); V8::GetCurrentPlatform() ->GetForegroundTaskRunner(thread.isolate()) ->PostTask(std::make_unique( reinterpret_cast(thread.isolate()), thread.wakeup_counter())); // Wait for client isolate to finish the minor GC and dispose of its isolate. while (test.main_isolate_wakeup_counter() < 2) { v8::platform::PumpMessageLoop( i::V8::GetCurrentPlatform(), test.main_isolate(), v8::platform::MessageLoopBehavior::kWaitForWork); } thread.Join(); } class ProtectExternalStringTableAddStringClientIsolateThread : public v8::base::Thread { public: ProtectExternalStringTableAddStringClientIsolateThread( const char* name, MultiClientIsolateTest* test, v8::Isolate* isolate) : v8::base::Thread(base::Thread::Options(name)), test_(test), isolate_(isolate), i_isolate_(reinterpret_cast(isolate)) {} void Run() override { const char* text = "worker_external_string"; { v8::Isolate::Scope isolate_scope(isolate_); for (int i = 0; i < 1'000; i++) { HandleScope scope(i_isolate_); DirectHandle string = i_isolate_->factory()->NewStringFromAsciiChecked( text, AllocationType::kOld); CHECK(HeapLayout::InWritableSharedSpace(*string)); CHECK(!string->IsShared()); CHECK( string->MakeExternal(i_isolate_, new StaticOneByteResource(text))); CHECK(IsExternalOneByteString(*string)); } } isolate_->Dispose(); V8::GetCurrentPlatform() ->GetForegroundTaskRunner(test_->main_isolate()) ->PostTask(std::make_unique( test_->i_main_isolate(), test_->main_isolate_wakeup_counter())); } private: MultiClientIsolateTest* test_; v8::Isolate* isolate_; Isolate* i_isolate_; }; UNINITIALIZED_TEST(ProtectExternalStringTableAddString) { v8_flags.shared_string_table = true; i::FlagList::EnforceFlagImplications(); ManualGCScope manual_gc_scope; MultiClientIsolateTest test; v8::Isolate* client = test.NewClientIsolate(); ProtectExternalStringTableAddStringClientIsolateThread thread("worker", &test, client); CHECK(thread.Start()); Isolate* isolate = test.i_main_isolate(); HandleScope scope(isolate); for (int i = 0; i < 1'000; i++) { isolate->factory() ->NewExternalStringFromOneByte( new StaticOneByteResource("main_external_string")) .Check(); } // Wait for client isolate to finish the minor GC and dispose of its isolate. while (test.main_isolate_wakeup_counter() < 1) { v8::platform::PumpMessageLoop( i::V8::GetCurrentPlatform(), test.main_isolate(), v8::platform::MessageLoopBehavior::kWaitForWork); } thread.Join(); } } // namespace test_shared_strings } // namespace internal } // namespace v8 #endif // V8_CAN_CREATE_SHARED_HEAP_BOOL && // !COMPRESS_POINTERS_IN_MULTIPLE_CAGES_BOOL