blob: 17afef88e4b4853c716ac07d1e2b24b9d85f1a64 [file] [log] [blame]
// 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 "src/base/optional.h"
#include "src/base/platform/platform.h"
#include "src/heap/heap.h"
#include "src/heap/parked-scope.h"
#include "test/unittests/heap/heap-utils.h"
#include "test/unittests/test-utils.h"
#include "testing/gtest/include/gtest/gtest.h"
#if V8_CAN_CREATE_SHARED_HEAP_BOOL
namespace v8 {
namespace internal {
using SharedHeapTest = TestJSSharedMemoryWithIsolate;
class SharedHeapNoClientsTest : public TestJSSharedMemoryWithPlatform {
public:
SharedHeapNoClientsTest() {
shared_space_isolate_wrapper.emplace(kNoCounters);
shared_space_isolate_ = shared_space_isolate_wrapper->i_isolate();
}
~SharedHeapNoClientsTest() override { shared_space_isolate_ = nullptr; }
v8::Isolate* shared_space_isolate() {
return reinterpret_cast<v8::Isolate*>(i_shared_space_isolate());
}
Isolate* i_shared_space_isolate() { return shared_space_isolate_; }
private:
Isolate* shared_space_isolate_;
base::Optional<IsolateWrapper> shared_space_isolate_wrapper;
};
namespace {
const int kNumIterations = 2000;
template <typename Callback>
void SetupClientIsolateAndRunCallback(Callback callback) {
IsolateWrapper isolate_wrapper(kNoCounters);
v8::Isolate* client_isolate = isolate_wrapper.isolate();
Isolate* i_client_isolate = reinterpret_cast<Isolate*>(client_isolate);
callback(client_isolate, i_client_isolate);
}
class SharedOldSpaceAllocationThread final : public ParkingThread {
public:
SharedOldSpaceAllocationThread()
: ParkingThread(base::Thread::Options("SharedOldSpaceAllocationThread")) {
}
void Run() override {
SetupClientIsolateAndRunCallback(
[](v8::Isolate* client_isolate, Isolate* i_client_isolate) {
HandleScope scope(i_client_isolate);
for (int i = 0; i < kNumIterations; i++) {
i_client_isolate->factory()->NewFixedArray(
10, AllocationType::kSharedOld);
}
CollectGarbage(OLD_SPACE, i_client_isolate);
v8::platform::PumpMessageLoop(i::V8::GetCurrentPlatform(),
client_isolate);
});
}
};
} // namespace
TEST_F(SharedHeapTest, ConcurrentAllocationInSharedOldSpace) {
std::vector<std::unique_ptr<SharedOldSpaceAllocationThread>> threads;
const int kThreads = 4;
ParkedScope parked(i_isolate()->main_thread_local_isolate());
for (int i = 0; i < kThreads; i++) {
auto thread = std::make_unique<SharedOldSpaceAllocationThread>();
CHECK(thread->Start());
threads.push_back(std::move(thread));
}
ParkingThread::ParkedJoinAll(parked, threads);
}
namespace {
class SharedLargeOldSpaceAllocationThread final : public ParkingThread {
public:
SharedLargeOldSpaceAllocationThread()
: ParkingThread(base::Thread::Options("SharedOldSpaceAllocationThread")) {
}
void Run() override {
SetupClientIsolateAndRunCallback(
[](v8::Isolate* client_isolate, Isolate* i_client_isolate) {
HandleScope scope(i_client_isolate);
const int kNumIterations = 50;
for (int i = 0; i < kNumIterations; i++) {
HandleScope scope(i_client_isolate);
Handle<FixedArray> fixed_array =
i_client_isolate->factory()->NewFixedArray(
kMaxRegularHeapObjectSize / kTaggedSize,
AllocationType::kSharedOld);
CHECK(MemoryChunk::FromHeapObject(*fixed_array)->IsLargePage());
}
CollectGarbage(OLD_SPACE, i_client_isolate);
v8::platform::PumpMessageLoop(i::V8::GetCurrentPlatform(),
client_isolate);
});
}
};
} // namespace
TEST_F(SharedHeapTest, ConcurrentAllocationInSharedLargeOldSpace) {
std::vector<std::unique_ptr<SharedLargeOldSpaceAllocationThread>> threads;
const int kThreads = 4;
ParkedScope parked(i_isolate()->main_thread_local_isolate());
for (int i = 0; i < kThreads; i++) {
auto thread = std::make_unique<SharedLargeOldSpaceAllocationThread>();
CHECK(thread->Start());
threads.push_back(std::move(thread));
}
ParkingThread::ParkedJoinAll(parked, threads);
}
namespace {
class SharedMapSpaceAllocationThread final : public ParkingThread {
public:
SharedMapSpaceAllocationThread()
: ParkingThread(base::Thread::Options("SharedMapSpaceAllocationThread")) {
}
void Run() override {
SetupClientIsolateAndRunCallback(
[](v8::Isolate* client_isolate, Isolate* i_client_isolate) {
HandleScope scope(i_client_isolate);
for (int i = 0; i < kNumIterations; i++) {
i_client_isolate->factory()->NewMap(
NATIVE_CONTEXT_TYPE, kVariableSizeSentinel,
TERMINAL_FAST_ELEMENTS_KIND, 0, AllocationType::kSharedMap);
}
CollectGarbage(OLD_SPACE, i_client_isolate);
v8::platform::PumpMessageLoop(i::V8::GetCurrentPlatform(),
client_isolate);
});
}
};
} // namespace
TEST_F(SharedHeapTest, ConcurrentAllocationInSharedMapSpace) {
std::vector<std::unique_ptr<SharedMapSpaceAllocationThread>> threads;
const int kThreads = 4;
ParkedScope parked(i_isolate()->main_thread_local_isolate());
for (int i = 0; i < kThreads; i++) {
auto thread = std::make_unique<SharedMapSpaceAllocationThread>();
CHECK(thread->Start());
threads.push_back(std::move(thread));
}
ParkingThread::ParkedJoinAll(parked, threads);
}
TEST_F(SharedHeapNoClientsTest, SharedCollectionWithoutClients) {
::v8::internal::CollectGarbage(OLD_SPACE, i_shared_space_isolate());
}
void AllocateInSharedHeap(int iterations = 100) {
SetupClientIsolateAndRunCallback([iterations](v8::Isolate* client_isolate,
Isolate* i_client_isolate) {
HandleScope outer_scope(i_client_isolate);
std::vector<Handle<FixedArray>> arrays_in_handles;
const int kKeptAliveInHandle = 1000;
const int kKeptAliveInHeap = 100;
Handle<FixedArray> arrays_in_heap =
i_client_isolate->factory()->NewFixedArray(kKeptAliveInHeap,
AllocationType::kYoung);
for (int i = 0; i < kNumIterations * iterations; i++) {
HandleScope scope(i_client_isolate);
Handle<FixedArray> array = i_client_isolate->factory()->NewFixedArray(
100, AllocationType::kSharedOld);
if (i < kKeptAliveInHandle) {
// Keep some of those arrays alive across GCs through handles.
arrays_in_handles.push_back(scope.CloseAndEscape(array));
}
if (i < kKeptAliveInHeap) {
// Keep some of those arrays alive across GCs through client heap
// references.
arrays_in_heap->set(i, *array);
}
i_client_isolate->factory()->NewFixedArray(100, AllocationType::kYoung);
}
for (Handle<FixedArray> array : arrays_in_handles) {
CHECK_EQ(array->length(), 100);
}
for (int i = 0; i < kKeptAliveInHeap; i++) {
FixedArray array = FixedArray::cast(arrays_in_heap->get(i));
CHECK_EQ(array.length(), 100);
}
});
}
TEST_F(SharedHeapTest, SharedCollectionWithOneClient) {
v8_flags.max_old_space_size = 8;
ParkedScope parked(i_isolate()->main_thread_local_isolate());
AllocateInSharedHeap();
}
namespace {
class SharedFixedArrayAllocationThread final : public ParkingThread {
public:
SharedFixedArrayAllocationThread()
: ParkingThread(
base::Thread::Options("SharedFixedArrayAllocationThread")) {}
void Run() override { AllocateInSharedHeap(5); }
};
} // namespace
TEST_F(SharedHeapTest, SharedCollectionWithMultipleClients) {
v8_flags.max_old_space_size = 8;
std::vector<std::unique_ptr<SharedFixedArrayAllocationThread>> threads;
const int kThreads = 4;
ParkedScope parked(i_isolate()->main_thread_local_isolate());
for (int i = 0; i < kThreads; i++) {
auto thread = std::make_unique<SharedFixedArrayAllocationThread>();
CHECK(thread->Start());
threads.push_back(std::move(thread));
}
ParkingThread::ParkedJoinAll(parked, threads);
}
namespace {
/**
* The following two classes implement a recurring pattern for testing the
* shared heap: two isolates (main and client), used respectively by the main
* thread and a concurrent thread, that execute arbitrary fragments of code
* (shown below in angular brackets) and synchronize in the following way
* using parked semaphores:
*
* main thread concurrent thread
* ---------------------------------------------------------------
* <SETUP>
* |
* start thread ----------\
* | \---------> <SETUP>
* | |
* | /-------- signal ready
* wait for ready <--------/ |
* <EXECUTE> |
* signal execute ---------\ |
* | \-----> wait for execute
* | <EXECUTE>
* | /------ signal complete
* wait for complete <------/ |
* | |
* <COMPLETE> <COMPLETE>
* | /----------- exit
* join thread <----------/
* <TEARDOWN>
*
* Both threads allocate an arbitrary state object on their stack, which
* may contain information that is shared between the executed fragments
* of code.
*/
template <typename State>
class ConcurrentThread final : public ParkingThread {
public:
using ThreadType = ConcurrentThread<State>;
using Callback = void(ThreadType*);
explicit ConcurrentThread(ParkingSemaphore* sema_ready = nullptr,
ParkingSemaphore* sema_execute_start = nullptr,
ParkingSemaphore* sema_execute_complete = nullptr)
: ParkingThread(Options("ConcurrentThread")),
sema_ready_(sema_ready),
sema_execute_start_(sema_execute_start),
sema_execute_complete_(sema_execute_complete) {}
void Run() override {
IsolateWrapper isolate_wrapper(kNoCounters);
i_client_isolate_ = isolate_wrapper.i_isolate();
// Allocate the state on the stack, so that handles, direct handles or raw
// pointers are stack-allocated.
State state;
state_ = &state;
if (setup_callback_) setup_callback_(this);
if (sema_ready_) sema_ready_->Signal();
if (sema_execute_start_) {
sema_execute_start_->ParkedWait(
i_client_isolate_->main_thread_local_isolate());
}
if (execute_callback_) execute_callback_(this);
if (sema_execute_complete_) sema_execute_complete_->Signal();
if (complete_callback_) complete_callback_(this);
i_client_isolate_ = nullptr;
state_ = nullptr;
}
Isolate* i_client_isolate() const {
DCHECK_NOT_NULL(i_client_isolate_);
return i_client_isolate_;
}
v8::Isolate* client_isolate() const {
return reinterpret_cast<v8::Isolate*>(i_client_isolate_);
}
State* state() {
DCHECK_NOT_NULL(state_);
return state_;
}
void with_setup(Callback* callback) { setup_callback_ = callback; }
void with_execute(Callback* callback) { execute_callback_ = callback; }
void with_complete(Callback* callback) { complete_callback_ = callback; }
private:
Isolate* i_client_isolate_ = nullptr;
State* state_ = nullptr;
ParkingSemaphore* sema_ready_ = nullptr;
ParkingSemaphore* sema_execute_start_ = nullptr;
ParkingSemaphore* sema_execute_complete_ = nullptr;
Callback* setup_callback_ = nullptr;
Callback* execute_callback_ = nullptr;
Callback* complete_callback_ = nullptr;
};
template <typename State, typename ThreadState>
class SharedHeapTestBase : public TestJSSharedMemoryWithNativeContext {
public:
using TestType = SharedHeapTestBase<State, ThreadState>;
using Callback = void(TestType*);
using ThreadType = ConcurrentThread<ThreadState>;
using ThreadCallback = typename ThreadType::Callback;
SharedHeapTestBase()
: thread_(std::make_unique<ThreadType>(&sema_ready_, &sema_execute_start_,
&sema_execute_complete_)) {}
void Interact() {
// Allocate the state on the stack, so that handles, direct handles or raw
// pointers are stack-allocated.
State state;
state_ = &state;
if (setup_callback_) setup_callback_(this);
CHECK(thread()->Start());
sema_ready_.ParkedWait(i_isolate()->main_thread_local_isolate());
if (execute_callback_) execute_callback_(this);
sema_execute_start_.Signal();
sema_execute_complete_.ParkedWait(i_isolate()->main_thread_local_isolate());
if (complete_callback_) complete_callback_(this);
thread()->ParkedJoin(i_isolate()->main_thread_local_isolate());
if (teardown_callback_) teardown_callback_(this);
}
ConcurrentThread<State>* thread() const {
DCHECK(thread_);
return thread_.get();
}
State* state() {
DCHECK_NOT_NULL(state_);
return state_;
}
void with_setup(Callback* callback) { setup_callback_ = callback; }
void with_execute(Callback* callback) { execute_callback_ = callback; }
void with_complete(Callback* callback) { complete_callback_ = callback; }
void with_teardown(Callback* callback) { teardown_callback_ = callback; }
private:
State* state_ = nullptr;
std::unique_ptr<ConcurrentThread<State>> thread_;
ParkingSemaphore sema_ready_{0};
ParkingSemaphore sema_execute_start_{0};
ParkingSemaphore sema_execute_complete_{0};
Callback* setup_callback_ = nullptr;
Callback* execute_callback_ = nullptr;
Callback* complete_callback_ = nullptr;
Callback* teardown_callback_ = nullptr;
};
} // namespace
namespace {
// Testing the shared heap using ordinary (indirect) handles.
struct StateWithHandle {
base::Optional<HandleScope> scope;
Handle<FixedArray> handle;
Global<v8::FixedArray> weak;
};
using SharedHeapTestStateWithHandle =
SharedHeapTestBase<StateWithHandle, StateWithHandle>;
template <typename TestType, AllocationType allocation, AllocationSpace space>
void ToEachTheirOwnWithHandle(TestType* test) {
using ThreadType = TestType::ThreadType;
auto thread = test->thread();
// Install all the callbacks.
test->with_setup([](TestType* test) {
auto isolate = test->i_isolate();
auto state = test->state();
// Install a handle scope.
state->scope.emplace(isolate);
// Allocate a fixed array, keep a handle and a weak reference.
state->handle = isolate->factory()->NewFixedArray(10, allocation);
auto l = Utils::Convert<FixedArray, v8::FixedArray>(state->handle);
state->weak.Reset(test->v8_isolate(), l);
state->weak.SetWeak();
});
thread->with_setup([](ThreadType* thread) {
auto isolate = thread->i_client_isolate();
auto state = thread->state();
// Install a handle scope.
state->scope.emplace(isolate);
// Allocate a fixed array, keep a handle and a weak reference.
state->handle = isolate->factory()->NewFixedArray(20, allocation);
auto l = Utils::Convert<FixedArray, v8::FixedArray>(state->handle);
state->weak.Reset(thread->client_isolate(), l);
state->weak.SetWeak();
});
test->with_execute(
[](TestType* test) { i::CollectGarbage(space, test->i_isolate()); });
thread->with_execute([](ThreadType* thread) {
i::CollectGarbage(space, thread->i_client_isolate());
});
test->with_complete([](TestType* test) {
// The handle should keep the fixed array from being reclaimed.
EXPECT_FALSE(test->state()->weak.IsEmpty());
});
thread->with_complete([](ThreadType* thread) {
// The handle should keep the fixed array from being reclaimed.
EXPECT_FALSE(thread->state()->weak.IsEmpty());
thread->state()->scope.reset(); // Deallocate the handle scope.
i::CollectGarbage(space, thread->i_client_isolate());
});
test->with_teardown([](TestType* test) {
test->state()->scope.reset(); // Deallocate the handle scope.
i::CollectGarbage(space, test->i_isolate());
});
// Perform the test.
test->Interact();
}
} // namespace
TEST_F(SharedHeapTestStateWithHandle, ToEachTheirOwnYoungYoung) {
ToEachTheirOwnWithHandle<SharedHeapTestStateWithHandle,
AllocationType::kYoung, NEW_SPACE>(this);
}
TEST_F(SharedHeapTestStateWithHandle, ToEachTheirOwnYoungOld) {
ToEachTheirOwnWithHandle<SharedHeapTestStateWithHandle,
AllocationType::kYoung, OLD_SPACE>(this);
}
TEST_F(SharedHeapTestStateWithHandle, ToEachTheirOwnOldYoung) {
ToEachTheirOwnWithHandle<SharedHeapTestStateWithHandle, AllocationType::kOld,
NEW_SPACE>(this);
}
TEST_F(SharedHeapTestStateWithHandle, ToEachTheirOwnOldOld) {
ToEachTheirOwnWithHandle<SharedHeapTestStateWithHandle, AllocationType::kOld,
OLD_SPACE>(this);
}
TEST_F(SharedHeapTestStateWithHandle, ToEachTheirOwnSharedYoung) {
ToEachTheirOwnWithHandle<SharedHeapTestStateWithHandle,
AllocationType::kSharedOld, NEW_SPACE>(this);
}
TEST_F(SharedHeapTestStateWithHandle, ToEachTheirOwnSharedOld) {
ToEachTheirOwnWithHandle<SharedHeapTestStateWithHandle,
AllocationType::kSharedOld, OLD_SPACE>(this);
}
#ifdef V8_ENABLE_CONSERVATIVE_STACK_SCANNING
namespace {
// Testing the shared heap using raw pointers.
// This works only with conservative stack scanning.
struct StateWithRawPointer {
Address ptr;
Global<v8::FixedArray> weak;
};
using SharedHeapTestStateWithRawPointer =
SharedHeapTestBase<StateWithRawPointer, StateWithRawPointer>;
template <typename TestType, AllocationType allocation, AllocationSpace space>
void ToEachTheirOwnWithRawPointer(TestType* test) {
using ThreadType = TestType::ThreadType;
auto thread = test->thread();
// Install all the callbacks.
test->with_setup([](TestType* test) {
auto isolate = test->i_isolate();
auto state = test->state();
{
// Allocate a fixed array, keep a raw pointer and a weak reference.
HandleScope scope(isolate);
auto h = isolate->factory()->NewFixedArray(10, allocation);
state->ptr = h->GetHeapObject().ptr();
auto l = Utils::Convert<FixedArray, v8::FixedArray>(h);
state->weak.Reset(test->v8_isolate(), l);
state->weak.SetWeak();
}
});
thread->with_setup([](ThreadType* thread) {
auto isolate = thread->i_client_isolate();
auto state = thread->state();
{
// Allocate a fixed array, keep a raw pointer and a weak reference.
HandleScope scope(isolate);
auto h = isolate->factory()->NewFixedArray(20, allocation);
state->ptr = h->GetHeapObject().ptr();
auto l = Utils::Convert<FixedArray, v8::FixedArray>(h);
state->weak.Reset(thread->client_isolate(), l);
state->weak.SetWeak();
}
});
test->with_execute(
[](TestType* test) { i::CollectGarbage(space, test->i_isolate()); });
thread->with_execute([](ThreadType* thread) {
i::CollectGarbage(space, thread->i_client_isolate());
});
test->with_complete([](TestType* test) {
// With conservative stack scanning, the raw pointer should keep the fixed
// array from being reclaimed.
EXPECT_FALSE(test->state()->weak.IsEmpty());
});
thread->with_complete([](ThreadType* thread) {
// With conservative stack scanning, the raw pointer should keep the fixed
// array from being reclaimed.
EXPECT_FALSE(thread->state()->weak.IsEmpty());
i::CollectGarbage(space, thread->i_client_isolate());
});
test->with_teardown(
[](TestType* test) { i::CollectGarbage(space, test->i_isolate()); });
// Perform the test.
test->Interact();
}
} // namespace
TEST_F(SharedHeapTestStateWithRawPointer, ToEachTheirOwnYoungYoung) {
ToEachTheirOwnWithRawPointer<SharedHeapTestStateWithRawPointer,
AllocationType::kYoung, NEW_SPACE>(this);
}
TEST_F(SharedHeapTestStateWithRawPointer, ToEachTheirOwnYoungOld) {
ToEachTheirOwnWithRawPointer<SharedHeapTestStateWithRawPointer,
AllocationType::kYoung, OLD_SPACE>(this);
}
TEST_F(SharedHeapTestStateWithRawPointer, ToEachTheirOwnOldYoung) {
ToEachTheirOwnWithRawPointer<SharedHeapTestStateWithRawPointer,
AllocationType::kOld, NEW_SPACE>(this);
}
TEST_F(SharedHeapTestStateWithRawPointer, ToEachTheirOwnOldOld) {
ToEachTheirOwnWithRawPointer<SharedHeapTestStateWithRawPointer,
AllocationType::kOld, OLD_SPACE>(this);
}
TEST_F(SharedHeapTestStateWithRawPointer, ToEachTheirOwnSharedYoung) {
ToEachTheirOwnWithRawPointer<SharedHeapTestStateWithRawPointer,
AllocationType::kSharedOld, NEW_SPACE>(this);
}
TEST_F(SharedHeapTestStateWithRawPointer, ToEachTheirOwnSharedOld) {
ToEachTheirOwnWithRawPointer<SharedHeapTestStateWithRawPointer,
AllocationType::kSharedOld, OLD_SPACE>(this);
}
#endif // V8_ENABLE_CONSERVATIVE_STACK_SCANNING
} // namespace internal
} // namespace v8
#endif // V8_CAN_CREATE_SHARED_HEAP